diff --git a/.editorconfig b/.editorconfig index fe6e86cc..f3ee77cc 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,26 +1,32 @@ -; EditorConfig is awesome: http://EditorConfig.org +# This file is for unifying the coding style for different editors and IDEs +# editorconfig.org -; top-most EditorConfig file root = true -; Unix-style newlines with a newline ending every file [*] -; end_of_line = lf -insert_final_newline = true +indent_style = space +indent_size = 2 +end_of_line = lf +charset = utf-8 trim_trailing_whitespace = true +insert_final_newline = true -; 4 space indentation -[*.py] +[*.{json,html,css}] indent_style = space -indent_size = 4 +indent_size = 2 -; Tab indentation (no size specified) [*.js] -; indent_style = tab -indent_style = space +# indent_style = space +# indent_size = 2 +indent_style = tab indent_size = 4 -; Indentation override for all JS under lib directory -[lib/**.js] +[*.coffee] indent_style = space indent_size = 2 + +[*.md] +indent_style = space + +[*.py] +indent_style = space diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 00000000..519e888e --- /dev/null +++ b/.eslintignore @@ -0,0 +1,10 @@ +bin +doc +lib +node_modules +test/** +!test/unit/**/*.js +*.min.js +build/** +dist/** +!dist/jquery.js diff --git a/.eslintrc.yaml b/.eslintrc.yaml new file mode 100644 index 00000000..1da635bc --- /dev/null +++ b/.eslintrc.yaml @@ -0,0 +1,113 @@ + +extends: + - "eslint:recommended" + - "jquery" + - "plugin:prettier/recommended" + +# Accept `window`, etc. +env: + browser: true + +# Known globals (`false`: read-only) +globals: + define: false + require: false + module: false + jQuery: false + +# Custom rules (see https://eslint.org/docs/rules/ ) +# 0:off, 1:warn, 2:error +rules: + # --- Best Practices --------------------------------------------------------- + block-scoped-var: warn + # complexity: [warn, 20] + # consistent-return: warn + no-alert: error + no-caller: error + guard-for-in: warn + # linebreak-style: warn + no-else-return: warn + # no-empty-function: warn + no-extend-native: error + no-eval: error + no-floating-decimal: error + no-implied-eval: error + # no-invalid-this: warn + no-labels: warn + no-lone-blocks: warn + no-loop-func: warn + no-new: error + no-new-func: warn + no-new-wrappers: warn + no-octal-escape: warn + no-return-assign: warn + no-script-url: warn + no-self-compare: warn + no-sequences: warn + no-throw-literal: error + no-unmodified-loop-condition: warn + no-unused-expressions: error + # Not enabled because we want to allow `self._superApply(self, args)`: + # no-useless-call: warn + no-useless-catch: warn + no-useless-return: warn + no-with: warn + prefer-promise-reject-errors: warn + radix: error + # vars-on-top: warn + wrap-iife: + - error + - any + yoda: warn + + # --- Strict Mode ------------------------------------------------------------ + # strict: error + + # --- Variables -------------------------------------------------------------- + # init-declarations: ["warn", "always"] + no-label-var: error + # no-shadow: warn + no-shadow-restricted-names: error + no-undef: error + no-undef-init: warn + # no-undefined: warn + no-use-before-define: error + # - error + # - functions: false + + # --- Stylistic Issues ------------------------------------------------------- + camelcase: error + # Not enabled because sometimes we set `node = this`: + # consistent-this: [warn, self] # use `self = this` + func-name-matching: warn + new-cap: + - error + - { "capIsNewExceptionPattern": "^\\$\\.." } # Allow `d = $.Deferred()` + no-bitwise: error + # no-multi-assign: warn + no-negated-condition: warn + no-unneeded-ternary: warn + no-new-object: error + one-var: # see also no-use-before-define + - warn + - consecutive + # one-var-declaration-per-line: warn + + # --- Possible Errors -------------------------------------------------------- + curly: error + eqeqeq: ["error", "always", {"null": "ignore"}] + no-cond-assign: + - error + - except-parens + no-constant-condition: + - error + - { "checkLoops": false } + no-empty: + - error + - {allowEmptyCatch: true} + # no-extra-parens: [warn, all, {conditionalAssign: false }] + no-nested-ternary: warn + no-unused-vars: + - error + # Allow unused vars in catch() and if start with '_' + - {args: none, caughtErrors: none, varsIgnorePattern: "^_" } diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..1ba5573c --- /dev/null +++ b/.gitattributes @@ -0,0 +1,20 @@ +# Set the default behavior, in case people don't have core.autocrlf set. +* text=auto + +*.css text eol=lf +*.js text eol=lf +*.html text eol=lf +*.md text eol=lf +*.py text eol=lf + +# Explicitly declare text files you want to always be normalized and converted +# to native line endings on checkout. +#*.c text +#*.h text + +# Declare files that will always have CRLF line endings on checkout. +#*.sln text eol=crlf + +# Denote all files that are truly binary and should not be modified. +*.png binary +*.jpg binary diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 00000000..21829710 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,9 @@ +# This is a comment. +# Each line is a file pattern followed by one or more owners. + +# ext-grid, scrollbar extension +# /demo/sample-3rd-grid-scrollbar.html @ewya + +# ext-fixed +# /src/jquery.fancytree.fixed.js @Mats0 +# /demo/sample-ext-fixed.html @Mats0 diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md new file mode 100644 index 00000000..756a21e3 --- /dev/null +++ b/.github/CONTRIBUTING.md @@ -0,0 +1,4 @@ +Thanks for contributing :-) + +Please have a look at the +[Contributing Guidelines](https://github.com/mar10/fancytree/wiki/HowtoContribute#report-issues). diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 00000000..fb2031cd --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,14 @@ +# These are supported funding model platforms + +github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] +patreon: # Replace with a single Patreon username +open_collective: # Replace with a single Open Collective username +ko_fi: # Replace with a single Ko-fi username +tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel +community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry +liberapay: # Replace with a single Liberapay username +issuehunt: # Replace with a single IssueHunt username +otechie: # Replace with a single Otechie username +lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry +# custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] +custom: ['https://www.paypal.com/donate/?hosted_button_id=RA6G29AZRUD44'] diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md new file mode 100644 index 00000000..f29734ba --- /dev/null +++ b/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,32 @@ +Thanks for contributing :-) + +Please also read the hints: + https://github.com/mar10/fancytree/wiki/HowtoContribute#report-issues +then remove all unneeded lines from this issue report. + +If you are going to ask a question, please use Stackoverflow instead: + + http://stackoverflow.com/questions/tagged/fancytree + + +### Expected and Actual Behavior + +... (Maybe even a screenshot? Any hints on the browser's debug console?) + + +### Steps to Reproduce the Problem + + 1. ... + 2. ... + +Could you set up a jsFiddle (http://jsfiddle.net/mar10/KcxRd/), +CodePen (https://codepen.io/mar10/pen/WMWrbq), or +Plunker (http://plnkr.co/edit/8sdy3r?p=preview) ? + + +### Environment + + - Browser type and version: + - jQuery and jQuery UI versions: + - Fancytree version: + enabled/affected extensions: diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 00000000..28d45975 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,79 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: "CodeQL" + +on: + push: + branches: [ "master" ] + pull_request: + # The branches below must be a subset of the branches above + branches: [ "master" ] + schedule: + - cron: '25 14 * * 6' + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: [ 'javascript' ] + # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] + # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support + paths: + - src + # paths-ignore: + # - src/node_modules + # - '**/*.test.js' + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v2 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + + # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs + # queries: security-extended,security-and-quality + + + # Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@v2 + + # ℹ️ Command-line programs to run using the OS shell. + # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun + + # If the Autobuild fails above, remove it and uncomment the following three lines. + # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. + + # - run: | + # echo "Run, Build Application using script" + # ./location_of_script_within_repo/buildscript.sh + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v2 + with: + category: "/language:${{matrix.language}}" diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml new file mode 100644 index 00000000..c59f1968 --- /dev/null +++ b/.github/workflows/stale.yml @@ -0,0 +1,43 @@ +# This workflow warns and then closes issues and PRs that have had no activity for a specified amount of time. +# +# You can adjust the behavior by modifying this file. +# For more information, see: +# https://github.com/actions/stale +name: Mark stale issues and pull requests + +on: + schedule: + - cron: '31 14 * * *' + +jobs: + stale: + + runs-on: ubuntu-latest + permissions: + issues: write + pull-requests: write + + steps: + - uses: actions/stale@v5 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + + days-before-stale: 90 + days-before-close: 14 + exempt-all-milestones: true + operations-per-run: 5 + + stale-issue-label: 'no-issue-activity' + exempt-issue-labels: 'pinned,security' + stale-issue-message: | + This issue has been automatically marked as stale because it has not had + recent activity. It will be closed if no further activity occurs. + Thank you for your contributions. + close-issue-reason: 'not_planned' + + stale-pr-label: 'no-pr-activity' + exempt-pr-labels: 'pinned,security' + stale-pr-message: | + This pull request has been automatically marked as stale because it has not had + recent activity. It will be closed if no further activity occurs. + Thank you for your contributions. diff --git a/.gitignore b/.gitignore index 47734c15..ef122ab3 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,10 @@ /doc/jsdoc_new /doc/jsdoc_old /node_modules +/doc/screenshots +/src/skin-custom-* *.log sauce_connect.* -/doc/screenshots +.pyftpsync-meta.json +.DS_Store +test/ajax_101k.json diff --git a/.jshintrc b/.jshintrc deleted file mode 100644 index 23a84015..00000000 --- a/.jshintrc +++ /dev/null @@ -1,29 +0,0 @@ - -{ - "bitwise": true, - "camelcase": true, - "curly": true, - "eqeqeq": true, - "expr": true, - "immed": true, - "latedef": true, - "newcap": true, - "noarg": true, - "nonew": true, - "onevar": true, - "quotmark": "double", - "trailing": true, - "undef": true, - "unused": "vars", - - "boss": true, - "eqnull": true, - "evil": false, - "smarttabs": true, - - "browser": true, - "globals": { - "define": true, - "jQuery": true - } -} diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 00000000..c4d6ec75 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,27 @@ +# Style guide rationale: +# Width 80 is default (and explicitly recommended) by prettier +# - 2 space indentation and trailing semicolons seem to be most popular +# https://hackernoon.com/what-javascript-code-style-is-the-most-popular-5a3f5bec1f6f +# It is also the prettier's default +# - Double quotes are default in prettier and mandatory in Black +# - Trailing comma produces smaller diffs +# BUT: +# As a first step, we keep the current whitespace setting: +# - use tabs +# - tabWitdh 4 + +printWidth: 80 +useTabs: true +tabWidth: 4 +# useTabs: false +# tabWidth: 2 +semi: true +singleQuote: false +trailingComma: "es5" +bracketSpacing: true # because it's prettier's default +#requirePragma: true + +#overrides: +# - files: "*.test.js" +# options: +# semi: true diff --git a/.travis.yml b/.travis.yml index b37e4211..f1e168b6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,9 +1,25 @@ language: node_js +sudo: false node_js: - - 0.10 + - "10" + before_script: - npm install -g grunt-cli -script: grunt travis --verbose + +addons: + hosts: + - travis.dev + - localhost + +matrix: + include: + - env: GRUNT_TASK=travis + # - env: GRUNT_TASK=travis-optional + # allow_failures: + # - env: GRUNT_TASK=travis-optional + +script: grunt $GRUNT_TASK --verbose + env: global: - secure: VmlzKmxE+V+QZpvDuj5W41u2HTu2uTvW0aUi2p+2yhCHd7J5TFdOoECwIhTa/4VDEpnZwjLJXPd2q9kEn3+G0HpEqRMtKVTP/sM8y0JKUkprSCWV/y+pVX+0B9jQBAhEcjtkLDEGI3xVI8n+WV0Fig4kWecSCcSSUN5Mlbq5glQ= diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 00000000..2ba986f6 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,15 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "chrome", + "request": "launch", + "name": "Launch Chrome against localhost", + "url": "http://localhost:8080", + "webRoot": "${workspaceFolder}" + } + ] +} \ No newline at end of file diff --git a/3rd-party/extensions/contextmenu/contextmenu.html b/3rd-party/extensions/contextmenu/contextmenu.html index 26aff702..00fbced5 100644 --- a/3rd-party/extensions/contextmenu/contextmenu.html +++ b/3rd-party/extensions/contextmenu/contextmenu.html @@ -4,71 +4,76 @@ Fancytree - 3rd Party Example: Context menu - - + + - - + + - - - + + + + + - - - + + + - + + - - + + - - + + - - - + + + - + + + + + + + + + - + + +
- +

- +
@@ -26,16 +40,44 @@ -
+
- + + + + -Fork me on GitHub + + + + Fork me on GitHub + + diff --git a/bin/jsdoc3-moogle/tmpl/mainpage.tmpl b/bin/jsdoc3-moogle/tmpl/mainpage.tmpl index 859a1ee0..64e9e594 100755 --- a/bin/jsdoc3-moogle/tmpl/mainpage.tmpl +++ b/bin/jsdoc3-moogle/tmpl/mainpage.tmpl @@ -1,4 +1,5 @@ diff --git a/bin/jsdoc3-moogle/tmpl/members.tmpl b/bin/jsdoc3-moogle/tmpl/members.tmpl index 36659390..154c17b6 100755 --- a/bin/jsdoc3-moogle/tmpl/members.tmpl +++ b/bin/jsdoc3-moogle/tmpl/members.tmpl @@ -1,22 +1,38 @@ + +

-
-

- - -

- -
-
- -
- -
- - - - - -
Example 1? 's':'' ?>
- - -
+ +

+ + + +
+ +
+ + + +
Type:
+ + + + + + +
Fires:
+ + + + +
Example 1? 's':'' ?>
+ + diff --git a/bin/jsdoc3-moogle/tmpl/method.tmpl b/bin/jsdoc3-moogle/tmpl/method.tmpl index 10e47e2a..3b887d65 100755 --- a/bin/jsdoc3-moogle/tmpl/method.tmpl +++ b/bin/jsdoc3-moogle/tmpl/method.tmpl @@ -1,69 +1,129 @@ - -
+ + + +

Constructor

+ + +

- - -

- + + + + + + + + + +

-
-
- - -
- -
- - - -
This:
- - - - -
Parameters:
- - - - - - -
Fires:
- - - - -
Throws:
- 1) { ?> - - - - -
Returns:
- 1) { ?> - - - - -
Example 1? 's':'' ?>
- - -
+ + + +
+ +
+ + + +
Extends:
+ + + + +
Type:
+ + + + +
This:
+ + + + +
Parameters:
+ + + + + + +
Requires:
+ + + + +
Fires:
+ + + + +
Listens to Events:
+ + + + +
Listeners of This Event:
+ + + + +
Throws:
+ 1) { ?> + + + + +
Returns:
+ 1) { ?> + + + + +
Yields:
+ 1) { ?> + + + + +
Example 1? 's':'' ?>
+ + diff --git a/bin/jsdoc3-moogle/tmpl/method_sig.tmpl b/bin/jsdoc3-moogle/tmpl/method_sig.tmpl deleted file mode 100755 index e9766ad6..00000000 --- a/bin/jsdoc3-moogle/tmpl/method_sig.tmpl +++ /dev/null @@ -1,35 +0,0 @@ -" + name + ""; - } - if (typeof param.defaultvalue !== 'undefined') { - name = name + "=" + param.defaultvalue + ""; - } - args.push(name); - }); - args = "(" + args.join(", ") + ")"; - } -?> - - - diff --git a/bin/jsdoc3-moogle/tmpl/methods.tmpl b/bin/jsdoc3-moogle/tmpl/methods_overview.tmpl old mode 100755 new mode 100644 similarity index 92% rename from bin/jsdoc3-moogle/tmpl/methods.tmpl rename to bin/jsdoc3-moogle/tmpl/methods_overview.tmpl index 1bd04641..27e5db18 --- a/bin/jsdoc3-moogle/tmpl/methods.tmpl +++ b/bin/jsdoc3-moogle/tmpl/methods_overview.tmpl @@ -1,4 +1,7 @@ + @@ -10,7 +13,7 @@ Details - + - - + + @@ -60,7 +63,7 @@ Details - - + - \ No newline at end of file + + diff --git a/bin/jsdoc3-moogle/tmpl/params.tmpl b/bin/jsdoc3-moogle/tmpl/params.tmpl index 190a53c2..1fb4049c 100755 --- a/bin/jsdoc3-moogle/tmpl/params.tmpl +++ b/bin/jsdoc3-moogle/tmpl/params.tmpl @@ -1,37 +1,56 @@ - - - Name - - - Type - - - Argument - - - - Default - - - Description - - - - - + + Name + + + Type + + + Attributes + + + + Default + + + Description + + + + + - + params.forEach(function(param) { + if (!param) { return; } + ?> + - + - - - | - + + + - + <optional>
- + <nullable>
+ + + <repeatable>
+ - + @@ -101,13 +119,13 @@ - +
Properties
- - - - \ No newline at end of file + + + + diff --git a/bin/jsdoc3-moogle/tmpl/properties.tmpl b/bin/jsdoc3-moogle/tmpl/properties.tmpl index 6f407b13..40e09097 100755 --- a/bin/jsdoc3-moogle/tmpl/properties.tmpl +++ b/bin/jsdoc3-moogle/tmpl/properties.tmpl @@ -1,11 +1,12 @@ - - Type - - - Name - - - - Argument - - - - Default - - - Description - - - - - + + Name + + + Type + + + Attributes + + + + Default + + + Description + + + + + - + props.forEach(function(prop) { + if (!prop) { return; } + ?> + - - - - | - - - - + + + + + + + <optional>
- + <nullable>
- + @@ -101,12 +97,12 @@ - + -
Properties
+
Properties
- - - - \ No newline at end of file + + + + diff --git a/bin/jsdoc3-moogle/tmpl/returns.tmpl b/bin/jsdoc3-moogle/tmpl/returns.tmpl index 257550f9..d0704592 100755 --- a/bin/jsdoc3-moogle/tmpl/returns.tmpl +++ b/bin/jsdoc3-moogle/tmpl/returns.tmpl @@ -1,27 +1,19 @@ - +
- - +
-
- Type -
-
- - | - -
+
+ Type +
+
+ +
- \ No newline at end of file + \ No newline at end of file diff --git a/bin/jsdoc3-moogle/tmpl/source.tmpl b/bin/jsdoc3-moogle/tmpl/source.tmpl new file mode 100644 index 00000000..e559b5d1 --- /dev/null +++ b/bin/jsdoc3-moogle/tmpl/source.tmpl @@ -0,0 +1,8 @@ + +
+
+
+
+
\ No newline at end of file diff --git a/bin/jsdoc3-moogle/tmpl/tutorial.tmpl b/bin/jsdoc3-moogle/tmpl/tutorial.tmpl index 783a8d24..88a0ad52 100755 --- a/bin/jsdoc3-moogle/tmpl/tutorial.tmpl +++ b/bin/jsdoc3-moogle/tmpl/tutorial.tmpl @@ -1,19 +1,19 @@
- +
0) { ?> +

-
+
-
+ diff --git a/bin/jsdoc3-moogle/tmpl/type.tmpl b/bin/jsdoc3-moogle/tmpl/type.tmpl new file mode 100644 index 00000000..ec2c6c0d --- /dev/null +++ b/bin/jsdoc3-moogle/tmpl/type.tmpl @@ -0,0 +1,7 @@ + + +| + \ No newline at end of file diff --git a/bin/make_doc.sh b/bin/make_doc.sh deleted file mode 100755 index 44a404f7..00000000 --- a/bin/make_doc.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/sh -../../../jsdoc3/jsdoc ../src ../doc/README.md --destination ../doc/jsdoc --verbose --template ../git/fancytree/bin/jsdoc3-moogle - -#cd ../../jsdoc-toolkit -#java -jar jsrun.jar app/run.js -t=templates/jsdoc --directory=../fancytree/doc/jsdoc2 --verbose ../fancytree/src/ diff --git a/bin/release-sample.js b/bin/release-sample.js deleted file mode 100644 index 60c82d0d..00000000 --- a/bin/release-sample.js +++ /dev/null @@ -1,247 +0,0 @@ -#!/usr/bin/env node -/* - * jQuery Core Release Management - */ - -// Debugging variables -var debug = false, - skipRemote = false; - -var fs = require("fs"), - child = require("child_process"), - path = require("path"); - -var releaseVersion, - nextVersion, - CDNFiles, - isBeta, - pkg, - branch, - - scpURL = "jqadmin@code.origin.jquery.com:/var/www/html/code.jquery.com/", - cdnURL = "http://code.origin.jquery.com/", - repoURL = "git@github.com:jquery/jquery.git", - - // Windows needs the .cmd version but will find the non-.cmd - // On Windows, ensure the HOME environment variable is set - gruntCmd = process.platform === "win32" ? "grunt.cmd" : "grunt", - - devFile = "dist/jquery.js", - minFile = "dist/jquery.min.js", - mapFile = "dist/jquery.min.map", - - releaseFiles = { - "jquery-VER.js": devFile, - "jquery-VER.min.js": minFile, - "jquery-VER.min.map": mapFile //, -// Disable these until 2.0 defeats 1.9 as the ONE TRUE JQUERY -// "jquery.js": devFile, -// "jquery.min.js": minFile, -// "jquery.min.map": mapFile, -// "jquery-latest.js": devFile, -// "jquery-latest.min.js": minFile, -// "jquery-latest.min.map": mapFile - }; - -steps( - initialize, - checkGitStatus, - tagReleaseVersion, - gruntBuild, - makeReleaseCopies, - setNextVersion, - uploadToCDN, - pushToGithub, - exit -); - -function initialize( next ) { - - if ( process.argv[2] === "-d" ) { - process.argv.shift(); - debug = true; - console.warn("=== DEBUG MODE ===" ); - } - - // First arg should be the version number being released - var newver, oldver, - rversion = /^(\d)\.(\d+)\.(\d)((?:a|b|rc)\d|pre)?$/, - version = ( process.argv[3] || "" ).toLowerCase().match( rversion ) || {}, - major = version[1], - minor = version[2], - patch = version[3], - xbeta = version[4]; - - branch = process.argv[2]; - releaseVersion = process.argv[3]; - isBeta = !!xbeta; - - if ( !branch || !major || !minor || !patch ) { - die( "Usage: " + process.argv[1] + " branch releaseVersion" ); - } - if ( xbeta === "pre" ) { - die( "Cannot release a 'pre' version!" ); - } - if ( !(fs.existsSync || path.existsSync)( "package.json" ) ) { - die( "No package.json in this directory" ); - } - pkg = JSON.parse( fs.readFileSync( "package.json" ) ); - - console.log( "Current version is " + pkg.version + "; generating release " + releaseVersion ); - version = pkg.version.match( rversion ); - oldver = ( +version[1] ) * 10000 + ( +version[2] * 100 ) + ( +version[3] ) - newver = ( +major ) * 10000 + ( +minor * 100 ) + ( +patch ); - if ( newver < oldver ) { - die( "Next version is older than current version!" ); - } - - nextVersion = major + "." + minor + "." + ( isBeta ? patch : +patch + 1 ) + "pre"; - next(); -} - -function checkGitStatus( next ) { - git( [ "status" ], function( error, stdout, stderr ) { - var onBranch = ((stdout||"").match( /On branch (\S+)/ ) || [])[1]; - if ( onBranch !== branch ) { - die( "Branches don't match: Wanted " + branch + ", got " + onBranch ); - } - if ( /Changes to be committed/i.test( stdout ) ) { - die( "Please commit changed files before attemping to push a release." ); - } - if ( /Changes not staged for commit/i.test( stdout ) ) { - die( "Please stash files before attempting to push a release." ); - } - next(); - }); -} - -function tagReleaseVersion( next ) { - updatePackageVersion( releaseVersion ); - git( [ "commit", "-a", "-m", "Tagging the " + releaseVersion + " release." ], function(){ - git( [ "tag", releaseVersion ], next, debug); - }, debug); -} - -function gruntBuild( next ) { - exec( gruntCmd, [], function( error, stdout ) { - if ( error ) { - die( error + stderr ); - } - console.log( stdout ); - next(); - }, debug); -} - -function makeReleaseCopies( next ) { - CDNFiles = {}; - Object.keys( releaseFiles ).forEach(function( key ) { - var text, - builtFile = releaseFiles[ key ], - releaseFile = key.replace( /VER/g, releaseVersion ); - - // Beta releases don't update the jquery-latest etc. copies - if ( !isBeta || key !== releaseFile ) { - - if ( /\.map$/.test( releaseFile ) ) { - // Map files need to reference the new uncompressed name; - // assume that all files reside in the same directory. - // "file":"jquery.min.js","sources":["jquery.js"] - text = fs.readFileSync( builtFile, "utf8" ) - .replace( /"file":"([^"]+)","sources":\["([^"]+)"\]/, - "\"file\":\"" + releaseFile.replace( /\.min\.map/, ".min.js" ) + - "\",\"sources\":[\"" + releaseFile.replace( /\.min\.map/, ".js" ) + "\"]" ); - console.log( "Modifying map " + builtFile + " to " + releaseFile ); - if ( !debug ) { - fs.writeFileSync( releaseFile, text ); - } - } else { - copy( builtFile, releaseFile ); - } - - CDNFiles[ releaseFile ] = builtFile; - } - }); - next(); -} - -function setNextVersion( next ) { - updatePackageVersion( nextVersion ); - git( [ "commit", "-a", "-m", "Updating the source version to " + nextVersion ], next, debug ); -} - -function uploadToCDN( next ) { - var cmds = []; - - Object.keys( CDNFiles ).forEach(function( name ) { - cmds.push(function( nxt ){ - exec( "scp", [ name, scpURL ], nxt, debug || skipRemote ); - }); - cmds.push(function( nxt ){ - exec( "curl", [ cdnURL + name + "?reload" ], nxt, debug || skipRemote ); - }); - }); - cmds.push( next ); - - steps.apply( this, cmds ); -} - -function pushToGithub( next ) { - git( [ "push", "--tags", repoURL, branch ], next, debug || skipRemote ); -} - -//============================== - -function steps() { - var cur = 0, - steps = arguments; - (function next(){ - process.nextTick(function(){ - steps[ cur++ ]( next ); - }); - })(); -} - -function updatePackageVersion( ver ) { - console.log( "Updating package.json version to " + ver ); - pkg.version = ver; - if ( !debug ) { - fs.writeFileSync( "package.json", JSON.stringify( pkg, null, "\t" ) + "\n" ); - } -} - -function copy( oldFile, newFile ) { - console.log( "Copying " + oldFile + " to " + newFile ); - if ( !debug ) { - fs.writeFileSync( newFile, fs.readFileSync( oldFile, "utf8" ) ); - } -} - -function git( args, fn, skip ) { - exec( "git", args, fn, skip ); -} - -function exec( cmd, args, fn, skip ) { - if ( skip ) { - console.log( "# " + cmd + " " + args.join(" ") ); - fn( "", "", "" ); - } else { - console.log( cmd + " " + args.join(" ") ); - child.execFile( cmd, args, { env: process.env }, - function( err, stdout, stderr ) { - if ( err ) { - die( stderr || stdout || err ); - } - fn.apply( this, arguments ); - } - ); - } -} - -function die( msg ) { - console.error( "ERROR: " + msg ); - process.exit( 1 ); -} - -function exit() { - process.exit( 0 ); -} diff --git a/bower.json b/bower.json index 5bd044d6..1b7c8690 100644 --- a/bower.json +++ b/bower.json @@ -1,32 +1,50 @@ { - "name": "jquery.fancytree", - "description": "Fancytree is a JavaScript tree view plugin for jQuery with support for persistence, keyboard, checkboxes, drag'n'drop, and lazy loading.", - "version": "2.0.1", - "main": ["dist/jquery.fancytree-custom.min.js"], - "license": "MIT", - "ignore": [ - "**/.*", - "/3rd-party", - "/archive", - "/bin", - "/build", - "/demo", - "/doc", - "/lib", - "/src", - "/test", - "bower.json", - "CONTRIBUTING.md", - "fancytree.jquery.json", - "Gruntfile.coffee", - "index.html" - ], - "keywords": ["ajax", "jquery", "table", "tabletree", "tree", "treegrid"], - "authors": [{ "name": "Martin Wendt", "homepage": "http://careers.stackoverflow.com/martin-wendt" }], - "repository": {"type": "git", "url": "git://github.com/mar10/fancytree.git" }, - "dependencies": { - "jquery": ">=1.7", - "jquery-ui": ">=1.8.6" - }, - "devDependencies": {} + "name": "jquery.fancytree", + "description": "JavaScript tree view / tree grid plugin with support for keyboard, inline editing, filtering, checkboxes, drag'n'drop, and lazy loading", + "version": "2.38.6-0", + "main": [ + "dist/jquery.fancytree-all-deps.min.js" + ], + "moduleType": "globals", + "license": "MIT", + "ignore": [ + "**/.*", + "/3rd-party", + "/archive", + "/bin", + "/build", + "/demo", + "/doc", + "/lib", + "/src", + "/test", + "CONTRIBUTING.md", + "fancytree.jquery.json", + "Gruntfile.coffee", + "index.html" + ], + "keywords": [ + "ajax", + "jquery-plugin", + "lazy", + "table", + "tabletree", + "tree", + "treegrid" + ], + "authors": [ + { + "name": "Martin Wendt", + "homepage": "https://wwWendt.de" + } + ], + "repository": { + "type": "git", + "url": "git://github.com/mar10/fancytree.git" + }, + "dependencies": { + "jquery": ">=1.7", + "jquery-ui": ">=1.8.6" + }, + "devDependencies": {} } \ No newline at end of file diff --git a/demo/ajax-parse-error.json b/demo/ajax-parse-error.json new file mode 100644 index 00000000..462365cb --- /dev/null +++ b/demo/ajax-parse-error.json @@ -0,0 +1,4 @@ +[ + {'title': "sub 1"}, + {"title": "sub 2"} +] \ No newline at end of file diff --git a/demo/ajax-pws-error.json b/demo/ajax-pws-error.json new file mode 100644 index 00000000..d03125dc --- /dev/null +++ b/demo/ajax-pws-error.json @@ -0,0 +1,6 @@ +{ + "status": "fault", + "faultMsg": "uh-oh", + "faultCode": -1, + "faulDetails": "..." +} diff --git a/demo/ajax-pws-ok.json b/demo/ajax-pws-ok.json new file mode 100644 index 00000000..778cf752 --- /dev/null +++ b/demo/ajax-pws-ok.json @@ -0,0 +1,7 @@ +{ + "status": "ok", + "result": [ + {"title": "sub 1"}, + {"title": "sub 2"} + ] +} \ No newline at end of file diff --git a/demo/ajax-sub.xml b/demo/ajax-sub.xml new file mode 100644 index 00000000..57cbb761 --- /dev/null +++ b/demo/ajax-sub.xml @@ -0,0 +1,8 @@ + + + SubNode 1 (lazy) + + + SubNode 1 (lazy) + + diff --git a/demo/ajax-tree-local.json b/demo/ajax-tree-local.json index da4a1ab6..c64fb1a0 100644 --- a/demo/ajax-tree-local.json +++ b/demo/ajax-tree-local.json @@ -3,12 +3,12 @@ {"key": "2", "title": "item1 with key and tooltip", "tooltip": "Look, a tool tip!" }, {"key": "3", "title": "item2 with html inside a span tag" }, {"key": "4", "title": "node 4" }, - {"key": "5", "title": "using href", "href": "http://www.wwWendt.de/" }, + {"key": "5", "title": "using href", "href": "https://wwWendt.de/" }, {"key": "6", "title": "node with some extra classes (will be added to the generated markup)", "extraClasses": "my-extra-class" }, {"key": "10", "title": "Folder 1", "folder": true, "children": [ {"key": "10_1", "title": "Sub-item 1.1", "children": [ {"key": "10_1_1", "title": "Sub-item 1.1.1"}, - {"key": "10_1_2", "title": "Sub-item 1.1.2"} + {"key": "10_1_2", "title": "Sub-item 1.1.2 r"} ]}, {"key": "10_2", "title": "Sub-item 1.2", "children": [ {"key": "10_2_1", "title": "Sub-item 1.2.1"}, diff --git a/demo/ajax-tree-mass-data.json b/demo/ajax-tree-mass-data.json new file mode 100644 index 00000000..b3f997a0 --- /dev/null +++ b/demo/ajax-tree-mass-data.json @@ -0,0 +1,3302 @@ +[ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "title": "
Test1
Test2
" + } + ], + "title": "Node 0.4" + } + ], + "title": "Node 0.3" + } + ], + "title": "Node 0.2" + } + ], + "title": "Node 0.1" + } + ], + "title": "Node 0.0" + }, + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "title": "Node 1.5" + } + ], + "title": "Node 1.4" + } + ], + "title": "Node 1.3" + } + ], + "title": "Node 1.2" + } + ], + "title": "Node 1.1" + } + ], + "title": "Node 1.0" + }, + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "title": "Node 2.5" + } + ], + "title": "Node 2.4" + } + ], + "title": "Node 2.3" + } + ], + "title": "Node 2.2" + } + ], + "title": "Node 2.1" + } + ], + "title": "Node 2.0" + }, + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "title": "Node 3.5" + } + ], + "title": "Node 3.4" + } + ], + "title": "Node 3.3" + } + ], + "title": "Node 3.2" + } + ], + "title": "Node 3.1" + } + ], + "title": "Node 3.0" + }, + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "title": "Node 4.5" + } + ], + "title": "Node 4.4" + } + ], + "title": "Node 4.3" + } + ], + "title": "Node 4.2" + } + ], + "title": "Node 4.1" + } + ], + "title": "Node 4.0" + }, + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "title": "Node 5.5" + } + ], + "title": "Node 5.4" + } + ], + "title": "Node 5.3" + } + ], + "title": "Node 5.2" + } + ], + "title": "Node 5.1" + } + ], + "title": "Node 5.0" + }, + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "title": "Node 6.5" + } + ], + "title": "Node 6.4" + } + ], + "title": "Node 6.3" + } + ], + "title": "Node 6.2" + } + ], + "title": "Node 6.1" + } + ], + "title": "Node 6.0" + }, + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "title": "Node 7.5" + } + ], + "title": "Node 7.4" + } + ], + "title": "Node 7.3" + } + ], + "title": "Node 7.2" + } + ], + "title": "Node 7.1" + } + ], + "title": "Node 7.0" + }, + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "title": "Node 8.5" + } + ], + "title": "Node 8.4" + } + ], + "title": "Node 8.3" + } + ], + "title": "Node 8.2" + } + ], + "title": "Node 8.1" + } + ], + "title": "Node 8.0" + }, + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "title": "Node 9.5" + } + ], + "title": "Node 9.4" + } + ], + "title": "Node 9.3" + } + ], + "title": "Node 9.2" + } + ], + "title": "Node 9.1" + } + ], + "title": "Node 9.0" + }, + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "title": "Node 10.5" + } + ], + "title": "Node 10.4" + } + ], + "title": "Node 10.3" + } + ], + "title": "Node 10.2" + } + ], + "title": "Node 10.1" + } + ], + "title": "Node 10.0" + }, + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "title": "Node 11.5" + } + ], + "title": "Node 11.4" + } + ], + "title": "Node 11.3" + } + ], + "title": "Node 11.2" + } + ], + "title": "Node 11.1" + } + ], + "title": "Node 11.0" + }, + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "title": "Node 12.5" + } + ], + "title": "Node 12.4" + } + ], + "title": "Node 12.3" + } + ], + "title": "Node 12.2" + } + ], + "title": "Node 12.1" + } + ], + "title": "Node 12.0" + }, + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "title": "Node 13.5" + } + ], + "title": "Node 13.4" + } + ], + "title": "Node 13.3" + } + ], + "title": "Node 13.2" + } + ], + "title": "Node 13.1" + } + ], + "title": "Node 13.0" + }, + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "title": "Node 14.5" + } + ], + "title": "Node 14.4" + } + ], + "title": "Node 14.3" + } + ], + "title": "Node 14.2" + } + ], + "title": "Node 14.1" + } + ], + "title": "Node 14.0" + }, + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "title": "Node 15.5" + } + ], + "title": "Node 15.4" + } + ], + "title": "Node 15.3" + } + ], + "title": "Node 15.2" + } + ], + "title": "Node 15.1" + } + ], + "title": "Node 15.0" + }, + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "title": "Node 16.5" + } + ], + "title": "Node 16.4" + } + ], + "title": "Node 16.3" + } + ], + "title": "Node 16.2" + } + ], + "title": "Node 16.1" + } + ], + "title": "Node 16.0" + }, + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "title": "Node 17.5" + } + ], + "title": "Node 17.4" + } + ], + "title": "Node 17.3" + } + ], + "title": "Node 17.2" + } + ], + "title": "Node 17.1" + } + ], + "title": "Node 17.0" + }, + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "title": "Node 18.5" + } + ], + "title": "Node 18.4" + } + ], + "title": "Node 18.3" + } + ], + "title": "Node 18.2" + } + ], + "title": "Node 18.1" + } + ], + "title": "Node 18.0" + }, + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "title": "Node 19.5" + } + ], + "title": "Node 19.4" + } + ], + "title": "Node 19.3" + } + ], + "title": "Node 19.2" + } + ], + "title": "Node 19.1" + } + ], + "title": "Node 19.0" + }, + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "title": "Node 20.5" + } + ], + "title": "Node 20.4" + } + ], + "title": "Node 20.3" + } + ], + "title": "Node 20.2" + } + ], + "title": "Node 20.1" + } + ], + "title": "Node 20.0" + }, + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "title": "Node 21.5" + } + ], + "title": "Node 21.4" + } + ], + "title": "Node 21.3" + } + ], + "title": "Node 21.2" + } + ], + "title": "Node 21.1" + } + ], + "title": "Node 21.0" + }, + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "title": "Node 22.5" + } + ], + "title": "Node 22.4" + } + ], + "title": "Node 22.3" + } + ], + "title": "Node 22.2" + } + ], + "title": "Node 22.1" + } + ], + "title": "Node 22.0" + }, + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "title": "Node 23.5" + } + ], + "title": "Node 23.4" + } + ], + "title": "Node 23.3" + } + ], + "title": "Node 23.2" + } + ], + "title": "Node 23.1" + } + ], + "title": "Node 23.0" + }, + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "title": "Node 24.5" + } + ], + "title": "Node 24.4" + } + ], + "title": "Node 24.3" + } + ], + "title": "Node 24.2" + } + ], + "title": "Node 24.1" + } + ], + "title": "Node 24.0" + }, + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "title": "Node 25.5" + } + ], + "title": "Node 25.4" + } + ], + "title": "Node 25.3" + } + ], + "title": "Node 25.2" + } + ], + "title": "Node 25.1" + } + ], + "title": "Node 25.0" + }, + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "title": "Node 26.5" + } + ], + "title": "Node 26.4" + } + ], + "title": "Node 26.3" + } + ], + "title": "Node 26.2" + } + ], + "title": "Node 26.1" + } + ], + "title": "Node 26.0" + }, + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "title": "Node 27.5" + } + ], + "title": "Node 27.4" + } + ], + "title": "Node 27.3" + } + ], + "title": "Node 27.2" + } + ], + "title": "Node 27.1" + } + ], + "title": "Node 27.0" + }, + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "title": "Node 28.5" + } + ], + "title": "Node 28.4" + } + ], + "title": "Node 28.3" + } + ], + "title": "Node 28.2" + } + ], + "title": "Node 28.1" + } + ], + "title": "Node 28.0" + }, + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "title": "Node 29.5" + } + ], + "title": "Node 29.4" + } + ], + "title": "Node 29.3" + } + ], + "title": "Node 29.2" + } + ], + "title": "Node 29.1" + } + ], + "title": "Node 29.0" + }, + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "title": "Node 30.5" + } + ], + "title": "Node 30.4" + } + ], + "title": "Node 30.3" + } + ], + "title": "Node 30.2" + } + ], + "title": "Node 30.1" + } + ], + "title": "Node 30.0" + }, + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "title": "Node 31.5" + } + ], + "title": "Node 31.4" + } + ], + "title": "Node 31.3" + } + ], + "title": "Node 31.2" + } + ], + "title": "Node 31.1" + } + ], + "title": "Node 31.0" + }, + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "title": "Node 32.5" + } + ], + "title": "Node 32.4" + } + ], + "title": "Node 32.3" + } + ], + "title": "Node 32.2" + } + ], + "title": "Node 32.1" + } + ], + "title": "Node 32.0" + }, + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "title": "Node 33.5" + } + ], + "title": "Node 33.4" + } + ], + "title": "Node 33.3" + } + ], + "title": "Node 33.2" + } + ], + "title": "Node 33.1" + } + ], + "title": "Node 33.0" + }, + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "title": "Node 34.5" + } + ], + "title": "Node 34.4" + } + ], + "title": "Node 34.3" + } + ], + "title": "Node 34.2" + } + ], + "title": "Node 34.1" + } + ], + "title": "Node 34.0" + }, + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "title": "Node 35.5" + } + ], + "title": "Node 35.4" + } + ], + "title": "Node 35.3" + } + ], + "title": "Node 35.2" + } + ], + "title": "Node 35.1" + } + ], + "title": "Node 35.0" + }, + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "title": "Node 36.5" + } + ], + "title": "Node 36.4" + } + ], + "title": "Node 36.3" + } + ], + "title": "Node 36.2" + } + ], + "title": "Node 36.1" + } + ], + "title": "Node 36.0" + }, + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "title": "Node 37.5" + } + ], + "title": "Node 37.4" + } + ], + "title": "Node 37.3" + } + ], + "title": "Node 37.2" + } + ], + "title": "Node 37.1" + } + ], + "title": "Node 37.0" + }, + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "title": "Node 38.5" + } + ], + "title": "Node 38.4" + } + ], + "title": "Node 38.3" + } + ], + "title": "Node 38.2" + } + ], + "title": "Node 38.1" + } + ], + "title": "Node 38.0" + }, + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "title": "Node 39.5" + } + ], + "title": "Node 39.4" + } + ], + "title": "Node 39.3" + } + ], + "title": "Node 39.2" + } + ], + "title": "Node 39.1" + } + ], + "title": "Node 39.0" + }, + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "title": "Node 40.5" + } + ], + "title": "Node 40.4" + } + ], + "title": "Node 40.3" + } + ], + "title": "Node 40.2" + } + ], + "title": "Node 40.1" + } + ], + "title": "Node 40.0" + }, + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "title": "Node 41.5" + } + ], + "title": "Node 41.4" + } + ], + "title": "Node 41.3" + } + ], + "title": "Node 41.2" + } + ], + "title": "Node 41.1" + } + ], + "title": "Node 41.0" + }, + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "title": "Node 42.5" + } + ], + "title": "Node 42.4" + } + ], + "title": "Node 42.3" + } + ], + "title": "Node 42.2" + } + ], + "title": "Node 42.1" + } + ], + "title": "Node 42.0" + }, + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "title": "Node 43.5" + } + ], + "title": "Node 43.4" + } + ], + "title": "Node 43.3" + } + ], + "title": "Node 43.2" + } + ], + "title": "Node 43.1" + } + ], + "title": "Node 43.0" + }, + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "title": "Node 44.5" + } + ], + "title": "Node 44.4" + } + ], + "title": "Node 44.3" + } + ], + "title": "Node 44.2" + } + ], + "title": "Node 44.1" + } + ], + "title": "Node 44.0" + }, + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "title": "Node 45.5" + } + ], + "title": "Node 45.4" + } + ], + "title": "Node 45.3" + } + ], + "title": "Node 45.2" + } + ], + "title": "Node 45.1" + } + ], + "title": "Node 45.0" + }, + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "title": "Node 46.5" + } + ], + "title": "Node 46.4" + } + ], + "title": "Node 46.3" + } + ], + "title": "Node 46.2" + } + ], + "title": "Node 46.1" + } + ], + "title": "Node 46.0" + }, + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "title": "Node 47.5" + } + ], + "title": "Node 47.4" + } + ], + "title": "Node 47.3" + } + ], + "title": "Node 47.2" + } + ], + "title": "Node 47.1" + } + ], + "title": "Node 47.0" + }, + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "title": "Node 48.5" + } + ], + "title": "Node 48.4" + } + ], + "title": "Node 48.3" + } + ], + "title": "Node 48.2" + } + ], + "title": "Node 48.1" + } + ], + "title": "Node 48.0" + }, + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "title": "Node 49.5" + } + ], + "title": "Node 49.4" + } + ], + "title": "Node 49.3" + } + ], + "title": "Node 49.2" + } + ], + "title": "Node 49.1" + } + ], + "title": "Node 49.0" + }, + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "title": "Node 50.5" + } + ], + "title": "Node 50.4" + } + ], + "title": "Node 50.3" + } + ], + "title": "Node 50.2" + } + ], + "title": "Node 50.1" + } + ], + "title": "Node 50.0" + }, + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "title": "Node 51.5" + } + ], + "title": "Node 51.4" + } + ], + "title": "Node 51.3" + } + ], + "title": "Node 51.2" + } + ], + "title": "Node 51.1" + } + ], + "title": "Node 51.0" + }, + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "title": "Node 52.5" + } + ], + "title": "Node 52.4" + } + ], + "title": "Node 52.3" + } + ], + "title": "Node 52.2" + } + ], + "title": "Node 52.1" + } + ], + "title": "Node 52.0" + }, + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "title": "Node 53.5" + } + ], + "title": "Node 53.4" + } + ], + "title": "Node 53.3" + } + ], + "title": "Node 53.2" + } + ], + "title": "Node 53.1" + } + ], + "title": "Node 53.0" + }, + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "title": "Node 54.5" + } + ], + "title": "Node 54.4" + } + ], + "title": "Node 54.3" + } + ], + "title": "Node 54.2" + } + ], + "title": "Node 54.1" + } + ], + "title": "Node 54.0" + }, + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "title": "Node 55.5" + } + ], + "title": "Node 55.4" + } + ], + "title": "Node 55.3" + } + ], + "title": "Node 55.2" + } + ], + "title": "Node 55.1" + } + ], + "title": "Node 55.0" + }, + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "title": "Node 56.5" + } + ], + "title": "Node 56.4" + } + ], + "title": "Node 56.3" + } + ], + "title": "Node 56.2" + } + ], + "title": "Node 56.1" + } + ], + "title": "Node 56.0" + }, + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "title": "Node 57.5" + } + ], + "title": "Node 57.4" + } + ], + "title": "Node 57.3" + } + ], + "title": "Node 57.2" + } + ], + "title": "Node 57.1" + } + ], + "title": "Node 57.0" + }, + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "title": "Node 58.5" + } + ], + "title": "Node 58.4" + } + ], + "title": "Node 58.3" + } + ], + "title": "Node 58.2" + } + ], + "title": "Node 58.1" + } + ], + "title": "Node 58.0" + }, + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "title": "Node 59.5" + } + ], + "title": "Node 59.4" + } + ], + "title": "Node 59.3" + } + ], + "title": "Node 59.2" + } + ], + "title": "Node 59.1" + } + ], + "title": "Node 59.0" + }, + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "title": "Node 60.5" + } + ], + "title": "Node 60.4" + } + ], + "title": "Node 60.3" + } + ], + "title": "Node 60.2" + } + ], + "title": "Node 60.1" + } + ], + "title": "Node 60.0" + }, + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "title": "Node 61.5" + } + ], + "title": "Node 61.4" + } + ], + "title": "Node 61.3" + } + ], + "title": "Node 61.2" + } + ], + "title": "Node 61.1" + } + ], + "title": "Node 61.0" + }, + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "title": "Node 62.5" + } + ], + "title": "Node 62.4" + } + ], + "title": "Node 62.3" + } + ], + "title": "Node 62.2" + } + ], + "title": "Node 62.1" + } + ], + "title": "Node 62.0" + }, + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "title": "Node 63.5" + } + ], + "title": "Node 63.4" + } + ], + "title": "Node 63.3" + } + ], + "title": "Node 63.2" + } + ], + "title": "Node 63.1" + } + ], + "title": "Node 63.0" + }, + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "title": "Node 64.5" + } + ], + "title": "Node 64.4" + } + ], + "title": "Node 64.3" + } + ], + "title": "Node 64.2" + } + ], + "title": "Node 64.1" + } + ], + "title": "Node 64.0" + }, + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "title": "Node 65.5" + } + ], + "title": "Node 65.4" + } + ], + "title": "Node 65.3" + } + ], + "title": "Node 65.2" + } + ], + "title": "Node 65.1" + } + ], + "title": "Node 65.0" + }, + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "title": "Node 66.5" + } + ], + "title": "Node 66.4" + } + ], + "title": "Node 66.3" + } + ], + "title": "Node 66.2" + } + ], + "title": "Node 66.1" + } + ], + "title": "Node 66.0" + }, + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "title": "Node 67.5" + } + ], + "title": "Node 67.4" + } + ], + "title": "Node 67.3" + } + ], + "title": "Node 67.2" + } + ], + "title": "Node 67.1" + } + ], + "title": "Node 67.0" + }, + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "title": "Node 68.5" + } + ], + "title": "Node 68.4" + } + ], + "title": "Node 68.3" + } + ], + "title": "Node 68.2" + } + ], + "title": "Node 68.1" + } + ], + "title": "Node 68.0" + }, + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "title": "Node 69.5" + } + ], + "title": "Node 69.4" + } + ], + "title": "Node 69.3" + } + ], + "title": "Node 69.2" + } + ], + "title": "Node 69.1" + } + ], + "title": "Node 69.0" + }, + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "title": "Node 70.5" + } + ], + "title": "Node 70.4" + } + ], + "title": "Node 70.3" + } + ], + "title": "Node 70.2" + } + ], + "title": "Node 70.1" + } + ], + "title": "Node 70.0" + }, + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "title": "Node 71.5" + } + ], + "title": "Node 71.4" + } + ], + "title": "Node 71.3" + } + ], + "title": "Node 71.2" + } + ], + "title": "Node 71.1" + } + ], + "title": "Node 71.0" + }, + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "title": "Node 72.5" + } + ], + "title": "Node 72.4" + } + ], + "title": "Node 72.3" + } + ], + "title": "Node 72.2" + } + ], + "title": "Node 72.1" + } + ], + "title": "Node 72.0" + }, + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "title": "Node 73.5" + } + ], + "title": "Node 73.4" + } + ], + "title": "Node 73.3" + } + ], + "title": "Node 73.2" + } + ], + "title": "Node 73.1" + } + ], + "title": "Node 73.0" + }, + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "title": "Node 74.5" + } + ], + "title": "Node 74.4" + } + ], + "title": "Node 74.3" + } + ], + "title": "Node 74.2" + } + ], + "title": "Node 74.1" + } + ], + "title": "Node 74.0" + }, + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "title": "Node 75.5" + } + ], + "title": "Node 75.4" + } + ], + "title": "Node 75.3" + } + ], + "title": "Node 75.2" + } + ], + "title": "Node 75.1" + } + ], + "title": "Node 75.0" + }, + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "title": "Node 76.5" + } + ], + "title": "Node 76.4" + } + ], + "title": "Node 76.3" + } + ], + "title": "Node 76.2" + } + ], + "title": "Node 76.1" + } + ], + "title": "Node 76.0" + }, + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "title": "Node 77.5" + } + ], + "title": "Node 77.4" + } + ], + "title": "Node 77.3" + } + ], + "title": "Node 77.2" + } + ], + "title": "Node 77.1" + } + ], + "title": "Node 77.0" + }, + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "title": "Node 78.5" + } + ], + "title": "Node 78.4" + } + ], + "title": "Node 78.3" + } + ], + "title": "Node 78.2" + } + ], + "title": "Node 78.1" + } + ], + "title": "Node 78.0" + }, + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "title": "Node 79.5" + } + ], + "title": "Node 79.4" + } + ], + "title": "Node 79.3" + } + ], + "title": "Node 79.2" + } + ], + "title": "Node 79.1" + } + ], + "title": "Node 79.0" + }, + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "title": "Node 80.5" + } + ], + "title": "Node 80.4" + } + ], + "title": "Node 80.3" + } + ], + "title": "Node 80.2" + } + ], + "title": "Node 80.1" + } + ], + "title": "Node 80.0" + }, + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "title": "Node 81.5" + } + ], + "title": "Node 81.4" + } + ], + "title": "Node 81.3" + } + ], + "title": "Node 81.2" + } + ], + "title": "Node 81.1" + } + ], + "title": "Node 81.0" + }, + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "title": "Node 82.5" + } + ], + "title": "Node 82.4" + } + ], + "title": "Node 82.3" + } + ], + "title": "Node 82.2" + } + ], + "title": "Node 82.1" + } + ], + "title": "Node 82.0" + }, + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "title": "Node 83.5" + } + ], + "title": "Node 83.4" + } + ], + "title": "Node 83.3" + } + ], + "title": "Node 83.2" + } + ], + "title": "Node 83.1" + } + ], + "title": "Node 83.0" + }, + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "title": "Node 84.5" + } + ], + "title": "Node 84.4" + } + ], + "title": "Node 84.3" + } + ], + "title": "Node 84.2" + } + ], + "title": "Node 84.1" + } + ], + "title": "Node 84.0" + }, + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "title": "Node 85.5" + } + ], + "title": "Node 85.4" + } + ], + "title": "Node 85.3" + } + ], + "title": "Node 85.2" + } + ], + "title": "Node 85.1" + } + ], + "title": "Node 85.0" + }, + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "title": "Node 86.5" + } + ], + "title": "Node 86.4" + } + ], + "title": "Node 86.3" + } + ], + "title": "Node 86.2" + } + ], + "title": "Node 86.1" + } + ], + "title": "Node 86.0" + }, + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "title": "Node 87.5" + } + ], + "title": "Node 87.4" + } + ], + "title": "Node 87.3" + } + ], + "title": "Node 87.2" + } + ], + "title": "Node 87.1" + } + ], + "title": "Node 87.0" + }, + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "title": "Node 88.5" + } + ], + "title": "Node 88.4" + } + ], + "title": "Node 88.3" + } + ], + "title": "Node 88.2" + } + ], + "title": "Node 88.1" + } + ], + "title": "Node 88.0" + }, + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "title": "Node 89.5" + } + ], + "title": "Node 89.4" + } + ], + "title": "Node 89.3" + } + ], + "title": "Node 89.2" + } + ], + "title": "Node 89.1" + } + ], + "title": "Node 89.0" + }, + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "title": "Node 90.5" + } + ], + "title": "Node 90.4" + } + ], + "title": "Node 90.3" + } + ], + "title": "Node 90.2" + } + ], + "title": "Node 90.1" + } + ], + "title": "Node 90.0" + }, + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "title": "Node 91.5" + } + ], + "title": "Node 91.4" + } + ], + "title": "Node 91.3" + } + ], + "title": "Node 91.2" + } + ], + "title": "Node 91.1" + } + ], + "title": "Node 91.0" + }, + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "title": "Node 92.5" + } + ], + "title": "Node 92.4" + } + ], + "title": "Node 92.3" + } + ], + "title": "Node 92.2" + } + ], + "title": "Node 92.1" + } + ], + "title": "Node 92.0" + }, + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "title": "Node 93.5" + } + ], + "title": "Node 93.4" + } + ], + "title": "Node 93.3" + } + ], + "title": "Node 93.2" + } + ], + "title": "Node 93.1" + } + ], + "title": "Node 93.0" + }, + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "title": "Node 94.5" + } + ], + "title": "Node 94.4" + } + ], + "title": "Node 94.3" + } + ], + "title": "Node 94.2" + } + ], + "title": "Node 94.1" + } + ], + "title": "Node 94.0" + }, + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "title": "Node 95.5" + } + ], + "title": "Node 95.4" + } + ], + "title": "Node 95.3" + } + ], + "title": "Node 95.2" + } + ], + "title": "Node 95.1" + } + ], + "title": "Node 95.0" + }, + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "title": "Node 96.5" + } + ], + "title": "Node 96.4" + } + ], + "title": "Node 96.3" + } + ], + "title": "Node 96.2" + } + ], + "title": "Node 96.1" + } + ], + "title": "Node 96.0" + }, + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "title": "Node 97.5" + } + ], + "title": "Node 97.4" + } + ], + "title": "Node 97.3" + } + ], + "title": "Node 97.2" + } + ], + "title": "Node 97.1" + } + ], + "title": "Node 97.0" + }, + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "title": "Node 98.5" + } + ], + "title": "Node 98.4" + } + ], + "title": "Node 98.3" + } + ], + "title": "Node 98.2" + } + ], + "title": "Node 98.1" + } + ], + "title": "Node 98.0" + }, + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "folder": true, + "children": [ + { + "title": "Node 99.5" + } + ], + "title": "Node 99.4" + } + ], + "title": "Node 99.3" + } + ], + "title": "Node 99.2" + } + ], + "title": "Node 99.1" + } + ], + "title": "Node 99.0" + } +] diff --git a/demo/ajax-tree-plain.json b/demo/ajax-tree-plain.json index a438c255..66708944 100644 --- a/demo/ajax-tree-plain.json +++ b/demo/ajax-tree-plain.json @@ -3,7 +3,7 @@ {"key": "2", "title": "item1 with key and tooltip", "tooltip": "Look, a tool tip!" }, {"key": "3", "title": "item2 with html inside a span tag" }, {"key": "4", "title": "node 4" }, - {"key": "5", "title": "using href", "href": "http://www.wwWendt.de/" }, + {"key": "5", "title": "using href", "href": "https://wwWendt.de/" }, {"key": "6", "title": "node with some extra classes (will be added to the generated markup)", "extraClasses": "my-extra-class" }, {"key": "10", "title": "Folder 1", "folder": true, "children": [ {"key": "10_1", "title": "Sub-item 1.1", "children": [ @@ -17,7 +17,9 @@ ]}, {"key": "20", "title": "Simple node with active children (expand)", "expanded": true, "children": [ {"key": "20_1", "title": "Sub-item 2.1", "children": [ - {"key": "20_1_1", "title": "Sub-item 2.1.1"}, + {"key": "20_1_1", "title": "Sub-item 2.1.1", "children": [ + {"key": "20_1_1_1", "title": "Sub-item 2.1.1.1"} + ]}, {"key": "20_1_2", "title": "Sub-item 2.1.2"} ]}, {"key": "20_2", "title": "Sub-item 2.2", "children": [ diff --git a/demo/ajax-tree-products.json b/demo/ajax-tree-products.json index 59eeb14d..e6fc6d47 100644 --- a/demo/ajax-tree-products.json +++ b/demo/ajax-tree-products.json @@ -1,36 +1,36 @@ [ - {"title": "Books & Audible", "expanded": true, "folder": true, "children": [ - {"title": "Books", "folder": true, "children": [ - {"title": "Books"}, - {"title": "Kindle Books"}, - {"title": "Books For Study"}, - {"title": "Audiobooks"} + {"title": "Books", "expanded": true, "folder": true, "children": [ + {"title": "Art of War", "type": "book", "author": "Sun Tzu", "year": -500, "qty": 21, "price": 5.95}, + {"title": "The Hobbit", "type": "book", "author": "J.R.R. Tolkien", "year": 1937, "qty": 32, "price": 8.97}, + {"title": "The Little Prince", "type": "book", "author": "Antoine de Saint-Exupery", "year": 1943, "qty": 2946, "price": 6.82}, + {"title": "Don Quixote", "type": "book", "author": "Miguel de Cervantes", "year": 1615, "qty": 932, "price": 15.99} + ]}, + {"title": "Music", "folder": true, "children": [ + {"title": "Nevermind", "type": "music", "author": "Nirvana", "year": 1991, "qty": 916, "price": 15.95}, + {"title": "Autobahn", "type": "music", "author": "Kraftwerk", "year": 1974, "qty": 2261, "price": 23.98}, + {"title": "Kind of Blue", "type": "music", "author": "Miles Davis", "year": 1959, "qty": 9735, "price": 21.90}, + {"title": "Back in Black", "type": "music", "author": "AC/DC", "year": 1980, "qty": 3895, "price": 17.99}, + {"title": "The Dark Side of the Moon", "type": "music", "author": "Pink Floyd", "year": 1973, "qty": 263, "price": 17.99}, + {"title": "Sgt. Pepper's Lonely Hearts Club Band", "type": "music", "author": "The Beatles", "year": 1967, "qty": 521, "price": 13.98} + ]}, + {"title": "Electronics & Computers", "expanded": true, "folder": true, "children": [ + {"title": "Cell Phones", "folder": true, "children": [ + {"title": "Moto G", "type": "phone", "author": "Motorola", "year": 2014, "qty": 332, "price": 224.99}, + {"title": "Galaxy S8", "type": "phone", "author": "Samsung", "year": 2016, "qty": 952, "price": 509.99}, + {"title": "iPhone SE", "type": "phone", "author": "Apple", "year": 2016, "qty": 444, "price": 282.75}, + {"title": "G6", "type": "phone", "author": "LG", "year": 2017, "qty": 951, "price": 309.99}, + {"title": "Lumia", "type": "phone", "author": "Microsoft", "year": 2014, "qty": 32, "price": 205.95}, + {"title": "Xperia", "type": "phone", "author": "Sony", "year": 2014, "qty": 77, "price": 195.95}, + {"title": "3210", "type": "phone", "author": "Nokia", "year": 1999, "qty": 3, "price": 85.99} ]}, - {"title": "Movies, TV, Music, Games", "folder": true, "children": [ - {"title": "Music"}, - {"title": "MP3 Downloads"}, - {"title": "Musical Instruments & DJ"}, - {"title": "Film & TV"}, - {"title": "Ble-ray"}, - {"title": "PC & Video Games"} - ]}, - {"title": "Electronics & Computers", "expanded": true, "folder": true, "children": [ - {"title": "Electronics", "folder": true, "children": [ - {"title": "Camera & Photo"}, - {"title": "TV & Home Cinema"}, - {"title": "Audio & HiFi"}, - {"title": "Sat Nav & Car Electronics"}, - {"title": "Phones"}, - {"title": "Electronic Accessories"} - ]}, - {"title": "Computers", "folder": true, "children": [ - {"title": "Laptops"}, - {"title": "Tablets"}, - {"title": "Computer & Accessories"}, - {"title": "Computer Components"}, - {"title": "Software"}, - {"title": "Printers & Ink"} - ]} + {"title": "Computers", "folder": true, "children": [ + {"title": "ThinkPad", "type": "computer", "author": "IBM", "year": 1992, "qty": 16, "price": 749.90}, + {"title": "C64", "type": "computer", "author": "Commodore", "year": 1982, "qty": 83, "price": 595.00}, + {"title": "MacBook Pro", "type": "computer", "author": "Apple", "year": 2006, "qty": 482, "price": 1949.95}, + {"title": "Sinclair ZX Spectrum", "type": "computer", "author": "Sinclair Research", "year": 1982, "qty": 1, "price": 529}, + {"title": "Apple II", "type": "computer", "author": "Apple", "year": 1977, "qty": 17, "price": 1298}, + {"title": "PC AT", "type": "computer", "author": "IBM", "year": 1984, "qty": 3, "price": 1235.00} ]} - ]} + ]}, + {"title": "More...", "folder": true, "lazy": true} ] diff --git a/demo/ajax-tree-products2.json b/demo/ajax-tree-products2.json new file mode 100644 index 00000000..c690a252 --- /dev/null +++ b/demo/ajax-tree-products2.json @@ -0,0 +1,36 @@ +[ + {"title": "Books & Audible", "expanded": true, "folder": true, "children": [ + {"title": "Books", "expanded": true, "folder": true, "children": [ + {"title": "General Books"}, + {"title": "Kindle Books"}, + {"title": "Books For Study"}, + {"title": "Audiobooks"} + ]}, + {"title": "Movies, TV, Music, Games", "folder": true, "children": [ + {"title": "Music"}, + {"title": "MP3 Downloads"}, + {"title": "Musical Instruments & DJ"}, + {"title": "Film & TV"}, + {"title": "Blue-ray"}, + {"title": "PC & Video Games"} + ]}, + {"title": "Electronics & Computers", "expanded": true, "folder": true, "children": [ + {"title": "Electronics", "folder": true, "children": [ + {"title": "Camera & Photo"}, + {"title": "TV & Home Cinema"}, + {"title": "Audio & HiFi"}, + {"title": "Sat Nav & Car Electronics"}, + {"title": "Phones"}, + {"title": "Electronic Accessories"} + ]}, + {"title": "Computers", "folder": true, "children": [ + {"title": "Laptops"}, + {"title": "Tablets"}, + {"title": "Computer & Accessories"}, + {"title": "Computer Components"}, + {"title": "Software"}, + {"title": "Printers & Ink"} + ]} + ]} + ]} +] diff --git a/demo/ajax-tree.json b/demo/ajax-tree.json index c3d3389c..77f5971a 100644 --- a/demo/ajax-tree.json +++ b/demo/ajax-tree.json @@ -3,7 +3,7 @@ {"key": "2", "title": "item1 with key and tooltip", "tooltip": "Look, a tool tip!" }, {"key": "3", "title": "item2 with html inside a span tag" }, {"key": "4", "title": "this nodes uses 'nolink', so no <a> tag is generated", "nolink": true}, - {"key": "5", "title": "using href", "href": "http://www.wwWendt.de/" }, + {"key": "5", "title": "using href", "href": "https://wwWendt.de/" }, {"key": "6", "title": "node with some extra classes (will be added to the generated markup)", "extraClasses": "my-extra-class" }, {"key": "10", "title": "Folder 1", "folder": true, "children": [ {"key": "10_1", "title": "Sub-item 1.1", "children": [ diff --git a/demo/ajax-tree.xml b/demo/ajax-tree.xml new file mode 100644 index 00000000..f4dba7bd --- /dev/null +++ b/demo/ajax-tree.xml @@ -0,0 +1,30 @@ + + + Node 1 + + + Node 2 (expanded folder) + + + Node 2.1 + + + Node 2.2 + + + + + Node 3 (collapsed folder) + + + Node 3.1 + + + Node 3.2 + + + + + Node 4 (lazy) + + diff --git a/demo/fancytree-server.js b/demo/fancytree-server.js index cf1a67f1..6b514488 100644 --- a/demo/fancytree-server.js +++ b/demo/fancytree-server.js @@ -7,7 +7,9 @@ * - See https://gist.github.com/701407 */ -/*jshint node:true */ +/* eslint-env node */ +/* eslint-disable one-var, no-console */ + // TODO enable strict mode again //"use strict"; @@ -20,10 +22,12 @@ var assert = require("assert"), */ var NODE_ATTRS = ["title", "key"]; -function copyNode(node, deep){ - var i, l, name, +function copyNode(node, deep) { + var i, + l, + name, node2 = {}; - for(i=0, l=NODE_ATTRS.length; i append(root, obj) - if(obj === undefined){ + if (obj === undefined) { obj = node; node = this; } assert.ok(obj.key && this.keyMap[obj.key] === undefined); - if(node.children){ + if (node.children) { node.children.push(obj); - }else{ - node.children = [ obj ]; + } else { + node.children = [obj]; } this.keyMap[obj.key] = obj; obj.parent = node; return obj; }; -TreeModel.prototype.remove = function(key){ +TreeModel.prototype.remove = function (key) { var node = this.keyMap[key], parent = node.parent, idx = parent.children.indexOf(node); @@ -64,17 +68,15 @@ TreeModel.prototype.remove = function(key){ delete this.keyMap[key]; }; - /* * Init a new tree with some sample data */ var _tree = new TreeModel(); -var n = _tree.append({title: "node 1", key: "1", folder: true}); -_tree.append(n, {title: "node 1.1", key: "1.1"}); -_tree.append(n, {title: "node 1.2", key: "1.2"}); -n = _tree.append({title: "node 2", key: "2"}); -_tree.append(n, {title: "node 2.1", key: "2.1"}); - +var n = _tree.append({ title: "node 1", key: "1", folder: true }); +_tree.append(n, { title: "node 1.1", key: "1.1" }); +_tree.append(n, { title: "node 1.2", key: "1.2" }); +n = _tree.append({ title: "node 2", key: "2" }); +_tree.append(n, { title: "node 2.1", key: "2.1" }); /** * Ajax server @@ -86,27 +88,26 @@ http.createServer(function (request, response) { parts = args.pathname.substring(1).split("/"), cmd = parts[0], node = _tree.find(query.key), - res = {error: "invalid command"}; + res = { error: "invalid command" }; console.log("args", args, parts); - switch(cmd){ - case "get": - res = copyNode(node); - break; - case "children": - res = []; - if(node.children){ - for(i=0; i - - + + Fancytree - Example Browser @@ -12,7 +12,7 @@ marginwidth="0" marginheight="0"> + marginwidth="0" marginheight="0" style="border-right: 1px solid gray"> diff --git a/demo/nav.html b/demo/nav.html index e896b625..2f52aa8a 100644 --- a/demo/nav.html +++ b/demo/nav.html @@ -3,22 +3,23 @@ - - + + - - - + + + Fancytree - Example Browser Nav @@ -104,83 +153,123 @@ diff --git a/demo/sample-3rd-confirm.html b/demo/sample-3rd-confirm.html new file mode 100644 index 00000000..8062a304 --- /dev/null +++ b/demo/sample-3rd-confirm.html @@ -0,0 +1,97 @@ + + + + + Fancytree - 3rd Party Example: jquery.confirm + + + + + + + + + + + + + + + + + + + + + + + + + + + +

Example: use 'jquery.confirm'

+ +
+

+ This example uses the + jquery-confirm plugin, + which is maintained independantly from Fancytree. +

+
+ +Click here for a demo: + + + +
+ + + + + + + + diff --git a/demo/sample-3rd-contextmenu-abs.html b/demo/sample-3rd-contextmenu-abs.html index a53aa2df..78f16898 100644 --- a/demo/sample-3rd-contextmenu-abs.html +++ b/demo/sample-3rd-contextmenu-abs.html @@ -4,22 +4,22 @@ Fancytree - Example: Context Menu - - + + - - - + + + - - + + - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+

Example: Viewport Feature with Scrollbar

+

+ This example uses the 3rd-party plugin + PlainScrollbar + to add scrollbar functionality to the + ext-grid viewport feature. +

+

+ Status: 3rd-party example. + Details: ext-grid. +

+
+ + +
+ + + + + + + +
+ + + + +
+ + (Please reload page for changes to take effect.) +
+ +
+ + + + + +
+
+ +
+ + + + + + + + + + + + +
# RowIdx Order
+
+
+ + + + +
+ + + diff --git a/demo/sample-3rd-jQuery-contextMenu.html b/demo/sample-3rd-jQuery-contextMenu.html new file mode 100644 index 00000000..35c3728e --- /dev/null +++ b/demo/sample-3rd-jQuery-contextMenu.html @@ -0,0 +1,114 @@ + + + + + Fancytree - 3rd Party Example: jQuery contextMenu + + + + + + + + + + + + + + + + + + + + + + + + + +

Example: use 'jQuery contextMenu'

+ +
+

+ This example integrates the + jQuery contextMenu plugin, + which is maintained independantly from Fancytree and is based on + jQuery UI Menu + (part of jQuery UI 1.9+). +

+

+ This is only one of more options. See the + menu overview for details. +

+
+ +
+ +
+ + +
+ +
+ +
Click right mouse button on node
+ + +
+ + + + + + + + diff --git a/demo/sample-3rd-ui-contextmenu.html b/demo/sample-3rd-ui-contextmenu.html index 8948974c..edb68c0b 100644 --- a/demo/sample-3rd-ui-contextmenu.html +++ b/demo/sample-3rd-ui-contextmenu.html @@ -2,29 +2,30 @@ - Fancytree - 3rd Party Example: Context menu + Fancytree - 3rd Party Example: jquery.ui-contextmenu - - - + + + - - + + - + - - - + + + @@ -32,7 +33,7 @@ - + + - - - - - - + + - - - + + + @@ -57,51 +54,46 @@ // and pass the tree options as an argument to the fancytree() function: $("#tree").fancytree({ autoCollapse: true, - icons: false, - renderNode: function(event, data){ - if(data.node.getLevel() === 1){ - $(data.node.span).addClass("ui-accordion-header"); + clickFolderMode: 3, + icon: function(event, data) { + return !data.node.isTopLevel(); + }, + source: {url: "ajax-tree-products.json"}, + lazyLoad: function(event, data){ + data.result = {url: "ajax-sub2.json"}; + }, + keydown: function(event, data){ + switch( $.ui.fancytree.eventToString(data.originalEvent) ) { + case "return": + case "space": + data.node.toggleExpanded(); + break; } } }); + // For our demo: toggle auto-collapse mode: + $("input[name=autoCollapse]").on("change", function(e){ + $.ui.fancytree.getTree().options.autoCollapse = $(this).is(":checked"); + }); }); + -

Example: accordion

+

Example: Accordion

- This sample skin that mimics an accordion. + This sample adds some custom CSS to a standard Fancytree, to mimic an accordion + widget.
+
-
    -
  • Foo -
      -
    • Node foo 1 -
        -
      • Node foo 1.1 -
      • Node foo 1.2 -
      • Node foo 1.3 -
      -
    • Node foo 2 -
    • Node foo 3 -
    -
  • Bar -
      -
    • Node bar 1 -
    • Node bar 2 -
    • Node bar 3 -
    -
  • Baz -
      -
    • Node baz 1 -
    • Node baz 2 -
    • Node baz 3 -
    -
diff --git a/demo/sample-api.html b/demo/sample-api.html index ea4b3f17..848cb55e 100644 --- a/demo/sample-api.html +++ b/demo/sample-api.html @@ -4,17 +4,17 @@ Fancytree - Example - - + + - - + + - - - + + + - @@ -265,8 +270,11 @@

Fancytree API

- This example demonstrates the usage of some Fancytree and FancytreeNode - API functions. + Demonstrate some Fancytree and FancytreeNode API methods. +
+ See the API Tutorial + for details.
diff --git a/demo/sample-aria-treegrid-old.html b/demo/sample-aria-treegrid-old.html new file mode 100644 index 00000000..fb2e5044 --- /dev/null +++ b/demo/sample-aria-treegrid-old.html @@ -0,0 +1,108 @@ + + + + + Fancytree - WAI-ARIA Tree Grid + + + + + + + + + + + + + + + + + + + + + +

Example: WAI-ARIA Tree Grid

+
+

+ This Fancytree Tree Grid has + WAI-ARIA + enabled.
+ Note: please + provide feedback + if you have suggestions for improvement. +

+

+ See also the ARIA Tree View example. +

+
+ + +
+ + + + + + + + + + + + + +
Item Index
+ + +
+ + +
+ + + + + diff --git a/demo/sample-aria-treegrid.html b/demo/sample-aria-treegrid.html new file mode 100644 index 00000000..3659dd77 --- /dev/null +++ b/demo/sample-aria-treegrid.html @@ -0,0 +1,234 @@ + + + + + Fancytree - WAI-ARIA Tree Grid + + + + + + + + + + + + + + + + + + + + + + + + + + +

Example: WAI-ARIA Tree Grid - Prototype

+
+

+ This Fancytree Tree Grid has uses a prototypical extension to + explore ARIA support as discussed here: + + + w3c/aria-practices/treegrid/examples/treegrid/treegrid-1.html + + + See also the Documentation. + + See also + here + and + here + + +

+ See also the ARIA Tree View example. +

+
+ +
+
+ + (Please reload page for changes to take effect.) +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
Select Item In Stock Favorite Details Qty Link Drop
>link
+ + +
+ + +

+

+
+ + + + + diff --git a/demo/sample-aria.html b/demo/sample-aria.html new file mode 100644 index 00000000..0dcbc172 --- /dev/null +++ b/demo/sample-aria.html @@ -0,0 +1,84 @@ + + + + + Fancytree - WAI-ARIA Tree View + + + + + + + + + + + + + + + + + + + + +

Example: WAI-ARIA Tree View

+
+

+ This Fancytree Tree View has + WAI-ARIA + enabled.
+ Note: please + provide feedback + if you have suggestions for improvement. +

+

+ See also the ARIA Tree Grid example. +

+
+ +
+ + +
+
+ + +
+ + +
+ + + + + diff --git a/demo/sample-configurator.html b/demo/sample-configurator.html index e0c2bb13..91c8698e 100644 --- a/demo/sample-configurator.html +++ b/demo/sample-configurator.html @@ -4,28 +4,52 @@ Fancytree - Configurator - - + - - - + + + - + + + + - - - + + @@ -35,59 +59,64 @@ var OPT_LIST = [ {name: "activeVisible", value: true, - hint: "Make sure, active nodes are visible (expanded)."}, - {name: "aria", value: false, - hint: "Enable WAI-ARIA support."}, + hint: "Make sure, active nodes are visible (expanded)"}, + {name: "aria", value: true, + hint: "Enable WAI-ARIA support"}, {name: "autoActivate", value: true, - hint: "Automatically activate a node when it is focused (using keys)."}, - {name: "autoCollapse", value: false, - hint: "Automatically collapse all siblings, when a node is expanded."}, + hint: "Automatically activate a node when it is focused using keyboard"}, + {name: "autoCollapse", value: false, + hint: "Automatically collapse all siblings, when a node is expanded"}, {name: "autoScroll", value: false, - hint: "Automatically scroll nodes into visible area."}, -// {name: "autoFocus", value: true, -// hint: "Set focus to first child, when expanding or lazy-loading."}, + hint: "Automatically scroll nodes into visible area"}, {name: "clickFolderMode", value: [{name: "activate", value: 1}, {name: "expand", value: 2}, {name: "activate and expand", value: 3}, {name: "activate (dblclick expands)", value: 4, selected: true}], hint: "1:activate, 2:expand, 3:activate and expand, 4:activate (dblclick expands)"}, {name: "checkbox", value: false, - hint: "Show checkboxes."}, + hint: "Show check boxes"}, + {name: "checkboxAutoHide", value: false, + hint: "Display check boxes on hover only"}, {name: "debugLevel", value: [{name: "quiet", value: 0}, - {name: "normal", value: 1}, - {name: "debug", value: 2}], - hint: "0:quiet, 1:normal, 2:debug"}, + {name: "errors", value: 1}, + {name: "warnings", value: 2}, + {name: "infos", value: 3}, + {name: "debug", value: 4}], + hint: "0:quiet, 1:errors, 2:warnings, 3:infos, 4:debug"}, {name: "disabled", value: false, hint: "Disable control"}, -// {name: "fx", value: null, -// hint: 'Animations, e.g. null or { height: "toggle", duration: 200 }'}, + {name: "focusOnSelect", value: false, + hint: "Set focus when node is checked by a mouse click"}, + {name: "escapeTitles", value: false, + hint: "Escape `node.title` content for display"}, {name: "generateIds", value: false, hint: "Generate id attributes like "}, {name: "idPrefix", value: "ft_", - hint: "Used to generate node id�s like ."}, - {name: "icons", value: true, - hint: "Display node icons."}, + hint: "Used to generate node id´s like "}, + {name: "icon", value: true, + hint: "Display node icons"}, {name: "keyboard", value: true, - hint: "Support keyboard navigation."}, + hint: "Support keyboard navigation"}, {name: "keyPathSeparator", value: "/", - hint: "Used by node.getKeyPath() and tree.loadKeyPath()."}, + hint: "Used by node.getKeyPath() and tree.loadKeyPath()"}, {name: "minExpandLevel", value: 1, hint: "1: root node is not collapsible"}, -// {name: "nolink", value: false, -// hint: "Use instead of tags for all nodes"}, -// {name: "persist", value: false, -// hint: "Persist expand-status to a cookie"}, + {name: "quicksearch", value: false, + hint: "Navigate to next node by typing the first letters"}, + {name: "rtl", value: false, + hint: "Enable RTL (right-to-left) mode"}, {name: "selectMode", value: [{name: "single", value: 1}, {name: "multi", value: 2, selected: true}, {name: "multi-hier", value: 3}], hint: "1:single, 2:multi, 3:multi-hier"}, -// {name: "strings", value: {loading: "Loading…", -// loadError: "Load error!"}, -// hint: "Translations"}, - {name: "tabbable", value: true, + {name: "tabindex", value: [{name: "tabbable", value: "0"}, + {name: "focusable", value: "-1"}, + {name: "off", value: ""}], hint: "Whole tree behaves as one single control"}, {name: "titlesTabbable", value: false, - hint: "Node titles can receive keyboard focus"} + hint: "Node titles can receive keyboard focus"}, + {name: "tooltip", value: false, + hint: "Use title as tooltip (also a callback could be specified)"} ]; @@ -103,6 +132,7 @@ data.result = {url: "ajax-sub2.json"} } }); + // Attach configurator plugin $("#tree").configurator({ pluginName: "fancytree", optionTarget: "div#options", @@ -127,20 +157,20 @@ -

Fancytree option configurator

+

Fancytree Option Configurator

- This sample shows, how titles could displayed as aligned columns. + Live preview of some Fancytree options.
-

- TODO: this is work in progress. -

-

-
- Fancytree options +
+ Sample Fancytree +
+
+
+ Widget Options
OPTIONS

diff --git a/demo/sample-default.html b/demo/sample-default.html index cb30a50d..0b5cfae8 100644 --- a/demo/sample-default.html +++ b/demo/sample-default.html @@ -4,17 +4,17 @@ Fancytree - Example - - + - - + + + - - - + + + - + + - - + + - - - + + + + + +

Example: Events

+

+

+
  • jQuery links @@ -207,9 +238,6 @@

    Example: Events

    Selected node list: -
    Focused node: -
    - - -

    Example: 'glyph' extension with bootstrap theme

    -
    -

    - The 'glyph' extension adds icon-... classes to the - node's span tags, so scalable vector icons as provided by - Font Awesome or Bootstrap Glyphicons can be used. -

    -

    - The theme defines some colors from bootstrap. To change it, run - grunt dev and edit the - LESS definition. -

    -

    - Status: alpha -

    -
    - -
    - Font size: -
    -
    - -

    - - -

    -
    -
    - - -
    - - - - - diff --git a/demo/sample-ext-childcounter.html b/demo/sample-ext-childcounter.html index cd56c332..a57f026f 100644 --- a/demo/sample-ext-childcounter.html +++ b/demo/sample-ext-childcounter.html @@ -4,43 +4,22 @@ Fancytree - Example: childcounter - - - + + + - - - + + + - - - + + + @@ -58,6 +37,10 @@ }, lazyLoad: function(event, data) { data.result = {url: "ajax-sub2.json"} + }, + loadChildren: function(event, data) { + // update node and parent counters after lazy loading + data.node.updateCounters(); } }); }); @@ -80,7 +63,7 @@

    Example: 'childcounter' extension

    Click the [View source code] link below, to see how an extension is used.

    - Status: pre-alpha + Status: beta

diff --git a/demo/sample-ext-clones.html b/demo/sample-ext-clones.html index 41b2234e..b5adc786 100644 --- a/demo/sample-ext-clones.html +++ b/demo/sample-ext-clones.html @@ -3,23 +3,19 @@ Fancytree - Example: ext-clones - - - - - - + + + + + + - - - + + + @@ -56,7 +81,10 @@ extensions: ["columnview"], checkbox: true, source: { - url: "ajax-tree-plain.json" + url: "ajax-tree-products.json" + }, + init: function(event, data) { + data.tree.findFirst("C64").setActive(); }, lazyLoad: function(event, data) { data.result = {url: "ajax-sub2.json"}; @@ -84,13 +112,15 @@ $("td#tags").on("click", "button.close", function(e){ // Bind live handler that deselects the node when user clicks 'x' of a tag var key = $(e.target).parent().data("key"), - node = $(":ui-fancytree").fancytree("getNodeByKey", key); + node = $.ui.fancytree.getTree().getNodeByKey(key); + node.setSelected(false); return false; // do not bubble and trigger span click }).on("click", "span.selTag", function(e){ // Bind live handler that activates the node, when tag is clicked var key = $(e.target).data("key"), - node = $(":ui-fancytree").fancytree("getNodeByKey", key); + node = $.ui.fancytree.getTree().getNodeByKey(key); + node.setActive(); }); }); @@ -100,11 +130,13 @@

Example: 'columnview' extension

- This sample shows, how titles could displayed as aligned columns - (as known from Apple Macintosh / OSX). + Display tree data in a column view as known from Apple Macintosh / OSX.

- Status: pre-alpha + Status: experimental. + Details: + ext-columnview.

@@ -114,15 +146,16 @@

Example: 'columnview' extension

- - - + + + - + + diff --git a/demo/sample-ext-dnd.html b/demo/sample-ext-dnd.html index 5b1bb828..9dd244cf 100644 --- a/demo/sample-ext-dnd.html +++ b/demo/sample-ext-dnd.html @@ -3,18 +3,20 @@ Fancytree - Example: Drag'n'drop - - - - - + + + + + + + - - - + + + + + + + +

Example: 'dnd5' extension

+
+

+ Native Drag-and-Drop support: +

+
    +
  • + Support drag and drop of tree nodes (inside the same or between different trees). +
  • +
  • + Support drag and drop between different frames, browser tabs, windows, or desktop. +
  • +
  • Requires IE 11 or later.
  • +
+

+ Status: production. + Details: + ext-dnd5. +

+
+ +
+ + +
+ +

+ + Drag me +
+ + Trashcan +

+ +
+
+ + +
+ + + + + diff --git a/demo/sample-ext-edit.html b/demo/sample-ext-edit.html index c2175f6f..19504a0b 100644 --- a/demo/sample-ext-edit.html +++ b/demo/sample-ext-edit.html @@ -4,74 +4,144 @@ Fancytree - Editable Nodes - - + + - - - + + + - - - + + + - + + + + +

Example: 'edit' extension

- Inline editing for tree nodes. + Rename or create nodes using inline editing.

- Edit the node titles with
- - dblclick
- - Shift + click
- - [F2]
- - or [Enter] (on Mac only)
+ Edit the node titles with `dblclick`, `Shift + click` [F2], or [Enter] (on Mac only). + Also a `slow click` (click again into already active node).

- Status: pre-alpha. + Status: production. + Details: + ext-edit.

+
+ +
+

+


Example: 'filter' extension

- Dynamic filter support. + Allow to dimm or hide nodes based on a matching expression.

- Status: alpha + Status: production. + Details: + ext-filter.

@@ -100,20 +154,66 @@

Example: 'filter' extension

- +

-

+ +

+ Options +
+ + + +
+ + + + +
+ + +
+ +

+ +
diff --git a/demo/sample-ext-fixed.html b/demo/sample-ext-fixed.html new file mode 100644 index 00000000..2ce8e49e --- /dev/null +++ b/demo/sample-ext-fixed.html @@ -0,0 +1,387 @@ + + + + + Fancytree - Example: Fixed Headers + + + + + + + + + + + + + + + + + + + + + + + + + + + +

Example: 'fixed' extension

+
+

+ Provide fixed table headers and columns for the table extension. +

+

+ Status: experimental / work in progress. + Details: + ext-fixed. +

+
+ +
+ + + +
+ +

+

+ +
+
1 2 3
? ? ?
? ? ?
Selected nodes:
preview
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
#TitleKeyHead R1 C5Head R1 C6Head R1 C7Head R1 C8Head R1 C9Head R1 C10Head R1 C11Head R1 C12Head R1 C13Head R1 C14Head R1 C15Head R1 C16Head R1 C17Head R1 C18Head R1 C19Head R1 C20Head R1 C21Head R1 C22Head R1 C23Head R1 C24Head R1 C25Head R1 C26Head R1 C27Head R1 C28Head R1 C29Head R1 C30Head R1 C31Head R1 C32Head R1 C33Head R1 C34
Head R2 C5Head R2 C6Head R2 C7Head R2 C8Head R2 C9Head R2 C10Head R2 C11Head R2 C12Head R2 C13Head R2 C14Head R2 C15Head R2 C16Head R2 C17Head R2 C18Head R2 C19Head R2 C20Head R2 C21Head R2 C22Head R2 C23Head R2 C24Head R2 C25Head R2 C26Head R2 C27Head R2 C28Head R2 C29Head R2 C30Head R2 C31Head R2 C32Head R2 C33Head R2 C34
+
+ + +
+ +

+

+
2016-12-23
+
13 Sek.
+
+

+ + + + diff --git a/demo/sample-ext-awesome.html b/demo/sample-ext-glyph-awesome3.html similarity index 54% rename from demo/sample-ext-awesome.html rename to demo/sample-ext-glyph-awesome3.html index cc3df350..917416b9 100644 --- a/demo/sample-ext-awesome.html +++ b/demo/sample-ext-glyph-awesome3.html @@ -2,30 +2,29 @@ - Fancytree - Example: Awesome Extension + Fancytree - Example: Font Awesome 3 - - - + + - - - + + + + + + + - - - - + + + - + @@ -37,22 +36,8 @@ checkbox: true, selectMode: 3, glyph: { - map: { - doc: "icon-file-alt", - docOpen: "icon-file-alt", - checkbox: "icon-check-empty", - checkboxSelected: "icon-check", - checkboxUnknown: "icon-check icon-muted", - error: "icon-exclamation-sign", - expanderClosed: "icon-caret-right", - expanderLazy: "icon-angle-right", - // expanderLazy: "icon-refresh icon-spin", - expanderOpen: "icon-caret-down", - folder: "icon-folder-close-alt", - folderOpen: "icon-folder-open-alt", - loading: "icon-refresh icon-spin" - // loading: "icon-spinner icon-spin" - } + preset: "awesome3", + map: {} }, // source: {url: "ajax-tree-plain.json", debugDelay: 1000}, source: {url: "ajax-tree-taxonomy.json", debugDelay: 1000}, @@ -78,23 +63,6 @@ change: setSize, slide: setSize }).slider("value", 10); -/* - $("#skinswitcher") - // .skinswitcher("option", "base", "../../src/") - .skinswitcher("addChoices", [ - {name: "Awesome", value: "awesome", href: "skin-awesome/ui.fancytree.css"} - ]) - .skinswitcher("change", "awesome"); -*/ -/* - addSampleButton({ - label: "Generate elements", - code: function(){ - $("#tree").fancytree("getTree").generateInput(); - $("#tree2").fancytree("getTree").generateInput(); - } - }); -*/ }); @@ -114,15 +82,14 @@

Example: 'glyph' extension with awesome theme

Status: pre-alpha

- +
+ Font size:
+
+
diff --git a/demo/sample-ext-glyph-awesome4.html b/demo/sample-ext-glyph-awesome4.html new file mode 100644 index 00000000..1c031f49 --- /dev/null +++ b/demo/sample-ext-glyph-awesome4.html @@ -0,0 +1,105 @@ + + + + + Fancytree - Example: Font Awesome 4 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

Example: 'glyph' extension with font-awesome theme

+
+

+ The 'glyph' extension adds fa-... classes to the + node's span tags, so scalable vector icons as provided by + Font Awesome + can be used.
+ See also + ext-glyph. +

+
+ +
+ Font size: +
+
+
+
+ + +

+

+
+ + + + + diff --git a/demo/sample-ext-glyph-awesome5.html b/demo/sample-ext-glyph-awesome5.html new file mode 100644 index 00000000..a1593578 --- /dev/null +++ b/demo/sample-ext-glyph-awesome5.html @@ -0,0 +1,116 @@ + + + + + Fancytree - Example: Font Awesome 5 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

Example: 'glyph' extension with 'font-awesome' theme and Font Awesome 5 Web Font/CSS icons

+
+

+ The 'glyph' extension adds fa-... classes to the + node's span tags, so scalable vector icons as provided by + Font Awesome + can be used.
+ See also + ext-glyph. +

+
+ +
+ Font size: +
+
+
+
+ + +

+

+
+ + + + + diff --git a/demo/sample-ext-glyph-bootstrap3.html b/demo/sample-ext-glyph-bootstrap3.html new file mode 100644 index 00000000..5338a185 --- /dev/null +++ b/demo/sample-ext-glyph-bootstrap3.html @@ -0,0 +1,264 @@ + + + + Fancytree - Example: Glyph Extension / Bootstrap Theme + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+

Example: Bootstrap theme

+
+
+

+ The 'glyph' extension adds icon-... classes to the + node's span tags, so scalable vector icons as provided by + Font Awesome + or Bootstrap Glyphicons + can be used. +

+

+ The theme defines some colors from bootstrap. To change it, run + grunt dev + and edit the + LESS definition. +

+

+ Status: beta
+ See also + ext-glyph. +

+
+
+ +

+ Font size: + pt +

+ +

Plain tree

+ +

+ Add container class:
+ + +

+ +
+
+ Taxonomy +
+
+
+ +
+ +

Table tree

+ +

+ Add table class:
+ + + + + + +

+ + + + + + + + + + + + + + + + +
Classification Folder
+ + + +
+ + + + + diff --git a/demo/sample-ext-glyph-material.html b/demo/sample-ext-glyph-material.html new file mode 100644 index 00000000..8a142c23 --- /dev/null +++ b/demo/sample-ext-glyph-material.html @@ -0,0 +1,212 @@ + + + + + Fancytree - Example: ext-glyph - material design + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+

Example: 'glyph' extension with skin-material theme and material icons

+
+
+ The 'glyph' extension adds ligature font icons to the + node's span tags, so scalable vector icons as provided by + Material Design + can be used.
+ See also + ext-glyph. +
+ +
+ +
+
+ + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ItemAuthorYearUnit price
+ + +

+

+
+ + + + + diff --git a/demo/sample-ext-glyph-svg.html b/demo/sample-ext-glyph-svg.html new file mode 100644 index 00000000..27f1d62a --- /dev/null +++ b/demo/sample-ext-glyph-svg.html @@ -0,0 +1,205 @@ + + + + + Fancytree - Example: SVG Icons + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

Example: 'glyph' extension with 'font-awesome' theme and Font Awesome 5 SVG/JS icons

+
+
    +
  • + The 'glyph' extension adds fa-... classes to the + node's span tags, so scalable vector icons as provided by + Font Awesome 5+ + can be used.
    + See also + ext-glyph. +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ Font size: +
+
+
+
+ + +

+

+
+ + + + + diff --git a/demo/sample-ext-grid.html b/demo/sample-ext-grid.html new file mode 100644 index 00000000..ef747163 --- /dev/null +++ b/demo/sample-ext-grid.html @@ -0,0 +1,445 @@ + + + + + Fancytree - Example: Viewport + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+

Example: Viewport Feature

+

+ Restrict rendering of DOM elements to sub range. +

+

+ This example loads a data model with + about 100k nodes + completely into the client browser, however only the rows visible in the + current viewport are rendered. +

+

+ NOTE: See also an + example with scrollbar support. +

+

+ Status: experimental. + Details: + ext-grid. +

+
+ + +
+ + + + + + + +
+ + + + +
+ + (Please reload page for changes to take effect.) +
+ +
+ + + +
+
+ +
+ + + + + + + + + + + + +
# RowIdx Order
+
+ + + + +
+ + + diff --git a/demo/sample-ext-logger.html b/demo/sample-ext-logger.html new file mode 100644 index 00000000..9c2938f2 --- /dev/null +++ b/demo/sample-ext-logger.html @@ -0,0 +1,96 @@ + + + + + Fancytree - Example: Logger + + + + + + + + + + + + + + + + + + + + + + +

Example: 'logger' extension

+
+

+ Log misc. events (useful for debugging). +

+

+ Hit F12 to open the browser's console. +

+
+ +
+ +
+ +

+

+ + +
+
+ + +
+ + + + + diff --git a/demo/sample-ext-menu-deprecated.html b/demo/sample-ext-menu-deprecated.html new file mode 100644 index 00000000..bdf15dbe --- /dev/null +++ b/demo/sample-ext-menu-deprecated.html @@ -0,0 +1,123 @@ + + + + + Fancytree - Example: Menu + + + + + + + + + + + + + + + + + + + + + + + +

DEPRECATED: 'menu' extension

+
+

+ Utilize the standard jQuery menu plugin + http://api.jqueryui.com/menu/ (requires jQueryUI 1.9+). +

+

+ Status: DEPRECATED!
+ See explanation and alternatives. +

+
+
+ +
+ +
+
+ + + + + +
+ + + + + diff --git a/demo/sample-ext-menu.html b/demo/sample-ext-menu.html index 27164d8c..6587b28f 100644 --- a/demo/sample-ext-menu.html +++ b/demo/sample-ext-menu.html @@ -4,122 +4,57 @@ Fancytree - Example: Menu - - - - - - - - - + + - - - + + + - -

Example: 'menu' extension

- Utilize the standard jQuery menu plugin - http://api.jqueryui.com/menu/ (requires jQueryUI 1.9+). -

-

- Status: deprecated!
- See examples in the 'Tweaks' and '3rd party' category instead. + Fancytree does not include a native context menu extension.
+ A context menu is typically used for multiple elements of a web + application and the appearance should be consistent everywhere. +

+ However, using an existing context menu plugin is easy. + It normally only requires to call $.ui.fancytree.getNode(element) + in the respective callback handler and then trigger the desired + operation. +

+ Examples of existing third party context menus include: +

    +
  • jQuery contextMenu
    + A popular, free context menu and polyfil + (project site).
    + See here for a Fancytree demo. + +
  • jQuery contextMenu (using a Fancytree extension)
    + The ext-contextMenu extension is a thin wrapper that + integrates the above plugin into Fancytree ( + demo ). + +
  • jquery.ui-contextmenu
    + A free jQuery plugin that turns a jQuery UI Menu + widget into a context menu + (project site).
    + See here for a Fancytree demo. + +
  • Plenty of other menus …
    + JeeGooContext and + more… +

-
- -
- -
-
- - - - -
- - - diff --git a/demo/sample-ext-multi.html b/demo/sample-ext-multi.html new file mode 100644 index 00000000..b0daf540 --- /dev/null +++ b/demo/sample-ext-multi.html @@ -0,0 +1,125 @@ + + + + + Fancytree - Multi-Select + + + + + + + + + + + + + + + + + + + + + + + + + + +

Example: 'multi' extension

+
+

+ Select node ranges using mouse and keyboard and + Ctrl, Command, Shift modifiers. +

+

+ Edit the node titles with `[Shift] + click` '[Shift] + [Down]', .... +

+

+ Status: experimental. + Details: + ext-multi. +

+
+
+ +
+
+ + + + + + +
+ +
+
+

Active: -

+ + +

+

+
+ + + + + diff --git a/demo/sample-ext-persist.html b/demo/sample-ext-persist.html index f5965960..312f1748 100644 --- a/demo/sample-ext-persist.html +++ b/demo/sample-ext-persist.html @@ -4,20 +4,22 @@ Fancytree - Example: Persist - - + + + + - - - - - + + + - - - + + + - - - + + + + - - - + + + - - + + + + + + + + + - - - - - - - - - + + + - + - + + + + + + + + +

Example: 'wide' extension

+
+

+ Stretch the selection bar to 100% of the container width + (details...). +

+
+
+ +
+

+

+ +

ext-wide: on

+
+
+ +

ext-wide: off

+
+
+ + +
+ + + + + diff --git a/demo/sample-form.html b/demo/sample-form.html index 1181bbf8..35b8c02c 100644 --- a/demo/sample-form.html +++ b/demo/sample-form.html @@ -4,18 +4,18 @@ Fancytree - Example: Forms - - - + + + - - + + - - - + + + + + + + + + + + + +

Testcase for issue #XXX

+ +
+
+
+ +
+
+

+

+
+
+
+ + +
+ + + + + diff --git a/demo/sample-load-xml.html b/demo/sample-load-xml.html new file mode 100644 index 00000000..eb40ca86 --- /dev/null +++ b/demo/sample-load-xml.html @@ -0,0 +1,100 @@ + + + + + Fancytree - Example + + + + + + + + + + + + + + + + + + +

Example: Load XML

+
+ This tree uses a custom XML parser. +
+
+ +
+
+
+ + +
+ + + + + diff --git a/demo/sample-multi-dnd.html b/demo/sample-multi-dnd.html new file mode 100644 index 00000000..39f3d33f --- /dev/null +++ b/demo/sample-multi-dnd.html @@ -0,0 +1,214 @@ + + + + + Test D'n'D - Fancytree + + + + + + + + + + + + + + + + + + + + +

Example: extended drag'n'drop sample

+
+ This sample shows how to +
    +
  • implement drag'n'drop with multiple selected nodes +
  • allow modifier keys Ctrl or Alt to force copy + instead of move operations +
+
+

+ This sample uses the jQuery UI based 'ext-dnd' extension, which is now deprecated.
+ See also the ext-dnd5 sample. +

+
+ +
+ +

+ Standard tree: +

+
+ +

+ Droppable. +

+ + +
+ + + + + diff --git a/demo/sample-multi-dnd5.html b/demo/sample-multi-dnd5.html new file mode 100644 index 00000000..21d3569f --- /dev/null +++ b/demo/sample-multi-dnd5.html @@ -0,0 +1,176 @@ + + + + + Test D'n'D - Fancytree + + + + + + + + + + + + + + + + + + + + + + +

Example: extended drag'n'drop sample

+
+ This sample shows how to +
    +
  • implement drag'n'drop with multiple selected nodes +
  • allow modifier keys Ctrl or Alt to force copy + instead of move operations +
+
+
+ +
+ +

+ Standard tree: +

+
+
+ +

+ Droppable. + +

+ + +
+ + + + + diff --git a/demo/sample-multi-ext.html b/demo/sample-multi-ext.html index 40f2d482..6779c96f 100644 --- a/demo/sample-multi-ext.html +++ b/demo/sample-multi-ext.html @@ -1,325 +1,444 @@ - - - Test Editable Grid - Fancytree + + + Multiple Extensions - Fancytree - - - + - - - + + + + + - - - - - - - - - - - - - + + + + + + - + - - + /* + * Context menu (https://github.com/mar10/jquery-ui-contextmenu) + */ + $("#tree").contextmenu({ + delegate: "span.fancytree-node", + menu: [ + { + title: "Edit [F2]", + cmd: "rename", + uiIcon: "ui-icon-pencil", + }, + { + title: "Delete [Del]", + cmd: "remove", + uiIcon: "ui-icon-trash", + }, + { title: "----" }, + { + title: "New sibling [Ctrl+N]", + cmd: "addSibling", + uiIcon: "ui-icon-plus", + }, + { + title: "New child [Ctrl+Shift+N]", + cmd: "addChild", + uiIcon: "ui-icon-arrowreturn-1-e", + }, + { title: "----" }, + { + title: "Cut Ctrl+X", + cmd: "cut", + uiIcon: "ui-icon-scissors", + }, + { + title: "Copy Ctrl-C", + cmd: "copy", + uiIcon: "ui-icon-copy", + }, + { + title: "Paste as childCtrl+V", + cmd: "paste", + uiIcon: "ui-icon-clipboard", + disabled: true, + }, + ], + beforeOpen: function(event, ui) { + var node = $.ui.fancytree.getNode(ui.target); + $("#tree").contextmenu( + "enableEntry", + "paste", + !!CLIPBOARD + ); + node.setActive(); + }, + select: function(event, ui) { + var that = this; + // delay the event, so the menu can close and the click event does + // not interfere with the edit control + setTimeout(function() { + $(that).trigger("nodeCommand", { cmd: ui.cmd }); + }, 100); + }, + }); + }); + + - -

Example: treegrid with keyboard navigation, DnD, and editing capabilites

-
- Bringing it all together: this sample combines different extensions and - custom events to implement an editable tree: -
    -
  • 'ext-dnd' to re-order nodes using drag-and-drop.
  • -
  • 'ext-table' + 'ext-gridnav' to implement a treegrid.
    - Try UP / DOWN / LEFT / RIGHT, TAB, Shift+TAB - to navigate between grid cells. Note that embedded input controls - remain functional. -
  • -
  • 'ext-edit': inline editing.
    - Try F2 to rename a node,
    - Ctrl+C, Ctrl+X, Ctrl+P for copy/paste,
    - Ctrl+N, Ctrl+Shift+N to add new nodes,
    - Ctrl+UP, Ctrl+DOWN, Ctrl+LEFT, Ctrl+RIGHTto move nodes around. -
  • -
  • 3rd-party contextmenu for additional edit commands
  • -
-
-
- -
+ +

+ Example: tree grid with keyboard navigation, DnD, and editing + capabilites +

+
+ Bringing it all together: this sample combines different extensions + and custom events to implement an editable tree grid: +
    +
  • 'ext-dnd5' to re-order nodes using drag-and-drop.
  • +
  • + 'ext-table' + 'ext-gridnav' to implement a tree grid.
    + Try UP / DOWN / LEFT / RIGHT, TAB, + Shift+TAB + to navigate between grid cells. Note that embedded input + controls remain functional. +
  • +
  • + 'ext-edit': inline editing.
    + Try F2 to rename a node.
    + Ctrl+N, Ctrl+Shift+N to add nodes + (Quick-enter: add new nodes until [enter] is hit on an empty + title). +
  • +
  • + Extended keyboard shortcuts:
    + Ctrl+C, Ctrl+X, Ctrl+V for + copy/paste,
    + Ctrl+UP, Ctrl+DOWN, + Ctrl+LEFT, Ctrl+RIGHT + to move nodes around and change indentation.
    + (On macOS, add Shift to the keystrokes.) +
  • +
  • + 3rd-party + contextmenu + for additional edit commands +
  • +
+
+
+ + +
-

Table Tree

-
- -
- - - - - - - - - - - - - - - - -
# Ed1 Ed2 Rb1 Rb2 Cb
+

Table Tree

+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
#Ed1Ed2Rb1Rb2Cb
+ + + + + +
- -
- - - - + +
+ + + + diff --git a/demo/sample-multiline.html b/demo/sample-multiline.html index 2477f716..cc8c7cf5 100644 --- a/demo/sample-multiline.html +++ b/demo/sample-multiline.html @@ -2,60 +2,89 @@ - Dynatree - Example + Fancytree - Example - - - + + + - - - + + - - - + + + + + + + + + + + + + + + +

Example: Playground

+ +
+ This tree is initialized from a data structure. +
+ +

+ + See the Pen Fancytree Demo + by Martin Wendt (@mar10) + on CodePen. +

+ + + + + + + + diff --git a/demo/sample-rtl.html b/demo/sample-rtl.html index 39c8f24a..2e4a5954 100644 --- a/demo/sample-rtl.html +++ b/demo/sample-rtl.html @@ -2,64 +2,52 @@ - Fancytree - Example: Table + Fancytree - Example: RTL - - + + - - - + + + - - - + + + - - + + + - - - - + + + - - - + + + @@ -88,16 +99,47 @@

Example: scrolling

- +

Standard tree:

+
-

Table tree:

+ +
+ + + + + + + + + + + + + + + + + + + + + +
# Key Like
+

Example: Selection and checkbox

- - +

Example: Selection and Checkboxes

- This tree has checkoxes and selectMode 1 (single-selection) enabled.
- A double-click handler selects a document node (not folders).
- A keydown handler selects on [space].
- The checkbox icons are replaced by radio buttons by adding - the 'fancytree-radio' class to the container.
- Note: the initialization data contains multiple selected nodes. This is - considered bad input data and not fixed automatically (only on - the first click). + Use different select modes for the tree and distinct nodes.

-
+ + + +

+ This tree has checkoxes and selectMode 1 (single-selection) enabled.
+ The checkbox icons are replaced by radio buttons by adding + the tree.checkbox: "radio" option. +

+
Active node: -
Selection: -
@@ -243,13 +226,11 @@

Example: Selection and checkbox

This tree has selectMode 2 (multi-selection) enabled.
- A single-click handler selects the node.
- A keydown handler selects on [space]. + Node `n3` uses the 'radiogroup' option and hides the checkbox.

- Select all - - Deselect all - - Toggle select + Select all - + Deselect all

Selected keys: -
@@ -259,26 +240,17 @@

Example: Selection and checkbox

This tree has checkoxes and selectMode 3 (hierarchical multi-selection) enabled.
- A double-click handler selects the node.
- A keydown handler selects on [space]. + Node `n3` features different variations of the unselectable mode. +

+

+ Select all - + Deselect all - + Get selected

Selected keys: -
Selected root keys: -
-
Selected root nodes: -
- - - - -

- This tree has selectMode 2 (multi-selection) enabled, but no checkboxes.
- A single-click handler selects the node.
- A keydown handler selects on [space].
- A double-click handler expands documents.
- A onQuerySelect handler prevents selection of folders. -

-
-
Selected keys: -
+ diff --git a/demo/sample-source.html b/demo/sample-source.html index f45ae6c7..33b5a926 100644 --- a/demo/sample-source.html +++ b/demo/sample-source.html @@ -4,114 +4,123 @@ Fancytree - Example - - + + - - + + - - - + + + + + + + -

Example: Initialization methods

+

Example: Initialization Methods

- This tree uses default options.
- It is initalized from a hidden <ul> element on this page. + Use different methods to initialize the tree (Ajax, embedded <ul>, + embedded JSON). +
+ Also distinct nodes contain custom data using `data="..."` attributes. +
+ See the LoadData Tutorial + for details.

-

- - - - +

-

Tree with UL markup

-
+ +

Load from embedded <UL> markup:

+
  • simple node (no explicit id, so a default key is generated)
  • Define key and tooltip using 'id' and 'title' attributes @@ -152,20 +161,17 @@

    Example: Initialization methods

-

Tree with Ajax data

+

Load from Ajax data:

-

Tree with embedded JSON data

+

Load from embedded JSON data:

+ {"foo": "bazbaz", "children": [ - {"title": "cäsar€"}, - {"title": "abc"} - ] -
- -

Tree with programmatic dataAjax data

-
+ {"title": "node 1"}, + {"title": "node 2", "folder": true } + ]}
diff --git a/demo/sample-theming.html b/demo/sample-theming.html index ee3d5447..1576df61 100644 --- a/demo/sample-theming.html +++ b/demo/sample-theming.html @@ -4,25 +4,23 @@ Fancytree - Example: Theming - - - + + + - - - + - + - - + + - - - + + + @@ -41,6 +39,7 @@ $("#tree").fancytree({ // Image folder used for data.icon attribute. imagePath: "skin-custom/", + // icon: false, renderNode: function(event, data) { // Optionally tweak data.node.span var node = data.node; @@ -57,33 +56,51 @@ } } }); + }); + + + +

Example: Theming

- Includes a custom CSS after the standard CSS to override theming.
+ Include a custom CSS after the standard CSS to override theming.
Some nodes have their data.addClass attribute set.
Finally, the last two nodes use the data.icon attribute. +
+ See the Theming Tutorial + for details.

+

+

  • Standard nodes, modified by extra CSS rules
      -
    • Sub-item 4.1 -
    • Sub-item 4.2 +
    • Sub-item 4.1 +
    • Sub-item 4.2
  • Override CSS style per node @@ -96,7 +113,7 @@

    Example: Theming

    • Node with standard CSS, but custom icon
    • Folder with standard CSS but custom icon -
    • 'iconclass' is directly added to the image <span>, so jQuery stock icons may be used +
    • 'icon' is directly added to the image <span>, so jQuery stock icons may be used
  • Use 'data-json' attribute to define additional data @@ -114,8 +131,6 @@

    Example: Theming


    -

    -

    Example: Default

    +
    + This tree uses default options.
    + It is initialized from a hidden <ul> element on this page. +
    +
    + +
    +
    + +
    + + +
    + + + + + diff --git a/demo/sample-types.html b/demo/sample-types.html new file mode 100644 index 00000000..5ec9e13f --- /dev/null +++ b/demo/sample-types.html @@ -0,0 +1,94 @@ + + + + + Fancytree - Example: Types + + + + + + + + + + + + + + + + + + + + + +

    Example: Node Types

    +
    + Used shared configuration per node type. +
    + See the Node Type Tutorial + for details. +
    +
    + +
    + +

    +

    + +
    +
    + + +
    + + + + + diff --git a/demo/sample-webservice.html b/demo/sample-webservice.html new file mode 100644 index 00000000..ae458da1 --- /dev/null +++ b/demo/sample-webservice.html @@ -0,0 +1,128 @@ + + + + + Fancytree - Webservice Example + + + + + + + + + + + + + + + + + + + + + +

    Example: Accessing external Webservices

    +
    + Load realtime data from the public + + ITIS + Web Services + + and convert the provided JSON data to Fancytree format. +
    + See the Taxonomy Browser + for a more sophisticated example. +
    +
    + +
    +

    +

    +
    + Integrated Taxonomic Information System +
    +
    +
    + + +
    + + + + + diff --git a/demo/sample-xxl.html b/demo/sample-xxl.html index f9ae6169..88342185 100644 --- a/demo/sample-xxl.html +++ b/demo/sample-xxl.html @@ -4,17 +4,17 @@ Fancytree - Example - - + + - - + + - - - + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +
    +

    Search GBIF

    +
    +
    + + + + + +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    KeyScientific NameAccording toAuthorPublished in
    + + + + +
    +
    +
    +
    + +
    +
    +
    + + +
    + +
    +
    +
    +
    + + + + + +
    +
    + +
    +
    + Disclaimer +
    +
    +

    + This site accesses data from external sources, namely the + Global Biodiversity Information Facility (GBIF) database. + There is no guarantee, that the display is correct, complete, or + permanently available. Please refer to those original sources for + authorative information. +

    +

    + Copyright © 2015 Martin Wendt. Created as a demo for + Fancytree. +

    +
    +
    + + diff --git a/demo/taxonomy-browser/index_itis.html b/demo/taxonomy-browser/index_itis.html new file mode 100644 index 00000000..90ed9499 --- /dev/null +++ b/demo/taxonomy-browser/index_itis.html @@ -0,0 +1,157 @@ + + + + + Taxonomy Browser + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +
    +

    Search ITIS

    +
    +
    + + + + + +
    + + + + + + + + + + + + + + +
    Scientific Name Common Names Match Type Author
    +
    +
    +
    + +
    +
    +
    + + +
    + +
    +
    +
    +
    + + + + +
    +
    + +
    + No data to display. +
    +
    +
    + wiki +
    +
    + ncbi +
    +
    +
    +
    + +
    +
    + Disclaimer +
    +
    +

    + This site accesses data from external sources, namely the + Integrated Taxonomic Information System (ITIS) database. + There is no guarantee, that the display is correct, complete, or + permanently available. Please refer to those original sources for + authorative information. +

    +

    + Copyright © 2015 Martin Wendt. Created as a demo for + Fancytree. +

    +
    +
    + + diff --git a/demo/taxonomy-browser/info-pane.tmpl.html b/demo/taxonomy-browser/info-pane.tmpl.html new file mode 100644 index 00000000..e65ee635 --- /dev/null +++ b/demo/taxonomy-browser/info-pane.tmpl.html @@ -0,0 +1,34 @@ + +

    + {{#if vernacularName}} {{vernacularName}} {{else}} {{canonicalName}} {{/if}} + {{#if profile.extinct}} Extinct {{/if}} + {{rank}} +

    + + + +
    +
    + {{> tmplDetails}} +
    +
    + {{> tmplMedia}} +
    +
    +
    + +Accessed via GBIF Secretariat: GBIF Backbone Taxonomy on {{now}}.
    +Open Authorative Information on GBIF. +
    diff --git a/demo/taxonomy-browser/media.tmpl.html b/demo/taxonomy-browser/media.tmpl.html new file mode 100644 index 00000000..a26cc0ba --- /dev/null +++ b/demo/taxonomy-browser/media.tmpl.html @@ -0,0 +1,34 @@ +
    + {{#if media}} + + + {{else}} + No media. + {{/if}} +
    \ No newline at end of file diff --git a/demo/taxonomy-browser/style.css b/demo/taxonomy-browser/style.css new file mode 100644 index 00000000..3799eb3b --- /dev/null +++ b/demo/taxonomy-browser/style.css @@ -0,0 +1,73 @@ +body { + padding: 0 1em; +} + +/* Style distinct search result columns */ +/* +#searchResultTree th:nth-child(1), +#searchResultTree td:nth-child(1) { +} +*/ + +/* Style tree container */ +#taxonTree { + padding-bottom: 20px; +} +#taxonTree .fancytree-container { + border: 1px solid #ccc; + border-radius: 3px; + max-height: 600px; + overflow: auto; +} + +#searchResultTree td > div.truncate { + max-height: 3em; + /*max-width: 100%;*/ + overflow: hidden; + text-overflow: ellipsis; + /*white-space: nowrap;*/ + /*color: red;*/ +} + +/* Set to different containers while requests are pending:; */ +.busy { + color: gray; + background-image: url("busy_bg_fff.png"); + background-repeat: repeat; +} + +/* Improve connection lines for bootstrap pane tabs */ +div.panel[role=tabpanel] { + border-top-width: 0; + border-top-right-radius: 0; + border-top-left-radius: 0; + padding: 3px; +} + +/* Fix alignment for bootstrap media list */ +.media-left { + display: inline-block; + vertical-align: top; +} +.media-body { + display: inline-block; +} + +/* Fix bootstrap media thumbnails */ +a.thumbnail { + overflow: hidden; +} +a.thumbnail:hover, +a.thumbnail:active { + text-decoration: none; +} + +/* Fix bootstrap media carousel */ +div.carousel-inner >.item >img { + max-height: 40em; + /*height: 50em;*/ +} +img.media-object { + max-height: 64px; + max-width: 64px; +} diff --git a/demo/taxonomy-browser/taxonomy-browser-itis.js b/demo/taxonomy-browser/taxonomy-browser-itis.js new file mode 100644 index 00000000..cf234d5f --- /dev/null +++ b/demo/taxonomy-browser/taxonomy-browser-itis.js @@ -0,0 +1,400 @@ +/*! + * Fancytree Taxonomy Browser + * + * Copyright (c) 2015, Martin Wendt (https://wwWendt.de) + * + * Released under the MIT license + * https://github.com/mar10/fancytree/wiki/LicenseInfo + * + * @version @VERSION + * @date @DATE + */ + +/* global Handlebars */ +/* eslint-disable no-console */ + +(function ($, window, document) { + "use strict"; + + /******************************************************************************* + * Private functions and variables + */ + + var taxonTree, + searchResultTree, + timerMap = {}, + tmplDetails, // = + USER_AGENT = "Fancytree Taxonomy Browser/1.0", + ITIS_URL = "//www.itis.gov/ITISWebService/jsonservice/", + glyphOpts = { + preset: "bootstrap3", + map: { + expanderClosed: "glyphicon glyphicon-menu-right", // glyphicon-plus-sign + expanderLazy: "glyphicon glyphicon-menu-right", // glyphicon-plus-sign + expanderOpen: "glyphicon glyphicon-menu-down", // glyphicon-collapse-down + }, + }; + + // Load and compile handlebar templates + + $.get("details.tmpl", function (data) { + tmplDetails = Handlebars.compile(data); + }); + + /** Update UI elements according to current status + */ + function updateControls() { + var query = $.trim($("input[name=query]").val()); + + $("#btnPin").attr("disabled", !taxonTree.getActiveNode()); + $("#btnUnpin") + .attr("disabled", !taxonTree.isFilterActive()) + .toggleClass("btn-success", taxonTree.isFilterActive()); + $("#btnResetSearch").attr("disabled", query.length === 0); + $("#btnSearch").attr("disabled", query.length < 2); + } + + /** + * Invoke callback after `ms` milliseconds. + * Any pending action of this type is cancelled before. + */ + function _delay(tag, ms, callback) { + /*jshint -W040:true */ + var self = this; + + tag = "" + (tag || "default"); + if (timerMap[tag] != null) { + clearTimeout(timerMap[tag]); + delete timerMap[tag]; + // console.log("Cancel timer '" + tag + "'"); + } + if (ms == null || callback == null) { + return; + } + // console.log("Start timer '" + tag + "'"); + timerMap[tag] = setTimeout(function () { + // console.log("Execute timer '" + tag + "'"); + callback.call(self); + }, +ms); + } + + /** + */ + function _callItis(cmd, data) { + return $.ajax({ + url: ITIS_URL + cmd, + data: $.extend( + { + jsonp: "itis_data", + }, + data + ), + cache: true, + headers: { "Api-User-Agent": USER_AGENT }, + jsonpCallback: "itis_data", + dataType: "jsonp", + }); + } + + /** + */ + // function countMatches(query) { + // $("#tsnDetails").text("Loading TSN " + tsn + "..."); + // _callItis("getAnyMatchCount", { + // srchKey: query + // }).done(function(result){ + // console.log("updateTsnDetails", result); + // $("#tsnDetails").html(tmplDetails(result)); + // updateControls(); + // }); + // } + + /** + */ + function updateTsnDetails(tsn) { + $("#tsnDetails").addClass("busy"); + // $("#tsnDetails").text("Loading TSN " + tsn + "..."); + $.bbq.pushState({ tsn: tsn }); + + _callItis("getFullRecordFromTSN", { + tsn: tsn, + }).done(function (result) { + console.log("updateTsnDetails", result); + $("#tsnDetails").html(tmplDetails(result)).removeClass("busy"); + + updateControls(); + }); + } + + /** + */ + function updateBreadcrumb(tsn, loadTreeNodes) { + // var $ol = $("ol.breadcrumb").text("..."); + var $ol = $("ol.breadcrumb").addClass("busy"); + _callItis("getFullHierarchyFromTSN", { + tsn: tsn, + }).done(function (result) { + console.log("updateBreadcrumb", result); + // Convert to simpler format + var list = []; + // Display as
      list (for Bootstrap breadcrumbs) + $ol.empty().removeClass("busy"); + $.each(result.hierarchyList, function (i, o) { + if (o.parentTsn === tsn) { + return; + } // skip direct children + list.push(o.tsn); + if (o.tsn === tsn) { + $ol.append( + $("
    1. ").append( + $("", { + text: o.taxonName, + title: o.rankName, + }) + ) + ); + } else { + $ol.append( + $("
    2. ").append( + $("", { + href: "#tsn=" + o.tsn, + text: o.taxonName, + title: o.rankName, + }) + ) + ); + } + }); + if (loadTreeNodes) { + console.log("updateBreadcrumb - loadKeyPath", list); + taxonTree.loadKeyPath( + "/" + list.join("/"), + function (node, status) { + // console.log("... updateBreadcrumb - loadKeyPath", status, node); + switch (status) { + case "loaded": + node.makeVisible(); + break; + case "ok": + node.setActive(); + break; + } + } + ); + } + }); + } + + /** + */ + function search(query) { + query = $.trim(query); + console.log("searching for '" + query + "'..."); + // NOTE: + // It seems that ITIS searches don't work with jsonp (always return valid + // but empty result sets). + // When debugging, make sure cross domain requests are allowed. + searchResultTree + .reload({ + url: ITIS_URL + "searchForAnyMatchPaged", + data: { + // jsonp: "itis_data", + srchKey: query, + pageSize: 10, + pageNum: 1, + ascend: false, + }, + cache: true, + // jsonpCallback: "itis_data", + // dataType: "jsonp" + }) + .done(function (result) { + // console.log("search returned", result); + // result.anyMatchList + updateControls(); + }); + } + + /******************************************************************************* + * Pageload Handler + */ + + $(function () { + $("#taxonTree").fancytree({ + extensions: ["filter", "glyph", "wide"], + filter: { + mode: "hide", + }, + glyph: glyphOpts, + activeVisible: true, + source: { + // We could use getKingdomNames, but that returns an individual JSON format. + // getHierarchyDownFromTSN?tsn=0 seems to work as well and allows + // unified parsing in postProcess. + // url: ITIS_URL + "getKingdomNames", + url: ITIS_URL + "getHierarchyDownFromTSN", + data: { + jsonp: "itis_data", + tsn: "0", + }, + cache: true, + jsonpCallback: "itis_data", + dataType: "jsonp", + }, + init: function (event, data) { + updateControls(); + $(window).trigger("hashchange"); // trigger on initial page load + }, + lazyLoad: function (event, data) { + data.result = { + url: ITIS_URL + "getHierarchyDownFromTSN", + data: { + jsonp: "itis_data", + tsn: data.node.key, + }, + cache: true, + jsonpCallback: "itis_data", + dataType: "jsonp", + }; + }, + postProcess: function (event, data) { + var response = data.response; + + data.node.info(response); + data.result = $.map(response.hierarchyList, function (o) { + return ( + o && { + title: o.taxonName, + key: o.tsn, + folder: true, + lazy: true, + } + ); + }); + }, + activate: function (event, data) { + $("#tsnDetails").addClass("busy"); //text("..."); + updateControls(); + _delay("showDetails", 1000, function () { + updateTsnDetails(data.node.key); + updateBreadcrumb(data.node.key); + }); + }, + }); + + $("#searchResultTree").fancytree({ + extensions: ["table", "wide"], + source: [{ title: "No Results." }], + minExpandLevel: 2, + icon: false, + table: { + nodeColumnIdx: 1, + }, + postProcess: function (event, data) { + var response = data.response; + + data.node.info("pp", response); + data.result = $.map(response.anyMatchList, function (o) { + if (!o) { + return; + } + var res = { + title: o.sciName, + key: o.tsn, + author: o.author, + matchType: o.matchType, + }; + res.commonNames = $.map( + o.commonNameList.commonNames, + function (x) { + return x && x.commonName + ? { name: x.commonName, language: x.language } + : undefined; + } + ); + return res; + }); + // console.log("pp2", data.result) + }, + renderColumns: function (event, data) { + var node = data.node, + $tdList = $(node.tr).find(">td"), + cnList = node.data.commonNames + ? $.map(node.data.commonNames, function (o) { + return o.name; + }) + : []; + + $tdList.eq(0).text(node.key); + $tdList.eq(2).text(cnList.join(", ")); + $tdList.eq(3).text(node.data.matchType); + $tdList.eq(4).text(node.data.author); + }, + activate: function (event, data) { + _delay("activateNode", 1000, function () { + updateTsnDetails(data.node.key); + updateBreadcrumb(data.node.key); + }); + }, + }); + + taxonTree = $.ui.fancytree.getTree("#taxonTree"); + searchResultTree = $.ui.fancytree.getTree("#searchResultTree"); + + // Bind a callback that executes when document.location.hash changes. + // (This code uses bbq: https://github.com/cowboy/jquery-bbq) + $(window).on("hashchange", function (e) { + var tsn = $.bbq.getState("tsn"); + console.log("bbq tsn", tsn); + if (tsn) { + updateBreadcrumb(tsn, true); + } + }); // don't trigger now, since we need the the taxonTree root nodes to be loaded first + + $("input[name=query]") + .on("keyup", function (e) { + var query = $.trim($(this).val()); + + if ((e && e.which === $.ui.keyCode.ESCAPE) || query === "") { + $("#btnResetSearch").trigger("click"); + return; + } + if (e && e.which === $.ui.keyCode.ENTER && query.length >= 2) { + $("#btnSearch").trigger("click"); + return; + } + $("#btnResetSearch").attr("disabled", query.length === 0); + $("#btnSearch").attr("disabled", query.length < 2); + }) + .trigger("focus"); + + $("#btnResetSearch").click(function (e) { + $("#searchResultPane").collapse("hide"); + $("input[name=query]").val(""); + searchResultTree.clear(); + // $("#btnSearch").attr("disabled", true); + // $(this).attr("disabled", true); + updateControls(); + }); + + $("#btnSearch") + .click(function (event) { + $("#searchResultPane").collapse("show"); + search($("input[name=query]").val()); + }) + .attr("disabled", true); + + $("#btnPin").click(function (event) { + taxonTree.filterBranches(function (n) { + return n.isActive(); + }); + updateControls(); + }); + $("#btnUnpin").click(function (event) { + taxonTree.clearFilter(); + updateControls(); + }); + + // ----------------------------------------------------------------------------- + }); // end of pageload handler +})(jQuery, window, document); diff --git a/demo/taxonomy-browser/taxonomy-browser.js b/demo/taxonomy-browser/taxonomy-browser.js new file mode 100644 index 00000000..c0210f51 --- /dev/null +++ b/demo/taxonomy-browser/taxonomy-browser.js @@ -0,0 +1,525 @@ +/*! + * Fancytree Taxonomy Browser + * + * Copyright (c) 2015, Martin Wendt (https://wwWendt.de) + * + * Released under the MIT license + * https://github.com/mar10/fancytree/wiki/LicenseInfo + * + * @version @VERSION + * @date @DATE + */ + +/* global Handlebars */ +/* eslint-disable no-console */ + +(function ($, window, document) { + "use strict"; + + /******************************************************************************* + * Private functions and variables + */ + + var taxonTree, + searchResultTree, + tmplDetails, + tmplInfoPane, + tmplMedia, + timerMap = {}, + // USER_AGENT = "Fancytree Taxonomy Browser/1.0", + GBIF_URL = "//api.gbif.org/v1/", + TAXONOMY_KEY = "d7dddbf4-2cf0-4f39-9b2a-bb099caae36c", // GBIF backbone taxonomy + SEARCH_PAGE_SIZE = 5, + CHILD_NODE_PAGE_SIZE = 200, + glyphOpts = { + preset: "bootstrap3", + map: { + expanderClosed: "glyphicon glyphicon-menu-right", // glyphicon-plus-sign + expanderLazy: "glyphicon glyphicon-menu-right", // glyphicon-plus-sign + expanderOpen: "glyphicon glyphicon-menu-down", // glyphicon-collapse-down + }, + }; + + // Load and compile handlebar templates + + $.get("details.tmpl.html", function (data) { + tmplDetails = Handlebars.compile(data); + Handlebars.registerPartial("tmplDetails", tmplDetails); + }); + $.get("media.tmpl.html", function (data) { + tmplMedia = Handlebars.compile(data); + Handlebars.registerPartial("tmplMedia", tmplMedia); + }); + $.get("info-pane.tmpl.html", function (data) { + tmplInfoPane = Handlebars.compile(data); + }); + + /** Update UI elements according to current status + */ + function updateControls() { + var query = $.trim($("input[name=query]").val()); + + $("#btnPin").attr("disabled", !taxonTree.getActiveNode()); + $("#btnUnpin") + .attr("disabled", !taxonTree.isFilterActive()) + .toggleClass("btn-success", taxonTree.isFilterActive()); + $("#btnResetSearch").attr("disabled", query.length === 0); + $("#btnSearch").attr("disabled", query.length < 2); + } + + /** + * Invoke callback after `ms` milliseconds. + * Any pending action of this type is cancelled before. + */ + function _delay(tag, ms, callback) { + /*jshint -W040:true */ + var self = this; + + tag = "" + (tag || "default"); + if (timerMap[tag] != null) { + clearTimeout(timerMap[tag]); + delete timerMap[tag]; + // console.log("Cancel timer '" + tag + "'"); + } + if (ms == null || callback == null) { + return; + } + // console.log("Start timer '" + tag + "'"); + timerMap[tag] = setTimeout(function () { + // console.log("Execute timer '" + tag + "'"); + callback.call(self); + }, +ms); + } + + /** + */ + function _callWebservice(cmd, data) { + return $.ajax({ + url: GBIF_URL + cmd, + data: $.extend({}, data), + cache: true, + // 2022-11-10: Datatype 'JSONP' no longer works: + // '[Error] Refused to execute http://api.gbif.org/v1/species/... as script because "X-Content-Type-Options: nosniff" was given and its Content-Type is not a script MIME type. + // We rely on CORS, but this only works if no additoinal header is set + // headers: { "Api-User-Agent": USER_AGENT }, + dataType: "json", + }); + } + + /** + */ + function updateItemDetails(key) { + $("#tmplDetails").addClass("busy"); + $.bbq.pushState({ key: key }); + + $.when( + _callWebservice("species/" + key), + _callWebservice("species/" + key + "/speciesProfiles"), + _callWebservice("species/" + key + "/synonyms"), + _callWebservice("species/" + key + "/descriptions"), + _callWebservice("species/" + key + "/media") + ).done(function (species, profiles, synonyms, descriptions, media) { + // Requests are resolved as: [ data, statusText, jqXHR ] + species = species[0]; + profiles = profiles[0]; + synonyms = synonyms[0]; + descriptions = descriptions[0]; + media = media[0]; + + var info = $.extend(species, { + profileList: profiles.results, // marine, extinct + profile: + profiles.results.length === 1 ? profiles.results[0] : null, // marine, extinct + synonyms: synonyms.results, + descriptions: descriptions.results, + descriptionsByLang: {}, + media: media.results, + now: new Date().toString(), + }); + + $.each(info.descriptions, function (i, o) { + if (!info.descriptionsByLang[o.language]) { + info.descriptionsByLang[o.language] = []; + } + info.descriptionsByLang[o.language].push(o); + }); + + console.log("updateItemDetails", info); + $("#tmplDetails") + // .html(tmplDetails(info)) + .removeClass("busy"); + $("#tmplMedia") + // .html(tmplMedia(info)) + .removeClass("busy"); + $("#tmplInfoPane").html(tmplInfoPane(info)).removeClass("busy"); + + $("[data-toggle='popover']").popover(); + $(".carousel").carousel(); + $("#mediaCounter").text("" + (media.results.length || "")); + // $("[data-toggle='collapse']").collapse(); + updateControls(); + }); + } + + /** + */ + function updateBreadcrumb(key, loadTreeNodes) { + var $ol = $("ol.breadcrumb").addClass("busy"), + activeNode = taxonTree.getActiveNode(); + + if (activeNode && activeNode.key !== key) { + activeNode.setActive(false); // deactivate, in case the new key is not found + } + $.when( + _callWebservice("species/" + key + "/parents"), + _callWebservice("species/" + key) + ).done(function (parents, node) { + // Both requests resolved (result format: [ data, statusText, jqXHR ]) + var nodeList = parents[0], + keyList = []; + + nodeList.push(node[0]); + + // Display as
        list (for Bootstrap breadcrumbs) + $ol.empty().removeClass("busy"); + $.each(nodeList, function (i, o) { + var name = + o.vernacularName || o.canonicalName || o.scientificName; + keyList.push(o.key); + if ("" + o.key === "" + key) { + $ol.append( + $("
      1. ").append( + $("", { + text: name, + title: o.rank, + }) + ) + ); + } else { + $ol.append( + $("
      2. ").append( + $("", { + href: "#key=" + o.key, + text: name, + title: o.rank, + }) + ) + ); + } + }); + if (loadTreeNodes) { + // console.log("updateBreadcrumb - loadKeyPath", keyList); + taxonTree.loadKeyPath( + "/" + keyList.join("/"), + function (n, status) { + // console.log("... updateBreadcrumb - loadKeyPath " + n.title + ": " + status); + switch (status) { + case "loaded": + n.makeVisible(); + break; + case "ok": + n.setActive(); + // n.makeVisible(); + break; + } + } + ); + } + }); + } + + /** + */ + function search(query) { + query = $.trim(query); + console.log("searching for '" + query + "'..."); + // Store the source options for optional paging + searchResultTree.lastSourceOpts = { + // url: GBIF_URL + "species/match", // Fuzzy matches scientific names against the GBIF Backbone Taxonomy + url: GBIF_URL + "species/search", // Full text search of name usages covering the scientific and vernacular name, the species description, distribution and the entire classification across all name usages of all or some checklists + data: { + q: query, + datasetKey: TAXONOMY_KEY, + // name: query, + // strict: "true", + // hl: true, + limit: SEARCH_PAGE_SIZE, + offset: 0, + }, + cache: true, + // headers: { "Api-User-Agent": USER_AGENT } + // dataType: "jsonp" + }; + $("#searchResultTree").addClass("busy"); + searchResultTree + .reload(searchResultTree.lastSourceOpts) + .done(function (result) { + // console.log("search returned", result); + if (result.length < 1) { + searchResultTree.getRootNode().setStatus("nodata"); + } + $("#searchResultTree").removeClass("busy"); + + // https://github.com/tbasse/jquery-truncate + // SLOW! + // $("div.truncate").truncate({ + // multiline: true + // }); + + updateControls(); + }); + } + + /******************************************************************************* + * Pageload Handler + */ + + $(function () { + $("#taxonTree").fancytree({ + extensions: ["filter", "glyph", "wide"], + filter: { + mode: "hide", + }, + glyph: glyphOpts, + autoCollapse: true, + activeVisible: true, + autoScroll: true, + source: { + url: GBIF_URL + "species/root/" + TAXONOMY_KEY, + data: {}, + cache: true, + // dataType: "jsonp" + }, + init: function (event, data) { + updateControls(); + $(window).trigger("hashchange"); // trigger on initial page load + }, + lazyLoad: function (event, data) { + data.result = { + url: GBIF_URL + "species/" + data.node.key + "/children", + data: { + limit: CHILD_NODE_PAGE_SIZE, + }, + cache: true, + // dataType: "jsonp" + }; + // store this request options for later paging + data.node.lastSourceOpts = data.result; + }, + postProcess: function (event, data) { + var response = data.response; + + data.node.info("taxonTree postProcess", response); + data.result = $.map(response.results, function (o) { + return ( + o && { + title: + o.vernacularName || + o.canonicalName || + o.scientificName, + key: o.key, + nubKey: o.nubKey, + folder: true, + lazy: true, + } + ); + }); + if (response.endOfRecords === false) { + // Allow paging + data.result.push({ + title: "(more)", + statusNodeType: "paging", + }); + } else { + // No need to store the extra data + delete data.node.lastSourceOpts; + } + }, + activate: function (event, data) { + $("#tmplDetails").addClass("busy"); + $("ol.breadcrumb").addClass("busy"); + updateControls(); + _delay("showDetails", 500, function () { + updateItemDetails(data.node.key); + updateBreadcrumb(data.node.key); + }); + }, + clickPaging: function (event, data) { + // Load the next page of results + var source = $.extend( + true, + {}, + data.node.parent.lastSourceOpts + ); + source.data.offset = data.node.parent.countChildren() - 1; + data.node.replaceWith(source); + }, + }); + + $("#searchResultTree").fancytree({ + extensions: ["table", "wide"], + source: [{ title: "No Results." }], + minExpandLevel: 2, + icon: false, + table: { + nodeColumnIdx: 2, + }, + postProcess: function (event, data) { + var response = data.response; + + data.node.info("search postProcess", response); + data.result = $.map(response.results, function (o) { + var res = $.extend( + { + title: o.scientificName, + key: o.key, + }, + o + ); + return res; + }); + // Append paging link + if ( + response.count != null && + response.offset + response.limit < response.count + ) { + data.result.push({ + title: + "(" + + (response.count - + response.offset - + response.limit) + + " more)", + statusNodeType: "paging", + }); + } + data.node.info("search postProcess 2", data.result); + }, + // loadChildren: function(event, data) { + // $("#searchResultTree td div.cell").truncate({ + // multiline: true + // }); + // }, + renderColumns: function (event, data) { + var i, + node = data.node, + $tdList = $(node.tr).find(">td"), + cnList = node.data.vernacularNames + ? $.map(node.data.vernacularNames, function (o) { + return o.vernacularName; + }) + : []; + + i = 0; + function _setCell($cell, text) { + $("
        ") + .attr("title", text) + .text(text) + .appendTo($cell); + } + $tdList.eq(i++).text(node.key); + $tdList.eq(i++).text(node.data.rank); + i++; // #1: node.title = scientificName + // $tdList.eq(i++).text(cnList.join(", ")); + _setCell($tdList.eq(i++), cnList.join(", ")); + $tdList.eq(i++).text(node.data.canonicalName); + // $tdList.eq(i++).text(node.data.accordingTo); + _setCell($tdList.eq(i++), node.data.accordingTo); + $tdList.eq(i++).text(node.data.taxonomicStatus); + $tdList.eq(i++).text(node.data.nameType); + $tdList.eq(i++).text(node.data.numOccurrences); + $tdList.eq(i++).text(node.data.numDescendants); + // $tdList.eq(i++).text(node.data.authorship); + _setCell($tdList.eq(i++), node.data.authorship); + // $tdList.eq(i++).text(node.data.publishedIn); + _setCell($tdList.eq(i++), node.data.publishedIn); + }, + activate: function (event, data) { + if (data.node.isStatusNode()) { + return; + } + _delay("activateNode", 500, function () { + updateItemDetails(data.node.key); + updateBreadcrumb(data.node.key); + }); + }, + clickPaging: function (event, data) { + // Load the next page of results + var source = $.extend( + true, + {}, + searchResultTree.lastSourceOpts + ); + source.data.offset = data.node.parent.countChildren() - 1; + data.node.replaceWith(source); + }, + }); + + taxonTree = $.ui.fancytree.getTree("#taxonTree"); + searchResultTree = $.ui.fancytree.getTree("#searchResultTree"); + + // Bind a callback that executes when document.location.hash changes. + // (This code uses bbq: https://github.com/cowboy/jquery-bbq) + $(window).on("hashchange", function (e) { + var key = $.bbq.getState("key"); + console.log("bbq key", key); + if (key) { + updateBreadcrumb(key, true); + } + }); // don't trigger now, since we need the the taxonTree root nodes to be loaded first + + $("input[name=query]") + .on("keyup", function (e) { + var query = $.trim($(this).val()), + lastQuery = $(this).data("lastQuery"); + + if ((e && e.which === $.ui.keyCode.ESCAPE) || query === "") { + $("#btnResetSearch").trigger("click"); + return; + } + if (e && e.which === $.ui.keyCode.ENTER && query.length >= 2) { + $("#btnSearch").trigger("click"); + return; + } + if (query === lastQuery || query.length < 2) { + console.log("Ignored query '" + query + "'"); + return; + } + $(this).data("lastQuery", query); + _delay("search", 1, function () { + $("#btnSearch").trigger("click"); + }); + $("#btnResetSearch").attr("disabled", query.length === 0); + $("#btnSearch").attr("disabled", query.length < 2); + }) + .trigger("focus"); + + $("#btnResetSearch").click(function (e) { + $("#searchResultPane").collapse("hide"); + $("input[name=query]").val(""); + searchResultTree.clear(); + updateControls(); + }); + + $("#btnSearch") + .click(function (event) { + $("#searchResultPane").collapse("show"); + search($("input[name=query]").val()); + }) + .attr("disabled", true); + + $("#btnPin").click(function (event) { + taxonTree.filterBranches(function (n) { + return n.isActive(); + }); + updateControls(); + }); + + $("#btnUnpin").click(function (event) { + taxonTree.clearFilter(); + updateControls(); + }); + + // ----------------------------------------------------------------------------- + }); // end of pageload handler +})(jQuery, window, document); diff --git a/demo/taxonomy-browser/test_gbif_ws.html b/demo/taxonomy-browser/test_gbif_ws.html new file mode 100644 index 00000000..93dc6c6b --- /dev/null +++ b/demo/taxonomy-browser/test_gbif_ws.html @@ -0,0 +1,39 @@ + + + + + Test GBIF Webservice + + + + + + + + + + + + + diff --git a/demo/taxonomy-browser/test_itis_ws.html b/demo/taxonomy-browser/test_itis_ws.html new file mode 100644 index 00000000..8cc075a9 --- /dev/null +++ b/demo/taxonomy-browser/test_itis_ws.html @@ -0,0 +1,88 @@ + + + + + Test ITIS Webservice + + + + + + + + + + + + + diff --git a/demo/top.html b/demo/top.html index 0b2e48a8..c1db59f3 100644 --- a/demo/top.html +++ b/demo/top.html @@ -7,8 +7,10 @@ -

        Fancytree - Example Browser

        +

        + Fancytree - Example Browser + +

        diff --git a/demo/welcome.html b/demo/welcome.html index 11da0b31..ed7dd6c4 100644 --- a/demo/welcome.html +++ b/demo/welcome.html @@ -4,14 +4,18 @@ Fancytree - Example - + - - + +

        Fancytree - Examples

        +

        + Fancytree is a JavaScript tree view / tree grid plugin with support for keyboard, + inline editing, filtering, checkboxes, drag'n'drop, and lazy loading. +

        This site presents some live examples for Fancytree.

        @@ -24,8 +28,12 @@

        Fancytree - Examples

        Have fun :-)

        - -Fork me on GitHub + + + Fork me on GitHub + diff --git a/lib/jquery-ui-contextmenu/MIT-LICENSE.txt b/dist/LICENSE.txt similarity index 89% rename from lib/jquery-ui-contextmenu/MIT-LICENSE.txt rename to dist/LICENSE.txt index 211def78..1a615710 100644 --- a/lib/jquery-ui-contextmenu/MIT-LICENSE.txt +++ b/dist/LICENSE.txt @@ -1,5 +1,5 @@ -Copyright 2013 Martin Wendt and others (see commiter list on GitHub) -https://github.com/mar10/jquery-ui-contextmenu +Copyright 2008-2023 Martin Wendt, +https://wwWendt.de/ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the diff --git a/dist/jquery.fancytree-all-deps.js b/dist/jquery.fancytree-all-deps.js new file mode 100644 index 00000000..e02f1dc2 --- /dev/null +++ b/dist/jquery.fancytree-all-deps.js @@ -0,0 +1,13839 @@ +/*! jQuery Fancytree Plugin - 2.38.5 - 2025-04-05T06:40:00Z + * https://github.com/mar10/fancytree + * Copyright (c) 2025 Martin Wendt; Licensed MIT + */ +/*! jQuery UI - v1.14.1 - 2025-03-26 +* https://jqueryui.com +* Includes: widget.js, position.js, jquery-patch.js, keycode.js, scroll-parent.js, unique-id.js +* Copyright OpenJS 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.14.1"; + + + /*! + * jQuery UI Widget 1.14.1 + * https://jqueryui.com + * + * Copyright OpenJS Foundation and other contributors + * Released under the MIT license. + * https://jquery.org/license + */ + + //>>label: Widget + //>>group: Core + //>>description: Provides a factory for creating stateful widgets with a common API. + //>>docs: https://api.jqueryui.com/jQuery.widget/ + //>>demos: https://jqueryui.com/widget/ + + + var widgetUuid = 0; + var widgetHasOwnProperty = Array.prototype.hasOwnProperty; + var widgetSlice = Array.prototype.slice; + + $.cleanData = ( function( orig ) { + return function( elems ) { + var events, elem, i; + for ( i = 0; ( elem = elems[ i ] ) != null; i++ ) { + + // Only trigger remove when necessary to save time + events = $._data( elem, "events" ); + if ( events && events.remove ) { + $( elem ).triggerHandler( "remove" ); + } + } + 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 ]; + if ( name === "__proto__" || name === "constructor" ) { + return $.error( "Invalid widget name: " + name ); + } + var fullName = namespace + "-" + name; + + if ( !prototype ) { + prototype = base; + base = $.Widget; + } + + if ( Array.isArray( prototype ) ) { + prototype = $.extend.apply( null, [ {} ].concat( prototype ) ); + } + + // Create selector for plugin + $.expr.pseudos[ 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 || !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 ( typeof value !== "function" ) { + 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 ( widgetHasOwnProperty.call( input[ inputIndex ], 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 ( typeof instance[ options ] !== "function" || + 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: "
        ", + + 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 bindRemoveEvent() { + var nodesToBind = []; + + options.element.each( function( _, element ) { + var isTracked = $.map( that.classesElementLookup, function( elements ) { + return elements; + } ) + .some( function( elements ) { + return elements.is( element ); + } ); + + if ( !isTracked ) { + nodesToBind.push( element ); + } + } ); + + that._on( $( nodesToBind ), { + remove: "_untrackClassesElement" + } ); + } + + function processClassString( classes, checkOption ) { + var current, i; + for ( i = 0; i < classes.length; i++ ) { + current = that.classesElementLookup[ classes[ i ] ] || $(); + if ( options.add ) { + bindRemoveEvent(); + current = $( $.uniqueSort( 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 ] ] ); + } + } + } + + 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() ); + } + } ); + + this._off( $( event.target ) ); + }, + + _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 ); + + // 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 !( typeof callback === "function" && + 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 }; + } else if ( options === true ) { + 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.14.1 + * https://jqueryui.com + * + * Copyright OpenJS Foundation and other contributors + * Released under the MIT license. + * https://jquery.org/license + * + * https://api.jqueryui.com/position/ + */ + + //>>label: Position + //>>group: Core + //>>description: Positions elements relative to other elements. + //>>docs: https://api.jqueryui.com/position/ + //>>demos: https://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 isWindow( obj ) { + return obj != null && obj === obj.window; + } + + 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 = $( "
        " + + "
        " ), + 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 ), + isElemWindow = isWindow( withinElement[ 0 ] ), + isDocument = !!withinElement[ 0 ] && withinElement[ 0 ].nodeType === 9, + hasOffset = !isElemWindow && !isDocument; + return { + element: withinElement, + isWindow: isElemWindow, + 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, + + // Make sure string options are treated as CSS selectors + target = typeof options.of === "string" ? + $( document ).find( options.of ) : + $( 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 Legacy jQuery Core patches 1.14.1 + * https://jqueryui.com + * + * Copyright OpenJS Foundation and other contributors + * Released under the MIT license. + * https://jquery.org/license + * + */ + + //>>label: Legacy jQuery Core patches + //>>group: Core + //>>description: Backport `.even()`, `.odd()` and `$.escapeSelector` to older jQuery Core versions (deprecated) + + + // Support: jQuery 2.2.x or older. + // This method has been defined in jQuery 3.0.0. + // Code from https://github.com/jquery/jquery/blob/e539bac79e666bba95bba86d690b4e609dca2286/src/selector/escapeSelector.js + if ( !$.escapeSelector ) { + $.escapeSelector = function( id ) { + return CSS.escape( id + "" ); + }; + } + + // Support: jQuery 3.4.x or older + // These methods have been defined in jQuery 3.5.0. + if ( !$.fn.even || !$.fn.odd ) { + $.fn.extend( { + even: function() { + return this.filter( function( i ) { + return i % 2 === 0; + } ); + }, + odd: function() { + return this.filter( function( i ) { + return i % 2 === 1; + } ); + } + } ); + } + + ; + /*! + * jQuery UI Keycode 1.14.1 + * https://jqueryui.com + * + * Copyright OpenJS Foundation and other contributors + * Released under the MIT license. + * https://jquery.org/license + */ + + //>>label: Keycode + //>>group: Core + //>>description: Provide keycodes as keynames + //>>docs: https://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.14.1 + * https://jqueryui.com + * + * Copyright OpenJS Foundation and other contributors + * Released under the MIT license. + * https://jquery.org/license + */ + + //>>label: scrollParent + //>>group: Core + //>>description: Get the closest ancestor element that is scrollable. + //>>docs: https://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.14.1 + * https://jqueryui.com + * + * Copyright OpenJS Foundation and other contributors + * Released under the MIT license. + * https://jquery.org/license + */ + + //>>label: uniqueId + //>>group: Core + //>>description: Functions to generate and remove uniqueId's + //>>docs: https://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" ); + } + } ); + } + } ); + +// 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-2023, Martin Wendt (https://wwWendt.de) + * Released under the MIT license + * https://github.com/mar10/fancytree/wiki/LicenseInfo + * + * @version 2.38.5 + * @date 2025-04-05T06:40:00Z + */ + +/** 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, // Escape those characters + REX_TOOLTIP = /[<>"'/]/g, // Don't escape `&` in tooltips + RECURSIVE_REQUEST_ERROR = "$recursive_request", + INVALID_REQUEST_TARGET_ERROR = "$request_target_invalid", + ENTITY_MAP = { + "&": "&", + "<": "<", + ">": ">", + '"': """, + "'": "'", + "/": "/", + }, + 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: "=", + // 91: null, 93: null, // ignore left and right meta + 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: "'", + }, + MODIFIERS = { + 16: "shift", + 17: "ctrl", + 18: "alt", + 91: "meta", + 93: "meta", + }, + 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 attributes, that can be set by dict + TREE_ATTRS = "columns types".split(" "), + // TREE_ATTR_MAP = {}, + // Top-level FancytreeNode attributes, that can be set by dict + NODE_ATTRS = + "checkbox expanded extraClasses folder icon iconTooltip key lazy partsel radiogroup refKey selected statusNodeType title tooltip type 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; + } + } + // for(i=0; i t; + } + } + return true; + } + + /** + * Deep-merge a list of objects (but replace array-type options). + * + * jQuery's $.extend(true, ...) method does a deep merge, that also merges Arrays. + * This variant is used to merge extension defaults with user options, and should + * merge objects, but override arrays (for example the `triggerStart: [...]` option + * of ext-edit). Also `null` values are copied over and not skipped. + * + * See issue #876 + * + * Example: + * _simpleDeepMerge({}, o1, o2); + */ + function _simpleDeepMerge() { + var options, + name, + src, + copy, + clone, + target = arguments[0] || {}, + i = 1, + length = arguments.length; + + // Handle case when target is a string or something (possible in deep copy) + if (typeof target !== "object" && !_isFunction(target)) { + target = {}; + } + if (i === length) { + throw Error("need at least two args"); + } + for (; i < length; i++) { + // Only deal with non-null/undefined values + if ((options = arguments[i]) != null) { + // Extend the base object + for (name in options) { + if (_hasProp(options, name)) { + src = target[name]; + copy = options[name]; + // Prevent never-ending loop + if (target === copy) { + continue; + } + // Recurse if we're merging plain objects + // (NOTE: unlike $.extend, we don't merge arrays, but replace them) + if (copy && $.isPlainObject(copy)) { + clone = src && $.isPlainObject(src) ? src : {}; + // Never move original objects, clone them + target[name] = _simpleDeepMerge(clone, copy); + // Don't bring in undefined values + } else if (copy !== undefined) { + target[name] = copy; + } + } + } + } + } + // Return the modified object + return target; + } + + /** 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(); + } + return $.Deferred(function () { + this.resolveWith(context, argArray); + }).promise(); + } + + function _getRejectedPromise(context, argArray) { + if (context === undefined) { + return $.Deferred(function () { + this.reject(); + }).promise(); + } + 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; + //
      3. 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.
        + * 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 ``.
        + * Note: use `node.add/remove/toggleClass()` to modify. + * @property {boolean} folder Folder nodes have different default icons and click behavior.
        + * 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 + * @property {string} iconTooltip Description used as hover popup for icon. @since 2.27 + * @property {string} type Node type, used with tree.types map. @since 2.27 + */ + function FancytreeNode(parent, obj) { + var i, l, name, cl; + + this.parent = parent; + this.tree = parent.tree; + this.ul = null; + this.li = null; //
      4. 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] && + (this.tree.options.copyFunctionsToData || + !_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])); + } + this.tree._callHook( + "treeStructureChanged", + this.tree, + "setChildren" + ); + }, + /** + * 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 { + // Returns null if insertBefore is not a direct child: + 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"); + }, + /** + * (experimental) Apply a modification (or navigation) operation. + * + * @param {string} cmd + * @param {object} [opts] + * @see Fancytree#applyCommand + * @since 2.32 + */ + applyCommand: function (cmd, opts) { + return this.tree.applyCommand(cmd, this, opts); + }, + /** + * 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) { + if (_hasProp(patch, name)) { + 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 (_hasProp(patch, "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 (_hasProp(patch, "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, FancytreeNode) 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 >= 4 (prepending node info) + * + * @param {*} msg string or object or array of such + */ + debug: function (msg) { + if (this.tree.options.debugLevel >= 4) { + 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); + }, + /** Write error to browser console if debugLevel >= 1 (prepending tree info) + * + * @param {*} msg string or object or array of such + */ + error: function (msg) { + if (this.tree.options.debugLevel >= 1) { + Array.prototype.unshift.call(arguments, this.toString()); + consoleApply("error", arguments); + } + }, + /**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; + }, + /** Find a node relative to self. + * + * @param {number|string} where The keyCode that would normally trigger this move, + * or a keyword ('down', 'first', 'last', 'left', 'parent', 'right', 'up'). + * @returns {FancytreeNode} + * @since v2.31 + */ + findRelatedNode: function (where, includeHidden) { + return this.tree.findRelatedNode(this, where, includeHidden); + }, + /* 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); + if (node.radiogroup) { + // #931: don't (de)select this branch + return "skip"; + } + }); + 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; + } + } + } + // eslint-disable-next-line no-nested-ternary + 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; + } + // #939: Keep a `partsel` flag that was explicitly set on a lazy node + if ( + node.partsel && + !node.selected && + node.lazy && + node.children == null + ) { + state = undefined; + } + 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; + } + } + } + // eslint-disable-next-line no-nested-ternary + 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". + * + * (Unlike `node.getPath()`, this method prepends a "/" and inverts the first argument.) + * + * @see FancytreeNode#getPath + * @param {boolean} [excludeSelf=false] + * @returns {string} + */ + getKeyPath: function (excludeSelf) { + var sep = this.tree.options.keyPathSeparator; + + return sep + this.getPath(!excludeSelf, "key", 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 a string representing the hierachical node path, e.g. "a/b/c". + * @param {boolean} [includeSelf=true] + * @param {string | function} [part="title"] node property name or callback + * @param {string} [separator="/"] + * @returns {string} + * @since v2.31 + */ + getPath: function (includeSelf, part, separator) { + includeSelf = includeSelf !== false; + part = part || "title"; + separator = separator || "/"; + + var val, + path = [], + isFunc = _isFunction(part); + + this.visitParents(function (n) { + if (n.parent) { + val = isFunc ? part(n) : n[part]; + path.unshift(val); + } + }, includeSelf); + return path.join(separator); + }, + /** 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 `className` defined in .extraClasses. + * + * @param {string} className class name (separate multiple classes by space) + * @returns {boolean} + * + * @since 2.32 + */ + hasClass: function (className) { + return ( + (" " + (this.extraClasses || "") + " ").indexOf( + " " + className + " " + ) >= 0 + ); + }, + /** 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 >= 3 (prepending node info) + * + * @param {*} msg string or object or array of such + */ + info: function (msg) { + if (this.tree.options.debugLevel >= 3) { + 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 vertically below `otherNode`, i.e. rendered in a subsequent row. + * @param {FancytreeNode} otherNode + * @returns {boolean} + * @since 2.28 + */ + isBelowOf: function (otherNode) { + return this.getIndexHier(".", 5) > otherNode.getIndexHier(".", 5); + }, + /** 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, + n, + hasFilter = this.tree.enableFilter, + parents = this.getParentList(false, false); + + // TODO: check $(n.span).is(":visible") + // i.e. return false for nodes (but not parents) that are hidden + // by a filter + if (hasFilter && !this.match && !this.subMatchCount) { + // this.debug( "isVisible: HIDDEN (" + hasFilter + ", " + this.match + ", " + this.match + ")" ); + return false; + } + + for (i = 0, l = parents.length; i < l; i++) { + n = parents[i]; + + if (!n.expanded) { + // this.debug("isVisible: HIDDEN (parent collapsed)"); + return false; + } + // if (hasFilter && !n.match && !n.subMatchCount) { + // this.debug("isVisible: HIDDEN (" + hasFilter + ", " + this.match + ", " + this.match + ")"); + // return false; + // } + } + // this.debug("isVisible: VISIBLE"); + return true; + }, + /** Deprecated. + * @deprecated since 2014-02-16: use load() instead. + */ + lazyLoad: function (discard) { + $.error( + "FancytreeNode.lazyLoad() is deprecated since 2014-02-16. Use .load() instead." + ); + }, + /** + * Load all children of a lazy node if neccessary. The expanded 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, + self = 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 () { + self.render(); + }); + } else { + res.always(function () { + self.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, + self = 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--) { + // self.debug("pushexpand" + parents[i]); + deferreds.push(parents[i].setExpanded(true, opts)); + } + $.when.apply($, deferreds).done(function () { + // All expands have finished + // self.debug("expand DONE", scroll); + if (scroll) { + self.scrollIntoView(effects).done(function () { + // self.debug("scroll DONE"); + dfd.resolve(); + }); + } else { + dfd.resolve(); + } + }); + return dfd.promise(); + }, + /** Move this node to targetNode. + * @param {FancytreeNode} targetNode + * @param {string} mode
        +		 *      '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.
        + * @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, + tree = this.tree, + 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
          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 (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 experimental!"); + 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); + // } + tree._callHook("treeStructureChanged", tree, "moveTo"); + + // 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. + * + * 'left' collapses the node if it is expanded, or move to the parent + * otherwise. + * 'right' expands the node if it is collapsed, or move to the first + * child otherwise. + * + * @param {string|number} where 'down', 'first', 'last', 'left', 'parent', 'right', or 'up'. + * (Alternatively the keyCode that would normally trigger this move, + * e.g. `$.ui.keyCode.LEFT` = 'left'. + * @param {boolean} [activate=true] + * @returns {$.Promise} + */ + navigate: function (where, activate) { + var node, + KC = $.ui.keyCode; + + // Handle optional expand/collapse action for LEFT/RIGHT + switch (where) { + case "left": + case KC.LEFT: + if (this.expanded) { + return this.setExpanded(false); + } + break; + case "right": + case KC.RIGHT: + if (!this.expanded && (this.children || this.lazy)) { + return this.setExpanded(); + } + break; + } + // Otherwise activate or focus the related node + node = this.findRelatedNode(where); + if (node) { + // setFocus/setActive will scroll later (if autoScroll is specified) + try { + node.makeVisible({ scrollIntoView: false }); + } catch (e) {} // #272 + if (activate === false) { + node.setFocus(); + return _getResolvedPromise(); + } + return node.setActive(); + } + this.warn("Could not find related node '" + where + "'."); + return _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.
          + * 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.
          + * Note: + *
            + *
          • 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. + *
          • {@link FancytreeNode#renderTitle} and {@link FancytreeNode#renderStatus} + * are implied. If changes are more local, calling only renderTitle() or + * renderStatus() may be sufficient and faster. + *
          + * + * @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 `` (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), + self = 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 = self.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 + self.children = null; + self.remove(); + // Redraw new nodes + parent.render(); + // TODO: set node.partload = false if this was tha last paging node? + // parent.addPagingNode(false); + }).fail(function () { + self.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)) { + throw Error( + "scrollIntoView() with 'topNode' option is deprecated since 2014-05-08. Use 'options.topNode' instead." + ); + } + // The scroll parent is typically the plain tree's
            container. + // For ext-table, we choose the nearest parent that has `position: relative` + // and `overflow` set. + // (This default can be overridden by the local or global `scrollParent` option.) + var opts = $.extend( + { + effects: + effects === true + ? { duration: 200, queue: false } + : effects, + scrollOfs: this.tree.options.scrollOfs, + scrollParent: this.tree.options.scrollParent, + topNode: null, + }, + options + ), + $scrollParent = opts.scrollParent, + $container = this.tree.$container, + overflowY = $container.css("overflow-y"); + + if (!$scrollParent) { + if (this.tree.tbody) { + $scrollParent = $container.scrollParent(); + } else if (overflowY === "scroll" || overflowY === "auto") { + $scrollParent = $container; + } else { + // #922 plain tree in a non-fixed-sized UL scrolls inside its parent + $scrollParent = $container.scrollParent(); + } + } else if (!$scrollParent.jquery) { + // Make sure we have a jQuery object + $scrollParent = $($scrollParent); + } + if ( + $scrollParent[0] === document || + $scrollParent[0] === document.body + ) { + // `document` may be returned by $().scrollParent(), if nothing is found, + // but would not work: (see #894) + this.debug( + "scrollIntoView(): normalizing scrollParent to 'window':", + $scrollParent[0] + ); + $scrollParent = $(window); + } + // eslint-disable-next-line one-var + var topNodeY, + nodeY, + horzScrollbarHeight, + containerOffsetTop, + dfd = new $.Deferred(), + self = this, + nodeHeight = $(this.span).height(), + topOfs = opts.scrollOfs.top || 0, + bottomOfs = opts.scrollOfs.bottom || 0, + containerHeight = $scrollParent.height(), + scrollTop = $scrollParent.scrollTop(), + $animateTarget = $scrollParent, + isParentWindow = $scrollParent[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.isRootNode() || !this.isVisible()) { + // We cannot calc offsets for hidden elements + this.info("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( + $scrollParent[0] !== document && + $scrollParent[0] !== document.body, + "scrollParent should be a simple element or `window`, not document or body." + ); + + containerOffsetTop = $scrollParent.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, + $scrollParent.innerHeight() - $scrollParent[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.isVisible(), + "topNode must be visible" + ); + if (topNodeY < newScrollTop) { + newScrollTop = topNodeY - topOfs; + // this.debug(" scrollIntoView(), TOP newScrollTop=" + newScrollTop); + } + } + } + + if (newScrollTop === null) { + dfd.resolveWith(this); + } else { + // this.debug(" scrollIntoView(), SET newScrollTop=" + newScrollTop); + if (opts.effects) { + opts.effects.complete = function () { + dfd.resolveWith(self); + }; + $animateTarget.stop(true).animate( + { + scrollTop: newScrollTop, + }, + opts.effects + ); + } else { + $animateTarget[0].scrollTop = newScrollTop; + dfd.resolveWith(this); + } + } + return dfd.promise(); + }, + + /**Activate this node. + * + * The `cell` option requires the ext-table and ext-ariagrid extensions. + * + * @param {boolean} [flag=true] pass false to deactivate + * @param {object} [opts] additional options. Defaults to {noEvents: false, noFocus: false, cell: null} + * @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'|'loading'|'nodata'|'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(); + + // eslint-disable-next-line no-nested-ternary + 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. + * Return `false` to ignore this node or `"skip"` to include this node without its children. + * @returns {NodeData} + */ + toDict: function (recursive, callback) { + var i, + l, + node, + res, + 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) { + res = callback(dict, self); + if (res === false) { + return false; // Don't include this node nor its children + } + if (res === "skip") { + recursive = false; // Include this node, but not the children + } + } + if (recursive) { + if (_isArray(this.children)) { + dict.children = []; + for (i = 0, l = this.children.length; i < l; i++) { + node = this.children[i]; + if (!node.isStatusNode()) { + res = node.toDict(true, callback); + if (res !== false) { + dict.children.push(res); + } + } + } + } + } + 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 + "[title='" + this.title + "']"; + // return ""; + }, + /** + * 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 in hierarchical order (depth-first).
            + * Stop iteration, if fn() returns false. Skip current branch, if fn() returns "skip".
            + * 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.
            + * Note: 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.
            + * Stop iteration, if fn() returns false.
            + * 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.
            + * Stop iteration, if fn() returns false.
            + * 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 if debugLevel >= 2 (prepending node info) + * + * @param {*} msg string or object or array of such + */ + warn: function (msg) { + if (this.tree.options.debugLevel >= 2) { + 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 `
              ` element (or `` element for ext-table). + * @property {jQueryObject} $div A jQuery object containing the element used to instantiate the tree widget (`widget.element`) + * @property {object|array} columns Recommended place to store shared column meta data. @since 2.27 + * @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} types Map for shared type specific meta data, used with node.type attribute. @since 2.27 + * @property {object} viewport See ext-vieport. @since v2.31 + * @property {object} widget Base widget instance. + */ + function Fancytree(widget) { + this.widget = widget; + this.$div = widget.element; + this.options = widget.options; + if (this.options) { + if (this.options.lazyload !== undefined) { + $.error( + "The 'lazyload' event is deprecated since 2014-02-25. Use 'lazyLoad' (with uppercase L) instead." + ); + } + if (this.options.loaderror !== undefined) { + $.error( + "The 'loaderror' event was renamed since 2014-07-03. Use 'loadError' (with uppercase E) instead." + ); + } + if (this.options.fx !== undefined) { + $.error( + "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 + this.types = {}; + this.columns = {}; + // allow to init tree.data.foo from
              + this.data = _getElementDataAsDict(this.$div); + // TODO: use widget.uuid instead? + this._id = "" + (this.options.treeId || $.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.lastSelectedNode = null; + this.systemFocusElement = null; + this.lastQuicksearchTerm = ""; + this.lastQuicksearchTime = 0; + this.viewport = null; // ext-grid + + 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 = $("
                ", { + id: "ft-id-" + this._id, + 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, + typeInfo: tree.types[obj.type] || {}, + }; + } 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 this tree has extension `name` enabled. + * + * @param {string} name name of the required extension + */ + _usesExtension: function (name) { + return $.inArray(name, this.options.extensions) >= 0; + }, + /* 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} name 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) { + if (before != null) { + 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 '" + thisName + "' (require yourself?)" + ); + + 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 + * @param {object} [opts] additional options. Defaults to {noEvents: false, noFocus: false} + * @returns {FancytreeNode} activated node (null, if not found) + */ + activateKey: function (key, opts) { + var node = this.getNodeByKey(key); + if (node) { + node.setActive(true, opts); + } else if (this.activeNode) { + this.activeNode.setActive(false, opts); + } + 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) Apply a modification (or navigation) operation. + * + * Valid commands: + * - 'moveUp', 'moveDown' + * - 'indent', 'outdent' + * - 'remove' + * - 'edit', 'addChild', 'addSibling': (reqires ext-edit extension) + * - 'cut', 'copy', 'paste': (use an internal singleton 'clipboard') + * - 'down', 'first', 'last', 'left', 'parent', 'right', 'up': navigate + * + * @param {string} cmd + * @param {FancytreeNode} [node=active_node] + * @param {object} [opts] Currently unused + * + * @since 2.32 + */ + applyCommand: function (cmd, node, opts_) { + var // clipboard, + refNode; + // opts = $.extend( + // { setActive: true, clipboard: CLIPBOARD }, + // opts_ + // ); + + node = node || this.getActiveNode(); + // clipboard = opts.clipboard; + + switch (cmd) { + // Sorting and indentation: + case "moveUp": + refNode = node.getPrevSibling(); + if (refNode) { + node.moveTo(refNode, "before"); + node.setActive(); + } + break; + case "moveDown": + refNode = node.getNextSibling(); + if (refNode) { + node.moveTo(refNode, "after"); + node.setActive(); + } + break; + case "indent": + refNode = node.getPrevSibling(); + if (refNode) { + node.moveTo(refNode, "child"); + refNode.setExpanded(); + node.setActive(); + } + break; + case "outdent": + if (!node.isTopLevel()) { + node.moveTo(node.getParent(), "after"); + node.setActive(); + } + break; + // Remove: + case "remove": + refNode = node.getPrevSibling() || node.getParent(); + node.remove(); + if (refNode) { + refNode.setActive(); + } + break; + // Add, edit (requires ext-edit): + case "addChild": + node.editCreateNode("child", ""); + break; + case "addSibling": + node.editCreateNode("after", ""); + break; + case "rename": + node.editStart(); + break; + // Simple clipboard simulation: + // case "cut": + // clipboard = { mode: cmd, data: node }; + // break; + // case "copy": + // clipboard = { + // mode: cmd, + // data: node.toDict(function(d, n) { + // delete d.key; + // }), + // }; + // break; + // case "clear": + // clipboard = null; + // break; + // case "paste": + // if (clipboard.mode === "cut") { + // // refNode = node.getPrevSibling(); + // clipboard.data.moveTo(node, "child"); + // clipboard.data.setActive(); + // } else if (clipboard.mode === "copy") { + // node.addChildren(clipboard.data).setActive(); + // } + // break; + // Navigation commands: + case "down": + case "first": + case "last": + case "left": + case "parent": + case "right": + case "up": + return node.navigate(cmd); + default: + $.error("Unhandled command: '" + cmd + "'"); + } + }, + /** (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 >= 4 (prepending tree name) + * + * @param {*} msg string or object or array of such + */ + debug: function (msg) { + if (this.options.debugLevel >= 4) { + Array.prototype.unshift.call(arguments, this.toString()); + consoleApply("log", arguments); + } + }, + /** Destroy this widget, restore previous markup and cleanup resources. + * + * @since 2.34 + */ + destroy: function () { + this.widget.destroy(); + }, + /** Enable (or disable) the tree control. + * + * @param {boolean} [flag=true] pass false to disable + * @since 2.30 + */ + enable: function (flag) { + if (flag === false) { + this.widget.disable(); + } else { + this.widget.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; + if (!!this._enableUpdate === !!flag) { + return flag; + } + this._enableUpdate = flag; + if (flag) { + this.debug("enableUpdate(true): redraw "); //, this._dirtyRoots); + this._callHook("treeStructureChanged", this, "enableUpdate"); + this.render(); + } else { + // this._dirtyRoots = null; + this.debug("enableUpdate(false)..."); + } + return !flag; // return previous value + }, + /** Write error to browser console if debugLevel >= 1 (prepending tree info) + * + * @param {*} msg string or object or array of such + */ + error: function (msg) { + if (this.options.debugLevel >= 1) { + Array.prototype.unshift.call(arguments, this.toString()); + consoleApply("error", arguments); + } + }, + /** Expand (or collapse) all parent nodes. + * + * This convenience method uses `tree.visit()` and `tree.setExpanded()` + * internally. + * + * @param {boolean} [flag=true] pass false to collapse + * @param {object} [opts] passed to setExpanded() + * @since 2.30 + */ + expandAll: function (flag, opts) { + var prev = this.enableUpdate(false); + + flag = flag !== false; + this.visit(function (node) { + if ( + node.hasChildren() !== false && + node.isExpanded() !== flag + ) { + node.setExpanded(flag, opts); + } + }); + this.enableUpdate(prev); + }, + /**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 res = null, + firstNode = this.getFirstChild(); + + match = + typeof match === "string" + ? _makeNodeTitleStartMatcher(match) + : match; + startNode = startNode || firstNode; + + function _checkNode(n) { + // console.log("_check " + n) + if (match(n)) { + res = n; + } + if (res || n === startNode) { + return false; + } + } + this.visitRows(_checkNode, { + start: startNode, + includeSelf: false, + }); + // Wrap around search + if (!res && startNode !== firstNode) { + this.visitRows(_checkNode, { + start: firstNode, + includeSelf: true, + }); + } + return res; + }, + /** Find a node relative to another node. + * + * @param {FancytreeNode} node + * @param {string|number} where 'down', 'first', 'last', 'left', 'parent', 'right', or 'up'. + * (Alternatively the keyCode that would normally trigger this move, + * e.g. `$.ui.keyCode.LEFT` = 'left'. + * @param {boolean} [includeHidden=false] Not yet implemented + * @returns {FancytreeNode|null} + * @since v2.31 + */ + findRelatedNode: function (node, where, includeHidden) { + var res = null, + KC = $.ui.keyCode; + + switch (where) { + case "parent": + case KC.BACKSPACE: + if (node.parent && node.parent.parent) { + res = node.parent; + } + break; + case "first": + case KC.HOME: + // First visible node + this.visit(function (n) { + if (n.isVisible()) { + res = n; + return false; + } + }); + break; + case "last": + case KC.END: + this.visit(function (n) { + // last visible node + if (n.isVisible()) { + res = n; + } + }); + break; + case "left": + case KC.LEFT: + if (node.expanded) { + node.setExpanded(false); + } else if (node.parent && node.parent.parent) { + res = node.parent; + } + break; + case "right": + case KC.RIGHT: + if (!node.expanded && (node.children || node.lazy)) { + node.setExpanded(); + res = node; + } else if (node.children && node.children.length) { + res = node.children[0]; + } + break; + case "up": + case KC.UP: + this.visitRows( + function (n) { + res = n; + return false; + }, + { start: node, reverse: true, includeSelf: false } + ); + break; + case "down": + case KC.DOWN: + this.visitRows( + function (n) { + res = n; + return false; + }, + { start: node, includeSelf: false } + ); + break; + default: + this.tree.warn("Unknown relation '" + where + "'."); + } + return res; + }, + // 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 = $("
                ", { + id: id, + }) + .hide() + .insertAfter(this.$container); + } + if (active !== false && this.activeNode) { + $result.append( + $("", { + type: "radio", + name: activeName, + value: this.activeNode.key, + checked: true, + }) + ); + } + function _appender(node) { + $result.append( + $("", { + 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 current option value. + * (Note: this is the preferred variant of `$().fancytree("option", "KEY")`) + * + * @param {string} name option name (may contain '.') + * @returns {any} + */ + getOption: function (optionName) { + return this.widget.option(optionName); + }, + /** + * 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). + var el, match; + // TODO: use tree.keyMap if available + // TODO: check opts.generateIds === true + 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 + searchRoot = searchRoot || this.rootNode; + match = null; + key = "" + key; // Convert to string (#1005) + searchRoot.visit(function (node) { + if (node.key === key) { + match = node; + return false; // Stop iteration + } + }, true); + return match; + }, + /** Return the invisible system root node. + * @returns {FancytreeNode} + */ + getRootNode: function () { + return this.rootNode; + }, + /** + * Return an array of selected nodes. + * + * Note: you cannot send this result via Ajax directly. Instead the + * node object need to be converted to plain objects, for example + * by using `$.map()` and `node.toDict()`. + * @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 () { + // var ae = document.activeElement, + // hasFocus = !!( + // ae && $(ae).closest(".fancytree-container").length + // ); + + // if (hasFocus !== !!this._hasFocus) { + // this.warn( + // "hasFocus(): fix inconsistent container state, now: " + + // hasFocus + // ); + // this._hasFocus = hasFocus; + // this.$container.toggleClass("fancytree-treefocus", hasFocus); + // } + // return hasFocus; + return !!this._hasFocus; + }, + /** Write to browser console if debugLevel >= 3 (prepending tree name) + * @param {*} msg string or object or array of such + */ + info: function (msg) { + if (this.options.debugLevel >= 3) { + Array.prototype.unshift.call(arguments, this.toString()); + consoleApply("info", arguments); + } + }, + /** Return true if any node is currently beeing loaded, i.e. a Ajax request is pending. + * @returns {boolean} + * @since 2.32 + */ + isLoading: function () { + var res = false; + + this.rootNode.visit(function (n) { + // also visit rootNode + if (n._isLoading || n._requestId) { + res = true; + return false; + } + }, true); + return res; + }, + /* + 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 meant for lazy hierarchies. + * A callback is executed for every node as we go. + * @example + * // Resolve using node.key: + * tree.loadKeyPath("/_3/_23/_26/_27", function(node, status){ + * if(status === "loaded") { + * console.log("loaded intermediate node " + node); + * }else if(status === "ok") { + * node.activate(); + * } + * }); + * // Use deferred promise: + * tree.loadKeyPath("/_3/_23/_26/_27").progress(function(data){ + * if(data.status === "loaded") { + * console.log("loaded intermediate node " + data.node); + * }else if(data.status === "ok") { + * node.activate(); + * } + * }).done(function(){ + * ... + * }); + * // Custom path segment resolver: + * tree.loadKeyPath("/321/431/21/2", { + * matchKey: function(node, key){ + * return node.data.refKey === key; + * }, + * callback: function(node, status){ + * if(status === "loaded") { + * console.log("loaded intermediate 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 | object} optsOrCallback callback(node, status) is called for every visited node ('loading', 'loaded', 'ok', 'error'). + * Pass an object to define custom key matchers for the path segments: {callback: function, matchKey: function}. + * @returns {$.Promise} + */ + loadKeyPath: function (keyPathList, optsOrCallback) { + var callback, + i, + path, + self = this, + dfd = new $.Deferred(), + parent = this.getRootNode(), + sep = this.options.keyPathSeparator, + pathSegList = [], + opts = $.extend({}, optsOrCallback); + + // Prepare options + if (typeof optsOrCallback === "function") { + callback = optsOrCallback; + } else if (optsOrCallback && optsOrCallback.callback) { + callback = optsOrCallback.callback; + } + opts.callback = function (ctx, node, status) { + if (callback) { + callback.call(ctx, node, status); + } + dfd.notifyWith(ctx, [{ node: node, status: status }]); + }; + if (opts.matchKey == null) { + opts.matchKey = function (node, key) { + return node.key === key; + }; + } + // Convert array of path strings to array of segment arrays + if (!_isArray(keyPathList)) { + keyPathList = [keyPathList]; + } + for (i = 0; i < keyPathList.length; i++) { + path = keyPathList[i]; + // strip leading slash + if (path.charAt(0) === sep) { + path = path.substr(1); + } + // segListMap[path] = { parent: parent, segList: path.split(sep) }; + pathSegList.push(path.split(sep)); + // targetList.push({ parent: parent, segList: path.split(sep)/* , path: path*/}); + } + // The timeout forces async behavior always (even if nodes are all loaded) + // This way a potential progress() event will fire. + setTimeout(function () { + self._loadKeyPathImpl(dfd, opts, parent, pathSegList).done( + function () { + dfd.resolve(); + } + ); + }, 0); + return dfd.promise(); + }, + /* + * Resolve a list of paths, relative to one parent node. + */ + _loadKeyPathImpl: function (dfd, opts, parent, pathSegList) { + var deferredList, + i, + key, + node, + nodeKey, + remain, + remainMap, + tmpParent, + segList, + subDfd, + self = this; + + function __findChild(parent, key) { + // console.log("__findChild", key, parent); + var i, + l, + cl = parent.children; + + if (cl) { + for (i = 0, l = cl.length; i < l; i++) { + if (opts.matchKey(cl[i], key)) { + return cl[i]; + } + } + } + return null; + } + + // console.log("_loadKeyPathImpl, parent=", parent, ", pathSegList=", pathSegList); + + // Pass 1: + // Handle all path segments for nodes that are already loaded. + // Collect distinct top-most lazy nodes in a map. + // Note that we can use node.key to de-dupe entries, even if a custom matcher would + // look for other node attributes. + // map[node.key] => {node: node, pathList: [list of remaining rest-paths]} + remainMap = {}; + + for (i = 0; i < pathSegList.length; i++) { + segList = pathSegList[i]; + // target = targetList[i]; + + // Traverse and pop path segments (i.e. keys), until we hit a lazy, unloaded node + tmpParent = parent; + while (segList.length) { + key = segList.shift(); + node = __findChild(tmpParent, key); + if (!node) { + this.warn( + "loadKeyPath: key not found: " + + key + + " (parent: " + + tmpParent + + ")" + ); + opts.callback(this, key, "error"); + break; + } else if (segList.length === 0) { + opts.callback(this, node, "ok"); + break; + } else if (!node.lazy || node.hasChildren() !== undefined) { + opts.callback(this, node, "loaded"); + tmpParent = node; + } else { + opts.callback(this, node, "loaded"); + key = node.key; //target.segList.join(sep); + if (remainMap[key]) { + remainMap[key].pathSegList.push(segList); + } else { + remainMap[key] = { + parent: node, + pathSegList: [segList], + }; + } + break; + } + } + } + // console.log("_loadKeyPathImpl AFTER pass 1, remainMap=", remainMap); + + // Now load all lazy nodes and continue iteration for remaining paths + deferredList = []; + + // Avoid jshint warning 'Don't make functions within a loop.': + function __lazyload(dfd, parent, pathSegList) { + // console.log("__lazyload", parent, "pathSegList=", pathSegList); + opts.callback(self, parent, "loading"); + parent + .load() + .done(function () { + self._loadKeyPathImpl + .call(self, dfd, opts, parent, pathSegList) + .always(_makeResolveFunc(dfd, self)); + }) + .fail(function (errMsg) { + self.warn("loadKeyPath: error loading lazy " + parent); + opts.callback(self, node, "error"); + dfd.rejectWith(self); + }); + } + // remainMap contains parent nodes, each with a list of relative sub-paths. + // We start loading all of them now, and pass the the list to each loader. + for (nodeKey in remainMap) { + if (_hasProp(remainMap, nodeKey)) { + remain = remainMap[nodeKey]; + // console.log("for(): remain=", remain, "remainMap=", remainMap); + // key = remain.segList.shift(); + // node = __findChild(remain.parent, key); + // if (node == null) { // #576 + // // Issue #576, refactored for v2.27: + // // The root cause was, that sometimes the wrong parent was used here + // // to find the next segment. + // // Falling back to getNodeByKey() was a hack that no longer works if a custom + // // matcher is used, because we cannot assume that a single segment-key is unique + // // throughout the tree. + // self.error("loadKeyPath: error loading child by key '" + key + "' (parent: " + target.parent + ")", target); + // // node = self.getNodeByKey(key); + // continue; + // } + subDfd = new $.Deferred(); + deferredList.push(subDfd); + __lazyload(subDfd, remain.parent, remain.pathSegList); + } + } + // 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); + }, + /**(De)select all nodes. + * @param {boolean} [flag=true] + * @since 2.28 + */ + selectAll: function (flag) { + this.visit(function (node) { + node.setSelected(flag); + }); + }, + // TODO: selectKey: function(key, select) + // TODO: serializeArray: function(stopOnParents) + /** + * @param {boolean} [flag=true] + */ + setFocus: function (flag) { + return this._callHook("treeSetFocus", this, flag); + }, + /** + * Set current option value. + * (Note: this is the preferred variant of `$().fancytree("option", "KEY", VALUE)`) + * @param {string} name option name (may contain '.') + * @param {any} new value + */ + setOption: function (optionName, value) { + return this.widget.option(optionName, value); + }, + /** + * Call console.time() when in debug mode (verbose >= 4). + * + * @param {string} label + */ + debugTime: function (label) { + if (this.options.debugLevel >= 4) { + window.console.time(this + " - " + label); + } + }, + /** + * Call console.timeEnd() when in debug mode (verbose >= 4). + * + * @param {string} label + */ + debugTimeEnd: function (label) { + if (this.options.debugLevel >= 4) { + window.console.timeEnd(this + " - " + label); + } + }, + /** + * 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. + * Return `false` to ignore this node or "skip" to include this node without its children. + * @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; + // return ""; + }, + /* _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 in hierarchical order (depth-first). + * + * @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); + }, + /** Call fn(node) for all nodes in vertical order, top down (or bottom up).
                + * Stop iteration, if fn() returns false.
                + * 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 {object} [options] + * Defaults: + * {start: First top node, reverse: false, includeSelf: true, includeHidden: false} + * @returns {boolean} false if iteration was cancelled + * @since 2.28 + */ + visitRows: function (fn, opts) { + if (!this.rootNode.hasChildren()) { + return false; + } + if (opts && opts.reverse) { + delete opts.reverse; + return this._visitRowsUp(fn, opts); + } + opts = opts || {}; + + var i, + nextIdx, + parent, + res, + siblings, + siblingOfs = 0, + skipFirstNode = opts.includeSelf === false, + includeHidden = !!opts.includeHidden, + checkFilter = !includeHidden && this.enableFilter, + node = opts.start || this.rootNode.children[0]; + + parent = node.parent; + while (parent) { + // visit siblings + siblings = parent.children; + nextIdx = siblings.indexOf(node) + siblingOfs; + _assert( + nextIdx >= 0, + "Could not find " + + node + + " in parent's children: " + + parent + ); + + for (i = nextIdx; i < siblings.length; i++) { + node = siblings[i]; + if (checkFilter && !node.match && !node.subMatchCount) { + continue; + } + if (!skipFirstNode && fn(node) === false) { + return false; + } + skipFirstNode = false; + // Dive into node's child nodes + if ( + node.children && + node.children.length && + (includeHidden || node.expanded) + ) { + // Disable warning: Functions declared within loops referencing an outer + // scoped variable may lead to confusing semantics: + /*jshint -W083 */ + res = node.visit(function (n) { + if (checkFilter && !n.match && !n.subMatchCount) { + return "skip"; + } + if (fn(n) === false) { + return false; + } + if (!includeHidden && n.children && !n.expanded) { + return "skip"; + } + }, false); + /*jshint +W083 */ + if (res === false) { + return false; + } + } + } + // Visit parent nodes (bottom up) + node = parent; + parent = parent.parent; + siblingOfs = 1; // + } + return true; + }, + /* Call fn(node) for all nodes in vertical order, bottom up. + */ + _visitRowsUp: function (fn, opts) { + var children, + idx, + parent, + includeHidden = !!opts.includeHidden, + node = opts.start || this.rootNode.children[0]; + + while (true) { + parent = node.parent; + children = parent.children; + + if (children[0] === node) { + // If this is already the first sibling, goto parent + node = parent; + if (!node.parent) { + break; // first node of the tree + } + children = parent.children; + } else { + // Otherwise, goto prev. sibling + idx = children.indexOf(node); + node = children[idx - 1]; + // If the prev. sibling has children, follow down to last descendant + while ( + // See: https://github.com/eslint/eslint/issues/11302 + // eslint-disable-next-line no-unmodified-loop-condition + (includeHidden || node.expanded) && + node.children && + node.children.length + ) { + children = node.children; + parent = node; + node = children[children.length - 1]; + } + } + // Skip invisible + if (!includeHidden && !node.isVisible()) { + continue; + } + if (fn(node) === false) { + return false; + } + } + }, + /** Write warning to browser console if debugLevel >= 2 (prepending tree info) + * + * @param {*} msg string or object or array of such + */ + warn: function (msg) { + if (this.options.debugLevel >= 2) { + 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 [writing extensions](https://github.com/mar10/fancytree/wiki/TutorialExtensions) + * @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 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 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, + // #909: Use event.key, to get unicode characters. + // We can't use `/\w/.test(key)`, because that would + // only detect plain ascii alpha-numerics. But we still need + // to ignore modifier-only, whitespace, cursor-keys, etc. + key = event.key || String.fromCharCode(which), + specialModifiers = !!( + event.altKey || + event.ctrlKey || + event.metaKey + ), + isAlnum = + !MODIFIERS[which] && + !SPECIAL_KEYCODES[which] && + !specialModifiers, + $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(): " + FT.eventToString(event) + ", key='" + key + "', isAlnum: " + isAlnum ); + + // 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 && + isAlnum && + !$target.is(":input:enabled") + ) { + // Allow to search for longer streaks if typed in quickly + stamp = Date.now(); + if (stamp - tree.lastQuicksearchTime > 500) { + tree.lastQuicksearchTerm = ""; + } + tree.lastQuicksearchTime = stamp; + tree.lastQuicksearchTerm += key; + // 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); + 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, + ajaxDfd = null, + resultDfd, + isAsync = true, + tree = ctx.tree, + node = ctx.node, + nodePrevParent = node.parent, + tag = "nodeLoadChildren", + requestId = Date.now(); + + // `source` is a callback: use the returned result instead: + if (_isFunction(source)) { + source = source.call(tree, { type: "source" }, ctx); + _assert( + !_isFunction(source), + "source callback must not return another function" + ); + } + // `source` is already a promise: + if (_isFunction(source.then)) { + // _assert(_isFunction(source.always), "Expected jQuery?"); + ajaxDfd = source; + } else if (source.url) { + // `source` is an Ajax options object + ajax = $.extend({}, ctx.options.ajax, source); + if (ajax.debugDelay) { + // Simulate a slow server + delay = ajax.debugDelay; + delete ajax.debugDelay; // remove debug option + 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 ..." + ); + ajaxDfd = $.Deferred(function (ajaxDfd) { + setTimeout(function () { + $.ajax(ajax) + .done(function () { + ajaxDfd.resolveWith(this, arguments); + }) + .fail(function () { + ajaxDfd.rejectWith(this, arguments); + }); + }, delay); + }); + } else { + ajaxDfd = $.ajax(ajax); + } + } else if ($.isPlainObject(source) || _isArray(source)) { + // `source` is already a constant dict or list, but we convert + // to a thenable for unified processing. + // 2020-01-03: refactored. + // `ajaxDfd = $.when(source)` would do the trick, but the returned + // promise will resolve async, which broke some tests and + // would probably also break current implementations out there. + // So we mock-up a thenable that resolves synchronously: + ajaxDfd = { + then: function (resolve, reject) { + resolve(source, null, null); + }, + }; + isAsync = false; + } else { + $.error("Invalid source type: " + source); + } + + // Check for overlapping requests + if (node._requestId) { + node.warn( + "Recursive load request #" + + requestId + + " while #" + + node._requestId + + " is pending." + ); + node._requestId = requestId; + // node.debug("Send load request #" + requestId); + } + + if (isAsync) { + tree.debugTime(tag); + tree.nodeSetStatus(ctx, "loading"); + } + + // The async Ajax request has now started... + // Defer the deferred: + // we want to be able to reject invalid responses, even if + // the raw HTTP Ajax XHR resolved as Ok. + // We use the ajaxDfd.then() syntax here, which is compatible with + // jQuery and ECMA6. + // However resultDfd is a jQuery deferred, which is currently the + // expected result type of nodeLoadChildren() + resultDfd = new $.Deferred(); + ajaxDfd.then( + function (data, textStatus, jqXHR) { + // ajaxDfd was resolved, but we reject or resolve resultDfd + // depending on the response data + var errorObj, res; + + if ( + (source.dataType === "json" || + source.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 + ")"); + resultDfd.rejectWith(this, [ + RECURSIVE_REQUEST_ERROR, + ]); + return; + // } else { + // node.debug("Response returned for load request #" + requestId); + } + if (node.parent === null && nodePrevParent !== null) { + resultDfd.rejectWith(this, [ + INVALID_REQUEST_TARGET_ERROR, + ]); + return; + } + // Allow to adjust the received response data in the `postProcess` event. + if (ctx.options.postProcess) { + // The handler may either + // - modify `ctx.response` in-place (and leave `ctx.result` undefined) + // => res = undefined + // - return a replacement in `ctx.result` + // => res = + // If res contains an `error` property, an error status is displayed + try { + res = tree._triggerNodeEvent( + "postProcess", + ctx, + ctx.originalEvent, + { + response: data, + error: null, + dataType: source.dataType, + } + ); + if (res.error) { + tree.warn( + "postProcess returned error:", + res + ); + } + } catch (e) { + res = { + error: e, + message: "" + e, + details: "postProcess failed", + }; + } + if (res.error) { + // Either postProcess failed with an exception, or the returned + // result object has an 'error' property attached: + errorObj = $.isPlainObject(res.error) + ? res.error + : { message: res.error }; + errorObj = tree._makeHookContext( + node, + null, + errorObj + ); + resultDfd.rejectWith(this, [errorObj]); + return; + } + if ( + _isArray(res) || + ($.isPlainObject(res) && _isArray(res.children)) + ) { + // Use `ctx.result` if valid + // (otherwise use existing data, which may have been modified in-place) + data = res; + } + } else if ( + data && + _hasProp(data, "d") && + ctx.options.enableAspx + ) { + // Process ASPX WebMethod JSON object inside "d" property + // (only if no postProcess event was defined) + if (ctx.options.enableAspx === 42) { + tree.warn( + "The default for enableAspx will change to `false` in the fututure. " + + "Pass `enableAspx: true` or implement postProcess to silence this warning." + ); + } + data = + typeof data.d === "string" + ? $.parseJSON(data.d) + : data.d; + } + resultDfd.resolveWith(this, [data]); + }, + function (jqXHR, textStatus, errorThrown) { + // ajaxDfd was rejected, so we reject resultDfd as well + var errorObj = tree._makeHookContext(node, null, { + error: jqXHR, + args: Array.prototype.slice.call(arguments), + message: errorThrown, + details: jqXHR.status + ": " + errorThrown, + }); + resultDfd.rejectWith(this, [errorObj]); + } + ); + + // The async Ajax request has now started. + // resultDfd will be resolved/rejected after the response arrived, + // was postProcessed, and checked. + // Now we implement the UI update and add the data to the tree. + // We also return this promise to the caller. + resultDfd + .done(function (data) { + tree.nodeSetStatus(ctx, "ok"); + var children, metaData, noDataRes; + + if ($.isPlainObject(data)) { + // 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(data.children), + "if an object is passed as source, it must contain a 'children' array (all other properties are added to 'tree.data')" + ); + metaData = data; + children = data.children; + delete metaData.children; + // Copy some attributes to tree.data + $.each(TREE_ATTRS, function (i, attr) { + if (metaData[attr] !== undefined) { + tree[attr] = metaData[attr]; + delete metaData[attr]; + } + }); + // Copy all other attributes to tree.data.NAME + $.extend(tree.data, metaData); + } else { + children = data; + } + _assert( + _isArray(children), + "expected array of children" + ); + node._setChildren(children); + + if (tree.options.nodata && children.length === 0) { + if (_isFunction(tree.options.nodata)) { + noDataRes = tree.options.nodata.call( + tree, + { type: "nodata" }, + ctx + ); + } else if ( + tree.options.nodata === true && + node.isRootNode() + ) { + noDataRes = tree.options.strings.noData; + } else if ( + typeof tree.options.nodata === "string" && + node.isRootNode() + ) { + noDataRes = tree.options.nodata; + } + if (noDataRes) { + node.setStatus("nodata", noDataRes); + } + } + // trigger fancytreeloadchildren + tree._triggerNodeEvent("loadChildren", node); + }) + .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 === INVALID_REQUEST_TARGET_ERROR) { + node.warn( + "Lazy parent node was removed while loading: discarding response." + ); + 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 + ); + } + }) + .always(function () { + node._requestId = null; + if (isAsync) { + tree.debugTimeEnd(tag); + } + }); + + return resultDfd.promise(); + }, + /** [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 `
              • .. ..
              • ` 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. + * + * ```html + *
              • + * + * + * // only present in checkbox mode + * + * Node 1 + * + *
                  // only present if node has children + *
                • child1 ...
                • + *
                • child2 ...
                • + *
                + *
              • + * ``` + * + * @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
                  + 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
                • + // node.debug("render..."); + if (node.li) { + // this.nodeRenderTitle(ctx); + this.nodeRenderStatus(ctx); + } else { + // 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 (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 + ); + } + } + // 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
                • 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
                • 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) { + childLI = childLI.nextSibling; + } else { + // node.debug("_fixOrder: mismatch at index " + i + ": " + childNode1 + " != " + childNode2); + node.ul.insertBefore( + childNode1.li, + childNode2.li + ); + } + } + } + } 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 `` (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, + iconTooltip, + 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( + "" + ); + } + // .. else (i.e. for root level) skip expander/connector alltogether + } else { + ares.push( + "" + ); + } + // 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( + "" + ); + } + // 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'" : ""; + + iconTooltip = FT.evalOption( + "iconTooltip", + node, + node, + opts, + null + ); + iconTooltip = iconTooltip + ? " title='" + _escapeTooltip(iconTooltip) + "'" + : ""; + + 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( + "" + ); + } else { + ares.push( + "" + ); + } + } else if (icon.text) { + ares.push( + "" + + FT.escapeHtml(icon.text) + + "" + ); + } else if (icon.html) { + ares.push( + "" + + icon.html + + "" + ); + } else { + // standard icon: theme css will take care of this + ares.push( + "" + ); + } + } + // 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 = + "" + + (opts.escapeTitles + ? FT.escapeHtml(node.title) + : node.title) + + ""; + } + 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 + 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.removeAttr("aria-expanded"); + } else { + $ariaElem.attr("aria-expanded", Boolean(node.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 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, + scroll = callOpts.scrollIntoView !== false, + isActive = node === tree.activeNode; + + // flag defaults to true + flag = flag !== false; + // node.debug("nodeSetActive", flag); + + if (isActive === flag) { + // Nothing to do + return _getResolvedPromise(node); + } + // #1042: don't scroll between mousedown/-up when clicking an embedded link + if ( + scroll && + ctx.originalEvent && + $(ctx.originalEvent.target).is("a,:checkbox") + ) { + node.info("Not scrolling while clicking an embedded link."); + scroll = false; + } + 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. + // scroll = noFocus && tree.focusNode == null; + // #863: scroll by default (unless `scrollIntoView: false` was passed) + node.makeVisible({ scrollIntoView: scroll }); + } + 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 finished. + */ + 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.li).hasClass(opts._classNames.animating)) { + node.warn( + "setExpanded(" + flag + ") while animating: ignored." + ); + return _getRejectedPromise(node, ["recursion"]); + } + + 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 && + tree._enableUpdate + ) { + // 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; + tree._callHook( + "treeStructureChanged", + ctx, + flag ? "expand" : "collapse" + ); + // 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 position: 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 + + if (_isFunction($(node.ul)[effect.effect])) { + // tree.debug( "use jquery." + effect.effect + " method" ); + $(node.ul)[effect.effect]({ + duration: effect.duration, + always: function () { + // node.debug("fancytree-animating end: " + node.li.className); + $(this).removeClass(cn.animating); // #716 + $(node.li).removeClass(cn.animating); // #717 + callback(); + }, + }); + } 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 + // tree.debug("use specified effect (" + effect.effect + ") with the jqueryui.toggle method"); + + // try to stop an animation that might be already in progress + $(node.ul).stop(true, true); //< does not work after resetLazy has been called for a node whose animation wasn't complete and effect was "blind" + + // dirty fix to remove a defunct animation (effect: "blind") after resetLazy has been called + $(node.ul) + .parent() + .find(".ui-effects-placeholder") + .remove(); + + $(node.ul).toggle( + effect.effect, + effect.options, + effect.duration, + function () { + // node.debug("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") + .trigger("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").trigger("focus"); + this._triggerNodeEvent("focus", ctx); + + // determine if we have focus on or inside tree container + var hasFancytreeFocus = + document.activeElement === tree.$container.get(0) || + $(document.activeElement, tree.$container).length >= 1; + + if (!hasFancytreeFocus) { + // We cannot set KB focus to a node, so use the tree container + // #563, #570: IE scrolls on every call to .trigger("focus"), if the container + // is partially outside the viewport. So do it only, when absolutely + // necessary. + $(tree.$container).trigger("focus"); + } + + // 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; // Confusing use of '!' + + // Nothing to do? + 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; + } + } + + 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(); + } + tree._callHook( + "treeStructureChanged", + ctx, + "clearStatusNode" + ); + } + } + 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]); + tree._callHook( + "treeStructureChanged", + ctx, + "setStatusNode" + ); + 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: message || 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; + tree._callHook("treeStructureChanged", ctx, "clear"); + }, + /** 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(); + if (this.$source) { + this.$source.removeClass("fancytree-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); + + // Copy some attributes to tree.data + $.each(TREE_ATTRS, function (i, attr) { + if (opts[attr] !== undefined) { + tree.info("Move option " + attr + " to tree"); + tree[attr] = opts[attr]; + delete opts[attr]; + } + }); + + if (opts.checkboxAutoHide) { + tree.$container.addClass("fancytree-checkbox-auto-hide"); + } + 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": + // There should be an embedded `
                    ` with initial nodes, + // but another `
                      ` is appended + // to the tree's
                      on startup anyway. + $ul = $container + .find(">ul") + .not(".fancytree-container") + .first(); + + if ($ul.length) { + $ul.addClass( + "ui-fancytree-source fancytree-helper-hidden" + ); + source = $.ui.fancytree.parseHtml($ul); + // allow to init tree.data.foo from
                        + this.data = $.extend( + this.data, + _getElementDataAsDict($ul) + ); + } else { + FT.warn( + "No `source` option was passed and container does not contain `
                          `: assuming `source: []`." + ); + source = []; + } + break; + case "json": + source = $.parseJSON($container.text()); + // $container already contains the
                            , 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: [...]} + _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; + // Copy some attributes to tree.data + $.each(TREE_ATTRS, function (i, attr) { + if (metaData[attr] !== undefined) { + tree[attr] = metaData[attr]; + delete metaData[attr]; + } + }); + // Copy extra properties to tree.data.foo + $.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"); + } + + // preInit is fired when the widget markup is created, but nodes + // not yet loaded + tree._triggerTreeEvent("preInit", null); + + // Trigger fancytreeinit after nodes have been loaded + dfd = this.nodeLoadChildren(rootCtx, source) + .done(function () { + tree._callHook( + "treeStructureChanged", + ctx, + "loadChildren" + ); + 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) { + ctx.tree._callHook( + "treeStructureChanged", + ctx, + add ? "addNode" : "removeNode" + ); + }, + /** 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).trigger("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(); + if (targetNode) { + targetNode.setFocus(); + } + } + } + }, + /** Widget option was set using `$().fancytree("option", "KEY", VALUE)`. + * + * Note: `key` may reference a nested option, e.g. 'dnd5.scroll'. + * In this case `value`contains the complete, modified `dnd5` option hash. + * We can check for changed values like + * if( value.scroll !== tree.options.dnd5.scroll ) {...} + * + * @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 "checkboxAutoHide": + tree.$container.toggleClass( + "fancytree-checkbox-auto-hide", + !!value + ); + 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 + } + }, + /** A Node was added, removed, moved, or it's visibility changed. + * @param {EventData} ctx + */ + treeStructureChanged: function (ctx, type) {}, + } + ); + + /******************************************************************************* + * jQuery UI widget boilerplate + */ + + /** + * The plugin (derrived from [jQuery.Widget](http://api.jqueryui.com/jQuery.widget/)). + * + * **Note:** + * These methods implement the standard jQuery UI widget API. + * It is recommended to use methods of the {Fancytree} instance instead + * + * @example + * // DEPRECATED: Access jQuery UI widget methods and members: + * var tree = $("#tree").fancytree("getTree"); + * var node = $("#tree").fancytree("getActiveNode"); + * + * // RECOMMENDED: Use the Fancytree object API + * var tree = $.ui.fancytree.getTree("#tree"); + * var node = tree.getActiveNode(); + * + * // or you may already have stored the tree instance upon creation: + * import {createTree, version} from 'jquery.fancytree' + * const tree = createTree('#tree', { ... }); + * var node = tree.getActiveNode(); + * + * @see {Fancytree_Static#getTree} + * @deprecated Use methods of the {Fancytree} instance instead + * @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, + copyFunctionsToData: false, + debugLevel: null, // 0..4 (null: use global setting $.ui.fancytree.debugLevel) + disabled: false, // TODO: required anymore? + enableAspx: 42, // TODO: this is truethy, but distinguishable from true: default will change to false in the future + escapeTitles: false, + extensions: [], + focusOnSelect: false, + generateIds: false, + icon: true, + idPrefix: "ft_", + keyboard: true, + keyPathSeparator: "/", + minExpandLevel: 1, + nodata: true, // (bool, string, or callback) display message, when no data available + quicksearch: false, + rtl: false, + scrollOfs: { top: 0, bottom: 0 }, + scrollParent: null, + selectMode: 2, + strings: { + loading: "Loading...", // … would be escaped when escapeTitles is true + loadError: "Load error!", + moreData: "More...", + noData: "No data.", + }, + tabindex: "0", + titlesTabbable: false, + toggleEffect: { effect: "slideToggle", duration: 200 }, //< "toggle" or "slideToggle" to use jQuery instead of jQueryUI for toggleEffect animation + tooltip: false, + treeId: null, + _classNames: { + active: "fancytree-active", + animating: "fancytree-animating", + combinedExpanderPrefix: "fancytree-exp-", + combinedIconPrefix: "fancytree-ico-", + error: "fancytree-error", + expanded: "fancytree-expanded", + focused: "fancytree-focused", + folder: "fancytree-folder", + hasChildren: "fancytree-has-children", + lastsib: "fancytree-lastsib", + lazy: "fancytree-lazy", + loading: "fancytree-loading", + node: "fancytree-node", + partload: "fancytree-partload", + partsel: "fancytree-partsel", + radio: "fancytree-radio", + selected: "fancytree-selected", + statusNodePrefix: "fancytree-statusnode-", + unselectable: "fancytree-unselectable", + }, + // events + lazyLoad: null, + postProcess: null, + }, + _deprecationWarning: function (name) { + var tree = this.tree; + + if (tree && tree.options.debugLevel >= 3) { + tree.warn( + "$().fancytree('" + + name + + "') is deprecated (see https://wwwendt.de/tech/fancytree/doc/jsdoc/Fancytree_Widget.html" + ); + } + }, + /* 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); + + // console.info("extend " + extName, extension.options, this.tree.options[extName]) + // issue #876: we want to replace custom array-options, not merge them + this.tree.options[extName] = _simpleDeepMerge( + {}, + extension.options, + this.tree.options[extName] + ); + // this.tree.options[extName] = $.extend(true, {}, extension.options, this.tree.options[extName]); + + // console.info("extend " + extName + " =>", this.tree.options[extName]) + // console.info("extend " + extName + " org default =>", extension.options) + + // 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) { + this.tree.warn( + "'icons' tree option is deprecated since v2.14.0: use 'icon' instead" + ); + opts.icon = opts.icons; + } else { + $.error( + "'icons' tree option is deprecated since v2.14.0: use 'icon' only instead" + ); + } + } + 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 event 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 self = 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"; + + if (!flag && node && $(event.target).is("a")) { + // #764 + node.debug( + "Ignored focusout on embedded element." + ); + return; + } + // tree.treeOnFocusInOut.call(tree, event); + // tree.debug("Tree container got event " + event.type, node, event, FT.getEventTarget(event)); + if (flag) { + if (tree._getExpiringValue("focusin")) { + // #789: IE 11 may send duplicate focusin events + tree.debug("Ignored double focusin."); + return; + } + tree._setExpiringValue("focusin", true, 50); + + if (!node) { + // #789: IE 11 may send focusin before mousdown(?) + node = tree._getExpiringValue("mouseDownNode"); + if (node) { + tree.debug( + "Reconstruct mouse target for focusin from recent event." + ); + } + } + } + if (node) { + // For example clicking into an 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(" 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); + // self.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 = self.tree, + prevPhase = tree.phase; + + // self.tree.debug("event(" + event.type + "): node: ", node); + if (!node) { + return true; // Allow bubbling of other events + } + ctx = tree._makeHookContext(node, event); + // self.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} + * @deprecated Use methods of the Fancytree instance instead (example above). + */ + getActiveNode: function () { + this._deprecationWarning("getActiveNode"); + return this.tree.activeNode; + }, + /** Return the matching node or null. + * @param {string} key + * @returns {FancytreeNode} + * @deprecated Use methods of the Fancytree instance instead (example above). + */ + getNodeByKey: function (key) { + this._deprecationWarning("getNodeByKey"); + return this.tree.getNodeByKey(key); + }, + /** Return the invisible system root node. + * @returns {FancytreeNode} + * @deprecated Use methods of the Fancytree instance instead (example above). + */ + getRootNode: function () { + this._deprecationWarning("getRootNode"); + return this.tree.rootNode; + }, + /** Return the current tree instance. + * @returns {Fancytree} + * @deprecated Use `$.ui.fancytree.getTree()` instead (example above). + */ + getTree: function () { + this._deprecationWarning("getTree"); + 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. + * This properties and methods can be accessed without instantiating a concrete + * Fancytree instance. + * + * @example + * // Access static members: + * var node = $.ui.fancytree.getNode(element); + * alert($.ui.fancytree.version); + * + * @mixin Fancytree_Static + */ + $.extend( + $.ui.fancytree, + /** @lends Fancytree_Static# */ + { + /** Version number `"MAJOR.MINOR.PATCH"` + * @type {string} */ + version: "2.38.5", // Set to semver by 'grunt release' + /** @type {string} + * @description `"production" for release builds` */ + buildType: "production", // Set to 'production' by 'grunt build' + /** @type {int} + * @description 0: silent .. 5: verbose (default: 3 for release builds). */ + debugLevel: 3, // Set to 3 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`. + * Useful to extend `$.ui.fancytree._FancytreeClass.prototype`. + * @type {Fancytree} + */ + _FancytreeClass: Fancytree, + /** Expose class object as $.ui.fancytree._FancytreeNodeClass + * Useful to extend `$.ui.fancytree._FancytreeNodeClass.prototype`. + * @type {FancytreeNode} + */ + _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); + return FT.getTree($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; + // eslint-disable-next-line no-unused-expressions + invokeAsap && !timer && fn.apply(ctx, args); + clearTimeout(timer); + timer = setTimeout(function () { + // eslint-disable-next-line no-unused-expressions + invokeAsap || fn.apply(ctx, args); + timer = null; + }, timeout); + }; + }, + /** Write message to console if debugLevel >= 4 + * @param {string} msg + */ + debug: function (msg) { + if ($.ui.fancytree.debugLevel >= 4) { + consoleApply("log", arguments); + } + }, + /** Write error message to console if debugLevel >= 1. + * @param {string} msg + */ + error: function (msg) { + if ($.ui.fancytree.debugLevel >= 1) { + consoleApply("error", arguments); + } + }, + /** Convert `<`, `>`, `&`, `"`, `'`, and `/` 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/ + * + * @param {object} opts + * @returns {object} the (potentially modified) original opts hash object + */ + 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 $target, + tree, + 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) { + $target = $(event.target); + if ($target.is("ul[role=group]")) { + // #nnn: Clicking right to a node may hit the surrounding UL + tree = res.node && res.node.tree; + (tree || FT).debug("Ignoring click on outer UL."); + res.node = null; + } else if ($target.closest(".fancytree-title").length) { + // #228: clicking an embedded element inside a title + res.type = "title"; + } else if ($target.closest(".fancytree-checkbox").length) { + // E.g. inside checkbox span + res.type = "checkbox"; + } else if ($target.closest(".fancytree-expander").length) { + res.type = "expander"; + } + } + 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(event); // Get tree for this mouse- or keyboard event + * $.ui.fancytree.getTree("foo"); // Get tree for this `opts.treeId` + * $.ui.fancytree.getTree("#tree"); // Get tree for this matching element + * + * @since 2.13 + */ + getTree: function (el) { + var widget, + orgEl = el; + + 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` may be a treeId or a selector: + el = $("#ft-id-" + orgEl).eq(0); + if (!el.length) { + el = $(orgEl).eq(0); // el was a selector: use first match + } + } else if ( + el instanceof Element || + el instanceof HTMLDocument + ) { + el = $(el); + } else if (el instanceof $) { + el = el.eq(0); // el was a jQuery object: use the first + } else if (el.originalEvent !== undefined) { + el = $(el.target); // el was an Event + } + // el is a jQuery object wit one element here + 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: + * + * If `tree.options.` is a callback that returns something, use that. + * Else if `node.` is defined, use that. + * Else if `tree.options.` is a value, use that. + * 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, + typeInfo: tree.types[node.type] || {}, + }; + res = treeOpt.call(tree, { type: optionName }, ctx); + if (res == null) { + res = nodeOpt; + } + } else { + res = nodeOpt == null ? treeOpt : nodeOpt; + } + if (res == null) { + res = defaultValue; // no option set at all: return default + } + return res; + }, + /** Set expander, checkbox, or node icon, supporting string and object format. + * + * @param {Element | jQueryObject} span + * @param {string} baseClass + * @param {string | object} icon + * @since 2.27 + */ + setSpanIcon: function (span, baseClass, icon) { + var $span = $(span); + + if (typeof icon === "string") { + $span.attr("class", baseClass + " " + icon); + } else { + // support object syntax: { text: ligature, addClasse: classname } + if (icon.text) { + $span.text("" + icon.text); + } else if (icon.html) { + span.innerHTML = icon.html; + } + $span.attr( + "class", + baseClass + " " + (icon.addClass || "") + ); + } + }, + /** 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); + 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 (et === "wheel") { + s.push(et); + } else if (!IGNORE_KEYCODES[which]) { + s.push( + SPECIAL_KEYCODES[which] || + String.fromCharCode(which).toLowerCase() + ); + } + return s.join("+"); + }, + /** Write message to console if debugLevel >= 3 + * @param {string} msg + */ + info: function (msg) { + if ($.ui.fancytree.debugLevel >= 3) { + 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 + * @param {object} [context] optional context + */ + overrideMethod: function (instance, methodName, handler, context) { + var prevSuper, + _super = instance[methodName] || $.noop; + + instance[methodName] = function () { + var self = context || this; + + try { + prevSuper = self._super; + self._super = _super; + return handler.apply(self, arguments); + } finally { + self._super = prevSuper; + } + }; + }, + /** + * Parse tree data from HTML
                              markup + * + * @param {jQueryObject} $ul + * @returns {NodeData[]} + */ + parseHtml: function ($ul) { + 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", this).first(), + $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
                            • 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
                            • tag is specified, use the trimmed string up to + // the next child
                                tag. + d.title = $li.html(); + iPos = d.title.search(/
                                  = 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 + } + tmp = $li.attr("id"); + if (tmp) { + d.key = tmp; + } + // Translate hideCheckbox -> checkbox:false + if ($li.attr("hideCheckbox")) { + d.checkbox = false; + } + // Add
                                • 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 (_hasProp(allData, 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; + }, + /** Replacement for the deprecated `jQuery.trim()`. + * + * @param {string} text + */ + trim: _trim, + /** 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 if debugLevel >= 2. + * @param {string} msg + */ + warn: function (msg) { + if ($.ui.fancytree.debugLevel >= 2) { + 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](https://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-2023, Martin Wendt (https://wwWendt.de) + * + * Released under the MIT license + * https://github.com/mar10/fancytree/wiki/LicenseInfo + * + * @version 2.38.5 + * @date 2025-04-05T06:40:00Z + */ + +// 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 /eslint compliance. + // But for this sample, we want to allow unused variables for demonstration purpose. + + /*eslint-disable no-unused-vars */ + + // Adding methods + // -------------- + + // New member functions can be added to the `Fancytree` class. + // This function will be available for every tree instance: + // + // var tree = $.ui.fancytree.getTree("#tree"); + // 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 = $("").appendTo( + $( + "span.fancytree-icon,span.fancytree-custom-icon", + node.span + ) + ); + } + $badge.text(count); + } else { + $badge.remove(); + } + if (extOpts.deep && !node.isTopLevel() && !node.isRootNode()) { + 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](https://wwWendt.de/tech/fancytree/demo/sample-ext-childcounter.html)): + // + // + // + // ... + // + // $("#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.38.5", + + // 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](https://wwWendt.de/tech/fancytree/doc/jsdoc/global.html#EventData) + // for details) and an extended calling context:
                                  + // `this` : the Fancytree instance
                                  + // `this._local`: the namespace that contains extension attributes and private methods (same as this.ext.EXTNAME)
                                  + // `this._super`: the virtual function that was overridden (member of previous extension or Fancytree) + // + // See also the [complete list of available hook functions](https://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,span.fancytree-custom-icon", + node.span + ).append( + $("").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-2023, Martin Wendt (https://wwWendt.de) + * + * Released under the MIT license + * https://github.com/mar10/fancytree/wiki/LicenseInfo + * + * @version 2.38.5 + * @date 2025-04-05T06:40:00Z + */ + +(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 _assert = $.ui.fancytree.assert; + + /* 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; + } + + /** + * JS Implementation of MurmurHash3 (r136) (as of May 20, 2011) + * + * @author
                                  Gary Court + * @see http://github.com/garycourt/murmurhash-js + * @author Austin Appleby + * @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) { + /*eslint-disable no-bitwise */ + 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) { + case 3: + k1 ^= (key.charCodeAt(i + 2) & 0xff) << 16; + // fall through + case 2: + k1 ^= (key.charCodeAt(i + 1) & 0xff) << 8; + // fall through + 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; + /*eslint-enable no-bitwise */ + } + + /* + * Return a unique key for node by calculating the hash of the parents refKey-list. + */ + function calcUniqueKey(node) { + var key, + h1, + path = $.map(node.getParentList(false, true), function (e) { + return e.refKey || e.key; + }); + + path = path.join("/"); + // 32-bit has a high probability of collisions, so we pump up to 64-bit + // https://security.stackexchange.com/q/209882/207588 + + h1 = hashMurmur3(path, true); + key = "id_" + h1 + hashMurmur3(h1 + path, true); + + return key; + } + + /** + * [ext-clones] Return a list of clone-nodes (i.e. same refKey) 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.38.5", + // 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) { + var other = keyMap[node.key], + msg = + "clones.treeRegisterNode: duplicate key '" + + node.key + + "': /" + + node.getPath(true) + + " => " + + other.getPath(true); + // Sometimes this exception is not visible in the console, + // so we also write it: + tree.error(msg); + $.error(msg); + } + 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-2023, Martin Wendt (https://wwWendt.de) + * + * Released under the MIT license + * https://github.com/mar10/fancytree/wiki/LicenseInfo + * + * @version 2.38.5 + * @date 2025-04-05T06:40:00Z + */ + +/* + #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 FT = $.ui.fancytree, + isMac = /Mac/.test(navigator.platform), + 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, + $dragImage, + $extraHelper, + SOURCE_NODE = null, + SOURCE_NODE_LIST = null, + $sourceList = null, + DRAG_ENTER_RESPONSE = null, + // SESSION_DATA = null, // plain object passed to events as `data` + SUGGESTED_DROP_EFFECT = null, + REQUESTED_DROP_EFFECT = null, + REQUESTED_EFFECT_ALLOWED = null, + LAST_HIT_MODE = null, + DRAG_OVER_STAMP = null; // Time when a node entered the 'over' hitmode + + /* */ + function _clearGlobals() { + DRAG_ENTER_RESPONSE = null; + DRAG_OVER_STAMP = null; + REQUESTED_DROP_EFFECT = null; + REQUESTED_EFFECT_ALLOWED = null; + SUGGESTED_DROP_EFFECT = null; + SOURCE_NODE = null; + SOURCE_NODE_LIST = null; + if ($sourceList) { + $sourceList.removeClass(classDragSource + " " + classDragRemove); + } + $sourceList = null; + if ($dropMarker) { + $dropMarker.hide(); + } + // Take this badge off of me - I can't use it anymore: + if ($extraHelper) { + $extraHelper.remove(); + $extraHelper = null; + } + } + + /* Convert number to string and prepend +/-; return empty string for 0.*/ + function offsetString(n) { + // eslint-disable-next-line no-nested-ternary + 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 (Array.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; + } + + /* Convert a dataTransfer.effectAllowed to a canonical form. + * Return false or plain object + * @param {string|boolean} r + * @return {object|false} + */ + // function normalizeEffectAllowed(r) { + // if (!r || r === "none") { + // return false; + // } + // var all = r === "all", + // res = { + // copy: all || /copy/i.test(r), + // link: all || /link/i.test(r), + // move: all || /move/i.test(r), + // }; + + // 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; + } + + /* Guess dropEffect from modifier keys. + * Using rules suggested here: + * https://ux.stackexchange.com/a/83769 + * @returns + * 'copy', 'link', 'move', or 'none' + */ + function evalEffectModifiers(tree, event, effectDefault) { + var res = effectDefault; + + if (isMac) { + if (event.metaKey && event.altKey) { + // Mac: [Control] + [Option] + res = "link"; + } else if (event.ctrlKey) { + // Chrome on Mac: [Control] + res = "link"; + } else if (event.metaKey) { + // Mac: [Command] + res = "move"; + } else if (event.altKey) { + // Mac: [Option] + res = "copy"; + } + } else { + if (event.ctrlKey) { + // Windows: [Ctrl] + res = "copy"; + } else if (event.shiftKey) { + // Windows: [Shift] + res = "move"; + } else if (event.altKey) { + // Windows: [Alt] + res = "link"; + } + } + if (res !== SUGGESTED_DROP_EFFECT) { + tree.info( + "evalEffectModifiers: " + + event.type + + " - evalEffectModifiers(): " + + SUGGESTED_DROP_EFFECT + + " -> " + + res + ); + } + SUGGESTED_DROP_EFFECT = res; + // tree.debug("evalEffectModifiers: " + res); + return res; + } + /* + * Check if the previous callback (dragEnter, dragOver, ...) has changed + * the `data` object and apply those settings. + * + * Safari: + * It seems that `dataTransfer.dropEffect` can only be set on dragStart, and will remain + * even if the cursor changes when [Alt] or [Ctrl] are pressed (?) + * Using rules suggested here: + * https://ux.stackexchange.com/a/83769 + * @returns + * 'copy', 'link', 'move', or 'none' + */ + function prepareDropEffectCallback(event, data) { + var tree = data.tree, + dataTransfer = data.dataTransfer; + + if (event.type === "dragstart") { + data.effectAllowed = tree.options.dnd5.effectAllowed; + data.dropEffect = tree.options.dnd5.dropEffectDefault; + } else { + data.effectAllowed = REQUESTED_EFFECT_ALLOWED; + data.dropEffect = REQUESTED_DROP_EFFECT; + } + data.dropEffectSuggested = evalEffectModifiers( + tree, + event, + tree.options.dnd5.dropEffectDefault + ); + data.isMove = data.dropEffect === "move"; + data.files = dataTransfer.files || []; + + // if (REQUESTED_EFFECT_ALLOWED !== dataTransfer.effectAllowed) { + // tree.warn( + // "prepareDropEffectCallback(" + + // event.type + + // "): dataTransfer.effectAllowed changed from " + + // REQUESTED_EFFECT_ALLOWED + + // " -> " + + // dataTransfer.effectAllowed + // ); + // } + // if (REQUESTED_DROP_EFFECT !== dataTransfer.dropEffect) { + // tree.warn( + // "prepareDropEffectCallback(" + + // event.type + + // "): dataTransfer.dropEffect changed from requested " + + // REQUESTED_DROP_EFFECT + + // " to " + + // dataTransfer.dropEffect + // ); + // } + } + + function applyDropEffectCallback(event, data, allowDrop) { + var tree = data.tree, + dataTransfer = data.dataTransfer; + + if ( + event.type !== "dragstart" && + REQUESTED_EFFECT_ALLOWED !== data.effectAllowed + ) { + tree.warn( + "effectAllowed should only be changed in dragstart event: " + + event.type + + ": data.effectAllowed changed from " + + REQUESTED_EFFECT_ALLOWED + + " -> " + + data.effectAllowed + ); + } + + if (allowDrop === false) { + tree.info("applyDropEffectCallback: allowDrop === false"); + data.effectAllowed = "none"; + data.dropEffect = "none"; + } + // if (REQUESTED_DROP_EFFECT !== data.dropEffect) { + // tree.debug( + // "applyDropEffectCallback(" + + // event.type + + // "): data.dropEffect changed from previous " + + // REQUESTED_DROP_EFFECT + + // " to " + + // data.dropEffect + // ); + // } + + data.isMove = data.dropEffect === "move"; + // data.isMove = data.dropEffectSuggested === "move"; + + // `effectAllowed` must only be defined in dragstart event, so we + // store it in a global variable for reference + if (event.type === "dragstart") { + REQUESTED_EFFECT_ALLOWED = data.effectAllowed; + REQUESTED_DROP_EFFECT = data.dropEffect; + } + + // if (REQUESTED_DROP_EFFECT !== dataTransfer.dropEffect) { + // data.tree.info( + // "applyDropEffectCallback(" + + // event.type + + // "): dataTransfer.dropEffect changed from " + + // REQUESTED_DROP_EFFECT + + // " -> " + + // dataTransfer.dropEffect + // ); + // } + dataTransfer.effectAllowed = REQUESTED_EFFECT_ALLOWED; + dataTransfer.dropEffect = REQUESTED_DROP_EFFECT; + + // tree.debug( + // "applyDropEffectCallback(" + + // event.type + + // "): set " + + // dataTransfer.dropEffect + + // "/" + + // dataTransfer.effectAllowed + // ); + // if (REQUESTED_DROP_EFFECT !== dataTransfer.dropEffect) { + // data.tree.warn( + // "applyDropEffectCallback(" + + // event.type + + // "): could not set dataTransfer.dropEffect to " + + // REQUESTED_DROP_EFFECT + + // ": got " + + // dataTransfer.dropEffect + // ); + // } + return REQUESTED_DROP_EFFECT; + } + + /* Handle dragover event (fired every x ms) on valid drop targets. + * + * - Auto-scroll when cursor is in border regions + * - Apply restrictioan like 'preventVoidMoves' + * - Calculate hit mode + * - Calculate drop effect + * - Trigger dragOver() callback to let user modify hit mode and drop effect + * - Adjust the drop marker accordingly + * + * @returns 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("Ignored dragover for non-node"); //, event, data); + return LAST_HIT_MODE; + } + + var markerOffsetX, + nodeOfs, + pos, + relPosY, + hitMode = null, + tree = data.tree, + options = tree.options, + dndOpts = options.dnd5, + targetNode = data.node, + sourceNode = data.otherNode, + markerAt = "center", + $target = $(targetNode.span), + $targetTitle = $target.find("span.fancytree-title"); + + if (DRAG_ENTER_RESPONSE === false) { + tree.debug("Ignored dragover, since dragenter returned false."); + return false; + } else if (typeof DRAG_ENTER_RESPONSE === "string") { + $.error("assert failed: dragenter returned string"); + } + // Calculate hitMode from relative cursor position. + nodeOfs = $target.offset(); + relPosY = (event.pageY - nodeOfs.top) / $target.height(); + if (event.pageY === undefined) { + tree.warn("event.pageY is undefined: see issue #1013."); + } + + 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 && data.dropEffect === "move") { + 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) { + prepareDropEffectCallback(event, data); + dndOpts.dragOver(targetNode, data); + var allowDrop = !!hitMode; + applyDropEffectCallback(event, data, allowDrop); + hitMode = data.hitMode; + } + 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; + } + + pos = { + my: "left" + offsetString(markerOffsetX) + " center", + at: "left " + markerAt, + of: $targetTitle, + }; + if (options.rtl) { + pos.my = "right" + offsetString(-markerOffsetX) + " center"; + pos.at = "right " + markerAt; + // console.log("rtl", pos); + } + $dropMarker + .toggleClass(classDropAfter, hitMode === "after") + .toggleClass(classDropOver, hitMode === "over") + .toggleClass(classDropBefore, hitMode === "before") + .show() + .position(FT.fixPositionOptions(pos)); + } else { + $dropMarker.hide(); + // console.log("hide dropmarker") + } + + $(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; + } + + /* + * Handle dragstart drag dragend events on the container + */ + function onDragEvent(event) { + var json, + tree = this, + dndOpts = tree.options.dnd5, + node = FT.getNode(event), + dataTransfer = + event.dataTransfer || event.originalEvent.dataTransfer, + data = { + tree: tree, + node: node, + options: tree.options, + originalEvent: event.originalEvent, + widget: tree.widget, + dataTransfer: dataTransfer, + useDefaultImage: true, + dropEffect: undefined, + dropEffectSuggested: undefined, + effectAllowed: undefined, // set by dragstart + files: undefined, // only for drop events + isCancelled: undefined, // set by dragend + isMove: undefined, + }; + + switch (event.type) { + case "dragstart": + if (!node) { + tree.info("Ignored dragstart on a non-node."); + return false; + } + // Store current source node in different formats + SOURCE_NODE = node; + + // Also optionally store selected nodes + if (dndOpts.multiSource === false) { + SOURCE_NODE_LIST = [node]; + } else if (dndOpts.multiSource === true) { + if (node.isSelected()) { + SOURCE_NODE_LIST = tree.getSelectedNodes(); + } else { + SOURCE_NODE_LIST = [node]; + } + } else { + SOURCE_NODE_LIST = dndOpts.multiSource(node, data); + } + // Cache as array of jQuery objects for faster access: + $sourceList = $( + $.map(SOURCE_NODE_LIST, function (n) { + return n.span; + }) + ); + // Set visual feedback + $sourceList.addClass(classDragSource); + + // 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. + var nodeData = node.toDict(true, dndOpts.sourceCopyHook); + nodeData.treeId = node.tree._id; + json = JSON.stringify(nodeData); + 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 drag modes (combinations of move, copy, and link) + // (effectAllowed can only be set in the dragstart event.) + // This can be overridden in the dragStart() callback + prepareDropEffectCallback(event, data); + + // Let user cancel or modify above settings + // Realize potential changes by previous callback + if (dndOpts.dragStart(node, data) === false) { + // Cancel dragging + // dataTransfer.dropEffect = "none"; + _clearGlobals(); + return false; + } + applyDropEffectCallback(event, data); + + // Unless user set `data.useDefaultImage` to false in dragStart, + // generata a default drag image now: + $extraHelper = null; + + if (data.useDefaultImage) { + // Set the title as drag image (otherwise it would contain the expander) + $dragImage = $(node.span).find(".fancytree-title"); + + if (SOURCE_NODE_LIST && SOURCE_NODE_LIST.length > 1) { + // Add a counter badge to node title if dragging more than one node. + // We want this, because the element that is used as drag image + // must be *visible* in the DOM, so we cannot create some hidden + // custom markup. + // See https://kryogenix.org/code/browser/custom-drag-image.html + // Also, since IE 11 and Edge don't support setDragImage() alltogether, + // it gives som feedback to the user. + // The badge will be removed later on drag end. + $extraHelper = $( + "" + ) + .text("+" + (SOURCE_NODE_LIST.length - 1)) + .appendTo($dragImage); + } + if (dataTransfer.setDragImage) { + // IE 11 and Edge do not support this + dataTransfer.setDragImage($dragImage[0], -10, -10); + } + } + return true; + + case "drag": + // Called every few milliseconds (no matter if the + // cursor is over a valid drop target) + // data.tree.info("drag", SOURCE_NODE) + prepareDropEffectCallback(event, data); + dndOpts.dragDrag(node, data); + applyDropEffectCallback(event, data); + + $sourceList.toggleClass(classDragRemove, data.isMove); + break; + + case "dragend": + // Called at the end of a d'n'd process (after drop) + // Note caveat: If drop removed the dragged source element, + // we may not get this event, since the target does not exist + // anymore + prepareDropEffectCallback(event, data); + + _clearGlobals(); + + data.isCancelled = !LAST_HIT_MODE; + dndOpts.dragEnd(node, data, !LAST_HIT_MODE); + // applyDropEffectCallback(event, data); + break; + } + } + /* + * Handle dragenter dragover dragleave drop events on the container + */ + function onDropEvent(event) { + var json, + allowAutoExpand, + nodeData, + isSourceFtNode, + r, + res, + tree = this, + dndOpts = tree.options.dnd5, + allowDrop = null, + node = FT.getNode(event), + dataTransfer = + event.dataTransfer || event.originalEvent.dataTransfer, + data = { + tree: tree, + node: node, + options: tree.options, + originalEvent: event.originalEvent, + widget: tree.widget, + hitMode: DRAG_ENTER_RESPONSE, + dataTransfer: dataTransfer, + otherNode: SOURCE_NODE || null, + otherNodeList: SOURCE_NODE_LIST || null, + otherNodeData: null, // set by drop event + useDefaultImage: true, + dropEffect: undefined, + dropEffectSuggested: undefined, + effectAllowed: undefined, // set by dragstart + files: null, // list of File objects (may be []) + isCancelled: undefined, // set by drop event + isMove: undefined, + }; + + // data.isMove = dropEffect === "move"; + + switch (event.type) { + case "dragenter": + // The dragenter event is fired when a dragged element or + // text selection enters a valid drop target. + + DRAG_OVER_STAMP = null; + 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); + + // Data is only readable in the dragstart and drop event, + // but we can check for the type: + isSourceFtNode = + $.inArray(nodeMimeType, dataTransfer.types) >= 0; + + if (dndOpts.preventNonNodes && !isSourceFtNode) { + 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; + } else if ( + dndOpts.preventSameParent && + data.otherNode && + data.otherNode.tree === node.tree && + node.parent === data.otherNode.parent + ) { + node.debug("Reject dropping as sibling (same parent)."); + DRAG_ENTER_RESPONSE = false; + break; + } else if ( + dndOpts.preventRecursion && + data.otherNode && + data.otherNode.tree === node.tree && + node.isDescendantOf(data.otherNode) + ) { + node.debug("Reject dropping below own ancestor."); + DRAG_ENTER_RESPONSE = false; + break; + } else if (dndOpts.preventLazyParents && !node.isLoaded()) { + node.warn("Drop over unloaded target node prevented."); + DRAG_ENTER_RESPONSE = false; + break; + } + $dropMarker.show(); + + // Call dragEnter() to figure out if (and where) dropping is allowed + prepareDropEffectCallback(event, data); + r = dndOpts.dragEnter(node, data); + + res = normalizeDragEnterResponse(r); + // alert("res:" + JSON.stringify(res)) + DRAG_ENTER_RESPONSE = res; + + allowDrop = res && (res.over || res.before || res.after); + + applyDropEffectCallback(event, data, allowDrop); + break; + + case "dragover": + if (!node) { + tree.debug( + "Ignore non-node " + + event.type + + ": " + + event.target.tagName + + "." + + event.target.className + ); + break; + } + // The dragover event is fired when an element or text + // selection is being dragged over a valid drop target + // (every few hundred milliseconds). + // tree.debug( + // event.type + + // ": dropEffect: " + + // dataTransfer.dropEffect + // ); + prepareDropEffectCallback(event, data); + LAST_HIT_MODE = handleDragOver(event, data); + + // The flag controls the preventDefault() below: + allowDrop = !!LAST_HIT_MODE; + allowAutoExpand = + LAST_HIT_MODE === "over" || LAST_HIT_MODE === false; + + if ( + allowAutoExpand && + !node.expanded && + node.hasChildren() !== false + ) { + if (!DRAG_OVER_STAMP) { + DRAG_OVER_STAMP = Date.now(); + } else if ( + dndOpts.autoExpandMS && + Date.now() - DRAG_OVER_STAMP > dndOpts.autoExpandMS && + !node.isLoading() && + (!dndOpts.dragExpand || + dndOpts.dragExpand(node, data) !== false) + ) { + node.setExpanded(); + } + } else { + DRAG_OVER_STAMP = null; + } + 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)."); + 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 (dragstart 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 + ); + + // Let user implement the actual drop operation + data.hitMode = LAST_HIT_MODE; + prepareDropEffectCallback(event, data, !LAST_HIT_MODE); + data.isCancelled = !LAST_HIT_MODE; + + var orgSourceElem = SOURCE_NODE && SOURCE_NODE.span, + orgSourceTree = SOURCE_NODE && SOURCE_NODE.tree; + + dndOpts.dragDrop(node, data); + // applyDropEffectCallback(event, data); + + // Prevent browser's default drop handling, i.e. open as link, ... + event.preventDefault(); + + if (orgSourceElem && !document.body.contains(orgSourceElem)) { + // The drop handler removed the original drag source from + // the DOM, so the dragend event will probaly not fire. + if (orgSourceTree === tree) { + tree.debug( + "Drop handler removed source element: generating dragEnd." + ); + dndOpts.dragEnd(SOURCE_NODE, data); + } else { + tree.warn( + "Drop handler removed source element: dragend event may be lost." + ); + } + } + + _clearGlobals(); + + break; + } + // Dnd API madness: we must PREVENT default handling to enable dropping + if (allowDrop) { + event.preventDefault(); + return false; + } + } + + /** [ext-dnd5] Return a Fancytree instance, from element, index, event, or jQueryObject. + * + * @returns {FancytreeNode[]} List of nodes (empty if no drag operation) + * @example + * $.ui.fancytree.getDragNodeList(); + * + * @alias Fancytree_Static#getDragNodeList + * @requires jquery.fancytree.dnd5.js + * @since 2.31 + */ + $.ui.fancytree.getDragNodeList = function () { + return SOURCE_NODE_LIST || []; + }; + + /** [ext-dnd5] Return the FancytreeNode that is currently being dragged. + * + * If multiple nodes are dragged, only the first is returned. + * + * @returns {FancytreeNode | null} dragged nodes or null if no drag operation + * @example + * $.ui.fancytree.getDragNode(); + * + * @alias Fancytree_Static#getDragNode + * @requires jquery.fancytree.dnd5.js + * @since 2.31 + */ + $.ui.fancytree.getDragNode = function () { + return SOURCE_NODE; + }; + + /****************************************************************************** + * + */ + + $.ui.fancytree.registerExtension({ + name: "dnd5", + version: "2.38.5", + // Default options for this extension. + options: { + autoExpandMS: 1500, // Expand nodes after n milliseconds of hovering + dropMarkerInsertOffsetX: -16, // Additional offset for drop-marker with hitMode = "before"/"after" + dropMarkerOffsetX: -24, // Absolute position offset for .fancytree-drop-marker relatively to ..fancytree-title (icon/img near a node accepting drop) + // #1021 `document.body` is not available yet + dropMarkerParent: "body", // Root Container used for drop marker (could be a shadow root) + multiSource: false, // true: Drag multiple (i.e. selected) nodes. Also a callback() is allowed + effectAllowed: "all", // Restrict the possible cursor shapes and modifier operations (can also be set in the dragStart event) + // dropEffect: "auto", // 'copy'|'link'|'move'|'auto'(calculate from `effectAllowed`+modifier keys) or callback(node, data) that returns such string. + dropEffectDefault: "move", // Default dropEffect ('copy', 'link', or 'move') when no modifier is pressed (overide in dragDrag, dragOver). + preventForeignNodes: false, // Prevent dropping nodes from different Fancytrees + preventLazyParents: true, // Prevent dropping items on unloaded lazy Fancytree nodes + preventNonNodes: false, // Prevent dropping items other than Fancytree nodes + preventRecursion: true, // Prevent dropping nodes on own descendants + preventSameParent: false, // Prevent dropping nodes under same direct parent + 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 + setTextTypeJson: false, // Allow dragging of nodes to different IE windows + sourceCopyHook: null, // Optional callback passed to `toDict` on dragStart @since 2.38 + // 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; + + 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." + ); + } + if (dndOpts.preventRecursiveMoves != null) { + $.error( + "preventRecursiveMoves was renamed to preventRecursion." + ); + } + + // Implement `opts.createNode` event to add the 'draggable' attribute + // #680: this must happen before calling super.treeInit() + if (dndOpts.dragStart) { + FT.overrideMethod( + ctx.options, + "createNode", + function (event, data) { + // Default processing if any + this._super.apply(this, arguments); + if (data.node.span) { + data.node.span.draggable = true; + } else { + data.node.warn( + "Cannot add `draggable`: no span tag" + ); + } + } + ); + } + 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 = $("").appendTo(this.$container); + this.$scrollParent = $temp.scrollParent(); + $temp.remove(); + + $dropMarker = $("#fancytree-drop-marker"); + if (!$dropMarker.length) { + $dropMarker = $("
                                  ") + .hide() + .css({ + "z-index": 1000, + // Drop marker should not steal dragenter/dragover events: + "pointer-events": "none", + }) + .prependTo(dndOpts.dropMarkerParent); + if (glyph) { + FT.setSpanIcon( + $dropMarker[0], + glyph.map._addClass, + glyph.map.dropMarker + ); + } + } + $dropMarker.toggleClass("fancytree-rtl", !!opts.rtl); + + // Enable drag support if dragStart() is specified: + if (dndOpts.dragStart) { + // Bind drag event handlers + tree.$container.on( + "dragstart drag dragend", + onDragEvent.bind(tree) + ); + } + // Enable drop support if dragEnter() is specified: + if (dndOpts.dragEnter) { + // Bind drop event handlers + tree.$container.on( + "dragenter dragover dragleave drop", + onDropEvent.bind(tree) + ); + } + }, + }); + // 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-2023, Martin Wendt (https://wwWendt.de) + * + * Released under the MIT license + * https://github.com/mar10/fancytree/wiki/LicenseInfo + * + * @version 2.38.5 + * @date 2025-04-05T06:40:00Z + */ + +(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, + trim = $.ui.fancytree.trim, + 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(); + + local.lastDraggableAttrValue = node.span.draggable; + if (local.lastDraggableAttrValue) { + node.span.draggable = false; + } + + // #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 = $("", { + 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 and bind keyboard handler + $input + .trigger("focus") + .change(function (event) { + $input.addClass("fancytree-edit-dirty"); + }) + .on("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 everything, 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(); + + if (node && local.lastDraggableAttrValue) { + node.span.draggable = true; + } + + // Set keyboard focus, even if setFocus() claims 'nothing to do' + tree.$container.get(0).focus({ preventScroll: true }); + 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.38.5", + // 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", "mac+enter", "shift+click"], + 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) { + var tree = ctx.tree; + + this._superApply(arguments); + + this.$container + .addClass("fancytree-ext-edit") + .on("fancytreebeforeupdateviewport", function (event, data) { + var editNode = tree.isEditing(); + // When scrolling, the TR may be re-used by another node, so the + // active cell marker an + if (editNode) { + editNode.info("Cancel edit due to scroll event."); + editNode.editEnd(false, event); + } + }); + }, + nodeClick: function (ctx) { + var eventStr = $.ui.fancytree.eventToString(ctx.originalEvent), + triggerStart = ctx.options.edit.triggerStart; + + if ( + eventStr === "shift+click" && + $.inArray("shift+click", triggerStart) >= 0 + ) { + if (ctx.originalEvent.shiftKey) { + ctx.node.editStart(); + return false; + } + } + if ( + eventStr === "click" && + $.inArray("clickActive", triggerStart) >= 0 + ) { + // Only when click was inside title text (not aynwhere else in the row) + if ( + ctx.node.isActive() && + !ctx.node.isEditing() && + $(ctx.originalEvent.target).hasClass("fancytree-title") + ) { + 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-2023, Martin Wendt (https://wwWendt.de) + * + * Released under the MIT license + * https://github.com/mar10/fancytree/wiki/LicenseInfo + * + * @version 2.38.5 + * @date 2025-04-05T06:40:00Z + */ + +(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, + exoticStartChar = "\uFFF7", + exoticEndChar = "\uFFF8"; + function _escapeRegex(str) { + return (str + "").replace(/([.?*+^$[\]\\(){}|-])/g, "\\$1"); + } + + function extractHtmlText(s) { + if (s.indexOf(">") >= 0) { + return $("
                                  ").html(s).text(); + } + return s; + } + + /** + * @description Marks the matching charecters of `text` either by `mark` or + * by exotic*Chars (if `escapeTitles` is `true`) based on `regexMatchArray` + * which is an array of matching groups. + * @param {string} text + * @param {RegExpMatchArray} regexMatchArray + */ + function _markFuzzyMatchedChars(text, regexMatchArray, escapeTitles) { + // It is extremely infuriating that we can not use `let` or `const` or arrow functions. + // Damn you IE!!! + var matchingIndices = []; + // get the indices of matched characters (Iterate through `RegExpMatchArray`) + for ( + var _matchingArrIdx = 1; + _matchingArrIdx < regexMatchArray.length; + _matchingArrIdx++ + ) { + var _mIdx = + // get matching char index by cumulatively adding + // the matched group length + regexMatchArray[_matchingArrIdx].length + + (_matchingArrIdx === 1 ? 0 : 1) + + (matchingIndices[matchingIndices.length - 1] || 0); + matchingIndices.push(_mIdx); + } + // Map each `text` char to its position and store in `textPoses`. + var textPoses = text.split(""); + if (escapeTitles) { + // If escaping the title, then wrap the matchng char within exotic chars + matchingIndices.forEach(function (v) { + textPoses[v] = exoticStartChar + textPoses[v] + exoticEndChar; + }); + } else { + // Otherwise, Wrap the matching chars within `mark`. + matchingIndices.forEach(function (v) { + textPoses[v] = "" + textPoses[v] + ""; + }); + } + // Join back the modified `textPoses` to create final highlight markup. + return textPoses.join(""); + } + $.ui.fancytree._FancytreeClass.prototype._applyFilterImpl = function ( + filter, + branchMode, + _opts + ) { + var match, + statusNode, + re, + reHighlight, + reExoticStartChar, + reExoticEndChar, + temp, + prevEnableUpdate, + 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("") + // Escaping the `filter` will not work because, + // it gets further split into individual characters. So, + // escape each character after splitting + .map(_escapeRegex) + .reduce(function (a, b) { + // create capture groups for parts that comes before + // the character + 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"); + if (escapeTitles) { + reExoticStartChar = new RegExp( + _escapeRegex(exoticStartChar), + "g" + ); + reExoticEndChar = new RegExp(_escapeRegex(exoticEndChar), "g"); + } + filter = function (node) { + if (!node.title) { + return false; + } + var text = escapeTitles + ? node.title + : extractHtmlText(node.title), + // `.match` instead of `.test` to get the capture groups + res = text.match(re); + if (res && opts.highlight) { + if (escapeTitles) { + if (opts.fuzzy) { + temp = _markFuzzyMatchedChars( + text, + res, + escapeTitles + ); + } else { + // #740: we must not apply the marks to escaped entity names, e.g. `"` + // Use some exotic characters to mark matches: + temp = text.replace(reHighlight, function (s) { + return exoticStartChar + s + exoticEndChar; + }); + } + // now we can escape the title... + node.titleWithHighlight = escapeHtml(temp) + // ... and finally insert the desired `` tags + .replace(reExoticStartChar, "") + .replace(reExoticEndChar, ""); + } else { + if (opts.fuzzy) { + node.titleWithHighlight = _markFuzzyMatchedChars( + text, + res + ); + } else { + node.titleWithHighlight = text.replace( + reHighlight, + function (s) { + return "" + s + ""; + } + ); + } + } + // node.debug("filter", escapeTitles, text, node.titleWithHighlight); + } + return !!res; + }; + } + + this.enableFilter = true; + this.lastFilterArgs = arguments; + + prevEnableUpdate = this.enableUpdate(false); + + 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.rootNode.subMatchCount = 0; + 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) { + if (p !== node) { + 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; + } + }, true); + } + }); + treeOpts.autoCollapse = prevAutoCollapse; + + if (count === 0 && opts.nodata && hideMode) { + statusNode = opts.nodata; + if (typeof statusNode === "function") { + 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._callHook("treeStructureChanged", this, "applyFilter"); + // this.render(); + this.enableUpdate(prevEnableUpdate); + 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); + }; + + /** + * [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] Re-apply current filter. + * + * @returns {integer} count + * @alias Fancytree#updateFilter + * @requires jquery.fancytree.filter.js + * @since 2.38 + */ + $.ui.fancytree._FancytreeClass.prototype.updateFilter = function () { + if ( + this.enableFilter && + this.lastFilterArgs && + this.options.filter.autoApply + ) { + this._applyFilterImpl.apply(this, this.lastFilterArgs); + } else { + this.warn("updateFilter(): no filter active."); + } + }; + + /** + * [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, + prevEnableUpdate = this.enableUpdate(false); + + if (statusNode) { + statusNode.remove(); + } + // we also counted root node's subMatchCount + delete this.rootNode.match; + delete this.rootNode.subMatchCount; + + 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._callHook("treeStructureChanged", this, "clearFilter"); + // this.render(); + this.enableUpdate(prevEnableUpdate); + }; + + /** + * [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.38.5", + // 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 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) { + var tree = ctx.tree; + + return this._superApply(arguments).done(function () { + if ( + tree.enableFilter && + tree.lastFilterArgs && + ctx.options.filter.autoApply + ) { + tree._applyFilterImpl.apply(tree, tree.lastFilterArgs); + } + }); + }, + nodeSetExpanded: function (ctx, flag, callOpts) { + var node = ctx.node; + + delete node._filterAutoExpanded; + // Make sure counter badge is displayed again, when node is beeing collapsed + if ( + !flag && + ctx.options.filter.hideExpandedCounter && + node.$subMatchBadge + ) { + 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.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 check 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, ligature-fonts, or SVG icons instead of icon sprites. + * (Extension module for jquery.fancytree.js: https://github.com/mar10/fancytree/) + * + * Copyright (c) 2008-2023, Martin Wendt (https://wwWendt.de) + * + * Released under the MIT license + * https://github.com/mar10/fancytree/wiki/LicenseInfo + * + * @version 2.38.5 + * @date 2025-04-05T06:40:00Z + */ + +(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: { + // Outdated! + _addClass: "", + checkbox: "icon-check-empty", + checkboxSelected: "icon-check", + checkboxUnknown: "icon-check icon-muted", + 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: "", + radio: "icon-circle-blank", + radioSelected: "icon-circle", + // radioUnknown: "icon-circle icon-muted", + // 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: { + _addClass: "fa", + checkbox: "fa-square-o", + checkboxSelected: "fa-check-square-o", + checkboxUnknown: "fa-square fancytree-helper-indeterminate-cb", + dragHelper: "fa-arrow-right", + dropMarker: "fa-long-arrow-right", + error: "fa-warning", + expanderClosed: "fa-caret-right", + expanderLazy: "fa-angle-right", + expanderOpen: "fa-caret-down", + // We may prevent wobbling rotations on FF by creating a separate sub element: + loading: { html: "" }, + nodata: "fa-meh-o", + noExpander: "", + radio: "fa-circle-thin", // "fa-circle-o" + radioSelected: "fa-circle", + // radioUnknown: "fa-dot-circle-o", + // Default node icons. + // (Use tree.options.icon callback to define custom icons based on node data) + doc: "fa-file-o", + docOpen: "fa-file-o", + folder: "fa-folder-o", + folderOpen: "fa-folder-open-o", + }, + awesome5: { + // fontawesome 5 have several different base classes + // "far, fas, fal and fab" The rendered svg puts that prefix + // in a different location so we have to keep them separate here + _addClass: "", + checkbox: "far fa-square", + checkboxSelected: "far fa-check-square", + // checkboxUnknown: "far fa-window-close", + checkboxUnknown: + "fas fa-square fancytree-helper-indeterminate-cb", + radio: "far fa-circle", + radioSelected: "fas fa-circle", + radioUnknown: "far fa-dot-circle", + dragHelper: "fas fa-arrow-right", + dropMarker: "fas fa-long-arrow-alt-right", + error: "fas fa-exclamation-triangle", + expanderClosed: "fas fa-caret-right", + expanderLazy: "fas fa-angle-right", + expanderOpen: "fas fa-caret-down", + loading: "fas fa-spinner fa-pulse", + nodata: "far fa-meh", + noExpander: "", + // Default node icons. + // (Use tree.options.icon callback to define custom icons based on node data) + doc: "far fa-file", + docOpen: "far fa-file", + folder: "far fa-folder", + folderOpen: "far fa-folder-open", + }, + bootstrap3: { + _addClass: "glyphicon", + checkbox: "glyphicon-unchecked", + checkboxSelected: "glyphicon-check", + checkboxUnknown: + "glyphicon-expand fancytree-helper-indeterminate-cb", // "glyphicon-share", + dragHelper: "glyphicon-play", + dropMarker: "glyphicon-arrow-right", + error: "glyphicon-warning-sign", + expanderClosed: "glyphicon-menu-right", // glyphicon-plus-sign + expanderLazy: "glyphicon-menu-right", // glyphicon-plus-sign + expanderOpen: "glyphicon-menu-down", // glyphicon-minus-sign + loading: "glyphicon-refresh fancytree-helper-spin", + nodata: "glyphicon-info-sign", + noExpander: "", + radio: "glyphicon-remove-circle", // "glyphicon-unchecked", + radioSelected: "glyphicon-ok-circle", // "glyphicon-check", + // radioUnknown: "glyphicon-ban-circle", + // Default node icons. + // (Use tree.options.icon callback to define custom icons based on node data) + doc: "glyphicon-file", + docOpen: "glyphicon-file", + folder: "glyphicon-folder-close", + folderOpen: "glyphicon-folder-open", + }, + material: { + _addClass: "material-icons", + checkbox: { text: "check_box_outline_blank" }, + checkboxSelected: { text: "check_box" }, + checkboxUnknown: { text: "indeterminate_check_box" }, + dragHelper: { text: "play_arrow" }, + dropMarker: { text: "arrow_forward" }, + error: { text: "warning" }, + expanderClosed: { text: "chevron_right" }, + expanderLazy: { text: "last_page" }, + expanderOpen: { text: "expand_more" }, + loading: { + text: "autorenew", + addClass: "fancytree-helper-spin", + }, + nodata: { text: "info" }, + noExpander: { text: "" }, + radio: { text: "radio_button_unchecked" }, + radioSelected: { text: "radio_button_checked" }, + // Default node icons. + // (Use tree.options.icon callback to define custom icons based on node data) + doc: { text: "insert_drive_file" }, + docOpen: { text: "insert_drive_file" }, + folder: { text: "folder" }, + folderOpen: { text: "folder_open" }, + }, + }; + + function setIcon(node, span, baseClass, opts, type) { + var map = opts.map, + icon = map[type], + $span = $(span), + $counter = $span.find(".fancytree-childcounter"), + setClass = baseClass + " " + (map._addClass || ""); + + // #871 Allow a callback + if (typeof icon === "function") { + icon = icon.call(this, node, span, type); + } + // node.debug( "setIcon(" + baseClass + ", " + type + "): " + "oldIcon" + " -> " + icon ); + // #871: propsed this, but I am not sure how robust this is, e.g. + // the prefix (fas, far) class changes are not considered? + // if (span.tagName === "svg" && opts.preset === "awesome5") { + // // fa5 script converts to so call a specific handler. + // var oldIcon = "fa-" + $span.data("icon"); + // // node.debug( "setIcon(" + baseClass + ", " + type + "): " + oldIcon + " -> " + icon ); + // if (typeof oldIcon === "string") { + // $span.removeClass(oldIcon); + // } + // if (typeof icon === "string") { + // $span.addClass(icon); + // } + // return; + // } + if (typeof icon === "string") { + // #883: remove inner html that may be added by prev. mode + span.innerHTML = ""; + $span.attr("class", setClass + " " + icon).append($counter); + } else if (icon) { + if (icon.text) { + span.textContent = "" + icon.text; + } else if (icon.html) { + span.innerHTML = icon.html; + } else { + span.innerHTML = ""; + } + $span + .attr("class", setClass + " " + (icon.addClass || "")) + .append($counter); + } + } + + $.ui.fancytree.registerExtension({ + name: "glyph", + version: "2.38.5", + // Default options for this extension. + options: { + preset: null, // 'awesome3', 'awesome4', 'bootstrap3', 'material' + 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, + icon, + res, + span, + node = ctx.node, + $span = $(node.span), + opts = ctx.options.glyph; + + res = this._super(ctx); + + if (node.isRootNode()) { + return res; + } + span = $span.children(".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]; + setIcon(node, span, "fancytree-expander", opts, icon); + } + + if (node.tr) { + span = $("td", node.tr).find(".fancytree-checkbox").get(0); + } else { + span = $span.children(".fancytree-checkbox").get(0); + } + if (span) { + checkbox = FT.evalOption("checkbox", node, node, opts, false); + if ( + (node.parent && node.parent.radiogroup) || + checkbox === "radio" + ) { + icon = node.selected ? "radioSelected" : "radio"; + setIcon( + node, + span, + "fancytree-checkbox fancytree-radio", + opts, + icon + ); + } else { + // eslint-disable-next-line no-nested-ternary + icon = node.selected + ? "checkboxSelected" + : node.partsel + ? "checkboxUnknown" + : "checkbox"; + // span.className = "fancytree-checkbox " + map[icon]; + setIcon(node, span, "fancytree-checkbox", opts, icon); + } + } + + // Standard icon (note that this does not match .fancytree-custom-icon, + // that might be set by opts.icon callbacks) + span = $span.children(".fancytree-icon").get(0); + if (span) { + if (node.statusNodeType) { + icon = node.statusNodeType; // loading, error + } else if (node.folder) { + icon = + node.expanded && node.hasChildren() + ? "folderOpen" + : "folder"; + } else { + icon = node.expanded ? "docOpen" : "doc"; + } + setIcon(node, span, "fancytree-icon", opts, 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 = $(".fancytree-expander", node.span).get(0); + if (span) { + setIcon(node, span, "fancytree-expander", opts, status); + } + } else { + // + span = $( + ".fancytree-statusnode-" + status, + node[this.nodeContainerAttrName] + ) + .find(".fancytree-icon") + .get(0); + if (span) { + setIcon(node, span, "fancytree-icon", 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-2023, Martin Wendt (https://wwWendt.de) + * + * Released under the MIT license + * https://github.com/mar10/fancytree/wiki/LicenseInfo + * + * @version 2.38.5 + * @date 2025-04-05T06:40:00Z + */ + +(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.38.5", + // 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") + .trigger("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().trigger("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").trigger("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.multi.js' *//*! + * jquery.fancytree.multi.js + * + * Allow multiple selection of nodes by mouse or keyboard. + * (Extension module for jquery.fancytree.js: https://github.com/mar10/fancytree/) + * + * Copyright (c) 2008-2023, Martin Wendt (https://wwWendt.de) + * + * Released under the MIT license + * https://github.com/mar10/fancytree/wiki/LicenseInfo + * + * @version 2.38.5 + * @date 2025-04-05T06:40:00Z + */ + +(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); + + /******************************************************************************* + * Extension code + */ + $.ui.fancytree.registerExtension({ + name: "multi", + version: "2.38.5", + // Default options for this extension. + options: { + allowNoSelect: false, // + mode: "sameParent", // + // Events: + // beforeSelect: $.noop // Return false to prevent cancel/save (data.input is available) + }, + + treeInit: function (ctx) { + this._superApply(arguments); + this.$container.addClass("fancytree-ext-multi"); + if (ctx.options.selectMode === 1) { + $.error( + "Fancytree ext-multi: selectMode: 1 (single) is not compatible." + ); + } + }, + nodeClick: function (ctx) { + var //pluginOpts = ctx.options.multi, + tree = ctx.tree, + node = ctx.node, + activeNode = tree.getActiveNode() || tree.getFirstChild(), + isCbClick = ctx.targetType === "checkbox", + isExpanderClick = ctx.targetType === "expander", + eventStr = $.ui.fancytree.eventToString(ctx.originalEvent); + + switch (eventStr) { + case "click": + if (isExpanderClick) { + break; + } // Default handler will expand/collapse + if (!isCbClick) { + tree.selectAll(false); + // Select clicked node (radio-button mode) + node.setSelected(); + } + // Default handler will toggle checkbox clicks and activate + break; + case "shift+click": + // node.debug("click") + tree.visitRows( + function (n) { + // n.debug("click2", n===node, node) + n.setSelected(); + if (n === node) { + return false; + } + }, + { + start: activeNode, + reverse: activeNode.isBelowOf(node), + } + ); + break; + case "ctrl+click": + case "meta+click": // Mac: [Command] + node.toggleSelected(); + return; + } + return this._superApply(arguments); + }, + nodeKeydown: function (ctx) { + var tree = ctx.tree, + node = ctx.node, + event = ctx.originalEvent, + eventStr = $.ui.fancytree.eventToString(event); + + switch (eventStr) { + case "up": + case "down": + tree.selectAll(false); + node.navigate(event.which, true); + tree.getActiveNode().setSelected(); + break; + case "shift+up": + case "shift+down": + node.navigate(event.which, true); + tree.getActiveNode().setSelected(); + break; + } + 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-2023, Martin Wendt (https://wwWendt.de) + * + * Released under the MIT license + * https://github.com/mar10/fancytree/wiki/LicenseInfo + * + * @version 2.38.5 + * @date 2025-04-05T06:40:00Z + */ + +(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 cookieStore = null, + localStorageStore = null, + sessionStorageStore = null, + _assert = $.ui.fancytree.assert, + ACTIVE = "active", + EXPANDED = "expanded", + FOCUS = "focus", + SELECTED = "selected"; + + // Accessing window.xxxStorage may raise security exceptions (see #1022) + try { + _assert(window.localStorage && window.localStorage.getItem); + localStorageStore = { + get: function (key) { + return window.localStorage.getItem(key); + }, + set: function (key, value) { + window.localStorage.setItem(key, value); + }, + remove: function (key) { + window.localStorage.removeItem(key); + }, + }; + } catch (e) { + $.ui.fancytree.warn("Could not access window.localStorage", e); + } + + try { + _assert(window.sessionStorage && window.sessionStorage.getItem); + sessionStorageStore = { + get: function (key) { + return window.sessionStorage.getItem(key); + }, + set: function (key, value) { + window.sessionStorage.setItem(key, value); + }, + remove: function (key) { + window.sessionStorage.removeItem(key); + }, + }; + } catch (e) { + $.ui.fancytree.warn("Could not access window.sessionStorage", e); + } + + if (typeof Cookies === "function") { + // Assume https://github.com/js-cookie/js-cookie + cookieStore = { + get: Cookies.get, + set: function (key, value) { + Cookies.set(key, value, this.options.persist.cookie); + }, + remove: Cookies.remove, + }; + } else if ($ && typeof $.cookie === "function") { + // Fall back to https://github.com/carhartl/jquery-cookie + cookieStore = { + get: $.cookie, + set: function (key, value) { + $.cookie(key, value, this.options.persist.cookie); + }, + remove: $.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."); + try { + node.setExpanded(true, expandOpts); + } catch (e) { + // #1157 + tree.warn( + "ext-persist: setExpanded failed for " + node, + e + ); + } + } + } 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 data of the given type(s). + * Called like + * $.ui.fancytree.getTree("#tree").clearCookies("active expanded focus selected"); + * + * @alias Fancytree#clearPersistData + * @requires jquery.fancytree.persist.js + */ + $.ui.fancytree._FancytreeClass.prototype.clearPersistData = 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); + } + }; + + $.ui.fancytree._FancytreeClass.prototype.clearCookies = function (types) { + this.warn( + "'tree.clearCookies()' is deprecated since v2.27.0: use 'clearPersistData()' instead." + ); + return this.clearPersistData(types); + }; + + /** + * [ext-persist] Return persistence information from cookies + * + * Called like + * $.ui.fancytree.getTree("#tree").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.38.5", + // Default options for this extension. + options: { + cookieDelimiter: "~", + cookiePrefix: undefined, // 'fancytree--' 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 store = this._local.store; + + if (value === undefined) { + return store.get.call(this, key); + } else if (value === null) { + store.remove.call(this, key); + } else { + store.set.call(this, key, value); + } + }, + + /* 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") || cookieStore, + // "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; + local.store = null; + + if (instOpts.store === "auto") { + instOpts.store = localStorageStore ? "local" : "cookie"; + } + if ($.isPlainObject(instOpts.store)) { + local.store = instOpts.store; + } else if (instOpts.store === "cookie") { + local.store = cookieStore; + } else if (instOpts.store === "local") { + local.store = + instOpts.store === "local" + ? localStorageStore + : sessionStorageStore; + } else if (instOpts.store === "session") { + local.store = + instOpts.store === "local" + ? localStorageStore + : sessionStorageStore; + } + _assert(local.store, "Need a valid store."); + + // 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") + .trigger("focus"); + } else { + $(tree.$container).trigger("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-2023, Martin Wendt (https://wwWendt.de) + * + * Released under the MIT license + * https://github.com/mar10/fancytree/wiki/LicenseInfo + * + * @version 2.38.5 + * @date 2025-04-05T06:40:00Z + */ + +(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 _assert = $.ui.fancytree.assert; + + 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, "prev.tr missing: " + prev); + // descend to lowest child (with a
                                  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; + } + + $.ui.fancytree.registerExtension({ + name: "table", + version: "2.38.5", + // Default options for this extension. + options: { + checkboxColumnIdx: null, // render the checkboxes into the this column index (default: nodeColumnIdx) + indentation: 16, // indent every node level by 16px + mergeStatusColumns: true, // display 'nodata', 'loading', 'error' centered in a single, merged TR + 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, + n, + $row, + $tbody, + tree = ctx.tree, + opts = ctx.options, + tableOpts = opts.table, + $table = tree.widget.element; + + if (tableOpts.customStatus != null) { + if (opts.renderStatusColumns == null) { + tree.warn( + "The 'customStatus' option is deprecated since v2.15.0. Use 'renderStatusColumns' instead." + ); + opts.renderStatusColumns = tableOpts.customStatus; + } else { + $.error( + "The 'customStatus' option is deprecated since v2.15.0. Use 'renderStatusColumns' only instead." + ); + } + } + 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"); + $tbody = $table.find(">tbody"); + if (!$tbody.length) { + // TODO: not sure if we can rely on browsers to insert missing before
                                  s: + if ($table.find(">tr").length) { + $.error( + "Expected table > tbody > tr. If you see this please open an issue." + ); + } + $tbody = $("").appendTo($table); + } + + tree.tbody = $tbody[0]; + + // Prepare row templates: + // Determine column count from table header if any + tree.columnCount = $("thead >tr", $table) + .last() + .find(">th", $table).length; + // Read TR templates from tbody if any + $row = $tbody.children("tr").first(); + if ($row.length) { + n = $row.children("td").length; + if (tree.columnCount && n !== tree.columnCount) { + tree.warn( + "Column count mismatch between thead (" + + tree.columnCount + + ") and tbody (" + + n + + "): using tbody." + ); + tree.columnCount = n; + } + $row = $row.clone(); + } else { + // Only thead is defined: create default row markup + _assert( + tree.columnCount >= 1, + "Need either or with elements to determine column count." + ); + $row = $("
                                  "); + for (i = 0; i < tree.columnCount; i++) { + $row.append(""); + } + } + $row.find(">td") + .eq(tableOpts.nodeColumnIdx) + .html("
                                  "); + 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
              elements + tree.statusClassPropName = "tr"; + tree.ariaPropName = "tr"; + this.nodeContainerAttrName = "tr"; + + // #489: make sure $container is set to
              , 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 (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); + } + } else { + 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 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) { + insertSiblingAfter(prevNode.tr, newRow); + } else { + _assert( + !prevNode.parent, + "prev. row must have a tr, or be system root: " + + prevNode + ); + // tree.tbody.appendChild(newRow); + insertFirstChild(tree.tbody, newRow); // #675 + } + 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); + } + } + } + // 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 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, + tree = ctx.tree, + 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( + tree, + { type: "renderStatusColumns" }, + ctx + ); + } else if (opts.table.mergeStatusColumns && node.isTopLevel()) { + $(node.tr) + .find(">td") + .eq(0) + .prop("colspan", tree.columnCount) + .text(node.title) + .addClass("fancytree-status-merged") + .nextAll() + .remove(); + } // else: default rendering for status node: leave other cells empty + } else if (opts.renderColumns) { + opts.renderColumns.call(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; + if (opts.rtl) { + $(node.span).css({ paddingRight: indent + "px" }); + } else { + $(node.span).css({ paddingLeft: 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, args) { + // ctx.tree.info("ok:" + ok, args); + if (ok) { + // #1108 minExpandLevel: 2 together with table extension does not work + // don't call when 'ok' is false: + setChildRowVisibility(ctx.node, flag); + 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, arguments); + }) + .fail(function () { + _afterExpand(false, arguments); + }); + 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(); + if (this.$source) { + this.$source.removeClass("fancytree-helper-hidden"); + } + return this._superApply(arguments); + }, + /*, + treeSetFocus: function(ctx, flag) { +// alert("treeSetFocus" + ctx.tree.$container); + ctx.tree.$container.trigger("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-2023, Martin Wendt (https://wwWendt.de) + * + * Released under the MIT license + * https://github.com/mar10/fancytree/wiki/LicenseInfo + * + * @version 2.38.5 + * @date 2025-04-05T06:40:00Z + */ + +(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.38.5", + // 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.on( + "mouseenter mouseleave", + ".fancytree-node", + 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-2023, Martin Wendt (https://wwWendt.de) + * + * Released under the MIT license + * https://github.com/mar10/fancytree/wiki/LicenseInfo + * + * @version 2.38.5 + * @date 2025-04-05T06:40:00Z + */ + +(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 = $("" ).appendTo( body ); + } -var tabId = 0, - listId = 0; + if ( o.opacity ) { // opacity option + if ( this.helper.css( "opacity" ) ) { + this._storedOpacity = this.helper.css( "opacity" ); + } + this.helper.css( "opacity", o.opacity ); + } -function getNextTabId() { - return ++tabId; -} + if ( o.zIndex ) { // zIndex option + if ( this.helper.css( "zIndex" ) ) { + this._storedZIndex = this.helper.css( "zIndex" ); + } + this.helper.css( "zIndex", o.zIndex ); + } -function getNextListId() { - return ++listId; -} + //Prepare scrolling + if ( this.scrollParent[ 0 ] !== this.document[ 0 ] && + this.scrollParent[ 0 ].tagName !== "HTML" ) { + this.overflowOffset = this.scrollParent.offset(); + } -$.widget( "ui.tabs", { - options: { - add: null, - ajaxOptions: null, - cache: false, - cookie: null, // e.g. { expires: 7, path: '/', domain: 'jquery.com', secure: true } - collapsible: false, - disable: null, - disabled: [], - enable: null, - event: "click", - fx: null, // e.g. { height: 'toggle', opacity: 'toggle', duration: 200 } - idPrefix: "ui-tabs-", - load: null, - panelTemplate: "
              ", - remove: null, - select: null, - show: null, - spinner: "Loading…", - tabTemplate: "
            • #{label}
            • " - }, + //Call callbacks + this._trigger( "start", event, this._uiHash() ); - _create: function() { - this._tabify( true ); - }, + //Recache the helper size + if ( !this._preserveHelperProportions ) { + this._cacheHelperProportions(); + } - _setOption: function( key, value ) { - if ( key == "selected" ) { - if (this.options.collapsible && value == this.options.selected ) { - return; + //Post "activate" events to possible containers + if ( !noActivation ) { + for ( i = this.containers.length - 1; i >= 0; i-- ) { + this.containers[ i ]._trigger( "activate", event, this._uiHash( this ) ); } - this.select( value ); - } else { - this.options[ key ] = value; - this._tabify(); } - }, - _tabId: function( a ) { - return a.title && a.title.replace( /\s/g, "_" ).replace( /[^\w\u00c0-\uFFFF-]/g, "" ) || - this.options.idPrefix + getNextTabId(); - }, + //Prepare possible droppables + if ( $.ui.ddmanager ) { + $.ui.ddmanager.current = this; + } - _sanitizeSelector: function( hash ) { - // we need this because an id may contain a ":" - return hash.replace( /:/g, "\\:" ); - }, + if ( $.ui.ddmanager && !o.dropBehaviour ) { + $.ui.ddmanager.prepareOffsets( this, event ); + } - _cookie: function() { - var cookie = this.cookie || - ( this.cookie = this.options.cookie.name || "ui-tabs-" + getNextListId() ); - return $.cookie.apply( null, [ cookie ].concat( $.makeArray( arguments ) ) ); - }, + this.dragging = true; - _ui: function( tab, panel ) { - return { - tab: tab, - panel: panel, - index: this.anchors.index( tab ) - }; - }, + this._addClass( this.helper, "ui-sortable-helper" ); + + // Execute the drag once - this causes the helper not to be visiblebefore getting its + // correct position + this._mouseDrag( event ); + return true; - _cleanup: function() { - // restore all former loading tabs labels - this.lis.filter( ".ui-state-processing" ) - .removeClass( "ui-state-processing" ) - .find( "span:data(label.tabs)" ) - .each(function() { - var el = $( this ); - el.html( el.data( "label.tabs" ) ).removeData( "label.tabs" ); - }); }, - _tabify: function( init ) { - var self = this, + _mouseDrag: function( event ) { + var i, item, itemElement, intersection, o = this.options, - fragmentId = /^#.+/; // Safari 2 reports '#' for an empty hash + scrolled = false; - this.list = this.element.find( "ol,ul" ).eq( 0 ); - this.lis = $( " > li:has(a[href])", this.list ); - this.anchors = this.lis.map(function() { - return $( "a", this )[ 0 ]; - }); - this.panels = $( [] ); - - this.anchors.each(function( i, a ) { - var href = $( a ).attr( "href" ); - // For dynamically created HTML that contains a hash as href IE < 8 expands - // such href to the full page url with hash and then misinterprets tab as ajax. - // Same consideration applies for an added tab with a fragment identifier - // since a[href=#fragment-identifier] does unexpectedly not match. - // Thus normalize href attribute... - var hrefBase = href.split( "#" )[ 0 ], - baseEl; - if ( hrefBase && ( hrefBase === location.toString().split( "#" )[ 0 ] || - ( baseEl = $( "base" )[ 0 ]) && hrefBase === baseEl.href ) ) { - href = a.hash; - a.href = href; - } - - // inline tab - if ( fragmentId.test( href ) ) { - self.panels = self.panels.add( self.element.find( self._sanitizeSelector( href ) ) ); - // remote tab - // prevent loading the page itself if href is just "#" - } else if ( href && href !== "#" ) { - // required for restore on destroy - $.data( a, "href.tabs", href ); - - // TODO until #3808 is fixed strip fragment identifier from url - // (IE fails to load from such url) - $.data( a, "load.tabs", href.replace( /#.*$/, "" ) ); - - var id = self._tabId( a ); - a.href = "#" + id; - var $panel = self.element.find( "#" + id ); - if ( !$panel.length ) { - $panel = $( o.panelTemplate ) - .attr( "id", id ) - .addClass( "ui-tabs-panel ui-widget-content ui-corner-bottom" ) - .insertAfter( self.panels[ i - 1 ] || self.list ); - $panel.data( "destroy.tabs", true ); - } - self.panels = self.panels.add( $panel ); - // invalid tab href - } else { - o.disabled.push( i ); - } - }); - - // initialization from scratch - if ( init ) { - // attach necessary classes for styling - this.element.addClass( "ui-tabs ui-widget ui-widget-content ui-corner-all" ); - this.list.addClass( "ui-tabs-nav ui-helper-reset ui-helper-clearfix ui-widget-header ui-corner-all" ); - this.lis.addClass( "ui-state-default ui-corner-top" ); - this.panels.addClass( "ui-tabs-panel ui-widget-content ui-corner-bottom" ); - - // Selected tab - // use "selected" option or try to retrieve: - // 1. from fragment identifier in url - // 2. from cookie - // 3. from selected class attribute on
            • - if ( o.selected === undefined ) { - if ( location.hash ) { - this.anchors.each(function( i, a ) { - if ( a.hash == location.hash ) { - o.selected = i; - return false; - } - }); - } - if ( typeof o.selected !== "number" && o.cookie ) { - o.selected = parseInt( self._cookie(), 10 ); - } - if ( typeof o.selected !== "number" && this.lis.filter( ".ui-tabs-selected" ).length ) { - o.selected = this.lis.index( this.lis.filter( ".ui-tabs-selected" ) ); - } - o.selected = o.selected || ( this.lis.length ? 0 : -1 ); - } else if ( o.selected === null ) { // usage of null is deprecated, TODO remove in next release - o.selected = -1; - } + //Compute the helpers position + this.position = this._generatePosition( event ); + this.positionAbs = this._convertPositionTo( "absolute" ); - // sanity check - default to first tab... - o.selected = ( ( o.selected >= 0 && this.anchors[ o.selected ] ) || o.selected < 0 ) - ? o.selected - : 0; + if ( !this.lastPositionAbs ) { + this.lastPositionAbs = this.positionAbs; + } - // Take disabling tabs via class attribute from HTML - // into account and update option properly. - // A selected tab cannot become disabled. - o.disabled = $.unique( o.disabled.concat( - $.map( this.lis.filter( ".ui-state-disabled" ), function( n, i ) { - return self.lis.index( n ); - }) - ) ).sort(); + //Do scrolling + if ( this.options.scroll ) { + if ( this.scrollParent[ 0 ] !== this.document[ 0 ] && + this.scrollParent[ 0 ].tagName !== "HTML" ) { + + if ( ( this.overflowOffset.top + this.scrollParent[ 0 ].offsetHeight ) - + event.pageY < o.scrollSensitivity ) { + this.scrollParent[ 0 ].scrollTop = + scrolled = this.scrollParent[ 0 ].scrollTop + o.scrollSpeed; + } else if ( event.pageY - this.overflowOffset.top < o.scrollSensitivity ) { + this.scrollParent[ 0 ].scrollTop = + scrolled = this.scrollParent[ 0 ].scrollTop - o.scrollSpeed; + } - if ( $.inArray( o.selected, o.disabled ) != -1 ) { - o.disabled.splice( $.inArray( o.selected, o.disabled ), 1 ); - } + if ( ( this.overflowOffset.left + this.scrollParent[ 0 ].offsetWidth ) - + event.pageX < o.scrollSensitivity ) { + this.scrollParent[ 0 ].scrollLeft = scrolled = + this.scrollParent[ 0 ].scrollLeft + o.scrollSpeed; + } else if ( event.pageX - this.overflowOffset.left < o.scrollSensitivity ) { + this.scrollParent[ 0 ].scrollLeft = scrolled = + this.scrollParent[ 0 ].scrollLeft - o.scrollSpeed; + } - // highlight selected tab - this.panels.addClass( "ui-tabs-hide" ); - this.lis.removeClass( "ui-tabs-selected ui-state-active" ); - // check for length avoids error when initializing empty list - if ( o.selected >= 0 && this.anchors.length ) { - self.element.find( self._sanitizeSelector( self.anchors[ o.selected ].hash ) ).removeClass( "ui-tabs-hide" ); - this.lis.eq( o.selected ).addClass( "ui-tabs-selected ui-state-active" ); + } else { + + if ( event.pageY - this.document.scrollTop() < o.scrollSensitivity ) { + scrolled = this.document.scrollTop( this.document.scrollTop() - o.scrollSpeed ); + } else if ( this.window.height() - ( event.pageY - this.document.scrollTop() ) < + o.scrollSensitivity ) { + scrolled = this.document.scrollTop( this.document.scrollTop() + o.scrollSpeed ); + } - // seems to be expected behavior that the show callback is fired - self.element.queue( "tabs", function() { - self._trigger( "show", null, - self._ui( self.anchors[ o.selected ], self.element.find( self._sanitizeSelector( self.anchors[ o.selected ].hash ) )[ 0 ] ) ); - }); + if ( event.pageX - this.document.scrollLeft() < o.scrollSensitivity ) { + scrolled = this.document.scrollLeft( + this.document.scrollLeft() - o.scrollSpeed + ); + } else if ( this.window.width() - ( event.pageX - this.document.scrollLeft() ) < + o.scrollSensitivity ) { + scrolled = this.document.scrollLeft( + this.document.scrollLeft() + o.scrollSpeed + ); + } - this.load( o.selected ); } - // clean up to avoid memory leaks in certain versions of IE 6 - // TODO: namespace this event - $( window ).bind( "unload", function() { - self.lis.add( self.anchors ).unbind( ".tabs" ); - self.lis = self.anchors = self.panels = null; - }); - // update selected after add/remove - } else { - o.selected = this.lis.index( this.lis.filter( ".ui-tabs-selected" ) ); + if ( scrolled !== false && $.ui.ddmanager && !o.dropBehaviour ) { + $.ui.ddmanager.prepareOffsets( this, event ); + } } - // update collapsible - // TODO: use .toggleClass() - this.element[ o.collapsible ? "addClass" : "removeClass" ]( "ui-tabs-collapsible" ); + //Regenerate the absolute position used for position checks + this.positionAbs = this._convertPositionTo( "absolute" ); - // set or update cookie after init and add/remove respectively - if ( o.cookie ) { - this._cookie( o.selected, o.cookie ); + //Set the helper position + if ( !this.options.axis || this.options.axis !== "y" ) { + this.helper[ 0 ].style.left = this.position.left + "px"; } - - // disable tabs - for ( var i = 0, li; ( li = this.lis[ i ] ); i++ ) { - $( li )[ $.inArray( i, o.disabled ) != -1 && - // TODO: use .toggleClass() - !$( li ).hasClass( "ui-tabs-selected" ) ? "addClass" : "removeClass" ]( "ui-state-disabled" ); + if ( !this.options.axis || this.options.axis !== "x" ) { + this.helper[ 0 ].style.top = this.position.top + "px"; } - // reset cache if switching from cached to not cached - if ( o.cache === false ) { - this.anchors.removeData( "cache.tabs" ); - } + //Rearrange + for ( i = this.items.length - 1; i >= 0; i-- ) { + + //Cache variables and intersection, continue if no intersection + item = this.items[ i ]; + itemElement = item.item[ 0 ]; + intersection = this._intersectsWithPointer( item ); + if ( !intersection ) { + continue; + } + + // Only put the placeholder inside the current Container, skip all + // items from other containers. This works because when moving + // an item from one container to another the + // currentContainer is switched before the placeholder is moved. + // + // Without this, moving items in "sub-sortables" can cause + // the placeholder to jitter between the outer and inner container. + if ( item.instance !== this.currentContainer ) { + continue; + } + + // Cannot intersect with itself + // no useless actions that have been done before + // no action if the item moved is the parent of the item checked + if ( itemElement !== this.currentItem[ 0 ] && + this.placeholder[ intersection === 1 ? "next" : "prev" ]()[ 0 ] !== itemElement && + !$.contains( this.placeholder[ 0 ], itemElement ) && + ( this.options.type === "semi-dynamic" ? + !$.contains( this.element[ 0 ], itemElement ) : + true + ) + ) { - // remove all handlers before, tabify may run on existing tabs after add or option change - this.lis.add( this.anchors ).unbind( ".tabs" ); + this.direction = intersection === 1 ? "down" : "up"; - if ( o.event !== "mouseover" ) { - var addState = function( state, el ) { - if ( el.is( ":not(.ui-state-disabled)" ) ) { - el.addClass( "ui-state-" + state ); + if ( this.options.tolerance === "pointer" || this._intersectsWithSides( item ) ) { + this._rearrange( event, item ); + } else { + break; } - }; - var removeState = function( state, el ) { - el.removeClass( "ui-state-" + state ); - }; - this.lis.bind( "mouseover.tabs" , function() { - addState( "hover", $( this ) ); - }); - this.lis.bind( "mouseout.tabs", function() { - removeState( "hover", $( this ) ); - }); - this.anchors.bind( "focus.tabs", function() { - addState( "focus", $( this ).closest( "li" ) ); - }); - this.anchors.bind( "blur.tabs", function() { - removeState( "focus", $( this ).closest( "li" ) ); - }); - } - - // set up animations - var hideFx, showFx; - if ( o.fx ) { - if ( $.isArray( o.fx ) ) { - hideFx = o.fx[ 0 ]; - showFx = o.fx[ 1 ]; - } else { - hideFx = showFx = o.fx; + + this._trigger( "change", event, this._uiHash() ); + break; } } - // Reset certain styles left over from animation - // and prevent IE's ClearType bug... - function resetStyle( $el, fx ) { - $el.css( "display", "" ); - if ( !$.support.opacity && fx.opacity ) { - $el[ 0 ].style.removeAttribute( "filter" ); - } + //Post events to containers + this._contactContainers( event ); + + //Interconnect with droppables + if ( $.ui.ddmanager ) { + $.ui.ddmanager.drag( this, event ); } - // Show a tab... - var showTab = showFx - ? function( clicked, $show ) { - $( clicked ).closest( "li" ).addClass( "ui-tabs-selected ui-state-active" ); - $show.hide().removeClass( "ui-tabs-hide" ) // avoid flicker that way - .animate( showFx, showFx.duration || "normal", function() { - resetStyle( $show, showFx ); - self._trigger( "show", null, self._ui( clicked, $show[ 0 ] ) ); - }); - } - : function( clicked, $show ) { - $( clicked ).closest( "li" ).addClass( "ui-tabs-selected ui-state-active" ); - $show.removeClass( "ui-tabs-hide" ); - self._trigger( "show", null, self._ui( clicked, $show[ 0 ] ) ); - }; + //Call callbacks + this._trigger( "sort", event, this._uiHash() ); - // Hide a tab, $show is optional... - var hideTab = hideFx - ? function( clicked, $hide ) { - $hide.animate( hideFx, hideFx.duration || "normal", function() { - self.lis.removeClass( "ui-tabs-selected ui-state-active" ); - $hide.addClass( "ui-tabs-hide" ); - resetStyle( $hide, hideFx ); - self.element.dequeue( "tabs" ); - }); - } - : function( clicked, $hide, $show ) { - self.lis.removeClass( "ui-tabs-selected ui-state-active" ); - $hide.addClass( "ui-tabs-hide" ); - self.element.dequeue( "tabs" ); - }; + this.lastPositionAbs = this.positionAbs; + return false; - // attach tab event handler, unbind to avoid duplicates from former tabifying... - this.anchors.bind( o.event + ".tabs", function() { - var el = this, - $li = $(el).closest( "li" ), - $hide = self.panels.filter( ":not(.ui-tabs-hide)" ), - $show = self.element.find( self._sanitizeSelector( el.hash ) ); - - // If tab is already selected and not collapsible or tab disabled or - // or is already loading or click callback returns false stop here. - // Check if click handler returns false last so that it is not executed - // for a disabled or loading tab! - if ( ( $li.hasClass( "ui-tabs-selected" ) && !o.collapsible) || - $li.hasClass( "ui-state-disabled" ) || - $li.hasClass( "ui-state-processing" ) || - self.panels.filter( ":animated" ).length || - self._trigger( "select", null, self._ui( this, $show[ 0 ] ) ) === false ) { - this.blur(); - return false; - } + }, - o.selected = self.anchors.index( this ); + _mouseStop: function( event, noPropagation ) { - self.abort(); + if ( !event ) { + return; + } - // if tab may be closed - if ( o.collapsible ) { - if ( $li.hasClass( "ui-tabs-selected" ) ) { - o.selected = -1; + //If we are using droppables, inform the manager about the drop + if ( $.ui.ddmanager && !this.options.dropBehaviour ) { + $.ui.ddmanager.drop( this, event ); + } + + if ( this.options.revert ) { + var that = this, + cur = this.placeholder.offset(), + axis = this.options.axis, + animation = {}; + + if ( !axis || axis === "x" ) { + animation.left = cur.left - this.offset.parent.left - this.margins.left + + ( this.offsetParent[ 0 ] === this.document[ 0 ].body ? + 0 : + this.offsetParent[ 0 ].scrollLeft + ); + } + if ( !axis || axis === "y" ) { + animation.top = cur.top - this.offset.parent.top - this.margins.top + + ( this.offsetParent[ 0 ] === this.document[ 0 ].body ? + 0 : + this.offsetParent[ 0 ].scrollTop + ); + } + this.reverting = true; + $( this.helper ).animate( + animation, + parseInt( this.options.revert, 10 ) || 500, + function() { + that._clear( event ); + } + ); + } else { + this._clear( event, noPropagation ); + } - if ( o.cookie ) { - self._cookie( o.selected, o.cookie ); - } + return false; - self.element.queue( "tabs", function() { - hideTab( el, $hide ); - }).dequeue( "tabs" ); + }, - this.blur(); - return false; - } else if ( !$hide.length ) { - if ( o.cookie ) { - self._cookie( o.selected, o.cookie ); - } + cancel: function() { - self.element.queue( "tabs", function() { - showTab( el, $show ); - }); + if ( this.dragging ) { - // TODO make passing in node possible, see also http://dev.jqueryui.com/ticket/3171 - self.load( self.anchors.index( this ) ); + this._mouseUp( new $.Event( "mouseup", { target: null } ) ); - this.blur(); - return false; - } + if ( this.options.helper === "original" ) { + this.currentItem.css( this._storedCSS ); + this._removeClass( this.currentItem, "ui-sortable-helper" ); + } else { + this.currentItem.show(); } - if ( o.cookie ) { - self._cookie( o.selected, o.cookie ); + //Post deactivating events to containers + for ( var i = this.containers.length - 1; i >= 0; i-- ) { + this.containers[ i ]._trigger( "deactivate", null, this._uiHash( this ) ); + if ( this.containers[ i ].containerCache.over ) { + this.containers[ i ]._trigger( "out", null, this._uiHash( this ) ); + this.containers[ i ].containerCache.over = 0; + } } - // show new tab - if ( $show.length ) { - if ( $hide.length ) { - self.element.queue( "tabs", function() { - hideTab( el, $hide ); - }); - } - self.element.queue( "tabs", function() { - showTab( el, $show ); - }); + } - self.load( self.anchors.index( this ) ); - } else { - throw "jQuery UI Tabs: Mismatching fragment identifier."; - } + if ( this.placeholder ) { - // Prevent IE from keeping other link focussed when using the back button - // and remove dotted border from clicked link. This is controlled via CSS - // in modern browsers; blur() removes focus from address bar in Firefox - // which can become a usability and annoying problem with tabs('rotate'). - if ( $.browser.msie ) { - this.blur(); + //$(this.placeholder[0]).remove(); would have been the jQuery way - unfortunately, + // it unbinds ALL events from the original node! + if ( this.placeholder[ 0 ].parentNode ) { + this.placeholder[ 0 ].parentNode.removeChild( this.placeholder[ 0 ] ); + } + if ( this.options.helper !== "original" && this.helper && + this.helper[ 0 ].parentNode ) { + this.helper.remove(); } - }); - // disable click in any case - this.anchors.bind( "click.tabs", function(){ - return false; - }); - }, + $.extend( this, { + helper: null, + dragging: false, + reverting: false, + _noFinalSort: null + } ); - _getIndex: function( index ) { - // meta-function to give users option to provide a href string instead of a numerical index. - // also sanitizes numerical indexes to valid values. - if ( typeof index == "string" ) { - index = this.anchors.index( this.anchors.filter( "[href$=" + index + "]" ) ); + if ( this.domPosition.prev ) { + $( this.domPosition.prev ).after( this.currentItem ); + } else { + $( this.domPosition.parent ).prepend( this.currentItem ); + } } - return index; - }, - - destroy: function() { - var o = this.options; + return this; - this.abort(); + }, - this.element - .unbind( ".tabs" ) - .removeClass( "ui-tabs ui-widget ui-widget-content ui-corner-all ui-tabs-collapsible" ) - .removeData( "tabs" ); + serialize: function( o ) { - this.list.removeClass( "ui-tabs-nav ui-helper-reset ui-helper-clearfix ui-widget-header ui-corner-all" ); + var items = this._getItemsAsjQuery( o && o.connected ), + str = []; + o = o || {}; - this.anchors.each(function() { - var href = $.data( this, "href.tabs" ); - if ( href ) { - this.href = href; + $( items ).each( function() { + var res = ( $( o.item || this ).attr( o.attribute || "id" ) || "" ) + .match( o.expression || ( /(.+)[\-=_](.+)/ ) ); + if ( res ) { + str.push( + ( o.key || res[ 1 ] + "[]" ) + + "=" + ( o.key && o.expression ? res[ 1 ] : res[ 2 ] ) ); } - var $this = $( this ).unbind( ".tabs" ); - $.each( [ "href", "load", "cache" ], function( i, prefix ) { - $this.removeData( prefix + ".tabs" ); - }); - }); + } ); - this.lis.unbind( ".tabs" ).add( this.panels ).each(function() { - if ( $.data( this, "destroy.tabs" ) ) { - $( this ).remove(); - } else { - $( this ).removeClass([ - "ui-state-default", - "ui-corner-top", - "ui-tabs-selected", - "ui-state-active", - "ui-state-hover", - "ui-state-focus", - "ui-state-disabled", - "ui-tabs-panel", - "ui-widget-content", - "ui-corner-bottom", - "ui-tabs-hide" - ].join( " " ) ); - } - }); - - if ( o.cookie ) { - this._cookie( null, o.cookie ); + if ( !str.length && o.key ) { + str.push( o.key + "=" ); } - return this; + return str.join( "&" ); + }, - add: function( url, label, index ) { - if ( index === undefined ) { - index = this.anchors.length; - } + toArray: function( o ) { - var self = this, - o = this.options, - $li = $( o.tabTemplate.replace( /#\{href\}/g, url ).replace( /#\{label\}/g, label ) ), - id = !url.indexOf( "#" ) ? url.replace( "#", "" ) : this._tabId( $( "a", $li )[ 0 ] ); + var items = this._getItemsAsjQuery( o && o.connected ), + ret = []; - $li.addClass( "ui-state-default ui-corner-top" ).data( "destroy.tabs", true ); + o = o || {}; - // try to find an existing element before creating a new one - var $panel = self.element.find( "#" + id ); - if ( !$panel.length ) { - $panel = $( o.panelTemplate ) - .attr( "id", id ) - .data( "destroy.tabs", true ); - } - $panel.addClass( "ui-tabs-panel ui-widget-content ui-corner-bottom ui-tabs-hide" ); + items.each( function() { + ret.push( $( o.item || this ).attr( o.attribute || "id" ) || "" ); + } ); + return ret; - if ( index >= this.lis.length ) { - $li.appendTo( this.list ); - $panel.appendTo( this.list[ 0 ].parentNode ); - } else { - $li.insertBefore( this.lis[ index ] ); - $panel.insertBefore( this.panels[ index ] ); - } + }, - o.disabled = $.map( o.disabled, function( n, i ) { - return n >= index ? ++n : n; - }); + /* Be careful with the following core functions */ + _intersectsWith: function( item ) { - this._tabify(); + var x1 = this.positionAbs.left, + x2 = x1 + this.helperProportions.width, + y1 = this.positionAbs.top, + y2 = y1 + this.helperProportions.height, + l = item.left, + r = l + item.width, + t = item.top, + b = t + item.height, + dyClick = this.offset.click.top, + dxClick = this.offset.click.left, + isOverElementHeight = ( this.options.axis === "x" ) || ( ( y1 + dyClick ) > t && + ( y1 + dyClick ) < b ), + isOverElementWidth = ( this.options.axis === "y" ) || ( ( x1 + dxClick ) > l && + ( x1 + dxClick ) < r ), + isOverElement = isOverElementHeight && isOverElementWidth; + + if ( this.options.tolerance === "pointer" || + this.options.forcePointerForContainers || + ( this.options.tolerance !== "pointer" && + this.helperProportions[ this.floating ? "width" : "height" ] > + item[ this.floating ? "width" : "height" ] ) + ) { + return isOverElement; + } else { - if ( this.anchors.length == 1 ) { - o.selected = 0; - $li.addClass( "ui-tabs-selected ui-state-active" ); - $panel.removeClass( "ui-tabs-hide" ); - this.element.queue( "tabs", function() { - self._trigger( "show", null, self._ui( self.anchors[ 0 ], self.panels[ 0 ] ) ); - }); + return ( l < x1 + ( this.helperProportions.width / 2 ) && // Right Half + x2 - ( this.helperProportions.width / 2 ) < r && // Left Half + t < y1 + ( this.helperProportions.height / 2 ) && // Bottom Half + y2 - ( this.helperProportions.height / 2 ) < b ); // Top Half - this.load( 0 ); } - - this._trigger( "add", null, this._ui( this.anchors[ index ], this.panels[ index ] ) ); - return this; }, - remove: function( index ) { - index = this._getIndex( index ); - var o = this.options, - $li = this.lis.eq( index ).remove(), - $panel = this.panels.eq( index ).remove(); + _intersectsWithPointer: function( item ) { + var verticalDirection, horizontalDirection, + isOverElementHeight = ( this.options.axis === "x" ) || + this._isOverAxis( + this.positionAbs.top + this.offset.click.top, item.top, item.height ), + isOverElementWidth = ( this.options.axis === "y" ) || + this._isOverAxis( + this.positionAbs.left + this.offset.click.left, item.left, item.width ), + isOverElement = isOverElementHeight && isOverElementWidth; - // If selected tab was removed focus tab to the right or - // in case the last tab was removed the tab to the left. - if ( $li.hasClass( "ui-tabs-selected" ) && this.anchors.length > 1) { - this.select( index + ( index + 1 < this.anchors.length ? 1 : -1 ) ); + if ( !isOverElement ) { + return false; } - o.disabled = $.map( - $.grep( o.disabled, function(n, i) { - return n != index; - }), - function( n, i ) { - return n >= index ? --n : n; - }); + verticalDirection = this._getDragVerticalDirection(); + horizontalDirection = this._getDragHorizontalDirection(); - this._tabify(); + return this.floating ? + ( ( horizontalDirection === "right" || verticalDirection === "down" ) ? 2 : 1 ) + : ( verticalDirection && ( verticalDirection === "down" ? 2 : 1 ) ); - this._trigger( "remove", null, this._ui( $li.find( "a" )[ 0 ], $panel[ 0 ] ) ); - return this; }, - enable: function( index ) { - index = this._getIndex( index ); - var o = this.options; - if ( $.inArray( index, o.disabled ) == -1 ) { - return; - } + _intersectsWithSides: function( item ) { - this.lis.eq( index ).removeClass( "ui-state-disabled" ); - o.disabled = $.grep( o.disabled, function( n, i ) { - return n != index; - }); + var isOverBottomHalf = this._isOverAxis( this.positionAbs.top + + this.offset.click.top, item.top + ( item.height / 2 ), item.height ), + isOverRightHalf = this._isOverAxis( this.positionAbs.left + + this.offset.click.left, item.left + ( item.width / 2 ), item.width ), + verticalDirection = this._getDragVerticalDirection(), + horizontalDirection = this._getDragHorizontalDirection(); - this._trigger( "enable", null, this._ui( this.anchors[ index ], this.panels[ index ] ) ); - return this; - }, + if ( this.floating && horizontalDirection ) { + return ( ( horizontalDirection === "right" && isOverRightHalf ) || + ( horizontalDirection === "left" && !isOverRightHalf ) ); + } else { + return verticalDirection && ( ( verticalDirection === "down" && isOverBottomHalf ) || + ( verticalDirection === "up" && !isOverBottomHalf ) ); + } - disable: function( index ) { - index = this._getIndex( index ); - var self = this, o = this.options; - // cannot disable already selected tab - if ( index != o.selected ) { - this.lis.eq( index ).addClass( "ui-state-disabled" ); + }, - o.disabled.push( index ); - o.disabled.sort(); + _getDragVerticalDirection: function() { + var delta = this.positionAbs.top - this.lastPositionAbs.top; + return delta !== 0 && ( delta > 0 ? "down" : "up" ); + }, - this._trigger( "disable", null, this._ui( this.anchors[ index ], this.panels[ index ] ) ); - } + _getDragHorizontalDirection: function() { + var delta = this.positionAbs.left - this.lastPositionAbs.left; + return delta !== 0 && ( delta > 0 ? "right" : "left" ); + }, + refresh: function( event ) { + this._refreshItems( event ); + this._setHandleClassName(); + this.refreshPositions(); return this; }, - select: function( index ) { - index = this._getIndex( index ); - if ( index == -1 ) { - if ( this.options.collapsible && this.options.selected != -1 ) { - index = this.options.selected; - } else { - return this; + _connectWith: function() { + var options = this.options; + return options.connectWith.constructor === String ? + [ options.connectWith ] : + options.connectWith; + }, + + _getItemsAsjQuery: function( connected ) { + + var i, j, cur, inst, + items = [], + queries = [], + connectWith = this._connectWith(); + + if ( connectWith && connected ) { + for ( i = connectWith.length - 1; i >= 0; i-- ) { + cur = $( connectWith[ i ], this.document[ 0 ] ); + for ( j = cur.length - 1; j >= 0; j-- ) { + inst = $.data( cur[ j ], this.widgetFullName ); + if ( inst && inst !== this && !inst.options.disabled ) { + queries.push( [ $.isFunction( inst.options.items ) ? + inst.options.items.call( inst.element ) : + $( inst.options.items, inst.element ) + .not( ".ui-sortable-helper" ) + .not( ".ui-sortable-placeholder" ), inst ] ); + } + } } } - this.anchors.eq( index ).trigger( this.options.event + ".tabs" ); - return this; - }, - - load: function( index ) { - index = this._getIndex( index ); - var self = this, - o = this.options, - a = this.anchors.eq( index )[ 0 ], - url = $.data( a, "load.tabs" ); - this.abort(); + queries.push( [ $.isFunction( this.options.items ) ? + this.options.items + .call( this.element, null, { options: this.options, item: this.currentItem } ) : + $( this.options.items, this.element ) + .not( ".ui-sortable-helper" ) + .not( ".ui-sortable-placeholder" ), this ] ); - // not remote or from cache - if ( !url || this.element.queue( "tabs" ).length !== 0 && $.data( a, "cache.tabs" ) ) { - this.element.dequeue( "tabs" ); - return; + function addItems() { + items.push( this ); + } + for ( i = queries.length - 1; i >= 0; i-- ) { + queries[ i ][ 0 ].each( addItems ); } - // load remote from here on - this.lis.eq( index ).addClass( "ui-state-processing" ); + return $( items ); - if ( o.spinner ) { - var span = $( "span", a ); - span.data( "label.tabs", span.html() ).html( o.spinner ); - } + }, - this.xhr = $.ajax( $.extend( {}, o.ajaxOptions, { - url: url, - success: function( r, s ) { - self.element.find( self._sanitizeSelector( a.hash ) ).html( r ); + _removeCurrentsFromItems: function() { - // take care of tab labels - self._cleanup(); + var list = this.currentItem.find( ":data(" + this.widgetName + "-item)" ); - if ( o.cache ) { - $.data( a, "cache.tabs", true ); + this.items = $.grep( this.items, function( item ) { + for ( var j = 0; j < list.length; j++ ) { + if ( list[ j ] === item.item[ 0 ] ) { + return false; } + } + return true; + } ); - self._trigger( "load", null, self._ui( self.anchors[ index ], self.panels[ index ] ) ); - try { - o.ajaxOptions.success( r, s ); - } - catch ( e ) {} - }, - error: function( xhr, s, e ) { - // take care of tab labels - self._cleanup(); + }, - self._trigger( "load", null, self._ui( self.anchors[ index ], self.panels[ index ] ) ); - try { - // Passing index avoid a race condition when this method is - // called after the user has selected another tab. - // Pass the anchor that initiated this request allows - // loadError to manipulate the tab content panel via $(a.hash) - o.ajaxOptions.error( xhr, s, index, a ); + _refreshItems: function( event ) { + + this.items = []; + this.containers = [ this ]; + + var i, j, cur, inst, targetData, _queries, item, queriesLength, + items = this.items, + queries = [ [ $.isFunction( this.options.items ) ? + this.options.items.call( this.element[ 0 ], event, { item: this.currentItem } ) : + $( this.options.items, this.element ), this ] ], + connectWith = this._connectWith(); + + //Shouldn't be run the first time through due to massive slow-down + if ( connectWith && this.ready ) { + for ( i = connectWith.length - 1; i >= 0; i-- ) { + cur = $( connectWith[ i ], this.document[ 0 ] ); + for ( j = cur.length - 1; j >= 0; j-- ) { + inst = $.data( cur[ j ], this.widgetFullName ); + if ( inst && inst !== this && !inst.options.disabled ) { + queries.push( [ $.isFunction( inst.options.items ) ? + inst.options.items + .call( inst.element[ 0 ], event, { item: this.currentItem } ) : + $( inst.options.items, inst.element ), inst ] ); + this.containers.push( inst ); + } } - catch ( e ) {} } - } ) ); - - // last, so that load event is fired before show... - self.element.dequeue( "tabs" ); + } - return this; - }, + for ( i = queries.length - 1; i >= 0; i-- ) { + targetData = queries[ i ][ 1 ]; + _queries = queries[ i ][ 0 ]; - abort: function() { - // stop possibly running animations - this.element.queue( [] ); - this.panels.stop( false, true ); + for ( j = 0, queriesLength = _queries.length; j < queriesLength; j++ ) { + item = $( _queries[ j ] ); - // "tabs" queue must not contain more than two elements, - // which are the callbacks for the latest clicked tab... - this.element.queue( "tabs", this.element.queue( "tabs" ).splice( -2, 2 ) ); + // Data for target checking (mouse manager) + item.data( this.widgetName + "-item", targetData ); - // terminate pending requests from other tabs - if ( this.xhr ) { - this.xhr.abort(); - delete this.xhr; + items.push( { + item: item, + instance: targetData, + width: 0, height: 0, + left: 0, top: 0 + } ); + } } - // take care of tab labels - this._cleanup(); - return this; - }, - - url: function( index, url ) { - this.anchors.eq( index ).removeData( "cache.tabs" ).data( "load.tabs", url ); - return this; }, - length: function() { - return this.anchors.length; - } -}); + refreshPositions: function( fast ) { -$.extend( $.ui.tabs, { - version: "1.8.17" -}); + // Determine whether items are being displayed horizontally + this.floating = this.items.length ? + this.options.axis === "x" || this._isFloating( this.items[ 0 ].item ) : + false; -/* - * Tabs Extensions - */ + //This has to be redone because due to the item being moved out/into the offsetParent, + // the offsetParent's position will change + if ( this.offsetParent && this.helper ) { + this.offset.parent = this._getParentOffset(); + } -/* - * Rotate - */ -$.extend( $.ui.tabs.prototype, { - rotation: null, - rotate: function( ms, continuing ) { - var self = this, - o = this.options; + var i, item, t, p; - var rotate = self._rotate || ( self._rotate = function( e ) { - clearTimeout( self.rotation ); - self.rotation = setTimeout(function() { - var t = o.selected; - self.select( ++t < self.anchors.length ? t : 0 ); - }, ms ); + for ( i = this.items.length - 1; i >= 0; i-- ) { + item = this.items[ i ]; - if ( e ) { - e.stopPropagation(); + //We ignore calculating positions of all connected containers when we're not over them + if ( item.instance !== this.currentContainer && this.currentContainer && + item.item[ 0 ] !== this.currentItem[ 0 ] ) { + continue; } - }); - var stop = self._unrotate || ( self._unrotate = !continuing - ? function(e) { - if (e.clientX) { // in case of a true click - self.rotate(null); - } + t = this.options.toleranceElement ? + $( this.options.toleranceElement, item.item ) : + item.item; + + if ( !fast ) { + item.width = t.outerWidth(); + item.height = t.outerHeight(); } - : function( e ) { - t = o.selected; - rotate(); - }); - // start rotation - if ( ms ) { - this.element.bind( "tabsshow", rotate ); - this.anchors.bind( o.event + ".tabs", stop ); - rotate(); - // stop rotation + p = t.offset(); + item.left = p.left; + item.top = p.top; + } + + if ( this.options.custom && this.options.custom.refreshContainers ) { + this.options.custom.refreshContainers.call( this ); } else { - clearTimeout( self.rotation ); - this.element.unbind( "tabsshow", rotate ); - this.anchors.unbind( o.event + ".tabs", stop ); - delete this._rotate; - delete this._unrotate; + for ( i = this.containers.length - 1; i >= 0; i-- ) { + p = this.containers[ i ].element.offset(); + this.containers[ i ].containerCache.left = p.left; + this.containers[ i ].containerCache.top = p.top; + this.containers[ i ].containerCache.width = + this.containers[ i ].element.outerWidth(); + this.containers[ i ].containerCache.height = + this.containers[ i ].element.outerHeight(); + } } return this; - } -}); + }, -})( jQuery ); -/* - * jQuery UI Datepicker 1.8.17 - * - * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about) - * Dual licensed under the MIT or GPL Version 2 licenses. - * http://jquery.org/license - * - * http://docs.jquery.com/UI/Datepicker - * - * Depends: - * jquery.ui.core.js - */ -(function( $, undefined ) { + _createPlaceholder: function( that ) { + that = that || this; + var className, + o = that.options; + + if ( !o.placeholder || o.placeholder.constructor === String ) { + className = o.placeholder; + o.placeholder = { + element: function() { -$.extend($.ui, { datepicker: { version: "1.8.17" } }); + var nodeName = that.currentItem[ 0 ].nodeName.toLowerCase(), + element = $( "<" + nodeName + ">", that.document[ 0 ] ); + + that._addClass( element, "ui-sortable-placeholder", + className || that.currentItem[ 0 ].className ) + ._removeClass( element, "ui-sortable-helper" ); + + if ( nodeName === "tbody" ) { + that._createTrPlaceholder( + that.currentItem.find( "tr" ).eq( 0 ), + $( "
            • ", that.document[ 0 ] ).appendTo( element ) + ); + } else if ( nodeName === "tr" ) { + that._createTrPlaceholder( that.currentItem, element ); + } else if ( nodeName === "img" ) { + element.attr( "src", that.currentItem.attr( "src" ) ); + } -var PROP_NAME = 'datepicker'; -var dpuuid = new Date().getTime(); -var instActive; + if ( !className ) { + element.css( "visibility", "hidden" ); + } -/* Date picker manager. - Use the singleton instance of this class, $.datepicker, to interact with the date picker. - Settings for (groups of) date pickers are maintained in an instance object, - allowing multiple different settings on the same page. */ + return element; + }, + update: function( container, p ) { -function Datepicker() { - this.debug = false; // Change this to true to start debugging - this._curInst = null; // The current instance in use - this._keyEvent = false; // If the last event was a key event - this._disabledInputs = []; // List of date picker inputs that have been disabled - this._datepickerShowing = false; // True if the popup picker is showing , false if not - this._inDialog = false; // True if showing within a "dialog", false if not - this._mainDivId = 'ui-datepicker-div'; // The ID of the main datepicker division - this._inlineClass = 'ui-datepicker-inline'; // The name of the inline marker class - this._appendClass = 'ui-datepicker-append'; // The name of the append marker class - this._triggerClass = 'ui-datepicker-trigger'; // The name of the trigger marker class - this._dialogClass = 'ui-datepicker-dialog'; // The name of the dialog marker class - this._disableClass = 'ui-datepicker-disabled'; // The name of the disabled covering marker class - this._unselectableClass = 'ui-datepicker-unselectable'; // The name of the unselectable cell marker class - this._currentClass = 'ui-datepicker-current-day'; // The name of the current day marker class - this._dayOverClass = 'ui-datepicker-days-cell-over'; // The name of the day hover marker class - this.regional = []; // Available regional settings, indexed by language code - this.regional[''] = { // Default regional settings - closeText: 'Done', // Display text for close link - prevText: 'Prev', // Display text for previous month link - nextText: 'Next', // Display text for next month link - currentText: 'Today', // Display text for current month link - monthNames: ['January','February','March','April','May','June', - 'July','August','September','October','November','December'], // Names of months for drop-down and formatting - monthNamesShort: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'], // For formatting - dayNames: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'], // For formatting - dayNamesShort: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'], // For formatting - dayNamesMin: ['Su','Mo','Tu','We','Th','Fr','Sa'], // Column headings for days starting at Sunday - weekHeader: 'Wk', // Column header for week of the year - dateFormat: 'mm/dd/yy', // See format options on parseDate - firstDay: 0, // The first day of the week, Sun = 0, Mon = 1, ... - isRTL: false, // True if right-to-left language, false if left-to-right - showMonthAfterYear: false, // True if the year select precedes month, false for month then year - yearSuffix: '' // Additional text to append to the year in the month headers - }; - this._defaults = { // Global defaults for all the date picker instances - showOn: 'focus', // 'focus' for popup on focus, - // 'button' for trigger button, or 'both' for either - showAnim: 'fadeIn', // Name of jQuery animation for popup - showOptions: {}, // Options for enhanced animations - defaultDate: null, // Used when field is blank: actual date, - // +/-number for offset from today, null for today - appendText: '', // Display text following the input box, e.g. showing the format - buttonText: '...', // Text for trigger button - buttonImage: '', // URL for trigger button image - buttonImageOnly: false, // True if the image appears alone, false if it appears on a button - hideIfNoPrevNext: false, // True to hide next/previous month links - // if not applicable, false to just disable them - navigationAsDateFormat: false, // True if date formatting applied to prev/today/next links - gotoCurrent: false, // True if today link goes back to current selection instead - changeMonth: false, // True if month can be selected directly, false if only prev/next - changeYear: false, // True if year can be selected directly, false if only prev/next - yearRange: 'c-10:c+10', // Range of years to display in drop-down, - // either relative to today's year (-nn:+nn), relative to currently displayed year - // (c-nn:c+nn), absolute (nnnn:nnnn), or a combination of the above (nnnn:-n) - showOtherMonths: false, // True to show dates in other months, false to leave blank - selectOtherMonths: false, // True to allow selection of dates in other months, false for unselectable - showWeek: false, // True to show week of the year, false to not show it - calculateWeek: this.iso8601Week, // How to calculate the week of the year, - // takes a Date and returns the number of the week for it - shortYearCutoff: '+10', // Short year values < this are in the current century, - // > this are in the previous century, - // string value starting with '+' for current year + value - minDate: null, // The earliest selectable date, or null for no limit - maxDate: null, // The latest selectable date, or null for no limit - duration: 'fast', // Duration of display/closure - beforeShowDay: null, // Function that takes a date and returns an array with - // [0] = true if selectable, false if not, [1] = custom CSS class name(s) or '', - // [2] = cell title (optional), e.g. $.datepicker.noWeekends - beforeShow: null, // Function that takes an input field and - // returns a set of custom settings for the date picker - onSelect: null, // Define a callback function when a date is selected - onChangeMonthYear: null, // Define a callback function when the month or year is changed - onClose: null, // Define a callback function when the datepicker is closed - numberOfMonths: 1, // Number of months to show at a time - showCurrentAtPos: 0, // The position in multipe months at which to show the current month (starting at 0) - stepMonths: 1, // Number of months to step back/forward - stepBigMonths: 12, // Number of months to step back/forward for the big links - altField: '', // Selector for an alternate field to store selected dates into - altFormat: '', // The date format to use for the alternate field - constrainInput: true, // The input is constrained by the current date format - showButtonPanel: false, // True to show button panel, false to not show it - autoSize: false, // True to size the input for the date format, false to leave as is - disabled: false // The initial disabled state - }; - $.extend(this._defaults, this.regional['']); - this.dpDiv = bindHover($('
              ')); -} + // 1. If a className is set as 'placeholder option, we don't force sizes - + // the class is responsible for that + // 2. The option 'forcePlaceholderSize can be enabled to force it even if a + // class name is specified + if ( className && !o.forcePlaceholderSize ) { + return; + } -$.extend(Datepicker.prototype, { - /* Class name added to elements to indicate already configured with a date picker. */ - markerClassName: 'hasDatepicker', + //If the element doesn't have a actual height by itself (without styles coming + // from a stylesheet), it receives the inline height from the dragged item + if ( !p.height() ) { + p.height( + that.currentItem.innerHeight() - + parseInt( that.currentItem.css( "paddingTop" ) || 0, 10 ) - + parseInt( that.currentItem.css( "paddingBottom" ) || 0, 10 ) ); + } + if ( !p.width() ) { + p.width( + that.currentItem.innerWidth() - + parseInt( that.currentItem.css( "paddingLeft" ) || 0, 10 ) - + parseInt( that.currentItem.css( "paddingRight" ) || 0, 10 ) ); + } + } + }; + } - //Keep track of the maximum number of rows displayed (see #7043) - maxRows: 4, + //Create the placeholder + that.placeholder = $( o.placeholder.element.call( that.element, that.currentItem ) ); - /* Debug logging (if enabled). */ - log: function () { - if (this.debug) - console.log.apply('', arguments); - }, + //Append it after the actual current item + that.currentItem.after( that.placeholder ); + + //Update the size of the placeholder (TODO: Logic to fuzzy, see line 316/317) + o.placeholder.update( that, that.placeholder ); - // TODO rename to "widget" when switching to widget factory - _widgetDatepicker: function() { - return this.dpDiv; }, - /* Override the default settings for all instances of the date picker. - @param settings object - the new settings to use as defaults (anonymous object) - @return the manager object */ - setDefaults: function(settings) { - extendRemove(this._defaults, settings || {}); - return this; + _createTrPlaceholder: function( sourceTr, targetTr ) { + var that = this; + + sourceTr.children().each( function() { + $( "
              ", that.document[ 0 ] ) + .attr( "colspan", $( this ).attr( "colspan" ) || 1 ) + .appendTo( targetTr ); + } ); }, - /* Attach the date picker to a jQuery selection. - @param target element - the target input field or division or span - @param settings object - the new settings to use for this date picker instance (anonymous) */ - _attachDatepicker: function(target, settings) { - // check for settings on the control itself - in namespace 'date:' - var inlineSettings = null; - for (var attrName in this._defaults) { - var attrValue = target.getAttribute('date:' + attrName); - if (attrValue) { - inlineSettings = inlineSettings || {}; - try { - inlineSettings[attrName] = eval(attrValue); - } catch (err) { - inlineSettings[attrName] = attrValue; + _contactContainers: function( event ) { + var i, j, dist, itemWithLeastDistance, posProperty, sizeProperty, cur, nearBottom, + floating, axis, + innermostContainer = null, + innermostIndex = null; + + // Get innermost container that intersects with item + for ( i = this.containers.length - 1; i >= 0; i-- ) { + + // Never consider a container that's located within the item itself + if ( $.contains( this.currentItem[ 0 ], this.containers[ i ].element[ 0 ] ) ) { + continue; + } + + if ( this._intersectsWith( this.containers[ i ].containerCache ) ) { + + // If we've already found a container and it's more "inner" than this, then continue + if ( innermostContainer && + $.contains( + this.containers[ i ].element[ 0 ], + innermostContainer.element[ 0 ] ) ) { + continue; + } + + innermostContainer = this.containers[ i ]; + innermostIndex = i; + + } else { + + // container doesn't intersect. trigger "out" event if necessary + if ( this.containers[ i ].containerCache.over ) { + this.containers[ i ]._trigger( "out", event, this._uiHash( this ) ); + this.containers[ i ].containerCache.over = 0; } } + } - var nodeName = target.nodeName.toLowerCase(); - var inline = (nodeName == 'div' || nodeName == 'span'); - if (!target.id) { - this.uuid += 1; - target.id = 'dp' + this.uuid; - } - var inst = this._newInst($(target), inline); - inst.settings = $.extend({}, settings || {}, inlineSettings || {}); - if (nodeName == 'input') { - this._connectDatepicker(target, inst); - } else if (inline) { - this._inlineDatepicker(target, inst); + + // If no intersecting containers found, return + if ( !innermostContainer ) { + return; } - }, - /* Create a new instance object. */ - _newInst: function(target, inline) { - var id = target[0].id.replace(/([^A-Za-z0-9_-])/g, '\\\\$1'); // escape jQuery meta chars - return {id: id, input: target, // associated target - selectedDay: 0, selectedMonth: 0, selectedYear: 0, // current selection - drawMonth: 0, drawYear: 0, // month being drawn - inline: inline, // is datepicker inline or not - dpDiv: (!inline ? this.dpDiv : // presentation div - bindHover($('
              ')))}; - }, + // Move the item into the container if it's not there already + if ( this.containers.length === 1 ) { + if ( !this.containers[ innermostIndex ].containerCache.over ) { + this.containers[ innermostIndex ]._trigger( "over", event, this._uiHash( this ) ); + this.containers[ innermostIndex ].containerCache.over = 1; + } + } else { + + // When entering a new container, we will find the item with the least distance and + // append our item near it + dist = 10000; + itemWithLeastDistance = null; + floating = innermostContainer.floating || this._isFloating( this.currentItem ); + posProperty = floating ? "left" : "top"; + sizeProperty = floating ? "width" : "height"; + axis = floating ? "pageX" : "pageY"; + + for ( j = this.items.length - 1; j >= 0; j-- ) { + if ( !$.contains( + this.containers[ innermostIndex ].element[ 0 ], this.items[ j ].item[ 0 ] ) + ) { + continue; + } + if ( this.items[ j ].item[ 0 ] === this.currentItem[ 0 ] ) { + continue; + } + + cur = this.items[ j ].item.offset()[ posProperty ]; + nearBottom = false; + if ( event[ axis ] - cur > this.items[ j ][ sizeProperty ] / 2 ) { + nearBottom = true; + } + + if ( Math.abs( event[ axis ] - cur ) < dist ) { + dist = Math.abs( event[ axis ] - cur ); + itemWithLeastDistance = this.items[ j ]; + this.direction = nearBottom ? "up" : "down"; + } + } + + //Check if dropOnEmpty is enabled + if ( !itemWithLeastDistance && !this.options.dropOnEmpty ) { + return; + } + + if ( this.currentContainer === this.containers[ innermostIndex ] ) { + if ( !this.currentContainer.containerCache.over ) { + this.containers[ innermostIndex ]._trigger( "over", event, this._uiHash() ); + this.currentContainer.containerCache.over = 1; + } + return; + } + + itemWithLeastDistance ? + this._rearrange( event, itemWithLeastDistance, null, true ) : + this._rearrange( event, null, this.containers[ innermostIndex ].element, true ); + this._trigger( "change", event, this._uiHash() ); + this.containers[ innermostIndex ]._trigger( "change", event, this._uiHash( this ) ); + this.currentContainer = this.containers[ innermostIndex ]; + + //Update the placeholder + this.options.placeholder.update( this.currentContainer, this.placeholder ); - /* Attach the date picker to an input field. */ - _connectDatepicker: function(target, inst) { - var input = $(target); - inst.append = $([]); - inst.trigger = $([]); - if (input.hasClass(this.markerClassName)) - return; - this._attachments(input, inst); - input.addClass(this.markerClassName).keydown(this._doKeyDown). - keypress(this._doKeyPress).keyup(this._doKeyUp). - bind("setData.datepicker", function(event, key, value) { - inst.settings[key] = value; - }).bind("getData.datepicker", function(event, key) { - return this._get(inst, key); - }); - this._autoSize(inst); - $.data(target, PROP_NAME, inst); - //If disabled option is true, disable the datepicker once it has been attached to the input (see ticket #5665) - if( inst.settings.disabled ) { - this._disableDatepicker( target ); + this.containers[ innermostIndex ]._trigger( "over", event, this._uiHash( this ) ); + this.containers[ innermostIndex ].containerCache.over = 1; } - }, - /* Make attachments based on settings. */ - _attachments: function(input, inst) { - var appendText = this._get(inst, 'appendText'); - var isRTL = this._get(inst, 'isRTL'); - if (inst.append) - inst.append.remove(); - if (appendText) { - inst.append = $('' + appendText + ''); - input[isRTL ? 'before' : 'after'](inst.append); - } - input.unbind('focus', this._showDatepicker); - if (inst.trigger) - inst.trigger.remove(); - var showOn = this._get(inst, 'showOn'); - if (showOn == 'focus' || showOn == 'both') // pop-up date picker when in the marked field - input.focus(this._showDatepicker); - if (showOn == 'button' || showOn == 'both') { // pop-up date picker when button clicked - var buttonText = this._get(inst, 'buttonText'); - var buttonImage = this._get(inst, 'buttonImage'); - inst.trigger = $(this._get(inst, 'buttonImageOnly') ? - $('').addClass(this._triggerClass). - attr({ src: buttonImage, alt: buttonText, title: buttonText }) : - $('').addClass(this._triggerClass). - html(buttonImage == '' ? buttonText : $('').attr( - { src:buttonImage, alt:buttonText, title:buttonText }))); - input[isRTL ? 'before' : 'after'](inst.trigger); - inst.trigger.click(function() { - if ($.datepicker._datepickerShowing && $.datepicker._lastInput == input[0]) - $.datepicker._hideDatepicker(); - else - $.datepicker._showDatepicker(input[0]); - return false; - }); - } }, - /* Apply the maximum length for the date format. */ - _autoSize: function(inst) { - if (this._get(inst, 'autoSize') && !inst.inline) { - var date = new Date(2009, 12 - 1, 20); // Ensure double digits - var dateFormat = this._get(inst, 'dateFormat'); - if (dateFormat.match(/[DM]/)) { - var findMax = function(names) { - var max = 0; - var maxI = 0; - for (var i = 0; i < names.length; i++) { - if (names[i].length > max) { - max = names[i].length; - maxI = i; - } - } - return maxI; - }; - date.setMonth(findMax(this._get(inst, (dateFormat.match(/MM/) ? - 'monthNames' : 'monthNamesShort')))); - date.setDate(findMax(this._get(inst, (dateFormat.match(/DD/) ? - 'dayNames' : 'dayNamesShort'))) + 20 - date.getDay()); - } - inst.input.attr('size', this._formatDate(inst, date).length); - } - }, + _createHelper: function( event ) { - /* Attach an inline date picker to a div. */ - _inlineDatepicker: function(target, inst) { - var divSpan = $(target); - if (divSpan.hasClass(this.markerClassName)) - return; - divSpan.addClass(this.markerClassName).append(inst.dpDiv). - bind("setData.datepicker", function(event, key, value){ - inst.settings[key] = value; - }).bind("getData.datepicker", function(event, key){ - return this._get(inst, key); - }); - $.data(target, PROP_NAME, inst); - this._setDate(inst, this._getDefaultDate(inst), true); - this._updateDatepicker(inst); - this._updateAlternate(inst); - //If disabled option is true, disable the datepicker before showing it (see ticket #5665) - if( inst.settings.disabled ) { - this._disableDatepicker( target ); + var o = this.options, + helper = $.isFunction( o.helper ) ? + $( o.helper.apply( this.element[ 0 ], [ event, this.currentItem ] ) ) : + ( o.helper === "clone" ? this.currentItem.clone() : this.currentItem ); + + //Add the helper to the DOM if that didn't happen already + if ( !helper.parents( "body" ).length ) { + $( o.appendTo !== "parent" ? + o.appendTo : + this.currentItem[ 0 ].parentNode )[ 0 ].appendChild( helper[ 0 ] ); + } + + if ( helper[ 0 ] === this.currentItem[ 0 ] ) { + this._storedCSS = { + width: this.currentItem[ 0 ].style.width, + height: this.currentItem[ 0 ].style.height, + position: this.currentItem.css( "position" ), + top: this.currentItem.css( "top" ), + left: this.currentItem.css( "left" ) + }; } - // Set display:block in place of inst.dpDiv.show() which won't work on disconnected elements - // http://bugs.jqueryui.com/ticket/7552 - A Datepicker created on a detached div has zero height - inst.dpDiv.css( "display", "block" ); - }, - /* Pop-up the date picker in a "dialog" box. - @param input element - ignored - @param date string or Date - the initial date to display - @param onSelect function - the function to call when a date is selected - @param settings object - update the dialog date picker instance's settings (anonymous object) - @param pos int[2] - coordinates for the dialog's position within the screen or - event - with x/y coordinates or - leave empty for default (screen centre) - @return the manager object */ - _dialogDatepicker: function(input, date, onSelect, settings, pos) { - var inst = this._dialogInst; // internal instance - if (!inst) { - this.uuid += 1; - var id = 'dp' + this.uuid; - this._dialogInput = $(''); - this._dialogInput.keydown(this._doKeyDown); - $('body').append(this._dialogInput); - inst = this._dialogInst = this._newInst(this._dialogInput, false); - inst.settings = {}; - $.data(this._dialogInput[0], PROP_NAME, inst); - } - extendRemove(inst.settings, settings || {}); - date = (date && date.constructor == Date ? this._formatDate(inst, date) : date); - this._dialogInput.val(date); - - this._pos = (pos ? (pos.length ? pos : [pos.pageX, pos.pageY]) : null); - if (!this._pos) { - var browserWidth = document.documentElement.clientWidth; - var browserHeight = document.documentElement.clientHeight; - var scrollX = document.documentElement.scrollLeft || document.body.scrollLeft; - var scrollY = document.documentElement.scrollTop || document.body.scrollTop; - this._pos = // should use actual width/height below - [(browserWidth / 2) - 100 + scrollX, (browserHeight / 2) - 150 + scrollY]; + if ( !helper[ 0 ].style.width || o.forceHelperSize ) { + helper.width( this.currentItem.width() ); + } + if ( !helper[ 0 ].style.height || o.forceHelperSize ) { + helper.height( this.currentItem.height() ); } - // move input on screen for focus, but hidden behind dialog - this._dialogInput.css('left', (this._pos[0] + 20) + 'px').css('top', this._pos[1] + 'px'); - inst.settings.onSelect = onSelect; - this._inDialog = true; - this.dpDiv.addClass(this._dialogClass); - this._showDatepicker(this._dialogInput[0]); - if ($.blockUI) - $.blockUI(this.dpDiv); - $.data(this._dialogInput[0], PROP_NAME, inst); - return this; - }, + return helper; - /* Detach a datepicker from its control. - @param target element - the target input field or division or span */ - _destroyDatepicker: function(target) { - var $target = $(target); - var inst = $.data(target, PROP_NAME); - if (!$target.hasClass(this.markerClassName)) { - return; - } - var nodeName = target.nodeName.toLowerCase(); - $.removeData(target, PROP_NAME); - if (nodeName == 'input') { - inst.append.remove(); - inst.trigger.remove(); - $target.removeClass(this.markerClassName). - unbind('focus', this._showDatepicker). - unbind('keydown', this._doKeyDown). - unbind('keypress', this._doKeyPress). - unbind('keyup', this._doKeyUp); - } else if (nodeName == 'div' || nodeName == 'span') - $target.removeClass(this.markerClassName).empty(); }, - /* Enable the date picker to a jQuery selection. - @param target element - the target input field or division or span */ - _enableDatepicker: function(target) { - var $target = $(target); - var inst = $.data(target, PROP_NAME); - if (!$target.hasClass(this.markerClassName)) { - return; + _adjustOffsetFromHelper: function( obj ) { + if ( typeof obj === "string" ) { + obj = obj.split( " " ); } - var nodeName = target.nodeName.toLowerCase(); - if (nodeName == 'input') { - target.disabled = false; - inst.trigger.filter('button'). - each(function() { this.disabled = false; }).end(). - filter('img').css({opacity: '1.0', cursor: ''}); + if ( $.isArray( obj ) ) { + obj = { left: +obj[ 0 ], top: +obj[ 1 ] || 0 }; } - else if (nodeName == 'div' || nodeName == 'span') { - var inline = $target.children('.' + this._inlineClass); - inline.children().removeClass('ui-state-disabled'); - inline.find("select.ui-datepicker-month, select.ui-datepicker-year"). - removeAttr("disabled"); + if ( "left" in obj ) { + this.offset.click.left = obj.left + this.margins.left; } - this._disabledInputs = $.map(this._disabledInputs, - function(value) { return (value == target ? null : value); }); // delete entry - }, - - /* Disable the date picker to a jQuery selection. - @param target element - the target input field or division or span */ - _disableDatepicker: function(target) { - var $target = $(target); - var inst = $.data(target, PROP_NAME); - if (!$target.hasClass(this.markerClassName)) { - return; + if ( "right" in obj ) { + this.offset.click.left = this.helperProportions.width - obj.right + this.margins.left; } - var nodeName = target.nodeName.toLowerCase(); - if (nodeName == 'input') { - target.disabled = true; - inst.trigger.filter('button'). - each(function() { this.disabled = true; }).end(). - filter('img').css({opacity: '0.5', cursor: 'default'}); + if ( "top" in obj ) { + this.offset.click.top = obj.top + this.margins.top; } - else if (nodeName == 'div' || nodeName == 'span') { - var inline = $target.children('.' + this._inlineClass); - inline.children().addClass('ui-state-disabled'); - inline.find("select.ui-datepicker-month, select.ui-datepicker-year"). - attr("disabled", "disabled"); + if ( "bottom" in obj ) { + this.offset.click.top = this.helperProportions.height - obj.bottom + this.margins.top; } - this._disabledInputs = $.map(this._disabledInputs, - function(value) { return (value == target ? null : value); }); // delete entry - this._disabledInputs[this._disabledInputs.length] = target; }, - /* Is the first field in a jQuery collection disabled as a datepicker? - @param target element - the target input field or division or span - @return boolean - true if disabled, false if enabled */ - _isDisabledDatepicker: function(target) { - if (!target) { - return false; + _getParentOffset: function() { + + //Get the offsetParent and cache its position + this.offsetParent = this.helper.offsetParent(); + var po = this.offsetParent.offset(); + + // This is a special case where we need to modify a offset calculated on start, since the + // following happened: + // 1. The position of the helper is absolute, so it's position is calculated based on the + // next positioned parent + // 2. The actual offset parent is a child of the scroll parent, and the scroll parent isn't + // the document, which means that the scroll is included in the initial calculation of the + // offset of the parent, and never recalculated upon drag + if ( this.cssPosition === "absolute" && this.scrollParent[ 0 ] !== this.document[ 0 ] && + $.contains( this.scrollParent[ 0 ], this.offsetParent[ 0 ] ) ) { + po.left += this.scrollParent.scrollLeft(); + po.top += this.scrollParent.scrollTop(); } - for (var i = 0; i < this._disabledInputs.length; i++) { - if (this._disabledInputs[i] == target) - return true; + + // This needs to be actually done for all browsers, since pageX/pageY includes this + // information with an ugly IE fix + if ( this.offsetParent[ 0 ] === this.document[ 0 ].body || + ( this.offsetParent[ 0 ].tagName && + this.offsetParent[ 0 ].tagName.toLowerCase() === "html" && $.ui.ie ) ) { + po = { top: 0, left: 0 }; } - return false; + + return { + top: po.top + ( parseInt( this.offsetParent.css( "borderTopWidth" ), 10 ) || 0 ), + left: po.left + ( parseInt( this.offsetParent.css( "borderLeftWidth" ), 10 ) || 0 ) + }; + }, - /* Retrieve the instance data for the target control. - @param target element - the target input field or division or span - @return object - the associated instance data - @throws error if a jQuery problem getting data */ - _getInst: function(target) { - try { - return $.data(target, PROP_NAME); - } - catch (err) { - throw 'Missing instance data for this datepicker'; + _getRelativeOffset: function() { + + if ( this.cssPosition === "relative" ) { + var p = this.currentItem.position(); + return { + top: p.top - ( parseInt( this.helper.css( "top" ), 10 ) || 0 ) + + this.scrollParent.scrollTop(), + left: p.left - ( parseInt( this.helper.css( "left" ), 10 ) || 0 ) + + this.scrollParent.scrollLeft() + }; + } else { + return { top: 0, left: 0 }; } + }, - /* Update or retrieve the settings for a date picker attached to an input field or division. - @param target element - the target input field or division or span - @param name object - the new settings to update or - string - the name of the setting to change or retrieve, - when retrieving also 'all' for all instance settings or - 'defaults' for all global defaults - @param value any - the new value for the setting - (omit if above is an object or to retrieve a value) */ - _optionDatepicker: function(target, name, value) { - var inst = this._getInst(target); - if (arguments.length == 2 && typeof name == 'string') { - return (name == 'defaults' ? $.extend({}, $.datepicker._defaults) : - (inst ? (name == 'all' ? $.extend({}, inst.settings) : - this._get(inst, name)) : null)); - } - var settings = name || {}; - if (typeof name == 'string') { - settings = {}; - settings[name] = value; - } - if (inst) { - if (this._curInst == inst) { - this._hideDatepicker(); - } - var date = this._getDateDatepicker(target, true); - var minDate = this._getMinMaxDate(inst, 'min'); - var maxDate = this._getMinMaxDate(inst, 'max'); - extendRemove(inst.settings, settings); - // reformat the old minDate/maxDate values if dateFormat changes and a new minDate/maxDate isn't provided - if (minDate !== null && settings['dateFormat'] !== undefined && settings['minDate'] === undefined) - inst.settings.minDate = this._formatDate(inst, minDate); - if (maxDate !== null && settings['dateFormat'] !== undefined && settings['maxDate'] === undefined) - inst.settings.maxDate = this._formatDate(inst, maxDate); - this._attachments($(target), inst); - this._autoSize(inst); - this._setDate(inst, date); - this._updateAlternate(inst); - this._updateDatepicker(inst); - } + _cacheMargins: function() { + this.margins = { + left: ( parseInt( this.currentItem.css( "marginLeft" ), 10 ) || 0 ), + top: ( parseInt( this.currentItem.css( "marginTop" ), 10 ) || 0 ) + }; }, - // change method deprecated - _changeDatepicker: function(target, name, value) { - this._optionDatepicker(target, name, value); + _cacheHelperProportions: function() { + this.helperProportions = { + width: this.helper.outerWidth(), + height: this.helper.outerHeight() + }; }, - /* Redraw the date picker attached to an input field or division. - @param target element - the target input field or division or span */ - _refreshDatepicker: function(target) { - var inst = this._getInst(target); - if (inst) { - this._updateDatepicker(inst); + _setContainment: function() { + + var ce, co, over, + o = this.options; + if ( o.containment === "parent" ) { + o.containment = this.helper[ 0 ].parentNode; + } + if ( o.containment === "document" || o.containment === "window" ) { + this.containment = [ + 0 - this.offset.relative.left - this.offset.parent.left, + 0 - this.offset.relative.top - this.offset.parent.top, + o.containment === "document" ? + this.document.width() : + this.window.width() - this.helperProportions.width - this.margins.left, + ( o.containment === "document" ? + ( this.document.height() || document.body.parentNode.scrollHeight ) : + this.window.height() || this.document[ 0 ].body.parentNode.scrollHeight + ) - this.helperProportions.height - this.margins.top + ]; } - }, - /* Set the dates for a jQuery selection. - @param target element - the target input field or division or span - @param date Date - the new date */ - _setDateDatepicker: function(target, date) { - var inst = this._getInst(target); - if (inst) { - this._setDate(inst, date); - this._updateDatepicker(inst); - this._updateAlternate(inst); + if ( !( /^(document|window|parent)$/ ).test( o.containment ) ) { + ce = $( o.containment )[ 0 ]; + co = $( o.containment ).offset(); + over = ( $( ce ).css( "overflow" ) !== "hidden" ); + + this.containment = [ + co.left + ( parseInt( $( ce ).css( "borderLeftWidth" ), 10 ) || 0 ) + + ( parseInt( $( ce ).css( "paddingLeft" ), 10 ) || 0 ) - this.margins.left, + co.top + ( parseInt( $( ce ).css( "borderTopWidth" ), 10 ) || 0 ) + + ( parseInt( $( ce ).css( "paddingTop" ), 10 ) || 0 ) - this.margins.top, + co.left + ( over ? Math.max( ce.scrollWidth, ce.offsetWidth ) : ce.offsetWidth ) - + ( parseInt( $( ce ).css( "borderLeftWidth" ), 10 ) || 0 ) - + ( parseInt( $( ce ).css( "paddingRight" ), 10 ) || 0 ) - + this.helperProportions.width - this.margins.left, + co.top + ( over ? Math.max( ce.scrollHeight, ce.offsetHeight ) : ce.offsetHeight ) - + ( parseInt( $( ce ).css( "borderTopWidth" ), 10 ) || 0 ) - + ( parseInt( $( ce ).css( "paddingBottom" ), 10 ) || 0 ) - + this.helperProportions.height - this.margins.top + ]; } - }, - /* Get the date(s) for the first entry in a jQuery selection. - @param target element - the target input field or division or span - @param noDefault boolean - true if no default date is to be used - @return Date - the current date */ - _getDateDatepicker: function(target, noDefault) { - var inst = this._getInst(target); - if (inst && !inst.inline) - this._setDateFromField(inst, noDefault); - return (inst ? this._getDate(inst) : null); }, - /* Handle keystrokes. */ - _doKeyDown: function(event) { - var inst = $.datepicker._getInst(event.target); - var handled = true; - var isRTL = inst.dpDiv.is('.ui-datepicker-rtl'); - inst._keyEvent = true; - if ($.datepicker._datepickerShowing) - switch (event.keyCode) { - case 9: $.datepicker._hideDatepicker(); - handled = false; - break; // hide on tab out - case 13: var sel = $('td.' + $.datepicker._dayOverClass + ':not(.' + - $.datepicker._currentClass + ')', inst.dpDiv); - if (sel[0]) - $.datepicker._selectDay(event.target, inst.selectedMonth, inst.selectedYear, sel[0]); - var onSelect = $.datepicker._get(inst, 'onSelect'); - if (onSelect) { - var dateStr = $.datepicker._formatDate(inst); - - // trigger custom callback - onSelect.apply((inst.input ? inst.input[0] : null), [dateStr, inst]); - } - else - $.datepicker._hideDatepicker(); - return false; // don't submit the form - break; // select the value on enter - case 27: $.datepicker._hideDatepicker(); - break; // hide on escape - case 33: $.datepicker._adjustDate(event.target, (event.ctrlKey ? - -$.datepicker._get(inst, 'stepBigMonths') : - -$.datepicker._get(inst, 'stepMonths')), 'M'); - break; // previous month/year on page up/+ ctrl - case 34: $.datepicker._adjustDate(event.target, (event.ctrlKey ? - +$.datepicker._get(inst, 'stepBigMonths') : - +$.datepicker._get(inst, 'stepMonths')), 'M'); - break; // next month/year on page down/+ ctrl - case 35: if (event.ctrlKey || event.metaKey) $.datepicker._clearDate(event.target); - handled = event.ctrlKey || event.metaKey; - break; // clear on ctrl or command +end - case 36: if (event.ctrlKey || event.metaKey) $.datepicker._gotoToday(event.target); - handled = event.ctrlKey || event.metaKey; - break; // current on ctrl or command +home - case 37: if (event.ctrlKey || event.metaKey) $.datepicker._adjustDate(event.target, (isRTL ? +1 : -1), 'D'); - handled = event.ctrlKey || event.metaKey; - // -1 day on ctrl or command +left - if (event.originalEvent.altKey) $.datepicker._adjustDate(event.target, (event.ctrlKey ? - -$.datepicker._get(inst, 'stepBigMonths') : - -$.datepicker._get(inst, 'stepMonths')), 'M'); - // next month/year on alt +left on Mac - break; - case 38: if (event.ctrlKey || event.metaKey) $.datepicker._adjustDate(event.target, -7, 'D'); - handled = event.ctrlKey || event.metaKey; - break; // -1 week on ctrl or command +up - case 39: if (event.ctrlKey || event.metaKey) $.datepicker._adjustDate(event.target, (isRTL ? -1 : +1), 'D'); - handled = event.ctrlKey || event.metaKey; - // +1 day on ctrl or command +right - if (event.originalEvent.altKey) $.datepicker._adjustDate(event.target, (event.ctrlKey ? - +$.datepicker._get(inst, 'stepBigMonths') : - +$.datepicker._get(inst, 'stepMonths')), 'M'); - // next month/year on alt +right - break; - case 40: if (event.ctrlKey || event.metaKey) $.datepicker._adjustDate(event.target, +7, 'D'); - handled = event.ctrlKey || event.metaKey; - break; // +1 week on ctrl or command +down - default: handled = false; - } - else if (event.keyCode == 36 && event.ctrlKey) // display the date picker on ctrl+home - $.datepicker._showDatepicker(this); - else { - handled = false; - } - if (handled) { - event.preventDefault(); - event.stopPropagation(); + _convertPositionTo: function( d, pos ) { + + if ( !pos ) { + pos = this.position; } + var mod = d === "absolute" ? 1 : -1, + scroll = this.cssPosition === "absolute" && + !( this.scrollParent[ 0 ] !== this.document[ 0 ] && + $.contains( this.scrollParent[ 0 ], this.offsetParent[ 0 ] ) ) ? + this.offsetParent : + this.scrollParent, + scrollIsRootNode = ( /(html|body)/i ).test( scroll[ 0 ].tagName ); + + return { + top: ( + + // The absolute mouse position + pos.top + + + // Only for relative positioned nodes: Relative offset from element to offset parent + this.offset.relative.top * mod + + + // The offsetParent's offset without borders (offset + border) + this.offset.parent.top * mod - + ( ( this.cssPosition === "fixed" ? + -this.scrollParent.scrollTop() : + ( scrollIsRootNode ? 0 : scroll.scrollTop() ) ) * mod ) + ), + left: ( + + // The absolute mouse position + pos.left + + + // Only for relative positioned nodes: Relative offset from element to offset parent + this.offset.relative.left * mod + + + // The offsetParent's offset without borders (offset + border) + this.offset.parent.left * mod - + ( ( this.cssPosition === "fixed" ? + -this.scrollParent.scrollLeft() : scrollIsRootNode ? 0 : + scroll.scrollLeft() ) * mod ) + ) + }; + }, - /* Filter entered characters - based on date format. */ - _doKeyPress: function(event) { - var inst = $.datepicker._getInst(event.target); - if ($.datepicker._get(inst, 'constrainInput')) { - var chars = $.datepicker._possibleChars($.datepicker._get(inst, 'dateFormat')); - var chr = String.fromCharCode(event.charCode == undefined ? event.keyCode : event.charCode); - return event.ctrlKey || event.metaKey || (chr < ' ' || !chars || chars.indexOf(chr) > -1); + _generatePosition: function( event ) { + + var top, left, + o = this.options, + pageX = event.pageX, + pageY = event.pageY, + scroll = this.cssPosition === "absolute" && + !( this.scrollParent[ 0 ] !== this.document[ 0 ] && + $.contains( this.scrollParent[ 0 ], this.offsetParent[ 0 ] ) ) ? + this.offsetParent : + this.scrollParent, + scrollIsRootNode = ( /(html|body)/i ).test( scroll[ 0 ].tagName ); + + // This is another very weird special case that only happens for relative elements: + // 1. If the css position is relative + // 2. and the scroll parent is the document or similar to the offset parent + // we have to refresh the relative offset during the scroll so there are no jumps + if ( this.cssPosition === "relative" && !( this.scrollParent[ 0 ] !== this.document[ 0 ] && + this.scrollParent[ 0 ] !== this.offsetParent[ 0 ] ) ) { + this.offset.relative = this._getRelativeOffset(); } - }, - /* Synchronise manual entry and field/alternate field. */ - _doKeyUp: function(event) { - var inst = $.datepicker._getInst(event.target); - if (inst.input.val() != inst.lastVal) { - try { - var date = $.datepicker.parseDate($.datepicker._get(inst, 'dateFormat'), - (inst.input ? inst.input.val() : null), - $.datepicker._getFormatConfig(inst)); - if (date) { // only if valid - $.datepicker._setDateFromField(inst); - $.datepicker._updateAlternate(inst); - $.datepicker._updateDatepicker(inst); + /* + * - Position constraining - + * Constrain the position to a mix of grid, containment. + */ + + if ( this.originalPosition ) { //If we are not dragging yet, we won't check for options + + if ( this.containment ) { + if ( event.pageX - this.offset.click.left < this.containment[ 0 ] ) { + pageX = this.containment[ 0 ] + this.offset.click.left; + } + if ( event.pageY - this.offset.click.top < this.containment[ 1 ] ) { + pageY = this.containment[ 1 ] + this.offset.click.top; + } + if ( event.pageX - this.offset.click.left > this.containment[ 2 ] ) { + pageX = this.containment[ 2 ] + this.offset.click.left; + } + if ( event.pageY - this.offset.click.top > this.containment[ 3 ] ) { + pageY = this.containment[ 3 ] + this.offset.click.top; } } - catch (event) { - $.datepicker.log(event); + + if ( o.grid ) { + top = this.originalPageY + Math.round( ( pageY - this.originalPageY ) / + o.grid[ 1 ] ) * o.grid[ 1 ]; + pageY = this.containment ? + ( ( top - this.offset.click.top >= this.containment[ 1 ] && + top - this.offset.click.top <= this.containment[ 3 ] ) ? + top : + ( ( top - this.offset.click.top >= this.containment[ 1 ] ) ? + top - o.grid[ 1 ] : top + o.grid[ 1 ] ) ) : + top; + + left = this.originalPageX + Math.round( ( pageX - this.originalPageX ) / + o.grid[ 0 ] ) * o.grid[ 0 ]; + pageX = this.containment ? + ( ( left - this.offset.click.left >= this.containment[ 0 ] && + left - this.offset.click.left <= this.containment[ 2 ] ) ? + left : + ( ( left - this.offset.click.left >= this.containment[ 0 ] ) ? + left - o.grid[ 0 ] : left + o.grid[ 0 ] ) ) : + left; } + } - return true; + + return { + top: ( + + // The absolute mouse position + pageY - + + // Click offset (relative to the element) + this.offset.click.top - + + // Only for relative positioned nodes: Relative offset from element to offset parent + this.offset.relative.top - + + // The offsetParent's offset without borders (offset + border) + this.offset.parent.top + + ( ( this.cssPosition === "fixed" ? + -this.scrollParent.scrollTop() : + ( scrollIsRootNode ? 0 : scroll.scrollTop() ) ) ) + ), + left: ( + + // The absolute mouse position + pageX - + + // Click offset (relative to the element) + this.offset.click.left - + + // Only for relative positioned nodes: Relative offset from element to offset parent + this.offset.relative.left - + + // The offsetParent's offset without borders (offset + border) + this.offset.parent.left + + ( ( this.cssPosition === "fixed" ? + -this.scrollParent.scrollLeft() : + scrollIsRootNode ? 0 : scroll.scrollLeft() ) ) + ) + }; + }, - /* Pop-up the date picker for a given input field. - If false returned from beforeShow event handler do not show. - @param input element - the input field attached to the date picker or - event - if triggered by focus */ - _showDatepicker: function(input) { - input = input.target || input; - if (input.nodeName.toLowerCase() != 'input') // find from button/image trigger - input = $('input', input.parentNode)[0]; - if ($.datepicker._isDisabledDatepicker(input) || $.datepicker._lastInput == input) // already here - return; - var inst = $.datepicker._getInst(input); - if ($.datepicker._curInst && $.datepicker._curInst != inst) { - $.datepicker._curInst.dpDiv.stop(true, true); - if ( inst && $.datepicker._datepickerShowing ) { - $.datepicker._hideDatepicker( $.datepicker._curInst.input[0] ); + _rearrange: function( event, i, a, hardRefresh ) { + + a ? a[ 0 ].appendChild( this.placeholder[ 0 ] ) : + i.item[ 0 ].parentNode.insertBefore( this.placeholder[ 0 ], + ( this.direction === "down" ? i.item[ 0 ] : i.item[ 0 ].nextSibling ) ); + + //Various things done here to improve the performance: + // 1. we create a setTimeout, that calls refreshPositions + // 2. on the instance, we have a counter variable, that get's higher after every append + // 3. on the local scope, we copy the counter variable, and check in the timeout, + // if it's still the same + // 4. this lets only the last addition to the timeout stack through + this.counter = this.counter ? ++this.counter : 1; + var counter = this.counter; + + this._delay( function() { + if ( counter === this.counter ) { + + //Precompute after each DOM insertion, NOT on mousemove + this.refreshPositions( !hardRefresh ); } - } - var beforeShow = $.datepicker._get(inst, 'beforeShow'); - var beforeShowSettings = beforeShow ? beforeShow.apply(input, [input, inst]) : {}; - if(beforeShowSettings === false){ - //false - return; - } - extendRemove(inst.settings, beforeShowSettings); - inst.lastVal = null; - $.datepicker._lastInput = input; - $.datepicker._setDateFromField(inst); - if ($.datepicker._inDialog) // hide cursor - input.value = ''; - if (!$.datepicker._pos) { // position below input - $.datepicker._pos = $.datepicker._findPos(input); - $.datepicker._pos[1] += input.offsetHeight; // add the height - } - var isFixed = false; - $(input).parents().each(function() { - isFixed |= $(this).css('position') == 'fixed'; - return !isFixed; - }); - if (isFixed && $.browser.opera) { // correction for Opera when fixed and scrolled - $.datepicker._pos[0] -= document.documentElement.scrollLeft; - $.datepicker._pos[1] -= document.documentElement.scrollTop; - } - var offset = {left: $.datepicker._pos[0], top: $.datepicker._pos[1]}; - $.datepicker._pos = null; - //to avoid flashes on Firefox - inst.dpDiv.empty(); - // determine sizing offscreen - inst.dpDiv.css({position: 'absolute', display: 'block', top: '-1000px'}); - $.datepicker._updateDatepicker(inst); - // fix width for dynamic number of date pickers - // and adjust position before showing - offset = $.datepicker._checkOffset(inst, offset, isFixed); - inst.dpDiv.css({position: ($.datepicker._inDialog && $.blockUI ? - 'static' : (isFixed ? 'fixed' : 'absolute')), display: 'none', - left: offset.left + 'px', top: offset.top + 'px'}); - if (!inst.inline) { - var showAnim = $.datepicker._get(inst, 'showAnim'); - var duration = $.datepicker._get(inst, 'duration'); - var postProcess = function() { - var cover = inst.dpDiv.find('iframe.ui-datepicker-cover'); // IE6- only - if( !! cover.length ){ - var borders = $.datepicker._getBorders(inst.dpDiv); - cover.css({left: -borders[0], top: -borders[1], - width: inst.dpDiv.outerWidth(), height: inst.dpDiv.outerHeight()}); - } - }; - inst.dpDiv.zIndex($(input).zIndex()+1); - $.datepicker._datepickerShowing = true; - if ($.effects && $.effects[showAnim]) - inst.dpDiv.show(showAnim, $.datepicker._get(inst, 'showOptions'), duration, postProcess); - else - inst.dpDiv[showAnim || 'show']((showAnim ? duration : null), postProcess); - if (!showAnim || !duration) - postProcess(); - if (inst.input.is(':visible') && !inst.input.is(':disabled')) - inst.input.focus(); - $.datepicker._curInst = inst; - } + } ); + }, - /* Generate the date picker content. */ - _updateDatepicker: function(inst) { - var self = this; - self.maxRows = 4; //Reset the max number of rows being displayed (see #7043) - var borders = $.datepicker._getBorders(inst.dpDiv); - instActive = inst; // for delegate hover events - inst.dpDiv.empty().append(this._generateHTML(inst)); - var cover = inst.dpDiv.find('iframe.ui-datepicker-cover'); // IE6- only - if( !!cover.length ){ //avoid call to outerXXXX() when not in IE6 - cover.css({left: -borders[0], top: -borders[1], width: inst.dpDiv.outerWidth(), height: inst.dpDiv.outerHeight()}) - } - inst.dpDiv.find('.' + this._dayOverClass + ' a').mouseover(); - var numMonths = this._getNumberOfMonths(inst); - var cols = numMonths[1]; - var width = 17; - inst.dpDiv.removeClass('ui-datepicker-multi-2 ui-datepicker-multi-3 ui-datepicker-multi-4').width(''); - if (cols > 1) - inst.dpDiv.addClass('ui-datepicker-multi-' + cols).css('width', (width * cols) + 'em'); - inst.dpDiv[(numMonths[0] != 1 || numMonths[1] != 1 ? 'add' : 'remove') + - 'Class']('ui-datepicker-multi'); - inst.dpDiv[(this._get(inst, 'isRTL') ? 'add' : 'remove') + - 'Class']('ui-datepicker-rtl'); - if (inst == $.datepicker._curInst && $.datepicker._datepickerShowing && inst.input && - // #6694 - don't focus the input if it's already focused - // this breaks the change event in IE - inst.input.is(':visible') && !inst.input.is(':disabled') && inst.input[0] != document.activeElement) - inst.input.focus(); - // deffered render of the years select (to avoid flashes on Firefox) - if( inst.yearshtml ){ - var origyearshtml = inst.yearshtml; - setTimeout(function(){ - //assure that inst.yearshtml didn't change. - if( origyearshtml === inst.yearshtml && inst.yearshtml ){ - inst.dpDiv.find('select.ui-datepicker-year:first').replaceWith(inst.yearshtml); - } - origyearshtml = inst.yearshtml = null; - }, 0); + _clear: function( event, noPropagation ) { + + this.reverting = false; + + // We delay all events that have to be triggered to after the point where the placeholder + // has been removed and everything else normalized again + var i, + delayedTriggers = []; + + // We first have to update the dom position of the actual currentItem + // Note: don't do it if the current item is already removed (by a user), or it gets + // reappended (see #4088) + if ( !this._noFinalSort && this.currentItem.parent().length ) { + this.placeholder.before( this.currentItem ); } - }, + this._noFinalSort = null; - /* Retrieve the size of left and top borders for an element. - @param elem (jQuery object) the element of interest - @return (number[2]) the left and top borders */ - _getBorders: function(elem) { - var convert = function(value) { - return {thin: 1, medium: 2, thick: 3}[value] || value; - }; - return [parseFloat(convert(elem.css('border-left-width'))), - parseFloat(convert(elem.css('border-top-width')))]; - }, + if ( this.helper[ 0 ] === this.currentItem[ 0 ] ) { + for ( i in this._storedCSS ) { + if ( this._storedCSS[ i ] === "auto" || this._storedCSS[ i ] === "static" ) { + this._storedCSS[ i ] = ""; + } + } + this.currentItem.css( this._storedCSS ); + this._removeClass( this.currentItem, "ui-sortable-helper" ); + } else { + this.currentItem.show(); + } - /* Check positioning to remain on screen. */ - _checkOffset: function(inst, offset, isFixed) { - var dpWidth = inst.dpDiv.outerWidth(); - var dpHeight = inst.dpDiv.outerHeight(); - var inputWidth = inst.input ? inst.input.outerWidth() : 0; - var inputHeight = inst.input ? inst.input.outerHeight() : 0; - var viewWidth = document.documentElement.clientWidth + $(document).scrollLeft(); - var viewHeight = document.documentElement.clientHeight + $(document).scrollTop(); - - offset.left -= (this._get(inst, 'isRTL') ? (dpWidth - inputWidth) : 0); - offset.left -= (isFixed && offset.left == inst.input.offset().left) ? $(document).scrollLeft() : 0; - offset.top -= (isFixed && offset.top == (inst.input.offset().top + inputHeight)) ? $(document).scrollTop() : 0; - - // now check if datepicker is showing outside window viewport - move to a better place if so. - offset.left -= Math.min(offset.left, (offset.left + dpWidth > viewWidth && viewWidth > dpWidth) ? - Math.abs(offset.left + dpWidth - viewWidth) : 0); - offset.top -= Math.min(offset.top, (offset.top + dpHeight > viewHeight && viewHeight > dpHeight) ? - Math.abs(dpHeight + inputHeight) : 0); + if ( this.fromOutside && !noPropagation ) { + delayedTriggers.push( function( event ) { + this._trigger( "receive", event, this._uiHash( this.fromOutside ) ); + } ); + } + if ( ( this.fromOutside || + this.domPosition.prev !== + this.currentItem.prev().not( ".ui-sortable-helper" )[ 0 ] || + this.domPosition.parent !== this.currentItem.parent()[ 0 ] ) && !noPropagation ) { - return offset; - }, + // Trigger update callback if the DOM position has changed + delayedTriggers.push( function( event ) { + this._trigger( "update", event, this._uiHash() ); + } ); + } - /* Find an object's position on the screen. */ - _findPos: function(obj) { - var inst = this._getInst(obj); - var isRTL = this._get(inst, 'isRTL'); - while (obj && (obj.type == 'hidden' || obj.nodeType != 1 || $.expr.filters.hidden(obj))) { - obj = obj[isRTL ? 'previousSibling' : 'nextSibling']; + // Check if the items Container has Changed and trigger appropriate + // events. + if ( this !== this.currentContainer ) { + if ( !noPropagation ) { + delayedTriggers.push( function( event ) { + this._trigger( "remove", event, this._uiHash() ); + } ); + delayedTriggers.push( ( function( c ) { + return function( event ) { + c._trigger( "receive", event, this._uiHash( this ) ); + }; + } ).call( this, this.currentContainer ) ); + delayedTriggers.push( ( function( c ) { + return function( event ) { + c._trigger( "update", event, this._uiHash( this ) ); + }; + } ).call( this, this.currentContainer ) ); + } } - var position = $(obj).offset(); - return [position.left, position.top]; - }, - /* Hide the date picker from view. - @param input element - the input field attached to the date picker */ - _hideDatepicker: function(input) { - var inst = this._curInst; - if (!inst || (input && inst != $.data(input, PROP_NAME))) - return; - if (this._datepickerShowing) { - var showAnim = this._get(inst, 'showAnim'); - var duration = this._get(inst, 'duration'); - var self = this; - var postProcess = function() { - $.datepicker._tidyDialog(inst); - self._curInst = null; + //Post events to containers + function delayEvent( type, instance, container ) { + return function( event ) { + container._trigger( type, event, instance._uiHash( instance ) ); }; - if ($.effects && $.effects[showAnim]) - inst.dpDiv.hide(showAnim, $.datepicker._get(inst, 'showOptions'), duration, postProcess); - else - inst.dpDiv[(showAnim == 'slideDown' ? 'slideUp' : - (showAnim == 'fadeIn' ? 'fadeOut' : 'hide'))]((showAnim ? duration : null), postProcess); - if (!showAnim) - postProcess(); - this._datepickerShowing = false; - var onClose = this._get(inst, 'onClose'); - if (onClose) - onClose.apply((inst.input ? inst.input[0] : null), - [(inst.input ? inst.input.val() : ''), inst]); - this._lastInput = null; - if (this._inDialog) { - this._dialogInput.css({ position: 'absolute', left: '0', top: '-100px' }); - if ($.blockUI) { - $.unblockUI(); - $('body').append(this.dpDiv); - } + } + for ( i = this.containers.length - 1; i >= 0; i-- ) { + if ( !noPropagation ) { + delayedTriggers.push( delayEvent( "deactivate", this, this.containers[ i ] ) ); + } + if ( this.containers[ i ].containerCache.over ) { + delayedTriggers.push( delayEvent( "out", this, this.containers[ i ] ) ); + this.containers[ i ].containerCache.over = 0; } - this._inDialog = false; } - }, - /* Tidy up after a dialog display. */ - _tidyDialog: function(inst) { - inst.dpDiv.removeClass(this._dialogClass).unbind('.ui-datepicker-calendar'); - }, + //Do what was originally in plugins + if ( this.storedCursor ) { + this.document.find( "body" ).css( "cursor", this.storedCursor ); + this.storedStylesheet.remove(); + } + if ( this._storedOpacity ) { + this.helper.css( "opacity", this._storedOpacity ); + } + if ( this._storedZIndex ) { + this.helper.css( "zIndex", this._storedZIndex === "auto" ? "" : this._storedZIndex ); + } - /* Close date picker if clicked elsewhere. */ - _checkExternalClick: function(event) { - if (!$.datepicker._curInst) - return; + this.dragging = false; - var $target = $(event.target), - inst = $.datepicker._getInst($target[0]); + if ( !noPropagation ) { + this._trigger( "beforeStop", event, this._uiHash() ); + } - if ( ( ( $target[0].id != $.datepicker._mainDivId && - $target.parents('#' + $.datepicker._mainDivId).length == 0 && - !$target.hasClass($.datepicker.markerClassName) && - !$target.hasClass($.datepicker._triggerClass) && - $.datepicker._datepickerShowing && !($.datepicker._inDialog && $.blockUI) ) ) || - ( $target.hasClass($.datepicker.markerClassName) && $.datepicker._curInst != inst ) ) - $.datepicker._hideDatepicker(); - }, + //$(this.placeholder[0]).remove(); would have been the jQuery way - unfortunately, + // it unbinds ALL events from the original node! + this.placeholder[ 0 ].parentNode.removeChild( this.placeholder[ 0 ] ); - /* Adjust one of the date sub-fields. */ - _adjustDate: function(id, offset, period) { - var target = $(id); - var inst = this._getInst(target[0]); - if (this._isDisabledDatepicker(target[0])) { - return; + if ( !this.cancelHelperRemoval ) { + if ( this.helper[ 0 ] !== this.currentItem[ 0 ] ) { + this.helper.remove(); + } + this.helper = null; } - this._adjustInstDate(inst, offset + - (period == 'M' ? this._get(inst, 'showCurrentAtPos') : 0), // undo positioning - period); - this._updateDatepicker(inst); - }, - /* Action for current link. */ - _gotoToday: function(id) { - var target = $(id); - var inst = this._getInst(target[0]); - if (this._get(inst, 'gotoCurrent') && inst.currentDay) { - inst.selectedDay = inst.currentDay; - inst.drawMonth = inst.selectedMonth = inst.currentMonth; - inst.drawYear = inst.selectedYear = inst.currentYear; - } - else { - var date = new Date(); - inst.selectedDay = date.getDate(); - inst.drawMonth = inst.selectedMonth = date.getMonth(); - inst.drawYear = inst.selectedYear = date.getFullYear(); + if ( !noPropagation ) { + for ( i = 0; i < delayedTriggers.length; i++ ) { + + // Trigger all delayed events + delayedTriggers[ i ].call( this, event ); + } + this._trigger( "stop", event, this._uiHash() ); } - this._notifyChange(inst); - this._adjustDate(target); - }, - /* Action for selecting a new month/year. */ - _selectMonthYear: function(id, select, period) { - var target = $(id); - var inst = this._getInst(target[0]); - inst['selected' + (period == 'M' ? 'Month' : 'Year')] = - inst['draw' + (period == 'M' ? 'Month' : 'Year')] = - parseInt(select.options[select.selectedIndex].value,10); - this._notifyChange(inst); - this._adjustDate(target); + this.fromOutside = false; + return !this.cancelHelperRemoval; + }, - /* Action for selecting a day. */ - _selectDay: function(id, month, year, td) { - var target = $(id); - if ($(td).hasClass(this._unselectableClass) || this._isDisabledDatepicker(target[0])) { - return; + _trigger: function() { + if ( $.Widget.prototype._trigger.apply( this, arguments ) === false ) { + this.cancel(); } - var inst = this._getInst(target[0]); - inst.selectedDay = inst.currentDay = $('a', td).html(); - inst.selectedMonth = inst.currentMonth = month; - inst.selectedYear = inst.currentYear = year; - this._selectDate(id, this._formatDate(inst, - inst.currentDay, inst.currentMonth, inst.currentYear)); }, - /* Erase the input field and hide the date picker. */ - _clearDate: function(id) { - var target = $(id); - var inst = this._getInst(target[0]); - this._selectDate(target, ''); - }, + _uiHash: function( _inst ) { + var inst = _inst || this; + return { + helper: inst.helper, + placeholder: inst.placeholder || $( [] ), + position: inst.position, + originalPosition: inst.originalPosition, + offset: inst.positionAbs, + item: inst.currentItem, + sender: _inst ? _inst.element : null + }; + } - /* Update the input field with the selected date. */ - _selectDate: function(id, dateStr) { - var target = $(id); - var inst = this._getInst(target[0]); - dateStr = (dateStr != null ? dateStr : this._formatDate(inst)); - if (inst.input) - inst.input.val(dateStr); - this._updateAlternate(inst); - var onSelect = this._get(inst, 'onSelect'); - if (onSelect) - onSelect.apply((inst.input ? inst.input[0] : null), [dateStr, inst]); // trigger custom callback - else if (inst.input) - inst.input.trigger('change'); // fire the change event - if (inst.inline) - this._updateDatepicker(inst); - else { - this._hideDatepicker(); - this._lastInput = inst.input[0]; - if (typeof(inst.input[0]) != 'object') - inst.input.focus(); // restore focus - this._lastInput = null; +} ); + + +/*! + * jQuery UI Spinner 1.12.1 + * http://jqueryui.com + * + * Copyright jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + */ + +//>>label: Spinner +//>>group: Widgets +//>>description: Displays buttons to easily input numbers via the keyboard or mouse. +//>>docs: http://api.jqueryui.com/spinner/ +//>>demos: http://jqueryui.com/spinner/ +//>>css.structure: ../../themes/base/core.css +//>>css.structure: ../../themes/base/spinner.css +//>>css.theme: ../../themes/base/theme.css + + + +function spinnerModifer( fn ) { + return function() { + var previous = this.element.val(); + fn.apply( this, arguments ); + this._refresh(); + if ( previous !== this.element.val() ) { + this._trigger( "change" ); } + }; +} + +$.widget( "ui.spinner", { + version: "1.12.1", + defaultElement: "", + widgetEventPrefix: "spin", + options: { + classes: { + "ui-spinner": "ui-corner-all", + "ui-spinner-down": "ui-corner-br", + "ui-spinner-up": "ui-corner-tr" + }, + culture: null, + icons: { + down: "ui-icon-triangle-1-s", + up: "ui-icon-triangle-1-n" + }, + incremental: true, + max: null, + min: null, + numberFormat: null, + page: 10, + step: 1, + + change: null, + spin: null, + start: null, + stop: null }, - /* Update any alternate field to synchronise with the main field. */ - _updateAlternate: function(inst) { - var altField = this._get(inst, 'altField'); - if (altField) { // update alternate field too - var altFormat = this._get(inst, 'altFormat') || this._get(inst, 'dateFormat'); - var date = this._getDate(inst); - var dateStr = this.formatDate(altFormat, date, this._getFormatConfig(inst)); - $(altField).each(function() { $(this).val(dateStr); }); + _create: function() { + + // handle string values that need to be parsed + this._setOption( "max", this.options.max ); + this._setOption( "min", this.options.min ); + this._setOption( "step", this.options.step ); + + // Only format if there is a value, prevents the field from being marked + // as invalid in Firefox, see #9573. + if ( this.value() !== "" ) { + + // Format the value, but don't constrain. + this._value( this.element.val(), true ); } - }, - /* Set as beforeShowDay function to prevent selection of weekends. - @param date Date - the date to customise - @return [boolean, string] - is this date selectable?, what is its CSS class? */ - noWeekends: function(date) { - var day = date.getDay(); - return [(day > 0 && day < 6), '']; + this._draw(); + this._on( this._events ); + this._refresh(); + + // Turning off autocomplete prevents the browser from remembering the + // value when navigating through history, so we re-enable autocomplete + // if the page is unloaded before the widget is destroyed. #7790 + this._on( this.window, { + beforeunload: function() { + this.element.removeAttr( "autocomplete" ); + } + } ); }, - /* Set as calculateWeek to determine the week of the year based on the ISO 8601 definition. - @param date Date - the date to get the week for - @return number - the number of the week within the year that contains this date */ - iso8601Week: function(date) { - var checkDate = new Date(date.getTime()); - // Find Thursday of this week starting on Monday - checkDate.setDate(checkDate.getDate() + 4 - (checkDate.getDay() || 7)); - var time = checkDate.getTime(); - checkDate.setMonth(0); // Compare with Jan 1 - checkDate.setDate(1); - return Math.floor(Math.round((time - checkDate) / 86400000) / 7) + 1; + _getCreateOptions: function() { + var options = this._super(); + var element = this.element; + + $.each( [ "min", "max", "step" ], function( i, option ) { + var value = element.attr( option ); + if ( value != null && value.length ) { + options[ option ] = value; + } + } ); + + return options; }, - /* Parse a string value into a date object. - See formatDate below for the possible formats. - - @param format string - the expected format of the date - @param value string - the date in the above format - @param settings Object - attributes include: - shortYearCutoff number - the cutoff year for determining the century (optional) - dayNamesShort string[7] - abbreviated names of the days from Sunday (optional) - dayNames string[7] - names of the days from Sunday (optional) - monthNamesShort string[12] - abbreviated names of the months (optional) - monthNames string[12] - names of the months (optional) - @return Date - the extracted date value or null if value is blank */ - parseDate: function (format, value, settings) { - if (format == null || value == null) - throw 'Invalid arguments'; - value = (typeof value == 'object' ? value.toString() : value + ''); - if (value == '') - return null; - var shortYearCutoff = (settings ? settings.shortYearCutoff : null) || this._defaults.shortYearCutoff; - shortYearCutoff = (typeof shortYearCutoff != 'string' ? shortYearCutoff : - new Date().getFullYear() % 100 + parseInt(shortYearCutoff, 10)); - var dayNamesShort = (settings ? settings.dayNamesShort : null) || this._defaults.dayNamesShort; - var dayNames = (settings ? settings.dayNames : null) || this._defaults.dayNames; - var monthNamesShort = (settings ? settings.monthNamesShort : null) || this._defaults.monthNamesShort; - var monthNames = (settings ? settings.monthNames : null) || this._defaults.monthNames; - var year = -1; - var month = -1; - var day = -1; - var doy = -1; - var literal = false; - // Check whether a format character is doubled - var lookAhead = function(match) { - var matches = (iFormat + 1 < format.length && format.charAt(iFormat + 1) == match); - if (matches) - iFormat++; - return matches; - }; - // Extract a number from the string value - var getNumber = function(match) { - var isDoubled = lookAhead(match); - var size = (match == '@' ? 14 : (match == '!' ? 20 : - (match == 'y' && isDoubled ? 4 : (match == 'o' ? 3 : 2)))); - var digits = new RegExp('^\\d{1,' + size + '}'); - var num = value.substring(iValue).match(digits); - if (!num) - throw 'Missing number at position ' + iValue; - iValue += num[0].length; - return parseInt(num[0], 10); - }; - // Extract a name from the string value and convert to an index - var getName = function(match, shortNames, longNames) { - var names = $.map(lookAhead(match) ? longNames : shortNames, function (v, k) { - return [ [k, v] ]; - }).sort(function (a, b) { - return -(a[1].length - b[1].length); - }); - var index = -1; - $.each(names, function (i, pair) { - var name = pair[1]; - if (value.substr(iValue, name.length).toLowerCase() == name.toLowerCase()) { - index = pair[0]; - iValue += name.length; - return false; + _events: { + keydown: function( event ) { + if ( this._start( event ) && this._keydown( event ) ) { + event.preventDefault(); + } + }, + keyup: "_stop", + focus: function() { + this.previous = this.element.val(); + }, + blur: function( event ) { + if ( this.cancelBlur ) { + delete this.cancelBlur; + return; + } + + this._stop(); + this._refresh(); + if ( this.previous !== this.element.val() ) { + this._trigger( "change", event ); + } + }, + mousewheel: function( event, delta ) { + if ( !delta ) { + return; + } + if ( !this.spinning && !this._start( event ) ) { + return false; + } + + this._spin( ( delta > 0 ? 1 : -1 ) * this.options.step, event ); + clearTimeout( this.mousewheelTimer ); + this.mousewheelTimer = this._delay( function() { + if ( this.spinning ) { + this._stop( event ); } - }); - if (index != -1) - return index + 1; - else - throw 'Unknown name at position ' + iValue; - }; - // Confirm that a literal character matches the string value - var checkLiteral = function() { - if (value.charAt(iValue) != format.charAt(iFormat)) - throw 'Unexpected literal at position ' + iValue; - iValue++; - }; - var iValue = 0; - for (var iFormat = 0; iFormat < format.length; iFormat++) { - if (literal) - if (format.charAt(iFormat) == "'" && !lookAhead("'")) - literal = false; - else - checkLiteral(); - else - switch (format.charAt(iFormat)) { - case 'd': - day = getNumber('d'); - break; - case 'D': - getName('D', dayNamesShort, dayNames); - break; - case 'o': - doy = getNumber('o'); - break; - case 'm': - month = getNumber('m'); - break; - case 'M': - month = getName('M', monthNamesShort, monthNames); - break; - case 'y': - year = getNumber('y'); - break; - case '@': - var date = new Date(getNumber('@')); - year = date.getFullYear(); - month = date.getMonth() + 1; - day = date.getDate(); - break; - case '!': - var date = new Date((getNumber('!') - this._ticksTo1970) / 10000); - year = date.getFullYear(); - month = date.getMonth() + 1; - day = date.getDate(); - break; - case "'": - if (lookAhead("'")) - checkLiteral(); - else - literal = true; - break; - default: - checkLiteral(); + }, 100 ); + event.preventDefault(); + }, + "mousedown .ui-spinner-button": function( event ) { + var previous; + + // We never want the buttons to have focus; whenever the user is + // interacting with the spinner, the focus should be on the input. + // If the input is focused then this.previous is properly set from + // when the input first received focus. If the input is not focused + // then we need to set this.previous based on the value before spinning. + previous = this.element[ 0 ] === $.ui.safeActiveElement( this.document[ 0 ] ) ? + this.previous : this.element.val(); + function checkFocus() { + var isActive = this.element[ 0 ] === $.ui.safeActiveElement( this.document[ 0 ] ); + if ( !isActive ) { + this.element.trigger( "focus" ); + this.previous = previous; + + // support: IE + // IE sets focus asynchronously, so we need to check if focus + // moved off of the input because the user clicked on the button. + this._delay( function() { + this.previous = previous; + } ); } - } - if (iValue < value.length){ - throw "Extra/unparsed characters found in date: " + value.substring(iValue); - } - if (year == -1) - year = new Date().getFullYear(); - else if (year < 100) - year += new Date().getFullYear() - new Date().getFullYear() % 100 + - (year <= shortYearCutoff ? 0 : -100); - if (doy > -1) { - month = 1; - day = doy; - do { - var dim = this._getDaysInMonth(year, month - 1); - if (day <= dim) - break; - month++; - day -= dim; - } while (true); - } - var date = this._daylightSavingAdjust(new Date(year, month - 1, day)); - if (date.getFullYear() != year || date.getMonth() + 1 != month || date.getDate() != day) - throw 'Invalid date'; // E.g. 31/02/00 - return date; - }, + } - /* Standard date formats. */ - ATOM: 'yy-mm-dd', // RFC 3339 (ISO 8601) - COOKIE: 'D, dd M yy', - ISO_8601: 'yy-mm-dd', - RFC_822: 'D, d M y', - RFC_850: 'DD, dd-M-y', - RFC_1036: 'D, d M y', - RFC_1123: 'D, d M yy', - RFC_2822: 'D, d M yy', - RSS: 'D, d M y', // RFC 822 - TICKS: '!', - TIMESTAMP: '@', - W3C: 'yy-mm-dd', // ISO 8601 - - _ticksTo1970: (((1970 - 1) * 365 + Math.floor(1970 / 4) - Math.floor(1970 / 100) + - Math.floor(1970 / 400)) * 24 * 60 * 60 * 10000000), + // Ensure focus is on (or stays on) the text field + event.preventDefault(); + checkFocus.call( this ); + + // Support: IE + // IE doesn't prevent moving focus even with event.preventDefault() + // so we set a flag to know when we should ignore the blur event + // and check (again) if focus moved off of the input. + this.cancelBlur = true; + this._delay( function() { + delete this.cancelBlur; + checkFocus.call( this ); + } ); + + if ( this._start( event ) === false ) { + return; + } - /* Format a date object into a string value. - The format can be combinations of the following: - d - day of month (no leading zero) - dd - day of month (two digit) - o - day of year (no leading zeros) - oo - day of year (three digit) - D - day name short - DD - day name long - m - month of year (no leading zero) - mm - month of year (two digit) - M - month name short - MM - month name long - y - year (two digit) - yy - year (four digit) - @ - Unix timestamp (ms since 01/01/1970) - ! - Windows ticks (100ns since 01/01/0001) - '...' - literal text - '' - single quote - - @param format string - the desired format of the date - @param date Date - the date value to format - @param settings Object - attributes include: - dayNamesShort string[7] - abbreviated names of the days from Sunday (optional) - dayNames string[7] - names of the days from Sunday (optional) - monthNamesShort string[12] - abbreviated names of the months (optional) - monthNames string[12] - names of the months (optional) - @return string - the date in the above format */ - formatDate: function (format, date, settings) { - if (!date) - return ''; - var dayNamesShort = (settings ? settings.dayNamesShort : null) || this._defaults.dayNamesShort; - var dayNames = (settings ? settings.dayNames : null) || this._defaults.dayNames; - var monthNamesShort = (settings ? settings.monthNamesShort : null) || this._defaults.monthNamesShort; - var monthNames = (settings ? settings.monthNames : null) || this._defaults.monthNames; - // Check whether a format character is doubled - var lookAhead = function(match) { - var matches = (iFormat + 1 < format.length && format.charAt(iFormat + 1) == match); - if (matches) - iFormat++; - return matches; - }; - // Format a number, with leading zero if necessary - var formatNumber = function(match, value, len) { - var num = '' + value; - if (lookAhead(match)) - while (num.length < len) - num = '0' + num; - return num; - }; - // Format a name, short or long as requested - var formatName = function(match, value, shortNames, longNames) { - return (lookAhead(match) ? longNames[value] : shortNames[value]); - }; - var output = ''; - var literal = false; - if (date) - for (var iFormat = 0; iFormat < format.length; iFormat++) { - if (literal) - if (format.charAt(iFormat) == "'" && !lookAhead("'")) - literal = false; - else - output += format.charAt(iFormat); - else - switch (format.charAt(iFormat)) { - case 'd': - output += formatNumber('d', date.getDate(), 2); - break; - case 'D': - output += formatName('D', date.getDay(), dayNamesShort, dayNames); - break; - case 'o': - output += formatNumber('o', - Math.round((new Date(date.getFullYear(), date.getMonth(), date.getDate()).getTime() - new Date(date.getFullYear(), 0, 0).getTime()) / 86400000), 3); - break; - case 'm': - output += formatNumber('m', date.getMonth() + 1, 2); - break; - case 'M': - output += formatName('M', date.getMonth(), monthNamesShort, monthNames); - break; - case 'y': - output += (lookAhead('y') ? date.getFullYear() : - (date.getYear() % 100 < 10 ? '0' : '') + date.getYear() % 100); - break; - case '@': - output += date.getTime(); - break; - case '!': - output += date.getTime() * 10000 + this._ticksTo1970; - break; - case "'": - if (lookAhead("'")) - output += "'"; - else - literal = true; - break; - default: - output += format.charAt(iFormat); - } + this._repeat( null, $( event.currentTarget ) + .hasClass( "ui-spinner-up" ) ? 1 : -1, event ); + }, + "mouseup .ui-spinner-button": "_stop", + "mouseenter .ui-spinner-button": function( event ) { + + // button will add ui-state-active if mouse was down while mouseleave and kept down + if ( !$( event.currentTarget ).hasClass( "ui-state-active" ) ) { + return; } - return output; + + if ( this._start( event ) === false ) { + return false; + } + this._repeat( null, $( event.currentTarget ) + .hasClass( "ui-spinner-up" ) ? 1 : -1, event ); + }, + + // TODO: do we really want to consider this a stop? + // shouldn't we just stop the repeater and wait until mouseup before + // we trigger the stop event? + "mouseleave .ui-spinner-button": "_stop" }, - /* Extract all possible characters from the date format. */ - _possibleChars: function (format) { - var chars = ''; - var literal = false; - // Check whether a format character is doubled - var lookAhead = function(match) { - var matches = (iFormat + 1 < format.length && format.charAt(iFormat + 1) == match); - if (matches) - iFormat++; - return matches; - }; - for (var iFormat = 0; iFormat < format.length; iFormat++) - if (literal) - if (format.charAt(iFormat) == "'" && !lookAhead("'")) - literal = false; - else - chars += format.charAt(iFormat); - else - switch (format.charAt(iFormat)) { - case 'd': case 'm': case 'y': case '@': - chars += '0123456789'; - break; - case 'D': case 'M': - return null; // Accept anything - case "'": - if (lookAhead("'")) - chars += "'"; - else - literal = true; - break; - default: - chars += format.charAt(iFormat); + // Support mobile enhanced option and make backcompat more sane + _enhance: function() { + this.uiSpinner = this.element + .attr( "autocomplete", "off" ) + .wrap( "" ) + .parent() + + // Add buttons + .append( + "" + ); + }, + + _draw: function() { + this._enhance(); + + this._addClass( this.uiSpinner, "ui-spinner", "ui-widget ui-widget-content" ); + this._addClass( "ui-spinner-input" ); + + this.element.attr( "role", "spinbutton" ); + + // Button bindings + this.buttons = this.uiSpinner.children( "a" ) + .attr( "tabIndex", -1 ) + .attr( "aria-hidden", true ) + .button( { + classes: { + "ui-button": "" } - return chars; + } ); + + // TODO: Right now button does not support classes this is already updated in button PR + this._removeClass( this.buttons, "ui-corner-all" ); + + this._addClass( this.buttons.first(), "ui-spinner-button ui-spinner-up" ); + this._addClass( this.buttons.last(), "ui-spinner-button ui-spinner-down" ); + this.buttons.first().button( { + "icon": this.options.icons.up, + "showLabel": false + } ); + this.buttons.last().button( { + "icon": this.options.icons.down, + "showLabel": false + } ); + + // IE 6 doesn't understand height: 50% for the buttons + // unless the wrapper has an explicit height + if ( this.buttons.height() > Math.ceil( this.uiSpinner.height() * 0.5 ) && + this.uiSpinner.height() > 0 ) { + this.uiSpinner.height( this.uiSpinner.height() ); + } }, - /* Get a setting value, defaulting if necessary. */ - _get: function(inst, name) { - return inst.settings[name] !== undefined ? - inst.settings[name] : this._defaults[name]; + _keydown: function( event ) { + var options = this.options, + keyCode = $.ui.keyCode; + + switch ( event.keyCode ) { + case keyCode.UP: + this._repeat( null, 1, event ); + return true; + case keyCode.DOWN: + this._repeat( null, -1, event ); + return true; + case keyCode.PAGE_UP: + this._repeat( null, options.page, event ); + return true; + case keyCode.PAGE_DOWN: + this._repeat( null, -options.page, event ); + return true; + } + + return false; }, - /* Parse existing date and initialise date picker. */ - _setDateFromField: function(inst, noDefault) { - if (inst.input.val() == inst.lastVal) { - return; + _start: function( event ) { + if ( !this.spinning && this._trigger( "start", event ) === false ) { + return false; } - var dateFormat = this._get(inst, 'dateFormat'); - var dates = inst.lastVal = inst.input ? inst.input.val() : null; - var date, defaultDate; - date = defaultDate = this._getDefaultDate(inst); - var settings = this._getFormatConfig(inst); - try { - date = this.parseDate(dateFormat, dates, settings) || defaultDate; - } catch (event) { - this.log(event); - dates = (noDefault ? '' : dates); + + if ( !this.counter ) { + this.counter = 1; } - inst.selectedDay = date.getDate(); - inst.drawMonth = inst.selectedMonth = date.getMonth(); - inst.drawYear = inst.selectedYear = date.getFullYear(); - inst.currentDay = (dates ? date.getDate() : 0); - inst.currentMonth = (dates ? date.getMonth() : 0); - inst.currentYear = (dates ? date.getFullYear() : 0); - this._adjustInstDate(inst); + this.spinning = true; + return true; }, - /* Retrieve the default date shown on opening. */ - _getDefaultDate: function(inst) { - return this._restrictMinMax(inst, - this._determineDate(inst, this._get(inst, 'defaultDate'), new Date())); + _repeat: function( i, steps, event ) { + i = i || 500; + + clearTimeout( this.timer ); + this.timer = this._delay( function() { + this._repeat( 40, steps, event ); + }, i ); + + this._spin( steps * this.options.step, event ); }, - /* A date may be specified as an exact value or a relative one. */ - _determineDate: function(inst, date, defaultDate) { - var offsetNumeric = function(offset) { - var date = new Date(); - date.setDate(date.getDate() + offset); - return date; - }; - var offsetString = function(offset) { - try { - return $.datepicker.parseDate($.datepicker._get(inst, 'dateFormat'), - offset, $.datepicker._getFormatConfig(inst)); - } - catch (e) { - // Ignore - } - var date = (offset.toLowerCase().match(/^c/) ? - $.datepicker._getDate(inst) : null) || new Date(); - var year = date.getFullYear(); - var month = date.getMonth(); - var day = date.getDate(); - var pattern = /([+-]?[0-9]+)\s*(d|D|w|W|m|M|y|Y)?/g; - var matches = pattern.exec(offset); - while (matches) { - switch (matches[2] || 'd') { - case 'd' : case 'D' : - day += parseInt(matches[1],10); break; - case 'w' : case 'W' : - day += parseInt(matches[1],10) * 7; break; - case 'm' : case 'M' : - month += parseInt(matches[1],10); - day = Math.min(day, $.datepicker._getDaysInMonth(year, month)); - break; - case 'y': case 'Y' : - year += parseInt(matches[1],10); - day = Math.min(day, $.datepicker._getDaysInMonth(year, month)); - break; - } - matches = pattern.exec(offset); - } - return new Date(year, month, day); - }; - var newDate = (date == null || date === '' ? defaultDate : (typeof date == 'string' ? offsetString(date) : - (typeof date == 'number' ? (isNaN(date) ? defaultDate : offsetNumeric(date)) : new Date(date.getTime())))); - newDate = (newDate && newDate.toString() == 'Invalid Date' ? defaultDate : newDate); - if (newDate) { - newDate.setHours(0); - newDate.setMinutes(0); - newDate.setSeconds(0); - newDate.setMilliseconds(0); + _spin: function( step, event ) { + var value = this.value() || 0; + + if ( !this.counter ) { + this.counter = 1; + } + + value = this._adjustValue( value + step * this._increment( this.counter ) ); + + if ( !this.spinning || this._trigger( "spin", event, { value: value } ) !== false ) { + this._value( value ); + this.counter++; } - return this._daylightSavingAdjust(newDate); }, - /* Handle switch to/from daylight saving. - Hours may be non-zero on daylight saving cut-over: - > 12 when midnight changeover, but then cannot generate - midnight datetime, so jump to 1AM, otherwise reset. - @param date (Date) the date to check - @return (Date) the corrected date */ - _daylightSavingAdjust: function(date) { - if (!date) return null; - date.setHours(date.getHours() > 12 ? date.getHours() + 2 : 0); - return date; + _increment: function( i ) { + var incremental = this.options.incremental; + + if ( incremental ) { + return $.isFunction( incremental ) ? + incremental( i ) : + Math.floor( i * i * i / 50000 - i * i / 500 + 17 * i / 200 + 1 ); + } + + return 1; }, - /* Set the date(s) directly. */ - _setDate: function(inst, date, noChange) { - var clear = !date; - var origMonth = inst.selectedMonth; - var origYear = inst.selectedYear; - var newDate = this._restrictMinMax(inst, this._determineDate(inst, date, new Date())); - inst.selectedDay = inst.currentDay = newDate.getDate(); - inst.drawMonth = inst.selectedMonth = inst.currentMonth = newDate.getMonth(); - inst.drawYear = inst.selectedYear = inst.currentYear = newDate.getFullYear(); - if ((origMonth != inst.selectedMonth || origYear != inst.selectedYear) && !noChange) - this._notifyChange(inst); - this._adjustInstDate(inst); - if (inst.input) { - inst.input.val(clear ? '' : this._formatDate(inst)); + _precision: function() { + var precision = this._precisionOf( this.options.step ); + if ( this.options.min !== null ) { + precision = Math.max( precision, this._precisionOf( this.options.min ) ); } + return precision; }, - /* Retrieve the date(s) directly. */ - _getDate: function(inst) { - var startDate = (!inst.currentYear || (inst.input && inst.input.val() == '') ? null : - this._daylightSavingAdjust(new Date( - inst.currentYear, inst.currentMonth, inst.currentDay))); - return startDate; + _precisionOf: function( num ) { + var str = num.toString(), + decimal = str.indexOf( "." ); + return decimal === -1 ? 0 : str.length - decimal - 1; }, - /* Generate the HTML for the current state of the date picker. */ - _generateHTML: function(inst) { - var today = new Date(); - today = this._daylightSavingAdjust( - new Date(today.getFullYear(), today.getMonth(), today.getDate())); // clear time - var isRTL = this._get(inst, 'isRTL'); - var showButtonPanel = this._get(inst, 'showButtonPanel'); - var hideIfNoPrevNext = this._get(inst, 'hideIfNoPrevNext'); - var navigationAsDateFormat = this._get(inst, 'navigationAsDateFormat'); - var numMonths = this._getNumberOfMonths(inst); - var showCurrentAtPos = this._get(inst, 'showCurrentAtPos'); - var stepMonths = this._get(inst, 'stepMonths'); - var isMultiMonth = (numMonths[0] != 1 || numMonths[1] != 1); - var currentDate = this._daylightSavingAdjust((!inst.currentDay ? new Date(9999, 9, 9) : - new Date(inst.currentYear, inst.currentMonth, inst.currentDay))); - var minDate = this._getMinMaxDate(inst, 'min'); - var maxDate = this._getMinMaxDate(inst, 'max'); - var drawMonth = inst.drawMonth - showCurrentAtPos; - var drawYear = inst.drawYear; - if (drawMonth < 0) { - drawMonth += 12; - drawYear--; + _adjustValue: function( value ) { + var base, aboveMin, + options = this.options; + + // Make sure we're at a valid step + // - find out where we are relative to the base (min or 0) + base = options.min !== null ? options.min : 0; + aboveMin = value - base; + + // - round to the nearest step + aboveMin = Math.round( aboveMin / options.step ) * options.step; + + // - rounding is based on 0, so adjust back to our base + value = base + aboveMin; + + // Fix precision from bad JS floating point math + value = parseFloat( value.toFixed( this._precision() ) ); + + // Clamp the value + if ( options.max !== null && value > options.max ) { + return options.max; } - if (maxDate) { - var maxDraw = this._daylightSavingAdjust(new Date(maxDate.getFullYear(), - maxDate.getMonth() - (numMonths[0] * numMonths[1]) + 1, maxDate.getDate())); - maxDraw = (minDate && maxDraw < minDate ? minDate : maxDraw); - while (this._daylightSavingAdjust(new Date(drawYear, drawMonth, 1)) > maxDraw) { - drawMonth--; - if (drawMonth < 0) { - drawMonth = 11; - drawYear--; - } - } + if ( options.min !== null && value < options.min ) { + return options.min; } - inst.drawMonth = drawMonth; - inst.drawYear = drawYear; - var prevText = this._get(inst, 'prevText'); - prevText = (!navigationAsDateFormat ? prevText : this.formatDate(prevText, - this._daylightSavingAdjust(new Date(drawYear, drawMonth - stepMonths, 1)), - this._getFormatConfig(inst))); - var prev = (this._canAdjustMonth(inst, -1, drawYear, drawMonth) ? - '' + prevText + '' : - (hideIfNoPrevNext ? '' : '' + prevText + '')); - var nextText = this._get(inst, 'nextText'); - nextText = (!navigationAsDateFormat ? nextText : this.formatDate(nextText, - this._daylightSavingAdjust(new Date(drawYear, drawMonth + stepMonths, 1)), - this._getFormatConfig(inst))); - var next = (this._canAdjustMonth(inst, +1, drawYear, drawMonth) ? - '' + nextText + '' : - (hideIfNoPrevNext ? '' : '' + nextText + '')); - var currentText = this._get(inst, 'currentText'); - var gotoDate = (this._get(inst, 'gotoCurrent') && inst.currentDay ? currentDate : today); - currentText = (!navigationAsDateFormat ? currentText : - this.formatDate(currentText, gotoDate, this._getFormatConfig(inst))); - var controls = (!inst.inline ? '' : ''); - var buttonPanel = (showButtonPanel) ? '
              ' + (isRTL ? controls : '') + - (this._isInRange(inst, gotoDate) ? '' : '') + (isRTL ? '' : controls) + '
              ' : ''; - var firstDay = parseInt(this._get(inst, 'firstDay'),10); - firstDay = (isNaN(firstDay) ? 0 : firstDay); - var showWeek = this._get(inst, 'showWeek'); - var dayNames = this._get(inst, 'dayNames'); - var dayNamesShort = this._get(inst, 'dayNamesShort'); - var dayNamesMin = this._get(inst, 'dayNamesMin'); - var monthNames = this._get(inst, 'monthNames'); - var monthNamesShort = this._get(inst, 'monthNamesShort'); - var beforeShowDay = this._get(inst, 'beforeShowDay'); - var showOtherMonths = this._get(inst, 'showOtherMonths'); - var selectOtherMonths = this._get(inst, 'selectOtherMonths'); - var calculateWeek = this._get(inst, 'calculateWeek') || this.iso8601Week; - var defaultDate = this._getDefaultDate(inst); - var html = ''; - for (var row = 0; row < numMonths[0]; row++) { - var group = ''; - this.maxRows = 4; - for (var col = 0; col < numMonths[1]; col++) { - var selectedDate = this._daylightSavingAdjust(new Date(drawYear, drawMonth, inst.selectedDay)); - var cornerClass = ' ui-corner-all'; - var calender = ''; - if (isMultiMonth) { - calender += '
              '; - } - calender += '
              ' + - (/all|left/.test(cornerClass) && row == 0 ? (isRTL ? next : prev) : '') + - (/all|right/.test(cornerClass) && row == 0 ? (isRTL ? prev : next) : '') + - this._generateMonthYearHeader(inst, drawMonth, drawYear, minDate, maxDate, - row > 0 || col > 0, monthNames, monthNamesShort) + // draw month headers - '
               
              ' + - ''; - var thead = (showWeek ? '' : ''); - for (var dow = 0; dow < 7; dow++) { // days of the week - var day = (dow + firstDay) % 7; - thead += '= 5 ? ' class="ui-datepicker-week-end"' : '') + '>' + - '' + dayNamesMin[day] + ''; - } - calender += thead + ''; - var daysInMonth = this._getDaysInMonth(drawYear, drawMonth); - if (drawYear == inst.selectedYear && drawMonth == inst.selectedMonth) - inst.selectedDay = Math.min(inst.selectedDay, daysInMonth); - var leadDays = (this._getFirstDayOfMonth(drawYear, drawMonth) - firstDay + 7) % 7; - var curRows = Math.ceil((leadDays + daysInMonth) / 7); // calculate the number of rows to generate - var numRows = (isMultiMonth ? this.maxRows > curRows ? this.maxRows : curRows : curRows); //If multiple months, use the higher number of rows (see #7043) - this.maxRows = numRows; - var printDate = this._daylightSavingAdjust(new Date(drawYear, drawMonth, 1 - leadDays)); - for (var dRow = 0; dRow < numRows; dRow++) { // create date picker rows - calender += ''; - var tbody = (!showWeek ? '' : ''); - for (var dow = 0; dow < 7; dow++) { // create date picker days - var daySettings = (beforeShowDay ? - beforeShowDay.apply((inst.input ? inst.input[0] : null), [printDate]) : [true, '']); - var otherMonth = (printDate.getMonth() != drawMonth); - var unselectable = (otherMonth && !selectOtherMonths) || !daySettings[0] || - (minDate && printDate < minDate) || (maxDate && printDate > maxDate); - tbody += ''; // display selectable date - printDate.setDate(printDate.getDate() + 1); - printDate = this._daylightSavingAdjust(printDate); - } - calender += tbody + ''; - } - drawMonth++; - if (drawMonth > 11) { - drawMonth = 0; - drawYear++; - } - calender += '
              ' + this._get(inst, 'weekHeader') + '
              ' + - this._get(inst, 'calculateWeek')(printDate) + '' + // actions - (otherMonth && !showOtherMonths ? ' ' : // display for other months - (unselectable ? '' + printDate.getDate() + '' : '' + printDate.getDate() + '')) + '
              ' + (isMultiMonth ? '
' + - ((numMonths[0] > 0 && col == numMonths[1]-1) ? '
' : '') : ''); - group += calender; - } - html += group; + + return value; + }, + + _stop: function( event ) { + if ( !this.spinning ) { + return; } - html += buttonPanel + ($.browser.msie && parseInt($.browser.version,10) < 7 && !inst.inline ? - '' : ''); - inst._keyEvent = false; - return html; + + clearTimeout( this.timer ); + clearTimeout( this.mousewheelTimer ); + this.counter = 0; + this.spinning = false; + this._trigger( "stop", event ); }, - /* Generate the month and year header. */ - _generateMonthYearHeader: function(inst, drawMonth, drawYear, minDate, maxDate, - secondary, monthNames, monthNamesShort) { - var changeMonth = this._get(inst, 'changeMonth'); - var changeYear = this._get(inst, 'changeYear'); - var showMonthAfterYear = this._get(inst, 'showMonthAfterYear'); - var html = '
'; - var monthHtml = ''; - // month selection - if (secondary || !changeMonth) - monthHtml += '' + monthNames[drawMonth] + ''; - else { - var inMinYear = (minDate && minDate.getFullYear() == drawYear); - var inMaxYear = (maxDate && maxDate.getFullYear() == drawYear); - monthHtml += ''; - } - if (!showMonthAfterYear) - html += monthHtml + (secondary || !(changeMonth && changeYear) ? ' ' : ''); - // year selection - if ( !inst.yearshtml ) { - inst.yearshtml = ''; - if (secondary || !changeYear) - html += '' + drawYear + ''; - else { - // determine range of years to display - var years = this._get(inst, 'yearRange').split(':'); - var thisYear = new Date().getFullYear(); - var determineYear = function(value) { - var year = (value.match(/c[+-].*/) ? drawYear + parseInt(value.substring(1), 10) : - (value.match(/[+-].*/) ? thisYear + parseInt(value, 10) : - parseInt(value, 10))); - return (isNaN(year) ? thisYear : year); - }; - var year = determineYear(years[0]); - var endYear = Math.max(year, determineYear(years[1] || '')); - year = (minDate ? Math.max(year, minDate.getFullYear()) : year); - endYear = (maxDate ? Math.min(endYear, maxDate.getFullYear()) : endYear); - inst.yearshtml += ''; + _setOption: function( key, value ) { + var prevValue, first, last; - html += inst.yearshtml; - inst.yearshtml = null; + if ( key === "culture" || key === "numberFormat" ) { + prevValue = this._parse( this.element.val() ); + this.options[ key ] = value; + this.element.val( this._format( prevValue ) ); + return; + } + + if ( key === "max" || key === "min" || key === "step" ) { + if ( typeof value === "string" ) { + value = this._parse( value ); } } - html += this._get(inst, 'yearSuffix'); - if (showMonthAfterYear) - html += (secondary || !(changeMonth && changeYear) ? ' ' : '') + monthHtml; - html += '
'; // Close datepicker_header - return html; - }, + if ( key === "icons" ) { + first = this.buttons.first().find( ".ui-icon" ); + this._removeClass( first, null, this.options.icons.up ); + this._addClass( first, null, value.up ); + last = this.buttons.last().find( ".ui-icon" ); + this._removeClass( last, null, this.options.icons.down ); + this._addClass( last, null, value.down ); + } - /* Adjust one of the date sub-fields. */ - _adjustInstDate: function(inst, offset, period) { - var year = inst.drawYear + (period == 'Y' ? offset : 0); - var month = inst.drawMonth + (period == 'M' ? offset : 0); - var day = Math.min(inst.selectedDay, this._getDaysInMonth(year, month)) + - (period == 'D' ? offset : 0); - var date = this._restrictMinMax(inst, - this._daylightSavingAdjust(new Date(year, month, day))); - inst.selectedDay = date.getDate(); - inst.drawMonth = inst.selectedMonth = date.getMonth(); - inst.drawYear = inst.selectedYear = date.getFullYear(); - if (period == 'M' || period == 'Y') - this._notifyChange(inst); + this._super( key, value ); }, - /* Ensure a date is within any min/max bounds. */ - _restrictMinMax: function(inst, date) { - var minDate = this._getMinMaxDate(inst, 'min'); - var maxDate = this._getMinMaxDate(inst, 'max'); - var newDate = (minDate && date < minDate ? minDate : date); - newDate = (maxDate && newDate > maxDate ? maxDate : newDate); - return newDate; - }, + _setOptionDisabled: function( value ) { + this._super( value ); - /* Notify change of month/year. */ - _notifyChange: function(inst) { - var onChange = this._get(inst, 'onChangeMonthYear'); - if (onChange) - onChange.apply((inst.input ? inst.input[0] : null), - [inst.selectedYear, inst.selectedMonth + 1, inst]); + this._toggleClass( this.uiSpinner, null, "ui-state-disabled", !!value ); + this.element.prop( "disabled", !!value ); + this.buttons.button( value ? "disable" : "enable" ); }, - /* Determine the number of months to show. */ - _getNumberOfMonths: function(inst) { - var numMonths = this._get(inst, 'numberOfMonths'); - return (numMonths == null ? [1, 1] : (typeof numMonths == 'number' ? [1, numMonths] : numMonths)); - }, + _setOptions: spinnerModifer( function( options ) { + this._super( options ); + } ), - /* Determine the current maximum date - ensure no time components are set. */ - _getMinMaxDate: function(inst, minMax) { - return this._determineDate(inst, this._get(inst, minMax + 'Date'), null); + _parse: function( val ) { + if ( typeof val === "string" && val !== "" ) { + val = window.Globalize && this.options.numberFormat ? + Globalize.parseFloat( val, 10, this.options.culture ) : +val; + } + return val === "" || isNaN( val ) ? null : val; }, - /* Find the number of days in a given month. */ - _getDaysInMonth: function(year, month) { - return 32 - this._daylightSavingAdjust(new Date(year, month, 32)).getDate(); + _format: function( value ) { + if ( value === "" ) { + return ""; + } + return window.Globalize && this.options.numberFormat ? + Globalize.format( value, this.options.numberFormat, this.options.culture ) : + value; }, - /* Find the day of the week of the first of a month. */ - _getFirstDayOfMonth: function(year, month) { - return new Date(year, month, 1).getDay(); + _refresh: function() { + this.element.attr( { + "aria-valuemin": this.options.min, + "aria-valuemax": this.options.max, + + // TODO: what should we do with values that can't be parsed? + "aria-valuenow": this._parse( this.element.val() ) + } ); }, - /* Determines if we should allow a "next/prev" month display change. */ - _canAdjustMonth: function(inst, offset, curYear, curMonth) { - var numMonths = this._getNumberOfMonths(inst); - var date = this._daylightSavingAdjust(new Date(curYear, - curMonth + (offset < 0 ? offset : numMonths[0] * numMonths[1]), 1)); - if (offset < 0) - date.setDate(this._getDaysInMonth(date.getFullYear(), date.getMonth())); - return this._isInRange(inst, date); + isValid: function() { + var value = this.value(); + + // Null is invalid + if ( value === null ) { + return false; + } + + // If value gets adjusted, it's invalid + return value === this._adjustValue( value ); }, - /* Is the given date in the accepted range? */ - _isInRange: function(inst, date) { - var minDate = this._getMinMaxDate(inst, 'min'); - var maxDate = this._getMinMaxDate(inst, 'max'); - return ((!minDate || date.getTime() >= minDate.getTime()) && - (!maxDate || date.getTime() <= maxDate.getTime())); + // Update the value without triggering change + _value: function( value, allowAny ) { + var parsed; + if ( value !== "" ) { + parsed = this._parse( value ); + if ( parsed !== null ) { + if ( !allowAny ) { + parsed = this._adjustValue( parsed ); + } + value = this._format( parsed ); + } + } + this.element.val( value ); + this._refresh(); }, - /* Provide the configuration settings for formatting/parsing. */ - _getFormatConfig: function(inst) { - var shortYearCutoff = this._get(inst, 'shortYearCutoff'); - shortYearCutoff = (typeof shortYearCutoff != 'string' ? shortYearCutoff : - new Date().getFullYear() % 100 + parseInt(shortYearCutoff, 10)); - return {shortYearCutoff: shortYearCutoff, - dayNamesShort: this._get(inst, 'dayNamesShort'), dayNames: this._get(inst, 'dayNames'), - monthNamesShort: this._get(inst, 'monthNamesShort'), monthNames: this._get(inst, 'monthNames')}; + _destroy: function() { + this.element + .prop( "disabled", false ) + .removeAttr( "autocomplete role aria-valuemin aria-valuemax aria-valuenow" ); + + this.uiSpinner.replaceWith( this.element ); }, - /* Format the given date for display. */ - _formatDate: function(inst, day, month, year) { - if (!day) { - inst.currentDay = inst.selectedDay; - inst.currentMonth = inst.selectedMonth; - inst.currentYear = inst.selectedYear; + stepUp: spinnerModifer( function( steps ) { + this._stepUp( steps ); + } ), + _stepUp: function( steps ) { + if ( this._start() ) { + this._spin( ( steps || 1 ) * this.options.step ); + this._stop(); } - var date = (day ? (typeof day == 'object' ? day : - this._daylightSavingAdjust(new Date(year, month, day))) : - this._daylightSavingAdjust(new Date(inst.currentYear, inst.currentMonth, inst.currentDay))); - return this.formatDate(this._get(inst, 'dateFormat'), date, this._getFormatConfig(inst)); - } -}); + }, -/* - * Bind hover events for datepicker elements. - * Done via delegate so the binding only occurs once in the lifetime of the parent div. - * Global instActive, set by _updateDatepicker allows the handlers to find their way back to the active picker. - */ -function bindHover(dpDiv) { - var selector = 'button, .ui-datepicker-prev, .ui-datepicker-next, .ui-datepicker-calendar td a'; - return dpDiv.bind('mouseout', function(event) { - var elem = $( event.target ).closest( selector ); - if ( !elem.length ) { - return; - } - elem.removeClass( "ui-state-hover ui-datepicker-prev-hover ui-datepicker-next-hover" ); - }) - .bind('mouseover', function(event) { - var elem = $( event.target ).closest( selector ); - if ($.datepicker._isDisabledDatepicker( instActive.inline ? dpDiv.parent()[0] : instActive.input[0]) || - !elem.length ) { - return; - } - elem.parents('.ui-datepicker-calendar').find('a').removeClass('ui-state-hover'); - elem.addClass('ui-state-hover'); - if (elem.hasClass('ui-datepicker-prev')) elem.addClass('ui-datepicker-prev-hover'); - if (elem.hasClass('ui-datepicker-next')) elem.addClass('ui-datepicker-next-hover'); - }); -} + stepDown: spinnerModifer( function( steps ) { + this._stepDown( steps ); + } ), + _stepDown: function( steps ) { + if ( this._start() ) { + this._spin( ( steps || 1 ) * -this.options.step ); + this._stop(); + } + }, -/* jQuery extend now ignores nulls! */ -function extendRemove(target, props) { - $.extend(target, props); - for (var name in props) - if (props[name] == null || props[name] == undefined) - target[name] = props[name]; - return target; -}; + pageUp: spinnerModifer( function( pages ) { + this._stepUp( ( pages || 1 ) * this.options.page ); + } ), -/* Determine whether an object is an array. */ -function isArray(a) { - return (a && (($.browser.safari && typeof a == 'object' && a.length) || - (a.constructor && a.constructor.toString().match(/\Array\(\)/)))); -}; + pageDown: spinnerModifer( function( pages ) { + this._stepDown( ( pages || 1 ) * this.options.page ); + } ), -/* Invoke the datepicker functionality. - @param options string - a command, optionally followed by additional parameters or - Object - settings for attaching new datepicker functionality - @return jQuery object */ -$.fn.datepicker = function(options){ + value: function( newVal ) { + if ( !arguments.length ) { + return this._parse( this.element.val() ); + } + spinnerModifer( this._value ).call( this, newVal ); + }, - /* Verify an empty collection wasn't passed - Fixes #6976 */ - if ( !this.length ) { - return this; + widget: function() { + return this.uiSpinner; } +} ); + +// DEPRECATED +// TODO: switch return back to widget declaration at top of file when this is removed +if ( $.uiBackCompat !== false ) { + + // Backcompat for spinner html extension points + $.widget( "ui.spinner", $.ui.spinner, { + _enhance: function() { + this.uiSpinner = this.element + .attr( "autocomplete", "off" ) + .wrap( this._uiSpinnerHtml() ) + .parent() - /* Initialise the date picker. */ - if (!$.datepicker.initialized) { - $(document).mousedown($.datepicker._checkExternalClick). - find('body').append($.datepicker.dpDiv); - $.datepicker.initialized = true; - } + // Add buttons + .append( this._buttonHtml() ); + }, + _uiSpinnerHtml: function() { + return ""; + }, - var otherArgs = Array.prototype.slice.call(arguments, 1); - if (typeof options == 'string' && (options == 'isDisabled' || options == 'getDate' || options == 'widget')) - return $.datepicker['_' + options + 'Datepicker']. - apply($.datepicker, [this[0]].concat(otherArgs)); - if (options == 'option' && arguments.length == 2 && typeof arguments[1] == 'string') - return $.datepicker['_' + options + 'Datepicker']. - apply($.datepicker, [this[0]].concat(otherArgs)); - return this.each(function() { - typeof options == 'string' ? - $.datepicker['_' + options + 'Datepicker']. - apply($.datepicker, [this].concat(otherArgs)) : - $.datepicker._attachDatepicker(this, options); - }); -}; + _buttonHtml: function() { + return ""; + } + } ); +} -$.datepicker = new Datepicker(); // singleton instance -$.datepicker.initialized = false; -$.datepicker.uuid = new Date().getTime(); -$.datepicker.version = "1.8.17"; +var widgetsSpinner = $.ui.spinner; -// Workaround for #4055 -// Add another global to avoid noConflict issues with inline event handlers -window['DP_jQuery_' + dpuuid] = $; -})(jQuery); -/* - * jQuery UI Progressbar 1.8.17 +/*! + * jQuery UI Tabs 1.12.1 + * http://jqueryui.com * - * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about) - * Dual licensed under the MIT or GPL Version 2 licenses. + * Copyright jQuery Foundation and other contributors + * Released under the MIT license. * http://jquery.org/license - * - * http://docs.jquery.com/UI/Progressbar - * - * Depends: - * jquery.ui.core.js - * jquery.ui.widget.js */ -(function( $, undefined ) { -$.widget( "ui.progressbar", { +//>>label: Tabs +//>>group: Widgets +//>>description: Transforms a set of container elements into a tab structure. +//>>docs: http://api.jqueryui.com/tabs/ +//>>demos: http://jqueryui.com/tabs/ +//>>css.structure: ../../themes/base/core.css +//>>css.structure: ../../themes/base/tabs.css +//>>css.theme: ../../themes/base/theme.css + + + +$.widget( "ui.tabs", { + version: "1.12.1", + delay: 300, options: { - value: 0, - max: 100 + active: null, + classes: { + "ui-tabs": "ui-corner-all", + "ui-tabs-nav": "ui-corner-all", + "ui-tabs-panel": "ui-corner-bottom", + "ui-tabs-tab": "ui-corner-top" + }, + collapsible: false, + event: "click", + heightStyle: "content", + hide: null, + show: null, + + // Callbacks + activate: null, + beforeActivate: null, + beforeLoad: null, + load: null }, - min: 0, + _isLocal: ( function() { + var rhash = /#.*$/; + + return function( anchor ) { + var anchorUrl, locationUrl; + + anchorUrl = anchor.href.replace( rhash, "" ); + locationUrl = location.href.replace( rhash, "" ); + + // Decoding may throw an error if the URL isn't UTF-8 (#9518) + try { + anchorUrl = decodeURIComponent( anchorUrl ); + } catch ( error ) {} + try { + locationUrl = decodeURIComponent( locationUrl ); + } catch ( error ) {} + + return anchor.hash.length > 1 && anchorUrl === locationUrl; + }; + } )(), _create: function() { - this.element - .addClass( "ui-progressbar ui-widget ui-widget-content ui-corner-all" ) - .attr({ - role: "progressbar", - "aria-valuemin": this.min, - "aria-valuemax": this.options.max, - "aria-valuenow": this._value() - }); + var that = this, + options = this.options; - this.valueDiv = $( "
" ) - .appendTo( this.element ); + this.running = false; - this.oldValue = this._value(); - this._refreshValue(); + this._addClass( "ui-tabs", "ui-widget ui-widget-content" ); + this._toggleClass( "ui-tabs-collapsible", null, options.collapsible ); + + this._processTabs(); + options.active = this._initialActive(); + + // Take disabling tabs via class attribute from HTML + // into account and update option properly. + if ( $.isArray( options.disabled ) ) { + options.disabled = $.unique( options.disabled.concat( + $.map( this.tabs.filter( ".ui-state-disabled" ), function( li ) { + return that.tabs.index( li ); + } ) + ) ).sort(); + } + + // Check for length avoids error when initializing empty list + if ( this.options.active !== false && this.anchors.length ) { + this.active = this._findActive( options.active ); + } else { + this.active = $(); + } + + this._refresh(); + + if ( this.active.length ) { + this.load( options.active ); + } }, - destroy: function() { - this.element - .removeClass( "ui-progressbar ui-widget ui-widget-content ui-corner-all" ) - .removeAttr( "role" ) - .removeAttr( "aria-valuemin" ) - .removeAttr( "aria-valuemax" ) - .removeAttr( "aria-valuenow" ); + _initialActive: function() { + var active = this.options.active, + collapsible = this.options.collapsible, + locationHash = location.hash.substring( 1 ); - this.valueDiv.remove(); + if ( active === null ) { + + // check the fragment identifier in the URL + if ( locationHash ) { + this.tabs.each( function( i, tab ) { + if ( $( tab ).attr( "aria-controls" ) === locationHash ) { + active = i; + return false; + } + } ); + } + + // Check for a tab marked active via a class + if ( active === null ) { + active = this.tabs.index( this.tabs.filter( ".ui-tabs-active" ) ); + } + + // No active tab, set to false + if ( active === null || active === -1 ) { + active = this.tabs.length ? 0 : false; + } + } + + // Handle numbers: negative, out of range + if ( active !== false ) { + active = this.tabs.index( this.tabs.eq( active ) ); + if ( active === -1 ) { + active = collapsible ? false : 0; + } + } + + // Don't allow collapsible: false and active: false + if ( !collapsible && active === false && this.anchors.length ) { + active = 0; + } - $.Widget.prototype.destroy.apply( this, arguments ); + return active; }, - value: function( newValue ) { - if ( newValue === undefined ) { - return this._value(); + _getCreateEventData: function() { + return { + tab: this.active, + panel: !this.active.length ? $() : this._getPanelForTab( this.active ) + }; + }, + + _tabKeydown: function( event ) { + var focusedTab = $( $.ui.safeActiveElement( this.document[ 0 ] ) ).closest( "li" ), + selectedIndex = this.tabs.index( focusedTab ), + goingForward = true; + + if ( this._handlePageNav( event ) ) { + return; } - this._setOption( "value", newValue ); - return this; + switch ( event.keyCode ) { + case $.ui.keyCode.RIGHT: + case $.ui.keyCode.DOWN: + selectedIndex++; + break; + case $.ui.keyCode.UP: + case $.ui.keyCode.LEFT: + goingForward = false; + selectedIndex--; + break; + case $.ui.keyCode.END: + selectedIndex = this.anchors.length - 1; + break; + case $.ui.keyCode.HOME: + selectedIndex = 0; + break; + case $.ui.keyCode.SPACE: + + // Activate only, no collapsing + event.preventDefault(); + clearTimeout( this.activating ); + this._activate( selectedIndex ); + return; + case $.ui.keyCode.ENTER: + + // Toggle (cancel delayed activation, allow collapsing) + event.preventDefault(); + clearTimeout( this.activating ); + + // Determine if we should collapse or activate + this._activate( selectedIndex === this.options.active ? false : selectedIndex ); + return; + default: + return; + } + + // Focus the appropriate tab, based on which key was pressed + event.preventDefault(); + clearTimeout( this.activating ); + selectedIndex = this._focusNextTab( selectedIndex, goingForward ); + + // Navigating with control/command key will prevent automatic activation + if ( !event.ctrlKey && !event.metaKey ) { + + // Update aria-selected immediately so that AT think the tab is already selected. + // Otherwise AT may confuse the user by stating that they need to activate the tab, + // but the tab will already be activated by the time the announcement finishes. + focusedTab.attr( "aria-selected", "false" ); + this.tabs.eq( selectedIndex ).attr( "aria-selected", "true" ); + + this.activating = this._delay( function() { + this.option( "active", selectedIndex ); + }, this.delay ); + } }, - _setOption: function( key, value ) { - if ( key === "value" ) { - this.options.value = value; - this._refreshValue(); - if ( this._value() === this.options.max ) { - this._trigger( "complete" ); + _panelKeydown: function( event ) { + if ( this._handlePageNav( event ) ) { + return; + } + + // Ctrl+up moves focus to the current tab + if ( event.ctrlKey && event.keyCode === $.ui.keyCode.UP ) { + event.preventDefault(); + this.active.trigger( "focus" ); + } + }, + + // Alt+page up/down moves focus to the previous/next tab (and activates) + _handlePageNav: function( event ) { + if ( event.altKey && event.keyCode === $.ui.keyCode.PAGE_UP ) { + this._activate( this._focusNextTab( this.options.active - 1, false ) ); + return true; + } + if ( event.altKey && event.keyCode === $.ui.keyCode.PAGE_DOWN ) { + this._activate( this._focusNextTab( this.options.active + 1, true ) ); + return true; + } + }, + + _findNextTab: function( index, goingForward ) { + var lastTabIndex = this.tabs.length - 1; + + function constrain() { + if ( index > lastTabIndex ) { + index = 0; } + if ( index < 0 ) { + index = lastTabIndex; + } + return index; + } + + while ( $.inArray( constrain(), this.options.disabled ) !== -1 ) { + index = goingForward ? index + 1 : index - 1; } - $.Widget.prototype._setOption.apply( this, arguments ); + return index; }, - _value: function() { - var val = this.options.value; - // normalize invalid value - if ( typeof val !== "number" ) { - val = 0; + _focusNextTab: function( index, goingForward ) { + index = this._findNextTab( index, goingForward ); + this.tabs.eq( index ).trigger( "focus" ); + return index; + }, + + _setOption: function( key, value ) { + if ( key === "active" ) { + + // _activate() will handle invalid values and update this.options + this._activate( value ); + return; + } + + this._super( key, value ); + + if ( key === "collapsible" ) { + this._toggleClass( "ui-tabs-collapsible", null, value ); + + // Setting collapsible: false while collapsed; open first panel + if ( !value && this.options.active === false ) { + this._activate( 0 ); + } + } + + if ( key === "event" ) { + this._setupEvents( value ); + } + + if ( key === "heightStyle" ) { + this._setupHeightStyle( value ); } - return Math.min( this.options.max, Math.max( this.min, val ) ); }, - _percentage: function() { - return 100 * this._value() / this.options.max; + _sanitizeSelector: function( hash ) { + return hash ? hash.replace( /[!"$%&'()*+,.\/:;<=>?@\[\]\^`{|}~]/g, "\\$&" ) : ""; }, - _refreshValue: function() { - var value = this.value(); - var percentage = this._percentage(); + refresh: function() { + var options = this.options, + lis = this.tablist.children( ":has(a[href])" ); - if ( this.oldValue !== value ) { - this.oldValue = value; - this._trigger( "change" ); - } + // Get disabled tabs from class attribute from HTML + // this will get converted to a boolean if needed in _refresh() + options.disabled = $.map( lis.filter( ".ui-state-disabled" ), function( tab ) { + return lis.index( tab ); + } ); - this.valueDiv - .toggle( value > this.min ) - .toggleClass( "ui-corner-right", value === this.options.max ) - .width( percentage.toFixed(0) + "%" ); - this.element.attr( "aria-valuenow", value ); - } -}); + this._processTabs(); -$.extend( $.ui.progressbar, { - version: "1.8.17" -}); + // Was collapsed or no tabs + if ( options.active === false || !this.anchors.length ) { + options.active = false; + this.active = $(); -})( jQuery ); -/* - * jQuery UI Effects 1.8.17 - * - * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about) - * Dual licensed under the MIT or GPL Version 2 licenses. - * http://jquery.org/license - * - * http://docs.jquery.com/UI/Effects/ - */ -;jQuery.effects || (function($, undefined) { + // was active, but active tab is gone + } else if ( this.active.length && !$.contains( this.tablist[ 0 ], this.active[ 0 ] ) ) { -$.effects = {}; + // all remaining tabs are disabled + if ( this.tabs.length === options.disabled.length ) { + options.active = false; + this.active = $(); + // activate previous tab + } else { + this._activate( this._findNextTab( Math.max( 0, options.active - 1 ), false ) ); + } + // was active, active tab still exists + } else { -/******************************************************************************/ -/****************************** COLOR ANIMATIONS ******************************/ -/******************************************************************************/ + // make sure active index is correct + options.active = this.tabs.index( this.active ); + } -// override the animation for color styles -$.each(['backgroundColor', 'borderBottomColor', 'borderLeftColor', - 'borderRightColor', 'borderTopColor', 'borderColor', 'color', 'outlineColor'], -function(i, attr) { - $.fx.step[attr] = function(fx) { - if (!fx.colorInit) { - fx.start = getColor(fx.elem, attr); - fx.end = getRGB(fx.end); - fx.colorInit = true; - } - - fx.elem.style[attr] = 'rgb(' + - Math.max(Math.min(parseInt((fx.pos * (fx.end[0] - fx.start[0])) + fx.start[0], 10), 255), 0) + ',' + - Math.max(Math.min(parseInt((fx.pos * (fx.end[1] - fx.start[1])) + fx.start[1], 10), 255), 0) + ',' + - Math.max(Math.min(parseInt((fx.pos * (fx.end[2] - fx.start[2])) + fx.start[2], 10), 255), 0) + ')'; - }; -}); + this._refresh(); + }, -// Color Conversion functions from highlightFade -// By Blair Mitchelmore -// http://jquery.offput.ca/highlightFade/ + _refresh: function() { + this._setOptionDisabled( this.options.disabled ); + this._setupEvents( this.options.event ); + this._setupHeightStyle( this.options.heightStyle ); -// Parse strings looking for color tuples [255,255,255] -function getRGB(color) { - var result; + this.tabs.not( this.active ).attr( { + "aria-selected": "false", + "aria-expanded": "false", + tabIndex: -1 + } ); + this.panels.not( this._getPanelForTab( this.active ) ) + .hide() + .attr( { + "aria-hidden": "true" + } ); - // Check if we're already dealing with an array of colors - if ( color && color.constructor == Array && color.length == 3 ) - return color; + // Make sure one tab is in the tab order + if ( !this.active.length ) { + this.tabs.eq( 0 ).attr( "tabIndex", 0 ); + } else { + this.active + .attr( { + "aria-selected": "true", + "aria-expanded": "true", + tabIndex: 0 + } ); + this._addClass( this.active, "ui-tabs-active", "ui-state-active" ); + this._getPanelForTab( this.active ) + .show() + .attr( { + "aria-hidden": "false" + } ); + } + }, - // Look for rgb(num,num,num) - if (result = /rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/.exec(color)) - return [parseInt(result[1],10), parseInt(result[2],10), parseInt(result[3],10)]; + _processTabs: function() { + var that = this, + prevTabs = this.tabs, + prevAnchors = this.anchors, + prevPanels = this.panels; - // Look for rgb(num%,num%,num%) - if (result = /rgb\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*\)/.exec(color)) - return [parseFloat(result[1])*2.55, parseFloat(result[2])*2.55, parseFloat(result[3])*2.55]; + this.tablist = this._getList().attr( "role", "tablist" ); + this._addClass( this.tablist, "ui-tabs-nav", + "ui-helper-reset ui-helper-clearfix ui-widget-header" ); - // Look for #a0b1c2 - if (result = /#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/.exec(color)) - return [parseInt(result[1],16), parseInt(result[2],16), parseInt(result[3],16)]; + // Prevent users from focusing disabled tabs via click + this.tablist + .on( "mousedown" + this.eventNamespace, "> li", function( event ) { + if ( $( this ).is( ".ui-state-disabled" ) ) { + event.preventDefault(); + } + } ) + + // Support: IE <9 + // Preventing the default action in mousedown doesn't prevent IE + // from focusing the element, so if the anchor gets focused, blur. + // We don't have to worry about focusing the previously focused + // element since clicking on a non-focusable element should focus + // the body anyway. + .on( "focus" + this.eventNamespace, ".ui-tabs-anchor", function() { + if ( $( this ).closest( "li" ).is( ".ui-state-disabled" ) ) { + this.blur(); + } + } ); - // Look for #fff - if (result = /#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])/.exec(color)) - return [parseInt(result[1]+result[1],16), parseInt(result[2]+result[2],16), parseInt(result[3]+result[3],16)]; + this.tabs = this.tablist.find( "> li:has(a[href])" ) + .attr( { + role: "tab", + tabIndex: -1 + } ); + this._addClass( this.tabs, "ui-tabs-tab", "ui-state-default" ); - // Look for rgba(0, 0, 0, 0) == transparent in Safari 3 - if (result = /rgba\(0, 0, 0, 0\)/.exec(color)) - return colors['transparent']; + this.anchors = this.tabs.map( function() { + return $( "a", this )[ 0 ]; + } ) + .attr( { + role: "presentation", + tabIndex: -1 + } ); + this._addClass( this.anchors, "ui-tabs-anchor" ); - // Otherwise, we're most likely dealing with a named color - return colors[$.trim(color).toLowerCase()]; -} + this.panels = $(); -function getColor(elem, attr) { - var color; + this.anchors.each( function( i, anchor ) { + var selector, panel, panelId, + anchorId = $( anchor ).uniqueId().attr( "id" ), + tab = $( anchor ).closest( "li" ), + originalAriaControls = tab.attr( "aria-controls" ); - do { - color = $.curCSS(elem, attr); + // Inline tab + if ( that._isLocal( anchor ) ) { + selector = anchor.hash; + panelId = selector.substring( 1 ); + panel = that.element.find( that._sanitizeSelector( selector ) ); - // Keep going until we find an element that has color, or we hit the body - if ( color != '' && color != 'transparent' || $.nodeName(elem, "body") ) - break; + // remote tab + } else { - attr = "backgroundColor"; - } while ( elem = elem.parentNode ); + // If the tab doesn't already have aria-controls, + // generate an id by using a throw-away element + panelId = tab.attr( "aria-controls" ) || $( {} ).uniqueId()[ 0 ].id; + selector = "#" + panelId; + panel = that.element.find( selector ); + if ( !panel.length ) { + panel = that._createPanel( panelId ); + panel.insertAfter( that.panels[ i - 1 ] || that.tablist ); + } + panel.attr( "aria-live", "polite" ); + } - return getRGB(color); -}; + if ( panel.length ) { + that.panels = that.panels.add( panel ); + } + if ( originalAriaControls ) { + tab.data( "ui-tabs-aria-controls", originalAriaControls ); + } + tab.attr( { + "aria-controls": panelId, + "aria-labelledby": anchorId + } ); + panel.attr( "aria-labelledby", anchorId ); + } ); -// Some named colors to work with -// From Interface by Stefan Petre -// http://interface.eyecon.ro/ - -var colors = { - aqua:[0,255,255], - azure:[240,255,255], - beige:[245,245,220], - black:[0,0,0], - blue:[0,0,255], - brown:[165,42,42], - cyan:[0,255,255], - darkblue:[0,0,139], - darkcyan:[0,139,139], - darkgrey:[169,169,169], - darkgreen:[0,100,0], - darkkhaki:[189,183,107], - darkmagenta:[139,0,139], - darkolivegreen:[85,107,47], - darkorange:[255,140,0], - darkorchid:[153,50,204], - darkred:[139,0,0], - darksalmon:[233,150,122], - darkviolet:[148,0,211], - fuchsia:[255,0,255], - gold:[255,215,0], - green:[0,128,0], - indigo:[75,0,130], - khaki:[240,230,140], - lightblue:[173,216,230], - lightcyan:[224,255,255], - lightgreen:[144,238,144], - lightgrey:[211,211,211], - lightpink:[255,182,193], - lightyellow:[255,255,224], - lime:[0,255,0], - magenta:[255,0,255], - maroon:[128,0,0], - navy:[0,0,128], - olive:[128,128,0], - orange:[255,165,0], - pink:[255,192,203], - purple:[128,0,128], - violet:[128,0,128], - red:[255,0,0], - silver:[192,192,192], - white:[255,255,255], - yellow:[255,255,0], - transparent: [255,255,255] -}; + this.panels.attr( "role", "tabpanel" ); + this._addClass( this.panels, "ui-tabs-panel", "ui-widget-content" ); + // Avoid memory leaks (#10056) + if ( prevTabs ) { + this._off( prevTabs.not( this.tabs ) ); + this._off( prevAnchors.not( this.anchors ) ); + this._off( prevPanels.not( this.panels ) ); + } + }, + // Allow overriding how to find the list for rare usage scenarios (#7715) + _getList: function() { + return this.tablist || this.element.find( "ol, ul" ).eq( 0 ); + }, -/******************************************************************************/ -/****************************** CLASS ANIMATIONS ******************************/ -/******************************************************************************/ + _createPanel: function( id ) { + return $( "
" ) + .attr( "id", id ) + .data( "ui-tabs-destroy", true ); + }, -var classAnimationActions = ['add', 'remove', 'toggle'], - shorthandStyles = { - border: 1, - borderBottom: 1, - borderColor: 1, - borderLeft: 1, - borderRight: 1, - borderTop: 1, - borderWidth: 1, - margin: 1, - padding: 1 - }; + _setOptionDisabled: function( disabled ) { + var currentItem, li, i; -function getElementStyles() { - var style = document.defaultView - ? document.defaultView.getComputedStyle(this, null) - : this.currentStyle, - newStyle = {}, - key, - camelCase; - - // webkit enumerates style porperties - if (style && style.length && style[0] && style[style[0]]) { - var len = style.length; - while (len--) { - key = style[len]; - if (typeof style[key] == 'string') { - camelCase = key.replace(/\-(\w)/g, function(all, letter){ - return letter.toUpperCase(); - }); - newStyle[camelCase] = style[key]; + if ( $.isArray( disabled ) ) { + if ( !disabled.length ) { + disabled = false; + } else if ( disabled.length === this.anchors.length ) { + disabled = true; } } - } else { - for (key in style) { - if (typeof style[key] === 'string') { - newStyle[key] = style[key]; + + // Disable tabs + for ( i = 0; ( li = this.tabs[ i ] ); i++ ) { + currentItem = $( li ); + if ( disabled === true || $.inArray( i, disabled ) !== -1 ) { + currentItem.attr( "aria-disabled", "true" ); + this._addClass( currentItem, null, "ui-state-disabled" ); + } else { + currentItem.removeAttr( "aria-disabled" ); + this._removeClass( currentItem, null, "ui-state-disabled" ); } } - } - return newStyle; -} + this.options.disabled = disabled; -function filterStyles(styles) { - var name, value; - for (name in styles) { - value = styles[name]; - if ( - // ignore null and undefined values - value == null || - // ignore functions (when does this occur?) - $.isFunction(value) || - // shorthand styles that need to be expanded - name in shorthandStyles || - // ignore scrollbars (break in IE) - (/scrollbar/).test(name) || - - // only colors or values that can be converted to numbers - (!(/color/i).test(name) && isNaN(parseFloat(value))) - ) { - delete styles[name]; - } - } + this._toggleClass( this.widget(), this.widgetFullName + "-disabled", null, + disabled === true ); + }, - return styles; -} + _setupEvents: function( event ) { + var events = {}; + if ( event ) { + $.each( event.split( " " ), function( index, eventName ) { + events[ eventName ] = "_eventHandler"; + } ); + } -function styleDifference(oldStyle, newStyle) { - var diff = { _: 0 }, // http://dev.jquery.com/ticket/5459 - name; + this._off( this.anchors.add( this.tabs ).add( this.panels ) ); - for (name in newStyle) { - if (oldStyle[name] != newStyle[name]) { - diff[name] = newStyle[name]; - } - } + // Always prevent the default action, even when disabled + this._on( true, this.anchors, { + click: function( event ) { + event.preventDefault(); + } + } ); + this._on( this.anchors, events ); + this._on( this.tabs, { keydown: "_tabKeydown" } ); + this._on( this.panels, { keydown: "_panelKeydown" } ); - return diff; -} + this._focusable( this.tabs ); + this._hoverable( this.tabs ); + }, -$.effects.animateClass = function(value, duration, easing, callback) { - if ($.isFunction(easing)) { - callback = easing; - easing = null; - } + _setupHeightStyle: function( heightStyle ) { + var maxHeight, + parent = this.element.parent(); - return this.queue(function() { - var that = $(this), - originalStyleAttr = that.attr('style') || ' ', - originalStyle = filterStyles(getElementStyles.call(this)), - newStyle, - className = that.attr('class'); + if ( heightStyle === "fill" ) { + maxHeight = parent.height(); + maxHeight -= this.element.outerHeight() - this.element.height(); - $.each(classAnimationActions, function(i, action) { - if (value[action]) { - that[action + 'Class'](value[action]); - } - }); - newStyle = filterStyles(getElementStyles.call(this)); - that.attr('class', className); + this.element.siblings( ":visible" ).each( function() { + var elem = $( this ), + position = elem.css( "position" ); - that.animate(styleDifference(originalStyle, newStyle), { - queue: false, - duration: duration, - easing: easing, - complete: function() { - $.each(classAnimationActions, function(i, action) { - if (value[action]) { that[action + 'Class'](value[action]); } - }); - // work around bug in IE by clearing the cssText before setting it - if (typeof that.attr('style') == 'object') { - that.attr('style').cssText = ''; - that.attr('style').cssText = originalStyleAttr; - } else { - that.attr('style', originalStyleAttr); + if ( position === "absolute" || position === "fixed" ) { + return; } - if (callback) { callback.apply(this, arguments); } - $.dequeue( this ); - } - }); - }); -}; - -$.fn.extend({ - _addClass: $.fn.addClass, - addClass: function(classNames, speed, easing, callback) { - return speed ? $.effects.animateClass.apply(this, [{ add: classNames },speed,easing,callback]) : this._addClass(classNames); - }, + maxHeight -= elem.outerHeight( true ); + } ); - _removeClass: $.fn.removeClass, - removeClass: function(classNames,speed,easing,callback) { - return speed ? $.effects.animateClass.apply(this, [{ remove: classNames },speed,easing,callback]) : this._removeClass(classNames); - }, + this.element.children().not( this.panels ).each( function() { + maxHeight -= $( this ).outerHeight( true ); + } ); - _toggleClass: $.fn.toggleClass, - toggleClass: function(classNames, force, speed, easing, callback) { - if ( typeof force == "boolean" || force === undefined ) { - if ( !speed ) { - // without speed parameter; - return this._toggleClass(classNames, force); - } else { - return $.effects.animateClass.apply(this, [(force?{add:classNames}:{remove:classNames}),speed,easing,callback]); - } - } else { - // without switch parameter; - return $.effects.animateClass.apply(this, [{ toggle: classNames },force,speed,easing]); + this.panels.each( function() { + $( this ).height( Math.max( 0, maxHeight - + $( this ).innerHeight() + $( this ).height() ) ); + } ) + .css( "overflow", "auto" ); + } else if ( heightStyle === "auto" ) { + maxHeight = 0; + this.panels.each( function() { + maxHeight = Math.max( maxHeight, $( this ).height( "" ).height() ); + } ).height( maxHeight ); } }, - switchClass: function(remove,add,speed,easing,callback) { - return $.effects.animateClass.apply(this, [{ add: add, remove: remove },speed,easing,callback]); - } -}); + _eventHandler: function( event ) { + var options = this.options, + active = this.active, + anchor = $( event.currentTarget ), + tab = anchor.closest( "li" ), + clickedIsActive = tab[ 0 ] === active[ 0 ], + collapsing = clickedIsActive && options.collapsible, + toShow = collapsing ? $() : this._getPanelForTab( tab ), + toHide = !active.length ? $() : this._getPanelForTab( active ), + eventData = { + oldTab: active, + oldPanel: toHide, + newTab: collapsing ? $() : tab, + newPanel: toShow + }; + event.preventDefault(); + if ( tab.hasClass( "ui-state-disabled" ) || -/******************************************************************************/ -/*********************************** EFFECTS **********************************/ -/******************************************************************************/ + // tab is already loading + tab.hasClass( "ui-tabs-loading" ) || -$.extend($.effects, { - version: "1.8.17", + // can't switch durning an animation + this.running || - // Saves a set of properties in a data storage - save: function(element, set) { - for(var i=0; i < set.length; i++) { - if(set[i] !== null) element.data("ec.storage."+set[i], element[0].style[set[i]]); - } - }, + // click on active header, but not collapsible + ( clickedIsActive && !options.collapsible ) || - // Restores a set of previously saved properties from a data storage - restore: function(element, set) { - for(var i=0; i < set.length; i++) { - if(set[i] !== null) element.css(set[i], element.data("ec.storage."+set[i])); + // allow canceling activation + ( this._trigger( "beforeActivate", event, eventData ) === false ) ) { + return; } - }, - - setMode: function(el, mode) { - if (mode == 'toggle') mode = el.is(':hidden') ? 'show' : 'hide'; // Set for toggle - return mode; - }, - - getBaseline: function(origin, original) { // Translates a [top,left] array into a baseline value - // this should be a little more flexible in the future to handle a string & hash - 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}; - }, - // Wraps the element around a wrapper that copies position properties - createWrapper: function(element) { + options.active = collapsing ? false : this.tabs.index( tab ); - // if the element is already wrapped, return it - if (element.parent().is('.ui-effects-wrapper')) { - return element.parent(); + this.active = clickedIsActive ? $() : tab; + if ( this.xhr ) { + this.xhr.abort(); } - // wrap the element - var props = { - width: element.outerWidth(true), - height: element.outerHeight(true), - 'float': element.css('float') - }, - wrapper = $('
') - .addClass('ui-effects-wrapper') - .css({ - fontSize: '100%', - background: 'transparent', - border: 'none', - margin: 0, - padding: 0 - }), - active = document.activeElement; - - element.wrap(wrapper); - - // Fixes #7595 - Elements lose focus when wrapped. - if ( element[ 0 ] === active || $.contains( element[ 0 ], active ) ) { - $( active ).focus(); - } - - wrapper = element.parent(); //Hotfix for jQuery 1.4 since some change in wrap() seems to actually loose the reference to the wrapped element - - // 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' }); + if ( !toHide.length && !toShow.length ) { + $.error( "jQuery UI Tabs: Mismatching fragment identifier." ); } - return wrapper.css(props).show(); + if ( toShow.length ) { + this.load( this.tabs.index( tab ), event ); + } + this._toggle( event, eventData ); }, - removeWrapper: function(element) { - var parent, - active = document.activeElement; + // Handles show/hide for selecting tabs + _toggle: function( event, eventData ) { + var that = this, + toShow = eventData.newPanel, + toHide = eventData.oldPanel; - if (element.parent().is('.ui-effects-wrapper')) { - parent = element.parent().replaceWith(element); - // Fixes #7595 - Elements lose focus when wrapped. - if ( element[ 0 ] === active || $.contains( element[ 0 ], active ) ) { - $( active ).focus(); - } - return parent; + this.running = true; + + function complete() { + that.running = false; + that._trigger( "activate", event, eventData ); } - return element; - }, + function show() { + that._addClass( eventData.newTab.closest( "li" ), "ui-tabs-active", "ui-state-active" ); - setTransition: function(element, list, factor, value) { - value = value || {}; - $.each(list, function(i, x){ - unit = element.cssUnit(x); - if (unit[0] > 0) value[x] = unit[0] * factor + unit[1]; - }); - return value; - } -}); + if ( toShow.length && that.options.show ) { + that._show( toShow, that.options.show, complete ); + } else { + toShow.show(); + complete(); + } + } + // Start out by hiding, then showing, then completing + if ( toHide.length && this.options.hide ) { + this._hide( toHide, this.options.hide, function() { + that._removeClass( eventData.oldTab.closest( "li" ), + "ui-tabs-active", "ui-state-active" ); + show(); + } ); + } else { + this._removeClass( eventData.oldTab.closest( "li" ), + "ui-tabs-active", "ui-state-active" ); + toHide.hide(); + show(); + } + + toHide.attr( "aria-hidden", "true" ); + eventData.oldTab.attr( { + "aria-selected": "false", + "aria-expanded": "false" + } ); + + // If we're switching tabs, remove the old tab from the tab order. + // If we're opening from collapsed state, remove the previous tab from the tab order. + // If we're collapsing, then keep the collapsing tab in the tab order. + if ( toShow.length && toHide.length ) { + eventData.oldTab.attr( "tabIndex", -1 ); + } else if ( toShow.length ) { + this.tabs.filter( function() { + return $( this ).attr( "tabIndex" ) === 0; + } ) + .attr( "tabIndex", -1 ); + } + + toShow.attr( "aria-hidden", "false" ); + eventData.newTab.attr( { + "aria-selected": "true", + "aria-expanded": "true", + tabIndex: 0 + } ); + }, + + _activate: function( index ) { + var anchor, + active = this._findActive( index ); + + // Trying to activate the already active panel + if ( active[ 0 ] === this.active[ 0 ] ) { + return; + } -function _normalizeArguments(effect, options, speed, callback) { - // shift params for method overloading - if (typeof effect == 'object') { - callback = options; - speed = null; - options = effect; - effect = options.effect; - } - if ($.isFunction(options)) { - callback = options; - speed = null; - options = {}; - } - if (typeof options == 'number' || $.fx.speeds[options]) { - callback = speed; - speed = options; - options = {}; - } - if ($.isFunction(speed)) { - callback = speed; - speed = null; - } + // Trying to collapse, simulate a click on the current active header + if ( !active.length ) { + active = this.active; + } - options = options || {}; + anchor = active.find( ".ui-tabs-anchor" )[ 0 ]; + this._eventHandler( { + target: anchor, + currentTarget: anchor, + preventDefault: $.noop + } ); + }, - speed = speed || options.duration; - speed = $.fx.off ? 0 : typeof speed == 'number' - ? speed : speed in $.fx.speeds ? $.fx.speeds[speed] : $.fx.speeds._default; + _findActive: function( index ) { + return index === false ? $() : this.tabs.eq( index ); + }, - callback = callback || options.complete; + _getIndex: function( index ) { - return [effect, options, speed, callback]; -} + // meta-function to give users option to provide a href string instead of a numerical index. + if ( typeof index === "string" ) { + index = this.anchors.index( this.anchors.filter( "[href$='" + + $.ui.escapeSelector( index ) + "']" ) ); + } -function standardSpeed( speed ) { - // valid standard speeds - if ( !speed || typeof speed === "number" || $.fx.speeds[ speed ] ) { - return true; - } + return index; + }, - // invalid strings - treat as "normal" speed - if ( typeof speed === "string" && !$.effects[ speed ] ) { - return true; - } + _destroy: function() { + if ( this.xhr ) { + this.xhr.abort(); + } - return false; -} + this.tablist + .removeAttr( "role" ) + .off( this.eventNamespace ); -$.fn.extend({ - effect: function(effect, options, speed, callback) { - var args = _normalizeArguments.apply(this, arguments), - // TODO: make effects take actual parameters instead of a hash - args2 = { - options: args[1], - duration: args[2], - callback: args[3] - }, - mode = args2.options.mode, - effectMethod = $.effects[effect]; + this.anchors + .removeAttr( "role tabIndex" ) + .removeUniqueId(); - if ( $.fx.off || !effectMethod ) { - // delegate to the original method (e.g., .show()) if possible - if ( mode ) { - return this[ mode ]( args2.duration, args2.callback ); + this.tabs.add( this.panels ).each( function() { + if ( $.data( this, "ui-tabs-destroy" ) ) { + $( this ).remove(); } else { - return this.each(function() { - if ( args2.callback ) { - args2.callback.call( this ); - } - }); + $( this ).removeAttr( "role tabIndex " + + "aria-live aria-busy aria-selected aria-labelledby aria-hidden aria-expanded" ); } - } + } ); + + this.tabs.each( function() { + var li = $( this ), + prev = li.data( "ui-tabs-aria-controls" ); + if ( prev ) { + li + .attr( "aria-controls", prev ) + .removeData( "ui-tabs-aria-controls" ); + } else { + li.removeAttr( "aria-controls" ); + } + } ); - return effectMethod.call(this, args2); - }, + this.panels.show(); - _show: $.fn.show, - show: function(speed) { - if ( standardSpeed( speed ) ) { - return this._show.apply(this, arguments); - } else { - var args = _normalizeArguments.apply(this, arguments); - args[1].mode = 'show'; - return this.effect.apply(this, args); + if ( this.options.heightStyle !== "content" ) { + this.panels.css( "height", "" ); } }, - _hide: $.fn.hide, - hide: function(speed) { - if ( standardSpeed( speed ) ) { - return this._hide.apply(this, arguments); - } else { - var args = _normalizeArguments.apply(this, arguments); - args[1].mode = 'hide'; - return this.effect.apply(this, args); + enable: function( index ) { + var disabled = this.options.disabled; + if ( disabled === false ) { + return; } - }, - // jQuery core overloads toggle and creates _toggle - __toggle: $.fn.toggle, - toggle: function(speed) { - if ( standardSpeed( speed ) || typeof speed === "boolean" || $.isFunction( speed ) ) { - return this.__toggle.apply(this, arguments); + if ( index === undefined ) { + disabled = false; } else { - var args = _normalizeArguments.apply(this, arguments); - args[1].mode = 'toggle'; - return this.effect.apply(this, args); + index = this._getIndex( index ); + if ( $.isArray( disabled ) ) { + disabled = $.map( disabled, function( num ) { + return num !== index ? num : null; + } ); + } else { + disabled = $.map( this.tabs, function( li, num ) { + return num !== index ? num : null; + } ); + } } + this._setOptionDisabled( disabled ); }, - // helper functions - 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; - } -}); - - - -/******************************************************************************/ -/*********************************** EASING ***********************************/ -/******************************************************************************/ - -/* - * jQuery Easing v1.3 - http://gsgd.co.uk/sandbox/jquery/easing/ - * - * Uses the built in easing capabilities added In jQuery 1.1 - * to offer multiple easing options - * - * TERMS OF USE - jQuery Easing - * - * Open source under the BSD License. - * - * Copyright 2008 George McGinley Smith - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, - * are permitted provided that the following conditions are met: - * - * Redistributions of source code must retain the above copyright notice, this list of - * conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above copyright notice, this list - * of conditions and the following disclaimer in the documentation and/or other materials - * provided with the distribution. - * - * Neither the name of the author nor the names of contributors may be used to endorse - * or promote products derived from this software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY - * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF - * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE - * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, - * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE - * GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED - * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED - * OF THE POSSIBILITY OF SUCH DAMAGE. - * -*/ - -// t: current time, b: begInnIng value, c: change In value, d: duration -$.easing.jswing = $.easing.swing; + disable: function( index ) { + var disabled = this.options.disabled; + if ( disabled === true ) { + return; + } -$.extend($.easing, -{ - def: 'easeOutQuad', - swing: function (x, t, b, c, d) { - //alert($.easing.default); - return $.easing[$.easing.def](x, t, b, c, d); - }, - easeInQuad: function (x, t, b, c, d) { - return c*(t/=d)*t + b; - }, - easeOutQuad: function (x, t, b, c, d) { - return -c *(t/=d)*(t-2) + b; - }, - easeInOutQuad: function (x, t, b, c, d) { - if ((t/=d/2) < 1) return c/2*t*t + b; - return -c/2 * ((--t)*(t-2) - 1) + b; - }, - easeInCubic: function (x, t, b, c, d) { - return c*(t/=d)*t*t + b; - }, - easeOutCubic: function (x, t, b, c, d) { - return c*((t=t/d-1)*t*t + 1) + b; - }, - easeInOutCubic: function (x, t, b, c, d) { - if ((t/=d/2) < 1) return c/2*t*t*t + b; - return c/2*((t-=2)*t*t + 2) + b; - }, - easeInQuart: function (x, t, b, c, d) { - return c*(t/=d)*t*t*t + b; - }, - easeOutQuart: function (x, t, b, c, d) { - return -c * ((t=t/d-1)*t*t*t - 1) + b; - }, - easeInOutQuart: function (x, t, b, c, d) { - if ((t/=d/2) < 1) return c/2*t*t*t*t + b; - return -c/2 * ((t-=2)*t*t*t - 2) + b; - }, - easeInQuint: function (x, t, b, c, d) { - return c*(t/=d)*t*t*t*t + b; - }, - easeOutQuint: function (x, t, b, c, d) { - return c*((t=t/d-1)*t*t*t*t + 1) + b; - }, - easeInOutQuint: function (x, t, b, c, d) { - if ((t/=d/2) < 1) return c/2*t*t*t*t*t + b; - return c/2*((t-=2)*t*t*t*t + 2) + b; - }, - easeInSine: function (x, t, b, c, d) { - return -c * Math.cos(t/d * (Math.PI/2)) + c + b; - }, - easeOutSine: function (x, t, b, c, d) { - return c * Math.sin(t/d * (Math.PI/2)) + b; - }, - easeInOutSine: function (x, t, b, c, d) { - return -c/2 * (Math.cos(Math.PI*t/d) - 1) + b; - }, - easeInExpo: function (x, t, b, c, d) { - return (t==0) ? b : c * Math.pow(2, 10 * (t/d - 1)) + b; - }, - easeOutExpo: function (x, t, b, c, d) { - return (t==d) ? b+c : c * (-Math.pow(2, -10 * t/d) + 1) + b; - }, - easeInOutExpo: function (x, t, b, c, d) { - if (t==0) return b; - if (t==d) return b+c; - if ((t/=d/2) < 1) return c/2 * Math.pow(2, 10 * (t - 1)) + b; - return c/2 * (-Math.pow(2, -10 * --t) + 2) + b; - }, - easeInCirc: function (x, t, b, c, d) { - return -c * (Math.sqrt(1 - (t/=d)*t) - 1) + b; - }, - easeOutCirc: function (x, t, b, c, d) { - return c * Math.sqrt(1 - (t=t/d-1)*t) + b; - }, - easeInOutCirc: function (x, t, b, c, d) { - if ((t/=d/2) < 1) return -c/2 * (Math.sqrt(1 - t*t) - 1) + b; - return c/2 * (Math.sqrt(1 - (t-=2)*t) + 1) + b; - }, - easeInElastic: function (x, t, b, c, d) { - var s=1.70158;var p=0;var a=c; - if (t==0) return b; if ((t/=d)==1) return b+c; if (!p) p=d*.3; - if (a < Math.abs(c)) { a=c; var s=p/4; } - else var s = p/(2*Math.PI) * Math.asin (c/a); - return -(a*Math.pow(2,10*(t-=1)) * Math.sin( (t*d-s)*(2*Math.PI)/p )) + b; - }, - easeOutElastic: function (x, t, b, c, d) { - var s=1.70158;var p=0;var a=c; - if (t==0) return b; if ((t/=d)==1) return b+c; if (!p) p=d*.3; - if (a < Math.abs(c)) { a=c; var s=p/4; } - else var s = p/(2*Math.PI) * Math.asin (c/a); - return a*Math.pow(2,-10*t) * Math.sin( (t*d-s)*(2*Math.PI)/p ) + c + b; - }, - easeInOutElastic: function (x, t, b, c, d) { - var s=1.70158;var p=0;var a=c; - if (t==0) return b; if ((t/=d/2)==2) return b+c; if (!p) p=d*(.3*1.5); - if (a < Math.abs(c)) { a=c; var s=p/4; } - else var s = p/(2*Math.PI) * Math.asin (c/a); - if (t < 1) return -.5*(a*Math.pow(2,10*(t-=1)) * Math.sin( (t*d-s)*(2*Math.PI)/p )) + b; - return a*Math.pow(2,-10*(t-=1)) * Math.sin( (t*d-s)*(2*Math.PI)/p )*.5 + c + b; - }, - easeInBack: function (x, t, b, c, d, s) { - if (s == undefined) s = 1.70158; - return c*(t/=d)*t*((s+1)*t - s) + b; - }, - easeOutBack: function (x, t, b, c, d, s) { - if (s == undefined) s = 1.70158; - return c*((t=t/d-1)*t*((s+1)*t + s) + 1) + b; - }, - easeInOutBack: function (x, t, b, c, d, s) { - if (s == undefined) s = 1.70158; - if ((t/=d/2) < 1) return c/2*(t*t*(((s*=(1.525))+1)*t - s)) + b; - return c/2*((t-=2)*t*(((s*=(1.525))+1)*t + s) + 2) + b; - }, - easeInBounce: function (x, t, b, c, d) { - return c - $.easing.easeOutBounce (x, d-t, 0, c, d) + b; - }, - easeOutBounce: function (x, t, b, c, d) { - if ((t/=d) < (1/2.75)) { - return c*(7.5625*t*t) + b; - } else if (t < (2/2.75)) { - return c*(7.5625*(t-=(1.5/2.75))*t + .75) + b; - } else if (t < (2.5/2.75)) { - return c*(7.5625*(t-=(2.25/2.75))*t + .9375) + b; + if ( index === undefined ) { + disabled = true; } else { - return c*(7.5625*(t-=(2.625/2.75))*t + .984375) + b; + index = this._getIndex( index ); + if ( $.inArray( index, disabled ) !== -1 ) { + return; + } + if ( $.isArray( disabled ) ) { + disabled = $.merge( [ index ], disabled ).sort(); + } else { + disabled = [ index ]; + } } + this._setOptionDisabled( disabled ); }, - easeInOutBounce: function (x, t, b, c, d) { - if (t < d/2) return $.easing.easeInBounce (x, t*2, 0, c, d) * .5 + b; - return $.easing.easeOutBounce (x, t*2-d, 0, c, d) * .5 + c*.5 + b; - } -}); -/* - * - * TERMS OF USE - EASING EQUATIONS - * - * Open source under the BSD License. - * - * Copyright 2001 Robert Penner - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, - * are permitted provided that the following conditions are met: - * - * Redistributions of source code must retain the above copyright notice, this list of - * conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above copyright notice, this list - * of conditions and the following disclaimer in the documentation and/or other materials - * provided with the distribution. - * - * Neither the name of the author nor the names of contributors may be used to endorse - * or promote products derived from this software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY - * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF - * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE - * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, - * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE - * GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED - * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED - * OF THE POSSIBILITY OF SUCH DAMAGE. - * - */ + load: function( index, event ) { + index = this._getIndex( index ); + var that = this, + tab = this.tabs.eq( index ), + anchor = tab.find( ".ui-tabs-anchor" ), + panel = this._getPanelForTab( tab ), + eventData = { + tab: tab, + panel: panel + }, + complete = function( jqXHR, status ) { + if ( status === "abort" ) { + that.panels.stop( false, true ); + } -})(jQuery); -/* - * jQuery UI Effects Blind 1.8.17 - * - * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about) - * Dual licensed under the MIT or GPL Version 2 licenses. - * http://jquery.org/license - * - * http://docs.jquery.com/UI/Effects/Blind - * - * Depends: - * jquery.effects.core.js - */ -(function( $, undefined ) { + that._removeClass( tab, "ui-tabs-loading" ); + panel.removeAttr( "aria-busy" ); -$.effects.blind = function(o) { + if ( jqXHR === that.xhr ) { + delete that.xhr; + } + }; - return this.queue(function() { + // Not remote + if ( this._isLocal( anchor[ 0 ] ) ) { + return; + } - // Create element - var el = $(this), props = ['position','top','bottom','left','right']; + this.xhr = $.ajax( this._ajaxSettings( anchor, event, eventData ) ); - // Set options - var mode = $.effects.setMode(el, o.options.mode || 'hide'); // Set Mode - var direction = o.options.direction || 'vertical'; // Default direction - - // Adjust - $.effects.save(el, props); el.show(); // Save & Show - var wrapper = $.effects.createWrapper(el).css({overflow:'hidden'}); // Create Wrapper - var ref = (direction == 'vertical') ? 'height' : 'width'; - var distance = (direction == 'vertical') ? wrapper.height() : wrapper.width(); - if(mode == 'show') wrapper.css(ref, 0); // Shift - - // Animation - var animation = {}; - animation[ref] = mode == 'show' ? distance : 0; - - // Animate - wrapper.animate(animation, o.duration, o.options.easing, function() { - if(mode == 'hide') el.hide(); // Hide - $.effects.restore(el, props); $.effects.removeWrapper(el); // Restore - if(o.callback) o.callback.apply(el[0], arguments); // Callback - el.dequeue(); - }); - - }); + // Support: jQuery <1.8 + // jQuery <1.8 returns false if the request is canceled in beforeSend, + // but as of 1.8, $.ajax() always returns a jqXHR object. + if ( this.xhr && this.xhr.statusText !== "canceled" ) { + this._addClass( tab, "ui-tabs-loading" ); + panel.attr( "aria-busy", "true" ); -}; + this.xhr + .done( function( response, status, jqXHR ) { -})(jQuery); -/* - * jQuery UI Effects Bounce 1.8.17 - * - * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about) - * Dual licensed under the MIT or GPL Version 2 licenses. - * http://jquery.org/license - * - * http://docs.jquery.com/UI/Effects/Bounce - * - * Depends: - * jquery.effects.core.js - */ -(function( $, undefined ) { + // support: jQuery <1.8 + // http://bugs.jquery.com/ticket/11778 + setTimeout( function() { + panel.html( response ); + that._trigger( "load", event, eventData ); -$.effects.bounce = function(o) { + complete( jqXHR, status ); + }, 1 ); + } ) + .fail( function( jqXHR, status ) { - return this.queue(function() { + // support: jQuery <1.8 + // http://bugs.jquery.com/ticket/11778 + setTimeout( function() { + complete( jqXHR, status ); + }, 1 ); + } ); + } + }, - // Create element - var el = $(this), props = ['position','top','bottom','left','right']; + _ajaxSettings: function( anchor, event, eventData ) { + var that = this; + return { - // Set options - var mode = $.effects.setMode(el, o.options.mode || 'effect'); // Set Mode - var direction = o.options.direction || 'up'; // Default direction - var distance = o.options.distance || 20; // Default distance - var times = o.options.times || 5; // Default # of times - var speed = o.duration || 250; // Default speed per bounce - if (/show|hide/.test(mode)) props.push('opacity'); // Avoid touching opacity to prevent clearType and PNG issues in IE - - // Adjust - $.effects.save(el, props); el.show(); // Save & Show - $.effects.createWrapper(el); // Create Wrapper - var ref = (direction == 'up' || direction == 'down') ? 'top' : 'left'; - var motion = (direction == 'up' || direction == 'left') ? 'pos' : 'neg'; - var distance = o.options.distance || (ref == 'top' ? el.outerHeight({margin:true}) / 3 : el.outerWidth({margin:true}) / 3); - if (mode == 'show') el.css('opacity', 0).css(ref, motion == 'pos' ? -distance : distance); // Shift - if (mode == 'hide') distance = distance / (times * 2); - if (mode != 'hide') times--; - - // Animate - if (mode == 'show') { // Show Bounce - var animation = {opacity: 1}; - animation[ref] = (motion == 'pos' ? '+=' : '-=') + distance; - el.animate(animation, speed / 2, o.options.easing); - distance = distance / 2; - times--; - }; - for (var i = 0; i < times; i++) { // Bounces - var animation1 = {}, animation2 = {}; - animation1[ref] = (motion == 'pos' ? '-=' : '+=') + distance; - animation2[ref] = (motion == 'pos' ? '+=' : '-=') + distance; - el.animate(animation1, speed / 2, o.options.easing).animate(animation2, speed / 2, o.options.easing); - distance = (mode == 'hide') ? distance * 2 : distance / 2; - }; - if (mode == 'hide') { // Last Bounce - var animation = {opacity: 0}; - animation[ref] = (motion == 'pos' ? '-=' : '+=') + distance; - el.animate(animation, speed / 2, o.options.easing, function(){ - el.hide(); // Hide - $.effects.restore(el, props); $.effects.removeWrapper(el); // Restore - if(o.callback) o.callback.apply(this, arguments); // Callback - }); - } else { - var animation1 = {}, animation2 = {}; - animation1[ref] = (motion == 'pos' ? '-=' : '+=') + distance; - animation2[ref] = (motion == 'pos' ? '+=' : '-=') + distance; - el.animate(animation1, speed / 2, o.options.easing).animate(animation2, speed / 2, o.options.easing, function(){ - $.effects.restore(el, props); $.effects.removeWrapper(el); // Restore - if(o.callback) o.callback.apply(this, arguments); // Callback - }); + // Support: IE <11 only + // Strip any hash that exists to prevent errors with the Ajax request + url: anchor.attr( "href" ).replace( /#.*$/, "" ), + beforeSend: function( jqXHR, settings ) { + return that._trigger( "beforeLoad", event, + $.extend( { jqXHR: jqXHR, ajaxSettings: settings }, eventData ) ); + } }; - el.queue('fx', function() { el.dequeue(); }); - el.dequeue(); - }); + }, -}; + _getPanelForTab: function( tab ) { + var id = $( tab ).attr( "aria-controls" ); + return this.element.find( this._sanitizeSelector( "#" + id ) ); + } +} ); -})(jQuery); -/* - * jQuery UI Effects Clip 1.8.17 +// DEPRECATED +// TODO: Switch return back to widget declaration at top of file when this is removed +if ( $.uiBackCompat !== false ) { + + // Backcompat for ui-tab class (now ui-tabs-tab) + $.widget( "ui.tabs", $.ui.tabs, { + _processTabs: function() { + this._superApply( arguments ); + this._addClass( this.tabs, "ui-tab" ); + } + } ); +} + +var widgetsTabs = $.ui.tabs; + + +/*! + * jQuery UI Tooltip 1.12.1 + * http://jqueryui.com * - * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about) - * Dual licensed under the MIT or GPL Version 2 licenses. + * Copyright jQuery Foundation and other contributors + * Released under the MIT license. * http://jquery.org/license - * - * http://docs.jquery.com/UI/Effects/Clip - * - * Depends: - * jquery.effects.core.js */ -(function( $, undefined ) { -$.effects.clip = function(o) { +//>>label: Tooltip +//>>group: Widgets +//>>description: Shows additional information for any element on hover or focus. +//>>docs: http://api.jqueryui.com/tooltip/ +//>>demos: http://jqueryui.com/tooltip/ +//>>css.structure: ../../themes/base/core.css +//>>css.structure: ../../themes/base/tooltip.css +//>>css.theme: ../../themes/base/theme.css - return this.queue(function() { - // Create element - var el = $(this), props = ['position','top','bottom','left','right','height','width']; - // Set options - var mode = $.effects.setMode(el, o.options.mode || 'hide'); // Set Mode - var direction = o.options.direction || 'vertical'; // Default direction - - // Adjust - $.effects.save(el, props); el.show(); // Save & Show - var wrapper = $.effects.createWrapper(el).css({overflow:'hidden'}); // Create Wrapper - var animate = el[0].tagName == 'IMG' ? wrapper : el; - var ref = { - size: (direction == 'vertical') ? 'height' : 'width', - position: (direction == 'vertical') ? 'top' : 'left' - }; - var distance = (direction == 'vertical') ? animate.height() : animate.width(); - if(mode == 'show') { animate.css(ref.size, 0); animate.css(ref.position, distance / 2); } // Shift +$.widget( "ui.tooltip", { + version: "1.12.1", + options: { + classes: { + "ui-tooltip": "ui-corner-all ui-widget-shadow" + }, + content: function() { - // Animation - var animation = {}; - animation[ref.size] = mode == 'show' ? distance : 0; - animation[ref.position] = mode == 'show' ? 0 : distance / 2; + // support: IE<9, Opera in jQuery <1.7 + // .text() can't accept undefined, so coerce to a string + var title = $( this ).attr( "title" ) || ""; - // Animate - animate.animate(animation, { queue: false, duration: o.duration, easing: o.options.easing, complete: function() { - if(mode == 'hide') el.hide(); // Hide - $.effects.restore(el, props); $.effects.removeWrapper(el); // Restore - if(o.callback) o.callback.apply(el[0], arguments); // Callback - el.dequeue(); - }}); + // Escape title, since we're going from an attribute to raw HTML + return $( "" ).text( title ).html(); + }, + hide: true, - }); + // Disabled elements have inconsistent behavior across browsers (#8661) + items: "[title]:not([disabled])", + position: { + my: "left top+15", + at: "left bottom", + collision: "flipfit flip" + }, + show: true, + track: false, -}; + // Callbacks + close: null, + open: null + }, -})(jQuery); -/* - * jQuery UI Effects Drop 1.8.17 - * - * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about) - * Dual licensed under the MIT or GPL Version 2 licenses. - * http://jquery.org/license - * - * http://docs.jquery.com/UI/Effects/Drop - * - * Depends: - * jquery.effects.core.js - */ -(function( $, undefined ) { + _addDescribedBy: function( elem, id ) { + var describedby = ( elem.attr( "aria-describedby" ) || "" ).split( /\s+/ ); + describedby.push( id ); + elem + .data( "ui-tooltip-id", id ) + .attr( "aria-describedby", $.trim( describedby.join( " " ) ) ); + }, -$.effects.drop = function(o) { + _removeDescribedBy: function( elem ) { + var id = elem.data( "ui-tooltip-id" ), + describedby = ( elem.attr( "aria-describedby" ) || "" ).split( /\s+/ ), + index = $.inArray( id, describedby ); - return this.queue(function() { + if ( index !== -1 ) { + describedby.splice( index, 1 ); + } - // Create element - var el = $(this), props = ['position','top','bottom','left','right','opacity']; + elem.removeData( "ui-tooltip-id" ); + describedby = $.trim( describedby.join( " " ) ); + if ( describedby ) { + elem.attr( "aria-describedby", describedby ); + } else { + elem.removeAttr( "aria-describedby" ); + } + }, - // Set options - var mode = $.effects.setMode(el, o.options.mode || 'hide'); // Set Mode - var direction = o.options.direction || 'left'; // Default Direction - - // Adjust - $.effects.save(el, props); el.show(); // Save & Show - $.effects.createWrapper(el); // Create Wrapper - var ref = (direction == 'up' || direction == 'down') ? 'top' : 'left'; - var motion = (direction == 'up' || direction == 'left') ? 'pos' : 'neg'; - var distance = o.options.distance || (ref == 'top' ? el.outerHeight({margin:true}) / 2 : el.outerWidth({margin:true}) / 2); - if (mode == 'show') el.css('opacity', 0).css(ref, motion == 'pos' ? -distance : distance); // Shift - - // Animation - var animation = {opacity: mode == 'show' ? 1 : 0}; - animation[ref] = (mode == 'show' ? (motion == 'pos' ? '+=' : '-=') : (motion == 'pos' ? '-=' : '+=')) + distance; - - // Animate - el.animate(animation, { queue: false, duration: o.duration, easing: o.options.easing, complete: function() { - if(mode == 'hide') el.hide(); // Hide - $.effects.restore(el, props); $.effects.removeWrapper(el); // Restore - if(o.callback) o.callback.apply(this, arguments); // Callback - el.dequeue(); - }}); - - }); + _create: function() { + this._on( { + mouseover: "open", + focusin: "open" + } ); -}; + // IDs of generated tooltips, needed for destroy + this.tooltips = {}; -})(jQuery); -/* - * jQuery UI Effects Explode 1.8.17 - * - * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about) - * Dual licensed under the MIT or GPL Version 2 licenses. - * http://jquery.org/license - * - * http://docs.jquery.com/UI/Effects/Explode - * - * Depends: - * jquery.effects.core.js - */ -(function( $, undefined ) { + // IDs of parent tooltips where we removed the title attribute + this.parents = {}; -$.effects.explode = function(o) { + // Append the aria-live region so tooltips announce correctly + this.liveRegion = $( "
" ) + .attr( { + role: "log", + "aria-live": "assertive", + "aria-relevant": "additions" + } ) + .appendTo( this.document[ 0 ].body ); + this._addClass( this.liveRegion, null, "ui-helper-hidden-accessible" ); - return this.queue(function() { + this.disabledTitles = $( [] ); + }, - var rows = o.options.pieces ? Math.round(Math.sqrt(o.options.pieces)) : 3; - var cells = o.options.pieces ? Math.round(Math.sqrt(o.options.pieces)) : 3; + _setOption: function( key, value ) { + var that = this; - o.options.mode = o.options.mode == 'toggle' ? ($(this).is(':visible') ? 'hide' : 'show') : o.options.mode; - var el = $(this).show().css('visibility', 'hidden'); - var offset = el.offset(); + this._super( key, value ); - //Substract the margins - not fixing the problem yet. - offset.top -= parseInt(el.css("marginTop"),10) || 0; - offset.left -= parseInt(el.css("marginLeft"),10) || 0; + if ( key === "content" ) { + $.each( this.tooltips, function( id, tooltipData ) { + that._updateContent( tooltipData.element ); + } ); + } + }, - var width = el.outerWidth(true); - var height = el.outerHeight(true); + _setOptionDisabled: function( value ) { + this[ value ? "_disable" : "_enable" ](); + }, - for(var i=0;i
') - .css({ - position: 'absolute', - visibility: 'visible', - left: -j*(width/cells), - top: -i*(height/rows) - }) - .parent() - .addClass('ui-effects-explode') - .css({ - position: 'absolute', - overflow: 'hidden', - width: width/cells, - height: height/rows, - left: offset.left + j*(width/cells) + (o.options.mode == 'show' ? (j-Math.floor(cells/2))*(width/cells) : 0), - top: offset.top + i*(height/rows) + (o.options.mode == 'show' ? (i-Math.floor(rows/2))*(height/rows) : 0), - opacity: o.options.mode == 'show' ? 0 : 1 - }).animate({ - left: offset.left + j*(width/cells) + (o.options.mode == 'show' ? 0 : (j-Math.floor(cells/2))*(width/cells)), - top: offset.top + i*(height/rows) + (o.options.mode == 'show' ? 0 : (i-Math.floor(rows/2))*(height/rows)), - opacity: o.options.mode == 'show' ? 1 : 0 - }, o.duration || 500); - } - } + _disable: function() { + var that = this; - // Set a timeout, to call the callback approx. when the other animations have finished - setTimeout(function() { + // Close open tooltips + $.each( this.tooltips, function( id, tooltipData ) { + var event = $.Event( "blur" ); + event.target = event.currentTarget = tooltipData.element[ 0 ]; + that.close( event, true ); + } ); + + // Remove title attributes to prevent native tooltips + this.disabledTitles = this.disabledTitles.add( + this.element.find( this.options.items ).addBack() + .filter( function() { + var element = $( this ); + if ( element.is( "[title]" ) ) { + return element + .data( "ui-tooltip-title", element.attr( "title" ) ) + .removeAttr( "title" ); + } + } ) + ); + }, - o.options.mode == 'show' ? el.css({ visibility: 'visible' }) : el.css({ visibility: 'visible' }).hide(); - if(o.callback) o.callback.apply(el[0]); // Callback - el.dequeue(); + _enable: function() { - $('div.ui-effects-explode').remove(); + // restore title attributes + this.disabledTitles.each( function() { + var element = $( this ); + if ( element.data( "ui-tooltip-title" ) ) { + element.attr( "title", element.data( "ui-tooltip-title" ) ); + } + } ); + this.disabledTitles = $( [] ); + }, - }, o.duration || 500); + open: function( event ) { + var that = this, + target = $( event ? event.target : this.element ) + // we need closest here due to mouseover bubbling, + // but always pointing at the same event target + .closest( this.options.items ); - }); + // No element to show a tooltip for or the tooltip is already open + if ( !target.length || target.data( "ui-tooltip-id" ) ) { + return; + } -}; + if ( target.attr( "title" ) ) { + target.data( "ui-tooltip-title", target.attr( "title" ) ); + } -})(jQuery); -/* - * jQuery UI Effects Fade 1.8.17 - * - * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about) - * Dual licensed under the MIT or GPL Version 2 licenses. - * http://jquery.org/license - * - * http://docs.jquery.com/UI/Effects/Fade - * - * Depends: - * jquery.effects.core.js - */ -(function( $, undefined ) { + target.data( "ui-tooltip-open", true ); -$.effects.fade = function(o) { - return this.queue(function() { - var elem = $(this), - mode = $.effects.setMode(elem, o.options.mode || 'hide'); + // Kill parent tooltips, custom or native, for hover + if ( event && event.type === "mouseover" ) { + target.parents().each( function() { + var parent = $( this ), + blurEvent; + if ( parent.data( "ui-tooltip-open" ) ) { + blurEvent = $.Event( "blur" ); + blurEvent.target = blurEvent.currentTarget = this; + that.close( blurEvent, true ); + } + if ( parent.attr( "title" ) ) { + parent.uniqueId(); + that.parents[ this.id ] = { + element: this, + title: parent.attr( "title" ) + }; + parent.attr( "title", "" ); + } + } ); + } - elem.animate({ opacity: mode }, { - queue: false, - duration: o.duration, - easing: o.options.easing, - complete: function() { - (o.callback && o.callback.apply(this, arguments)); - elem.dequeue(); - } - }); - }); -}; + this._registerCloseHandlers( event, target ); + this._updateContent( target, event ); + }, -})(jQuery); -/* - * jQuery UI Effects Fold 1.8.17 - * - * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about) - * Dual licensed under the MIT or GPL Version 2 licenses. - * http://jquery.org/license - * - * http://docs.jquery.com/UI/Effects/Fold - * - * Depends: - * jquery.effects.core.js - */ -(function( $, undefined ) { + _updateContent: function( target, event ) { + var content, + contentOption = this.options.content, + that = this, + eventType = event ? event.type : null; -$.effects.fold = function(o) { + if ( typeof contentOption === "string" || contentOption.nodeType || + contentOption.jquery ) { + return this._open( event, target, contentOption ); + } - return this.queue(function() { + content = contentOption.call( target[ 0 ], function( response ) { - // Create element - var el = $(this), props = ['position','top','bottom','left','right']; + // IE may instantly serve a cached response for ajax requests + // delay this call to _open so the other call to _open runs first + that._delay( function() { - // Set options - var mode = $.effects.setMode(el, o.options.mode || 'hide'); // Set Mode - var size = o.options.size || 15; // Default fold size - var horizFirst = !(!o.options.horizFirst); // Ensure a boolean value - var duration = o.duration ? o.duration / 2 : $.fx.speeds._default / 2; - - // Adjust - $.effects.save(el, props); el.show(); // Save & Show - var wrapper = $.effects.createWrapper(el).css({overflow:'hidden'}); // Create Wrapper - var widthFirst = ((mode == 'show') != horizFirst); - var ref = widthFirst ? ['width', 'height'] : ['height', 'width']; - var distance = widthFirst ? [wrapper.width(), wrapper.height()] : [wrapper.height(), wrapper.width()]; - var percent = /([0-9]+)%/.exec(size); - if(percent) size = parseInt(percent[1],10) / 100 * distance[mode == 'hide' ? 0 : 1]; - if(mode == 'show') wrapper.css(horizFirst ? {height: 0, width: size} : {height: size, width: 0}); // Shift - - // Animation - var animation1 = {}, animation2 = {}; - animation1[ref[0]] = mode == 'show' ? distance[0] : size; - animation2[ref[1]] = mode == 'show' ? distance[1] : 0; - - // Animate - wrapper.animate(animation1, duration, o.options.easing) - .animate(animation2, duration, o.options.easing, function() { - if(mode == 'hide') el.hide(); // Hide - $.effects.restore(el, props); $.effects.removeWrapper(el); // Restore - if(o.callback) o.callback.apply(el[0], arguments); // Callback - el.dequeue(); - }); - - }); + // Ignore async response if tooltip was closed already + if ( !target.data( "ui-tooltip-open" ) ) { + return; + } -}; + // JQuery creates a special event for focusin when it doesn't + // exist natively. To improve performance, the native event + // object is reused and the type is changed. Therefore, we can't + // rely on the type being correct after the event finished + // bubbling, so we set it back to the previous value. (#8740) + if ( event ) { + event.type = eventType; + } + this._open( event, target, response ); + } ); + } ); + if ( content ) { + this._open( event, target, content ); + } + }, -})(jQuery); -/* - * jQuery UI Effects Highlight 1.8.17 - * - * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about) - * Dual licensed under the MIT or GPL Version 2 licenses. - * http://jquery.org/license - * - * http://docs.jquery.com/UI/Effects/Highlight - * - * Depends: - * jquery.effects.core.js - */ -(function( $, undefined ) { + _open: function( event, target, content ) { + var tooltipData, tooltip, delayedShow, a11yContent, + positionOption = $.extend( {}, this.options.position ); -$.effects.highlight = function(o) { - return this.queue(function() { - var elem = $(this), - props = ['backgroundImage', 'backgroundColor', 'opacity'], - mode = $.effects.setMode(elem, o.options.mode || 'show'), - animation = { - backgroundColor: elem.css('backgroundColor') - }; + if ( !content ) { + return; + } - if (mode == 'hide') { - animation.opacity = 0; + // Content can be updated multiple times. If the tooltip already + // exists, then just update the content and bail. + tooltipData = this._find( target ); + if ( tooltipData ) { + tooltipData.tooltip.find( ".ui-tooltip-content" ).html( content ); + return; } - $.effects.save(elem, props); - elem - .show() - .css({ - backgroundImage: 'none', - backgroundColor: o.options.color || '#ffff99' - }) - .animate(animation, { - queue: false, - duration: o.duration, - easing: o.options.easing, - complete: function() { - (mode == 'hide' && elem.hide()); - $.effects.restore(elem, props); - (mode == 'show' && !$.support.opacity && this.style.removeAttribute('filter')); - (o.callback && o.callback.apply(this, arguments)); - elem.dequeue(); - } - }); - }); -}; + // If we have a title, clear it to prevent the native tooltip + // we have to check first to avoid defining a title if none exists + // (we don't want to cause an element to start matching [title]) + // + // We use removeAttr only for key events, to allow IE to export the correct + // accessible attributes. For mouse events, set to empty string to avoid + // native tooltip showing up (happens only when removing inside mouseover). + if ( target.is( "[title]" ) ) { + if ( event && event.type === "mouseover" ) { + target.attr( "title", "" ); + } else { + target.removeAttr( "title" ); + } + } -})(jQuery); -/* - * jQuery UI Effects Pulsate 1.8.17 - * - * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about) - * Dual licensed under the MIT or GPL Version 2 licenses. - * http://jquery.org/license - * - * http://docs.jquery.com/UI/Effects/Pulsate - * - * Depends: - * jquery.effects.core.js - */ -(function( $, undefined ) { + tooltipData = this._tooltip( target ); + tooltip = tooltipData.tooltip; + this._addDescribedBy( target, tooltip.attr( "id" ) ); + tooltip.find( ".ui-tooltip-content" ).html( content ); -$.effects.pulsate = function(o) { - return this.queue(function() { - var elem = $(this), - mode = $.effects.setMode(elem, o.options.mode || 'show'); - times = ((o.options.times || 5) * 2) - 1; - duration = o.duration ? o.duration / 2 : $.fx.speeds._default / 2, - isVisible = elem.is(':visible'), - animateTo = 0; + // Support: Voiceover on OS X, JAWS on IE <= 9 + // JAWS announces deletions even when aria-relevant="additions" + // Voiceover will sometimes re-read the entire log region's contents from the beginning + this.liveRegion.children().hide(); + a11yContent = $( "
" ).html( tooltip.find( ".ui-tooltip-content" ).html() ); + a11yContent.removeAttr( "name" ).find( "[name]" ).removeAttr( "name" ); + a11yContent.removeAttr( "id" ).find( "[id]" ).removeAttr( "id" ); + a11yContent.appendTo( this.liveRegion ); - if (!isVisible) { - elem.css('opacity', 0).show(); - animateTo = 1; + function position( event ) { + positionOption.of = event; + if ( tooltip.is( ":hidden" ) ) { + return; + } + tooltip.position( positionOption ); } + if ( this.options.track && event && /^mouse/.test( event.type ) ) { + this._on( this.document, { + mousemove: position + } ); - if ((mode == 'hide' && isVisible) || (mode == 'show' && !isVisible)) { - times--; + // trigger once to override element-relative positioning + position( event ); + } else { + tooltip.position( $.extend( { + of: target + }, this.options.position ) ); } - for (var i = 0; i < times; i++) { - elem.animate({ opacity: animateTo }, duration, o.options.easing); - animateTo = (animateTo + 1) % 2; + tooltip.hide(); + + this._show( tooltip, this.options.show ); + + // Handle tracking tooltips that are shown with a delay (#8644). As soon + // as the tooltip is visible, position the tooltip using the most recent + // event. + // Adds the check to add the timers only when both delay and track options are set (#14682) + if ( this.options.track && this.options.show && this.options.show.delay ) { + delayedShow = this.delayedShow = setInterval( function() { + if ( tooltip.is( ":visible" ) ) { + position( positionOption.of ); + clearInterval( delayedShow ); + } + }, $.fx.interval ); } - elem.animate({ opacity: animateTo }, duration, o.options.easing, function() { - if (animateTo == 0) { - elem.hide(); + this._trigger( "open", event, { tooltip: tooltip } ); + }, + + _registerCloseHandlers: function( event, target ) { + var events = { + keyup: function( event ) { + if ( event.keyCode === $.ui.keyCode.ESCAPE ) { + var fakeEvent = $.Event( event ); + fakeEvent.currentTarget = target[ 0 ]; + this.close( fakeEvent, true ); + } } - (o.callback && o.callback.apply(this, arguments)); - }); + }; - elem - .queue('fx', function() { elem.dequeue(); }) - .dequeue(); - }); -}; + // Only bind remove handler for delegated targets. Non-delegated + // tooltips will handle this in destroy. + if ( target[ 0 ] !== this.element[ 0 ] ) { + events.remove = function() { + this._removeTooltip( this._find( target ).tooltip ); + }; + } -})(jQuery); -/* - * jQuery UI Effects Scale 1.8.17 - * - * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about) - * Dual licensed under the MIT or GPL Version 2 licenses. - * http://jquery.org/license - * - * http://docs.jquery.com/UI/Effects/Scale - * - * Depends: - * jquery.effects.core.js - */ -(function( $, undefined ) { - -$.effects.puff = function(o) { - return this.queue(function() { - var elem = $(this), - mode = $.effects.setMode(elem, o.options.mode || 'hide'), - percent = parseInt(o.options.percent, 10) || 150, - factor = percent / 100, - original = { height: elem.height(), width: elem.width() }; - - $.extend(o.options, { - fade: true, - mode: mode, - percent: mode == 'hide' ? percent : 100, - from: mode == 'hide' - ? original - : { - height: original.height * factor, - width: original.width * factor - } - }); - - elem.effect('scale', o.options, o.duration, o.callback); - elem.dequeue(); - }); -}; + if ( !event || event.type === "mouseover" ) { + events.mouseleave = "close"; + } + if ( !event || event.type === "focusin" ) { + events.focusout = "close"; + } + this._on( true, target, events ); + }, -$.effects.scale = function(o) { + close: function( event ) { + var tooltip, + that = this, + target = $( event ? event.currentTarget : this.element ), + tooltipData = this._find( target ); + + // The tooltip may already be closed + if ( !tooltipData ) { + + // We set ui-tooltip-open immediately upon open (in open()), but only set the + // additional data once there's actually content to show (in _open()). So even if the + // tooltip doesn't have full data, we always remove ui-tooltip-open in case we're in + // the period between open() and _open(). + target.removeData( "ui-tooltip-open" ); + return; + } - return this.queue(function() { + tooltip = tooltipData.tooltip; - // Create element - var el = $(this); + // Disabling closes the tooltip, so we need to track when we're closing + // to avoid an infinite loop in case the tooltip becomes disabled on close + if ( tooltipData.closing ) { + return; + } - // Set options - var options = $.extend(true, {}, o.options); - var mode = $.effects.setMode(el, o.options.mode || 'effect'); // Set Mode - var percent = parseInt(o.options.percent,10) || (parseInt(o.options.percent,10) == 0 ? 0 : (mode == 'hide' ? 0 : 100)); // Set default scaling percent - var direction = o.options.direction || 'both'; // Set default axis - var origin = o.options.origin; // The origin of the scaling - if (mode != 'effect') { // Set default origin and restore for show/hide - options.origin = origin || ['middle','center']; - options.restore = true; - } - var original = {height: el.height(), width: el.width()}; // Save original - el.from = o.options.from || (mode == 'show' ? {height: 0, width: 0} : original); // Default from state - - // Adjust - var factor = { // Set scaling factor - y: direction != 'horizontal' ? (percent / 100) : 1, - x: direction != 'vertical' ? (percent / 100) : 1 - }; - el.to = {height: original.height * factor.y, width: original.width * factor.x}; // Set to state + // Clear the interval for delayed tracking tooltips + clearInterval( this.delayedShow ); - if (o.options.fade) { // Fade option to support puff - if (mode == 'show') {el.from.opacity = 0; el.to.opacity = 1;}; - if (mode == 'hide') {el.from.opacity = 1; el.to.opacity = 0;}; - }; + // Only set title if we had one before (see comment in _open()) + // If the title attribute has changed since open(), don't restore + if ( target.data( "ui-tooltip-title" ) && !target.attr( "title" ) ) { + target.attr( "title", target.data( "ui-tooltip-title" ) ); + } - // Animation - options.from = el.from; options.to = el.to; options.mode = mode; + this._removeDescribedBy( target ); - // Animate - el.effect('size', options, o.duration, o.callback); - el.dequeue(); - }); + tooltipData.hiding = true; + tooltip.stop( true ); + this._hide( tooltip, this.options.hide, function() { + that._removeTooltip( $( this ) ); + } ); -}; + target.removeData( "ui-tooltip-open" ); + this._off( target, "mouseleave focusout keyup" ); -$.effects.size = function(o) { + // Remove 'remove' binding only on delegated targets + if ( target[ 0 ] !== this.element[ 0 ] ) { + this._off( target, "remove" ); + } + this._off( this.document, "mousemove" ); - return this.queue(function() { + if ( event && event.type === "mouseleave" ) { + $.each( this.parents, function( id, parent ) { + $( parent.element ).attr( "title", parent.title ); + delete that.parents[ id ]; + } ); + } - // Create element - var el = $(this), props = ['position','top','bottom','left','right','width','height','overflow','opacity']; - var props1 = ['position','top','bottom','left','right','overflow','opacity']; // Always restore - var props2 = ['width','height','overflow']; // Copy for children - var cProps = ['fontSize']; - var vProps = ['borderTopWidth', 'borderBottomWidth', 'paddingTop', 'paddingBottom']; - var hProps = ['borderLeftWidth', 'borderRightWidth', 'paddingLeft', 'paddingRight']; + tooltipData.closing = true; + this._trigger( "close", event, { tooltip: tooltip } ); + if ( !tooltipData.hiding ) { + tooltipData.closing = false; + } + }, - // Set options - var mode = $.effects.setMode(el, o.options.mode || 'effect'); // Set Mode - var restore = o.options.restore || false; // Default restore - var scale = o.options.scale || 'both'; // Default scale mode - var origin = o.options.origin; // The origin of the sizing - var original = {height: el.height(), width: el.width()}; // Save original - el.from = o.options.from || original; // Default from state - el.to = o.options.to || original; // Default to state - // Adjust - if (origin) { // Calculate baseline shifts - var baseline = $.effects.getBaseline(origin, original); - el.from.top = (original.height - el.from.height) * baseline.y; - el.from.left = (original.width - el.from.width) * baseline.x; - el.to.top = (original.height - el.to.height) * baseline.y; - el.to.left = (original.width - el.to.width) * baseline.x; - }; - var factor = { // Set scaling factor - from: {y: el.from.height / original.height, x: el.from.width / original.width}, - to: {y: el.to.height / original.height, x: el.to.width / original.width} - }; - if (scale == 'box' || scale == 'both') { // Scale the css box - if (factor.from.y != factor.to.y) { // Vertical props scaling - props = props.concat(vProps); - el.from = $.effects.setTransition(el, vProps, factor.from.y, el.from); - el.to = $.effects.setTransition(el, vProps, factor.to.y, el.to); - }; - if (factor.from.x != factor.to.x) { // Horizontal props scaling - props = props.concat(hProps); - el.from = $.effects.setTransition(el, hProps, factor.from.x, el.from); - el.to = $.effects.setTransition(el, hProps, factor.to.x, el.to); - }; - }; - if (scale == 'content' || scale == 'both') { // Scale the content - if (factor.from.y != factor.to.y) { // Vertical props scaling - props = props.concat(cProps); - el.from = $.effects.setTransition(el, cProps, factor.from.y, el.from); - el.to = $.effects.setTransition(el, cProps, factor.to.y, el.to); - }; - }; - $.effects.save(el, restore ? props : props1); el.show(); // Save & Show - $.effects.createWrapper(el); // Create Wrapper - el.css('overflow','hidden').css(el.from); // Shift - - // Animate - if (scale == 'content' || scale == 'both') { // Scale the children - vProps = vProps.concat(['marginTop','marginBottom']).concat(cProps); // Add margins/font-size - hProps = hProps.concat(['marginLeft','marginRight']); // Add margins - props2 = props.concat(vProps).concat(hProps); // Concat - el.find("*[width]").each(function(){ - child = $(this); - if (restore) $.effects.save(child, props2); - var c_original = {height: child.height(), width: child.width()}; // Save original - child.from = {height: c_original.height * factor.from.y, width: c_original.width * factor.from.x}; - child.to = {height: c_original.height * factor.to.y, width: c_original.width * factor.to.x}; - if (factor.from.y != factor.to.y) { // Vertical props scaling - child.from = $.effects.setTransition(child, vProps, factor.from.y, child.from); - child.to = $.effects.setTransition(child, vProps, factor.to.y, child.to); - }; - if (factor.from.x != factor.to.x) { // Horizontal props scaling - child.from = $.effects.setTransition(child, hProps, factor.from.x, child.from); - child.to = $.effects.setTransition(child, hProps, factor.to.x, child.to); - }; - child.css(child.from); // Shift children - child.animate(child.to, o.duration, o.options.easing, function(){ - if (restore) $.effects.restore(child, props2); // Restore children - }); // Animate children - }); + _tooltip: function( element ) { + var tooltip = $( "
" ).attr( "role", "tooltip" ), + content = $( "
" ).appendTo( tooltip ), + id = tooltip.uniqueId().attr( "id" ); + + this._addClass( content, "ui-tooltip-content" ); + this._addClass( tooltip, "ui-tooltip", "ui-widget ui-widget-content" ); + + tooltip.appendTo( this._appendTo( element ) ); + + return this.tooltips[ id ] = { + element: element, + tooltip: tooltip }; + }, - // Animate - el.animate(el.to, { queue: false, duration: o.duration, easing: o.options.easing, complete: function() { - if (el.to.opacity === 0) { - el.css('opacity', el.from.opacity); - } - if(mode == 'hide') el.hide(); // Hide - $.effects.restore(el, restore ? props : props1); $.effects.removeWrapper(el); // Restore - if(o.callback) o.callback.apply(this, arguments); // Callback - el.dequeue(); - }}); + _find: function( target ) { + var id = target.data( "ui-tooltip-id" ); + return id ? this.tooltips[ id ] : null; + }, - }); + _removeTooltip: function( tooltip ) { + tooltip.remove(); + delete this.tooltips[ tooltip.attr( "id" ) ]; + }, -}; + _appendTo: function( target ) { + var element = target.closest( ".ui-front, dialog" ); -})(jQuery); -/* - * jQuery UI Effects Shake 1.8.17 - * - * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about) - * Dual licensed under the MIT or GPL Version 2 licenses. - * http://jquery.org/license - * - * http://docs.jquery.com/UI/Effects/Shake - * - * Depends: - * jquery.effects.core.js - */ -(function( $, undefined ) { + if ( !element.length ) { + element = this.document[ 0 ].body; + } -$.effects.shake = function(o) { + return element; + }, - return this.queue(function() { + _destroy: function() { + var that = this; - // Create element - var el = $(this), props = ['position','top','bottom','left','right']; + // Close open tooltips + $.each( this.tooltips, function( id, tooltipData ) { - // Set options - var mode = $.effects.setMode(el, o.options.mode || 'effect'); // Set Mode - var direction = o.options.direction || 'left'; // Default direction - var distance = o.options.distance || 20; // Default distance - var times = o.options.times || 3; // Default # of times - var speed = o.duration || o.options.duration || 140; // Default speed per shake - - // Adjust - $.effects.save(el, props); el.show(); // Save & Show - $.effects.createWrapper(el); // Create Wrapper - var ref = (direction == 'up' || direction == 'down') ? 'top' : 'left'; - var motion = (direction == 'up' || direction == 'left') ? 'pos' : 'neg'; - - // Animation - var animation = {}, animation1 = {}, animation2 = {}; - animation[ref] = (motion == 'pos' ? '-=' : '+=') + distance; - animation1[ref] = (motion == 'pos' ? '+=' : '-=') + distance * 2; - animation2[ref] = (motion == 'pos' ? '-=' : '+=') + distance * 2; - - // Animate - el.animate(animation, speed, o.options.easing); - for (var i = 1; i < times; i++) { // Shakes - el.animate(animation1, speed, o.options.easing).animate(animation2, speed, o.options.easing); - }; - el.animate(animation1, speed, o.options.easing). - animate(animation, speed / 2, o.options.easing, function(){ // Last shake - $.effects.restore(el, props); $.effects.removeWrapper(el); // Restore - if(o.callback) o.callback.apply(this, arguments); // Callback - }); - el.queue('fx', function() { el.dequeue(); }); - el.dequeue(); - }); + // Delegate to close method to handle common cleanup + var event = $.Event( "blur" ), + element = tooltipData.element; + event.target = event.currentTarget = element[ 0 ]; + that.close( event, true ); -}; + // Remove immediately; destroying an open tooltip doesn't use the + // hide animation + $( "#" + id ).remove(); -})(jQuery); -/* - * jQuery UI Effects Slide 1.8.17 - * - * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about) - * Dual licensed under the MIT or GPL Version 2 licenses. - * http://jquery.org/license - * - * http://docs.jquery.com/UI/Effects/Slide - * - * Depends: - * jquery.effects.core.js - */ -(function( $, undefined ) { + // Restore the title + if ( element.data( "ui-tooltip-title" ) ) { -$.effects.slide = function(o) { + // If the title attribute has changed since open(), don't restore + if ( !element.attr( "title" ) ) { + element.attr( "title", element.data( "ui-tooltip-title" ) ); + } + element.removeData( "ui-tooltip-title" ); + } + } ); + this.liveRegion.remove(); + } +} ); - return this.queue(function() { +// DEPRECATED +// TODO: Switch return back to widget declaration at top of file when this is removed +if ( $.uiBackCompat !== false ) { - // Create element - var el = $(this), props = ['position','top','bottom','left','right']; + // Backcompat for tooltipClass option + $.widget( "ui.tooltip", $.ui.tooltip, { + options: { + tooltipClass: null + }, + _tooltip: function() { + var tooltipData = this._superApply( arguments ); + if ( this.options.tooltipClass ) { + tooltipData.tooltip.addClass( this.options.tooltipClass ); + } + return tooltipData; + } + } ); +} - // Set options - var mode = $.effects.setMode(el, o.options.mode || 'show'); // Set Mode - var direction = o.options.direction || 'left'; // Default Direction - - // Adjust - $.effects.save(el, props); el.show(); // Save & Show - $.effects.createWrapper(el).css({overflow:'hidden'}); // Create Wrapper - var ref = (direction == 'up' || direction == 'down') ? 'top' : 'left'; - var motion = (direction == 'up' || direction == 'left') ? 'pos' : 'neg'; - var distance = o.options.distance || (ref == 'top' ? el.outerHeight({margin:true}) : el.outerWidth({margin:true})); - if (mode == 'show') el.css(ref, motion == 'pos' ? (isNaN(distance) ? "-" + distance : -distance) : distance); // Shift - - // Animation - var animation = {}; - animation[ref] = (mode == 'show' ? (motion == 'pos' ? '+=' : '-=') : (motion == 'pos' ? '-=' : '+=')) + distance; - - // Animate - el.animate(animation, { queue: false, duration: o.duration, easing: o.options.easing, complete: function() { - if(mode == 'hide') el.hide(); // Hide - $.effects.restore(el, props); $.effects.removeWrapper(el); // Restore - if(o.callback) o.callback.apply(this, arguments); // Callback - el.dequeue(); - }}); - - }); +var widgetsTooltip = $.ui.tooltip; -}; -})(jQuery); -/* - * jQuery UI Effects Transfer 1.8.17 - * - * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about) - * Dual licensed under the MIT or GPL Version 2 licenses. - * http://jquery.org/license - * - * http://docs.jquery.com/UI/Effects/Transfer - * - * Depends: - * jquery.effects.core.js - */ -(function( $, undefined ) { -$.effects.transfer = function(o) { - return this.queue(function() { - var elem = $(this), - target = $(o.options.to), - endPosition = target.offset(), - animation = { - top: endPosition.top, - left: endPosition.left, - height: target.innerHeight(), - width: target.innerWidth() - }, - startPosition = elem.offset(), - transfer = $('
') - .appendTo(document.body) - .addClass(o.options.className) - .css({ - top: startPosition.top, - left: startPosition.left, - height: elem.innerHeight(), - width: elem.innerWidth(), - position: 'absolute' - }) - .animate(animation, o.duration, o.options.easing, function() { - transfer.remove(); - (o.callback && o.callback.apply(elem[0], arguments)); - elem.dequeue(); - }); - }); -}; -})(jQuery); +})); \ No newline at end of file diff --git a/lib/jquery.configurator.js b/lib/jquery.configurator.js index 7bad0990..ace87c45 100644 --- a/lib/jquery.configurator.js +++ b/lib/jquery.configurator.js @@ -4,9 +4,10 @@ * Expose the available options of a jQueryUI widget and let the user modify * them (useful to create live demos). * - * Copyright (c) 2014, Martin Wendt (http://wwWendt.de) - * Dual licensed under the MIT or GPL Version 2 licenses. - * http://code.google.com/p/fancytree/wiki/LicenseInfo + * Copyright (c) 2008-2018, Martin Wendt (http://wwWendt.de) + * + * Released under the MIT license + * https://github.com/mar10/fancytree/wiki/LicenseInfo * * @see http://wwwendt.de/tech/fancytree/demo/sample-configurator.html */ @@ -55,7 +56,7 @@ $(this.options.optionTarget) .addClass("ui-configurator-options") .empty() - .delegate("input,select", "change", $.proxy(this._onOptionChange, this)); + .on("change", "input,select", $.proxy(this._onOptionChange, this)); $(this.options.sourceTarget) .addClass("ui-configurator-source") .empty(); @@ -120,7 +121,7 @@ value: actualOpts[o.name] //o.value }) ); - }else if( $.isArray(o.value) ){ + }else if( Array.isArray(o.value) ){ $li = $("
  • ") .append( $("