├── .editorconfig ├── .github └── workflows │ ├── ci.yml │ ├── codeql-analysis.yml │ ├── size.yml │ └── static.yml ├── .gitignore ├── .npmignore ├── .prettier ├── LICENSE ├── README.md ├── bower.json ├── config ├── .postcssrc ├── babel.config.json ├── babel.coverage.config.json ├── jest.config.js ├── playwright.config.js ├── production └── rollup.config.mjs ├── css └── jqtree.postcss ├── devserver ├── devserver.js ├── devserver_scroll.js ├── index.html ├── test_index.html ├── test_scroll.html └── test_scroll_container.html ├── docs ├── .ruby-version ├── Gemfile ├── Gemfile.lock ├── _config.yml ├── _entries │ ├── events │ │ ├── index.md │ │ ├── tree-click.md │ │ ├── tree-close.md │ │ ├── tree-contextmenu.md │ │ ├── tree-dblclick.md │ │ ├── tree-init.md │ │ ├── tree-load-data.md │ │ ├── tree-loading-data.md │ │ ├── tree-move.md │ │ ├── tree-open.md │ │ ├── tree-refresh.md │ │ └── tree-select.md │ ├── functions │ │ ├── addnodeafter.md │ │ ├── addnodebefore.md │ │ ├── addparentnode.md │ │ ├── appendnode.md │ │ ├── closenode.md │ │ ├── destroy.md │ │ ├── getnodebycallback.md │ │ ├── getnodebyhtmlelement.md │ │ ├── getnodebyid.md │ │ ├── getselectednode.md │ │ ├── getstate.md │ │ ├── gettree.md │ │ ├── index.md │ │ ├── is-node-selected.md │ │ ├── isdragging.md │ │ ├── loaddata.md │ │ ├── loaddatafromurl.md │ │ ├── movedown.md │ │ ├── movenode.md │ │ ├── moveup.md │ │ ├── opennode.md │ │ ├── prependnode.md │ │ ├── refresh.md │ │ ├── reload.md │ │ ├── removenode.md │ │ ├── scrolltonode.md │ │ ├── selectnode.md │ │ ├── setoption.md │ │ ├── setstate.md │ │ ├── toggle.md │ │ ├── tojson.md │ │ └── updatenode.md │ ├── general │ │ ├── changelog.md │ │ ├── demo.html │ │ ├── downloads.md │ │ ├── examples.md │ │ ├── features.md │ │ ├── index.md │ │ ├── introduction.md │ │ ├── requirements.md │ │ ├── tutorial.md │ │ └── usecases.md │ ├── multiple_selection │ │ ├── add-to-selection.md │ │ ├── get-selected-nodes.md │ │ ├── index.md │ │ └── remove-from-selection.md │ ├── node │ │ ├── children.md │ │ ├── getdata.md │ │ ├── getlevel.md │ │ ├── getnextnode.md │ │ ├── getnextsibling.md │ │ ├── getnextvisiblenode.md │ │ ├── getpreviousnode.md │ │ ├── getprevioussibling.md │ │ ├── getpreviousvisiblenode.md │ │ ├── index.md │ │ └── parent.md │ └── options │ │ ├── animationspeed.md │ │ ├── autoescape.md │ │ ├── autoopen.md │ │ ├── buttonleft.md │ │ ├── closedicon.md │ │ ├── data-url.md │ │ ├── data.md │ │ ├── datafilter.md │ │ ├── draganddrop.md │ │ ├── index.md │ │ ├── keyboardsupport.md │ │ ├── oncanmove.md │ │ ├── oncanmoveto.md │ │ ├── oncanselectnode.md │ │ ├── oncreateli.md │ │ ├── ondragmove.md │ │ ├── ondragstop.md │ │ ├── onismovehandle.md │ │ ├── onloadfailed.md │ │ ├── onloading.md │ │ ├── openedicon.md │ │ ├── openfolderdelay.md │ │ ├── rtl.md │ │ ├── savestate.md │ │ ├── selectable.md │ │ ├── showemptyfolder.md │ │ ├── slide.md │ │ ├── start_dnd_delay.md │ │ ├── tabindex.md │ │ └── usecontextmenu.md ├── _examples │ ├── 01_load_json_data.html │ ├── 02_load_json_data_from_server.html │ ├── 03_drag_and_drop.html │ ├── 04_save_state.html │ ├── 05_load_on_demand.html │ ├── 06_autoescape.html │ ├── 07_autoscroll.html │ ├── 08_multiple_select.html │ ├── 09_custom_html.html │ ├── 10_icon_buttons.html │ ├── 11_right-to-left.html │ ├── 12_button_on_right.html │ ├── 13_drag_outside.html │ └── 14_filter.html ├── _layouts │ ├── example.html │ └── page.html ├── copy_vendor_files ├── documentation.css ├── index.html ├── package.json ├── pnpm-lock.yaml ├── postcss.config.js └── static │ ├── documentation.js │ ├── example.postcss │ ├── example_data.js │ ├── examples │ ├── autoescape.js │ ├── autoscroll.js │ ├── button-on-right.js │ ├── custom_html.js │ ├── drag-outside.js │ ├── drag_and_drop.js │ ├── filter.js │ ├── icon_buttons.js │ ├── load_json_data.js │ ├── load_json_data_from_server.js │ ├── load_on_demand.js │ ├── multiple_select.js │ ├── right-to-left.js │ └── save_state.js │ ├── monokai.css │ └── spinner.gif ├── eslint.config.mjs ├── jqtree-circle.png ├── jqtree.css ├── package.json ├── pnpm-lock.yaml ├── screenshot.png ├── src ├── dataLoader.ts ├── dragAndDropHandler │ ├── binarySearch.ts │ ├── dragElement.ts │ ├── generateHitAreas.ts │ ├── index.ts │ ├── iterateVisibleNodes.ts │ └── types.ts ├── elementsRenderer.ts ├── header.txt ├── jqtreeMethodTypes.ts ├── jqtreeOptions.ts ├── keyHandler.ts ├── mouseHandler.ts ├── mouseUtils.ts ├── node.ts ├── nodeElement │ ├── borderDropHint.ts │ ├── folderElement.ts │ ├── ghostDropHint.ts │ └── index.ts ├── nodeUtils.ts ├── playwright │ ├── coverage.ts │ ├── playwright.test.ts │ ├── playwright.test.ts-snapshots │ │ ├── with-dragAndDrop-moves-a-node-1-Chromium-darwin.png │ │ ├── with-dragAndDrop-moves-a-node-1-Chromium-linux.png │ │ ├── without-dragAndDrop-displays-a-tree-1-Chromium-darwin.png │ │ ├── without-dragAndDrop-displays-a-tree-1-Chromium-linux.png │ │ ├── without-dragAndDrop-selects-a-node-1-Chromium-darwin.png │ │ └── without-dragAndDrop-selects-a-node-1-Chromium-linux.png │ └── testUtils.ts ├── saveStateHandler.ts ├── scrollHandler.ts ├── scrollHandler │ ├── containerScrollParent.ts │ ├── createScrollParent.ts │ ├── documentScrollParent.ts │ └── scrollParent.ts ├── selectNodeHandler.ts ├── simple.widget.ts ├── test │ ├── dataLoader.test.ts │ ├── dragAndDropHandler │ │ ├── binarySearch.test.ts │ │ ├── dragElement.test.ts │ │ ├── generateHitAreas.test.ts │ │ └── index.test.ts │ ├── jqTree │ │ ├── accessibility.test.ts │ │ ├── create.test.ts │ │ ├── events.test.ts │ │ ├── keyboard.test.ts │ │ ├── loadOnDemand.test.ts │ │ ├── methods.test.ts │ │ ├── mouse.test.ts │ │ └── options.test.ts │ ├── mouseHandler.test.ts │ ├── node.test.ts │ ├── nodeElement │ │ ├── borderDropHint.test.ts │ │ ├── ghostDropHint.test.ts │ │ └── index.test.ts │ ├── nodeUtils.test.ts │ ├── saveStateHandler.test.ts │ ├── scrollHandler │ │ ├── containerScrollParent.test.ts │ │ └── documentScrollParent.test.ts │ ├── selectNodeHandler.test.ts │ ├── support │ │ ├── exampleData.ts │ │ ├── jqTreeMatchers.ts │ │ ├── matchers.d.ts │ │ ├── setupTests.ts │ │ ├── testUtil.ts │ │ └── treeStructure.ts │ └── util.test.ts ├── tree.jquery.d.ts ├── tree.jquery.ts ├── typings.d.ts ├── util.ts └── version.ts ├── tree.jquery.debug.js ├── tree.jquery.debug.js.map ├── tree.jquery.js ├── tree.jquery.js.map └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain 2 | # consistent coding styles between different editors and IDEs. 3 | 4 | root = true 5 | 6 | [*] 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | indent_style = space 12 | indent_size = 4 13 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Continuous integration 2 | 3 | on: [push] 4 | 5 | jobs: 6 | runner-job: 7 | runs-on: ubuntu-latest 8 | 9 | env: 10 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 11 | 12 | steps: 13 | - name: Check out repository code 14 | uses: actions/checkout@v4 15 | - name: Setup node 16 | uses: actions/setup-node@v4 17 | with: 18 | node-version: "20" 19 | - name: Install pnpm 20 | run: npm install -g pnpm 21 | - name: Get pnpm store directory 22 | id: pnpm-cache 23 | run: | 24 | echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT 25 | - uses: actions/cache@v4 26 | name: Setup pnpm cache 27 | with: 28 | path: ${{ steps.pnpm-cache.outputs.STORE_PATH }} 29 | key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} 30 | restore-keys: | 31 | ${{ runner.os }}-pnpm-store- 32 | - uses: actions/cache@v4 33 | name: Setup Playwright browsers cache 34 | with: 35 | path: /home/runner/.cache/ms-playwright/ 36 | key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} 37 | restore-keys: | 38 | ${{ runner.os }}-playwright- 39 | - name: Install packages 40 | run: | 41 | pnpm install 42 | - name: Build 43 | run: | 44 | pnpm run production 45 | - name: Lint 46 | run: | 47 | pnpm run lint 48 | - name: Check types 49 | run: | 50 | pnpm run tsc 51 | - name: Clean coverage 52 | run: rm -rf .nyc_output && npx jest --clearCache 53 | - name: Run jest tests 54 | run: pnpm run jest 55 | - name: Playwright install 56 | run: npx playwright install chromium 57 | - name: Copy vendor files 58 | run: pnpm install && pnpm run copy_vendor_files 59 | working-directory: ./docs 60 | - name: Run playwright tests 61 | run: pnpm run playwright 62 | - name: Screenshots artifact 63 | if: always() 64 | uses: actions/upload-artifact@v4 65 | with: 66 | if-no-files-found: ignore 67 | name: screenshots 68 | path: test-results/ 69 | - name: Merge coverage 70 | run: cp jest-coverage/coverage-final.json .nyc_output/coverage_jsdom.json 71 | - name: Codecov 72 | uses: codecov/codecov-action@v5 73 | with: 74 | directory: .nyc_output 75 | verbose: true 76 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | push: 5 | branches: [dev] 6 | pull_request: 7 | branches: [dev] 8 | 9 | jobs: 10 | analyze: 11 | name: Analyze 12 | runs-on: ubuntu-latest 13 | 14 | strategy: 15 | fail-fast: false 16 | matrix: 17 | language: ["javascript"] 18 | 19 | steps: 20 | - name: Checkout repository 21 | uses: actions/checkout@v4 22 | 23 | - name: Initialize CodeQL 24 | uses: github/codeql-action/init@v3 25 | with: 26 | languages: ${{ matrix.language }} 27 | 28 | - name: Autobuild 29 | uses: github/codeql-action/autobuild@v3 30 | 31 | - name: Perform CodeQL Analysis 32 | uses: github/codeql-action/analyze@v3 33 | -------------------------------------------------------------------------------- /.github/workflows/size.yml: -------------------------------------------------------------------------------- 1 | name: Compressed Size 2 | on: [pull_request] 3 | 4 | jobs: 5 | runner-job: 6 | runs-on: ubuntu-latest 7 | 8 | steps: 9 | - name: Check out repository code 10 | uses: actions/checkout@v4 11 | - name: Setup node 12 | uses: actions/setup-node@v4 13 | with: 14 | node-version: "20" 15 | - name: Install pnpm 16 | run: npm install -g pnpm 17 | - name: Install packages 18 | run: pnpm install 19 | - name: Check compressed size 20 | uses: preactjs/compressed-size-action@v2 21 | with: 22 | build-script: "production" 23 | pattern: "./lib/**/*.js" 24 | exclude: "{./lib/**/*.d.js,./lib/playwright/**,./lib/test/**}" 25 | -------------------------------------------------------------------------------- /.github/workflows/static.yml: -------------------------------------------------------------------------------- 1 | name: Deploy static content to Pages 2 | 3 | on: 4 | push: 5 | branches: ["master", "test-docs"] 6 | workflow_dispatch: 7 | 8 | permissions: 9 | contents: read 10 | pages: write 11 | id-token: write 12 | 13 | concurrency: 14 | group: "pages" 15 | cancel-in-progress: false 16 | 17 | jobs: 18 | deploy: 19 | environment: 20 | name: github-pages 21 | url: ${{ steps.deployment.outputs.page_url }} 22 | env: 23 | BUNDLE_GEMFILE: ${{ github.workspace }}/docs/Gemfile 24 | runs-on: ubuntu-latest 25 | steps: 26 | - name: Checkout 27 | uses: actions/checkout@v4 28 | - name: Setup ruby 29 | uses: ruby/setup-ruby@v1 30 | with: 31 | ruby-version: '3.3.6' 32 | - name: Setup node 33 | uses: actions/setup-node@v4 34 | with: 35 | node-version: 20 36 | - name: Install pnpm 37 | run: npm install -g pnpm 38 | - name: Install ruby packages 39 | working-directory: ./docs 40 | run: bundle 41 | - name: Install javascript packages 42 | working-directory: ./docs 43 | run: pnpm install 44 | - name: Build docs css 45 | working-directory: ./docs 46 | run: pnpm run build_docs_css 47 | - name: Jekyll Build 48 | working-directory: ./docs 49 | run: bundle exec jekyll build 50 | - name: Setup Pages 51 | uses: actions/configure-pages@v5 52 | - name: Upload artifact 53 | uses: actions/upload-pages-artifact@v3 54 | with: 55 | path: './docs/_site' 56 | - name: Deploy to GitHub Pages 57 | id: deployment 58 | uses: actions/deploy-pages@v4 59 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .nyc_output/ 2 | .tool-versions 3 | .DS_Store 4 | .vscode/ 5 | **/.DS_Store 6 | docs/_site/ 7 | docs/jqtree.css 8 | docs/static/documentation.css 9 | docs/static/example.css 10 | docs/static/vendor/ 11 | docs/tree.jquery.js 12 | jest-coverage/ 13 | lib/ 14 | node_modules/ 15 | test-results/ 16 | todo.txt 17 | update.txt 18 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .github 2 | config 3 | css 4 | devserver 5 | docs 6 | src/playwright 7 | src/test 8 | .editorconfig 9 | .eslintrc 10 | .prettier 11 | tsconfig.json 12 | -------------------------------------------------------------------------------- /.prettier: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 4 3 | } 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Build](https://github.com/mbraak/jqTree/workflows/Continuous%20integration/badge.svg) [![codecov](https://codecov.io/gh/mbraak/jqTree/branch/dev/graph/badge.svg?token=DKzjY5YUlq)](https://codecov.io/gh/mbraak/jqTree) 2 | 3 | [![NPM version](https://img.shields.io/npm/v/jqtree.svg)](https://www.npmjs.com/package/jqtree) 4 | 5 | # jqTree 6 | 7 | JqTree is a tree widget. Read more in the [documentation](https://mbraak.github.io/jqTree/). 8 | 9 | ![screenshot](https://raw.github.com/mbraak/jqTree/master/screenshot.png) 10 | 11 | ## Features 12 | 13 | - Create a tree from JSON data 14 | - Drag and drop 15 | - Works on all modern browsers 16 | - Written in Typescript 17 | 18 | The project is hosted on [github](https://github.com/mbraak/jqTree). 19 | 20 | ## Examples 21 | 22 | Example with ajax data: 23 | 24 | ```html 25 |
26 | ``` 27 | 28 | ```js 29 | $("#tree1").tree(); 30 | ``` 31 | 32 | Example with static data: 33 | 34 | ```js 35 | var data = [ 36 | { 37 | label: "node1", 38 | id: 1, 39 | children: [ 40 | { label: "child1", id: 2 }, 41 | { label: "child2", id: 3 }, 42 | ], 43 | }, 44 | { 45 | label: "node2", 46 | id: 4, 47 | children: [{ label: "child3", id: 5 }], 48 | }, 49 | ]; 50 | $("#tree1").tree({ 51 | data: data, 52 | autoOpen: true, 53 | dragAndDrop: true, 54 | }); 55 | ``` 56 | 57 | ## Documentation 58 | 59 | The documentation is on http://mbraak.github.io/jqTree/. 60 | 61 | ## Thanks 62 | 63 | The code for the mouse widget is heavily inspired by the mouse widget from jquery ui. 64 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jqTree", 3 | "version": "1.8.10", 4 | "main": [ 5 | "jqtree.css", 6 | "jqtree-circle.png", 7 | "tree.jquery.js" 8 | ], 9 | "dependencies": { 10 | "jquery": ">=1.9" 11 | }, 12 | "license": "Apache-2.0", 13 | "ignore": [], 14 | "keywords": [ 15 | "jquery-plugin", 16 | "tree" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /config/.postcssrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": { 3 | "postcss-nested": {}, 4 | "autoprefixer": {} 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /config/babel.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-typescript", "@babel/preset-env"], 3 | "env": { 4 | "test": { 5 | "presets": [ 6 | "@babel/preset-typescript", 7 | "@babel/preset-env" 8 | ] 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /config/babel.coverage.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-typescript", "@babel/preset-env"], 3 | "plugins": ["istanbul"] 4 | } 5 | -------------------------------------------------------------------------------- /config/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | coverageDirectory: "jest-coverage", 3 | coverageReporters: ["json"], 4 | modulePathIgnorePatterns: [ 5 | "/docs/_site/", 6 | "/docs/static/", 7 | "/lib/", 8 | ], 9 | rootDir: "../", 10 | setupFilesAfterEnv: [ 11 | "/src/test/support/setupTests.ts", 12 | "givens/setup.js", 13 | "jest-extended/all", 14 | ], 15 | testEnvironment: "jest-fixed-jsdom", 16 | testEnvironmentOptions: { 17 | customExportConditions: [""], 18 | }, 19 | testRegex: "\\/src\\/test\\/.*\\.test\\.ts", 20 | transform: { 21 | "\\.tsx?$": ["babel-jest", { root: __dirname }], 22 | }, 23 | }; 24 | -------------------------------------------------------------------------------- /config/playwright.config.js: -------------------------------------------------------------------------------- 1 | const { devices } = require("@playwright/test"); 2 | 3 | const config = { 4 | testDir: "../src/playwright", 5 | projects: [ 6 | { 7 | name: "Chromium", 8 | use: { ...devices["Desktop Chrome"] }, 9 | }, 10 | ], 11 | webServer: { 12 | command: "pnpm devserver-with-coverage", 13 | cwd: "..", 14 | port: 8080, 15 | }, 16 | }; 17 | 18 | module.exports = config; 19 | -------------------------------------------------------------------------------- /config/production: -------------------------------------------------------------------------------- 1 | rm -rf ./lib && 2 | mkdir lib && 3 | rollup -c config/rollup.config.mjs && 4 | DEBUG_BUILD=true rollup -c config/rollup.config.mjs && 5 | babel src --config-file ./config/babel.config.json --out-dir lib --extensions .ts && 6 | postcss --config ./config -o jqtree.css css/jqtree.postcss 7 | -------------------------------------------------------------------------------- /config/rollup.config.mjs: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | import jsonfile from "jsonfile"; 3 | import template from "lodash/template.js"; 4 | import { babel } from "@rollup/plugin-babel"; 5 | import resolve from "@rollup/plugin-node-resolve"; 6 | import serve from "rollup-plugin-serve"; 7 | import terser from "@rollup/plugin-terser"; 8 | 9 | const getBanner = () => { 10 | const headerTemplate = fs.readFileSync("./src/header.txt", "utf8"); 11 | const { version } = jsonfile.readFileSync("package.json"); 12 | 13 | const data = { 14 | version, 15 | year: new Date().getFullYear(), 16 | }; 17 | 18 | const banner = template(headerTemplate)(data); 19 | return `/*\n${banner}\n*/`; 20 | }; 21 | 22 | const debugBuild = Boolean(process.env.DEBUG_BUILD); 23 | const devServer = Boolean(process.env.SERVE); 24 | const includeCoverage = Boolean(process.env.COVERAGE); 25 | 26 | const resolvePlugin = resolve({ extensions: [".ts"] }); 27 | 28 | const babelConfigFile = includeCoverage 29 | ? "babel.coverage.config.json" 30 | : "babel.config.json"; 31 | 32 | const babelPlugin = babel({ 33 | babelHelpers: "bundled", 34 | configFile: `./config/${babelConfigFile}`, 35 | extensions: [".ts"], 36 | }); 37 | 38 | const plugins = [resolvePlugin, babelPlugin]; 39 | 40 | if (!debugBuild) { 41 | const terserPlugin = terser({ 42 | output: { 43 | comments: /@license/, 44 | }, 45 | }); 46 | plugins.push(terserPlugin); 47 | } 48 | 49 | if (devServer) { 50 | const servePlugin = serve({ 51 | contentBase: ["./devserver", "./docs/static", "./"], 52 | port: 8080, 53 | }); 54 | plugins.push(servePlugin); 55 | } 56 | 57 | export default { 58 | input: "src/tree.jquery.ts", 59 | output: { 60 | banner: getBanner(), 61 | file: debugBuild ? "tree.jquery.debug.js" : "tree.jquery.js", 62 | format: "iife", 63 | globals: { 64 | jquery: "jQuery", 65 | }, 66 | name: "jqtree", 67 | sourcemap: true, 68 | }, 69 | external: ["jquery"], 70 | plugins, 71 | }; 72 | -------------------------------------------------------------------------------- /devserver/devserver.js: -------------------------------------------------------------------------------- 1 | const $tree = $("#tree1"); 2 | 3 | $tree.tree({ 4 | autoOpen: 0, 5 | data: ExampleData.exampleData, 6 | dragAndDrop: true, 7 | }); 8 | -------------------------------------------------------------------------------- /devserver/devserver_scroll.js: -------------------------------------------------------------------------------- 1 | const $tree = $("#tree1"); 2 | 3 | $tree.tree({ 4 | autoOpen: true, 5 | data: ExampleData.exampleData, 6 | dragAndDrop: true, 7 | useContextMenu: false, 8 | }); 9 | -------------------------------------------------------------------------------- /devserver/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | JqTree devserver 6 | 7 | 11 | 12 | 13 | 14 |
15 |
16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /devserver/test_index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 18 | 19 | 20 |
21 | 22 | 23 | -------------------------------------------------------------------------------- /devserver/test_scroll.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | JqTree devserver 6 | 7 | 11 | 12 | 20 | 21 | 22 |
23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /devserver/test_scroll_container.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | JqTree devserver 7 | 8 | 9 | 10 | 29 | 30 | 31 | 32 |
33 |
34 |
35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /docs/.ruby-version: -------------------------------------------------------------------------------- 1 | 3.3.6 2 | -------------------------------------------------------------------------------- /docs/Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | ruby Bundler.root.join('.ruby-version').read 3 | 4 | gem "jekyll", "~> 4.3.3" 5 | gem "minima", "~> 2.5" 6 | -------------------------------------------------------------------------------- /docs/Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | addressable (2.8.7) 5 | public_suffix (>= 2.0.2, < 7.0) 6 | bigdecimal (3.1.8) 7 | colorator (1.1.0) 8 | concurrent-ruby (1.3.4) 9 | em-websocket (0.5.3) 10 | eventmachine (>= 0.12.9) 11 | http_parser.rb (~> 0) 12 | eventmachine (1.2.7) 13 | ffi (1.17.0-arm64-darwin) 14 | ffi (1.17.0-x86_64-linux-gnu) 15 | forwardable-extended (2.6.0) 16 | google-protobuf (4.28.2-arm64-darwin) 17 | bigdecimal 18 | rake (>= 13) 19 | google-protobuf (4.28.2-x86_64-linux) 20 | bigdecimal 21 | rake (>= 13) 22 | http_parser.rb (0.8.0) 23 | i18n (1.14.6) 24 | concurrent-ruby (~> 1.0) 25 | jekyll (4.3.4) 26 | addressable (~> 2.4) 27 | colorator (~> 1.0) 28 | em-websocket (~> 0.5) 29 | i18n (~> 1.0) 30 | jekyll-sass-converter (>= 2.0, < 4.0) 31 | jekyll-watch (~> 2.0) 32 | kramdown (~> 2.3, >= 2.3.1) 33 | kramdown-parser-gfm (~> 1.0) 34 | liquid (~> 4.0) 35 | mercenary (>= 0.3.6, < 0.5) 36 | pathutil (~> 0.9) 37 | rouge (>= 3.0, < 5.0) 38 | safe_yaml (~> 1.0) 39 | terminal-table (>= 1.8, < 4.0) 40 | webrick (~> 1.7) 41 | jekyll-feed (0.17.0) 42 | jekyll (>= 3.7, < 5.0) 43 | jekyll-sass-converter (3.0.0) 44 | sass-embedded (~> 1.54) 45 | jekyll-seo-tag (2.8.0) 46 | jekyll (>= 3.8, < 5.0) 47 | jekyll-watch (2.2.1) 48 | listen (~> 3.0) 49 | kramdown (2.4.0) 50 | rexml 51 | kramdown-parser-gfm (1.1.0) 52 | kramdown (~> 2.0) 53 | liquid (4.0.4) 54 | listen (3.9.0) 55 | rb-fsevent (~> 0.10, >= 0.10.3) 56 | rb-inotify (~> 0.9, >= 0.9.10) 57 | mercenary (0.4.0) 58 | minima (2.5.2) 59 | jekyll (>= 3.5, < 5.0) 60 | jekyll-feed (~> 0.9) 61 | jekyll-seo-tag (~> 2.1) 62 | pathutil (0.16.2) 63 | forwardable-extended (~> 2.6) 64 | public_suffix (6.0.1) 65 | rake (13.2.1) 66 | rb-fsevent (0.11.2) 67 | rb-inotify (0.11.1) 68 | ffi (~> 1.0) 69 | rexml (3.3.9) 70 | rouge (4.4.0) 71 | safe_yaml (1.0.5) 72 | sass-embedded (1.79.4-arm64-darwin) 73 | google-protobuf (~> 4.27) 74 | sass-embedded (1.79.4-x86_64-linux-gnu) 75 | google-protobuf (~> 4.27) 76 | terminal-table (3.0.2) 77 | unicode-display_width (>= 1.1.1, < 3) 78 | unicode-display_width (2.6.0) 79 | webrick (1.8.2) 80 | 81 | PLATFORMS 82 | arm64-darwin-22 83 | arm64-darwin-23 84 | arm64-darwin-24 85 | x86_64-linux 86 | 87 | DEPENDENCIES 88 | jekyll (~> 4.3.3) 89 | minima (~> 2.5) 90 | 91 | RUBY VERSION 92 | ruby 3.3.6 93 | 94 | BUNDLED WITH 95 | 2.4.10 96 | -------------------------------------------------------------------------------- /docs/_config.yml: -------------------------------------------------------------------------------- 1 | # Site settings 2 | title: jqTree 3 | email: your-email@domain.com 4 | description: "JqTree docs" 5 | baseurl: "/jqTree" 6 | url: "http://mbraak.github.io/jqTree/" 7 | 8 | collections: 9 | entries: 10 | output: false 11 | order: 12 | - general/index.md 13 | - general/introduction.md 14 | - general/features.md 15 | - general/demo.html 16 | - general/requirements.md 17 | - general/downloads.md 18 | - general/tutorial.md 19 | - general/examples.md 20 | - general/usecases.md 21 | - general/changelog.md 22 | - options/index.md 23 | - options/animationspeed.md 24 | - options/autoescape.md 25 | - options/autoopen.md 26 | - options/buttonleft.md 27 | - options/closedicon.md 28 | - options/data.md 29 | - options/datafilter.md 30 | - options/data-url.md 31 | - options/draganddrop.md 32 | - options/keyboardsupport.md 33 | - options/oncanmove.md 34 | - options/oncanmoveto.md 35 | - options/oncanselectnode.md 36 | - options/oncreateli.md 37 | - options/ondragmove.md 38 | - options/ondragstop.md 39 | - options/onismovehandle.md 40 | - options/onloadfailed.md 41 | - options/onloading.md 42 | - options/openedicon.md 43 | - options/openfolderdelay.md 44 | - options/rtl.md 45 | - options/savestate.md 46 | - options/selectable.md 47 | - options/showemptyfolder.md 48 | - options/slide.md 49 | - options/start_dnd_delay.md 50 | - options/tabindex.md 51 | - options/usecontextmenu.md 52 | - functions/index.md 53 | - functions/addparentnode.md 54 | - functions/addnodeafter.md 55 | - functions/addnodebefore.md 56 | - functions/appendnode.md 57 | - functions/closenode.md 58 | - functions/destroy.md 59 | - functions/getnodebycallback.md 60 | - functions/getnodebyid.md 61 | - functions/getnodebyhtmlelement.md 62 | - functions/getselectednode.md 63 | - functions/getstate.md 64 | - functions/gettree.md 65 | - functions/isdragging.md 66 | - functions/is-node-selected.md 67 | - functions/loaddata.md 68 | - functions/loaddatafromurl.md 69 | - functions/movedown.md 70 | - functions/movenode.md 71 | - functions/moveup.md 72 | - functions/opennode.md 73 | - functions/prependnode.md 74 | - functions/refresh.md 75 | - functions/reload.md 76 | - functions/removenode.md 77 | - functions/selectnode.md 78 | - functions/scrolltonode.md 79 | - functions/setoption.md 80 | - functions/setstate.md 81 | - functions/toggle.md 82 | - functions/tojson.md 83 | - functions/updatenode.md 84 | - events/index.md 85 | - events/tree-click.md 86 | - events/tree-close.md 87 | - events/tree-contextmenu.md 88 | - events/tree-dblclick.md 89 | - events/tree-init.md 90 | - events/tree-load-data.md 91 | - events/tree-loading-data.md 92 | - events/tree-move.md 93 | - events/tree-refresh.md 94 | - events/tree-open.md 95 | - events/tree-select.md 96 | - multiple_selection/index.md 97 | - multiple_selection/add-to-selection.md 98 | - multiple_selection/get-selected-nodes.md 99 | - multiple_selection/remove-from-selection.md 100 | - node/index.md 101 | - node/children.md 102 | - node/getdata.md 103 | - node/getlevel.md 104 | - node/getnextnode.md 105 | - node/getnextsibling.md 106 | - node/getnextvisiblenode.md 107 | - node/getpreviousnode.md 108 | - node/getprevioussibling.md 109 | - node/getpreviousvisiblenode.md 110 | - node/parent.md 111 | examples: 112 | output: true 113 | 114 | defaults: 115 | - scope: 116 | path: "" 117 | type: "examples" 118 | values: 119 | layout: "example" 120 | 121 | jqtree_version: 1.8.10 122 | 123 | # Build settings 124 | markdown: kramdown 125 | permalink: pretty 126 | exclude: ["node_modules"] 127 | -------------------------------------------------------------------------------- /docs/_entries/events/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Events 3 | name: events 4 | section: true 5 | --- 6 | 7 | -------------------------------------------------------------------------------- /docs/_entries/events/tree-click.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: tree.click 3 | name: event-tree-click 4 | --- 5 | 6 | Triggered when a tree node is clicked. The event contains the following properties: 7 | 8 | * **node**: the node that is clicked on 9 | * **click_event**: the original click event 10 | 11 | {% highlight js %} 12 | // create tree 13 | $('#tree1').tree({ 14 | data: data 15 | }); 16 | 17 | // bind 'tree.click' event 18 | $('#tree1').on( 19 | 'tree.click', 20 | function(event) { 21 | // The clicked node is 'event.node' 22 | var node = event.node; 23 | alert(node.name); 24 | } 25 | ); 26 | {% endhighlight %} 27 | 28 | The default action is to select the node. You can prevent the selection by calling **preventDefault**: 29 | 30 | {% highlight js %} 31 | $('#tree1').on( 32 | 'tree.click', 33 | function(event) { 34 | event.preventDefault(); 35 | } 36 | ); 37 | {% endhighlight %} 38 | -------------------------------------------------------------------------------- /docs/_entries/events/tree-close.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: tree.close 3 | name: event-tree-close 4 | --- 5 | 6 | Called when a node is closed. 7 | 8 | {% highlight js %} 9 | $('#tree1').on( 10 | 'tree.close', 11 | function(e) { 12 | console.log(e.node); 13 | } 14 | ); 15 | {% endhighlight %} 16 | -------------------------------------------------------------------------------- /docs/_entries/events/tree-contextmenu.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: tree.contextmenu 3 | name: event-tree-contextmenu 4 | --- 5 | 6 | Triggered when the user right-clicks a tree node. The event contains the following properties: 7 | 8 | * **node**: the node that is clicked on 9 | * **click_event**: the original click event 10 | 11 | {% highlight js %} 12 | // bind 'tree.contextmenu' event 13 | $('#tree1').on( 14 | 'tree.contextmenu', 15 | function(event) { 16 | // The clicked node is 'event.node' 17 | var node = event.node; 18 | alert(node.name); 19 | } 20 | ); 21 | {% endhighlight %} 22 | -------------------------------------------------------------------------------- /docs/_entries/events/tree-dblclick.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: tree.dblclick 3 | name: event-tree-dblclick 4 | --- 5 | 6 | The **tree.dblclick** is fired when a tree node is double-clicked. The event contains the following properties: 7 | 8 | * **node**: the node that is clicked on 9 | * **click_event**: the original click event 10 | 11 | {% highlight js %} 12 | $('#tree1').on( 13 | 'tree.dblclick', 14 | function(event) { 15 | // event.node is the clicked node 16 | console.log(event.node); 17 | } 18 | ); 19 | {% endhighlight %} 20 | -------------------------------------------------------------------------------- /docs/_entries/events/tree-init.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: tree.init 3 | name: event-tree-init 4 | --- 5 | 6 | Called when the tree is initialized. This is particularly useful when the data is loaded from the server. 7 | 8 | {% highlight js %} 9 | $('#tree1').on( 10 | 'tree.init', 11 | function() { 12 | // initializing code 13 | } 14 | ); 15 | {% endhighlight %} 16 | -------------------------------------------------------------------------------- /docs/_entries/events/tree-load-data.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: tree.load_data 3 | name: event-load-data 4 | --- 5 | 6 | Called after data is loaded using ajax. 7 | 8 | {% highlight js %} 9 | $('#tree1').on( 10 | 'tree.load_data', 11 | function(e) { 12 | console.log(e.tree_data); 13 | } 14 | ); 15 | {% endhighlight %} 16 | -------------------------------------------------------------------------------- /docs/_entries/events/tree-loading-data.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: tree.loading_data 3 | name: event-loading-data 4 | --- 5 | 6 | Called before and after data is loaded using ajax. 7 | 8 | The event data looks like this: 9 | 10 | * **isLoading**: true / false 11 | * **node**: 12 | * null; when loading the whole tree 13 | * a node; when a node is loaded on demand 14 | * **$el**: dom element 15 | * whole tree; when loading the whole tree 16 | * dom element of node; when a node is loaded on demand 17 | 18 | Example code: 19 | 20 | {% highlight js %} 21 | $('#tree1').on( 22 | 'tree.loading_data', 23 | function(e) { 24 | console.log(e.isLoading, e.node, e.$el); 25 | } 26 | ); 27 | {% endhighlight %} 28 | -------------------------------------------------------------------------------- /docs/_entries/events/tree-move.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: tree.move 3 | name: event-tree-move 4 | --- 5 | 6 | Triggered when the user moves a node. 7 | 8 | Note that this event is called **before** the node is moved. See note about `do_move` below. 9 | 10 | Event.move_info contains: 11 | 12 | * moved_node 13 | * target_node 14 | * position: (before, after or inside) 15 | * previous_parent 16 | 17 | {% highlight js %} 18 | $('#tree1').tree({ 19 | data: data, 20 | dragAndDrop: true 21 | }); 22 | 23 | $('#tree1').on( 24 | 'tree.move', 25 | function(event) { 26 | console.log('moved_node', event.move_info.moved_node); 27 | console.log('target_node', event.move_info.target_node); 28 | console.log('position', event.move_info.position); 29 | console.log('previous_parent', event.move_info.previous_parent); 30 | } 31 | ); 32 | {% endhighlight %} 33 | 34 | You can prevent the move by calling **event.preventDefault()** 35 | 36 | {% highlight js %} 37 | $('#tree1').on( 38 | 'tree.move', 39 | function(event) { 40 | event.preventDefault(); 41 | } 42 | ); 43 | {% endhighlight %} 44 | 45 | You can later call **event.move_info.move_info.do_move()** to move the node. This way you can ask the user before moving the node: 46 | 47 | {% highlight js %} 48 | $('#tree1').on( 49 | 'tree.move', 50 | function(event) { 51 | event.preventDefault(); 52 | 53 | if (confirm('Really move?')) { 54 | event.move_info.do_move(); 55 | } 56 | } 57 | ); 58 | {% endhighlight %} 59 | 60 | Note that if you want to serialise the tree, for example to POST back to a server, you need to let tree complete the move first: 61 | 62 | {% highlight js %} 63 | $('#tree1').on( 64 | 'tree.move', 65 | function(event) 66 | { 67 | event.preventDefault(); 68 | // do the move first, and _then_ POST back. 69 | event.move_info.do_move(); 70 | $.post('your_url', {tree: $(this).tree('toJson')}); 71 | } 72 | ); 73 | {% endhighlight %} 74 | -------------------------------------------------------------------------------- /docs/_entries/events/tree-open.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: tree.open 3 | name: event-tree-open 4 | --- 5 | 6 | Called when a node is opened. 7 | 8 | {% highlight js %} 9 | $('#tree1').on( 10 | 'tree.open', 11 | function(e) { 12 | console.log(e.node); 13 | } 14 | ); 15 | {% endhighlight %} 16 | -------------------------------------------------------------------------------- /docs/_entries/events/tree-refresh.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: tree.refresh 3 | name: event-tree-refresh 4 | --- 5 | 6 | The `tree.refresh` event is triggered when the tree is repainted. 7 | 8 | Examples when the `tree.refresh` event is fired: 9 | 10 | - after the first draw of the tree 11 | - after a node is moved 12 | - after the `updateNode` method is called 13 | -------------------------------------------------------------------------------- /docs/_entries/events/tree-select.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: tree.select 3 | name: event-tree-select 4 | --- 5 | 6 | Triggered when a tree node is selected or deselected. 7 | 8 | If a node is selected, then **event.node** contains the selected node. 9 | 10 | If a node is deselected, then the **event.node** property is null. 11 | 12 | {% highlight js %} 13 | $('#tree1').on( 14 | 'tree.select', 15 | function(event) { 16 | if (event.node) { 17 | // A node was selected 18 | const node = event.node; 19 | alert(node.name); 20 | } 21 | else { 22 | // event.node is null 23 | // A node was deselected 24 | // event.previous_node contains the deselected node 25 | } 26 | } 27 | ); 28 | {% endhighlight %} 29 | -------------------------------------------------------------------------------- /docs/_entries/functions/addnodeafter.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: addNodeAfter 3 | name: functions-addnodeafter 4 | --- 5 | 6 | **function addNodeAfter(newNodeInfo, existingNode);** 7 | 8 | Add a new node after this existing node. 9 | 10 | {% highlight js %} 11 | var node1 = $('#tree1').tree('getNodeByName', 'node1'); 12 | $('#tree1').tree( 13 | 'addNodeAfter', 14 | { 15 | name: 'new_node', 16 | id: 456 17 | }, 18 | node1 19 | ); 20 | {% endhighlight %} 21 | -------------------------------------------------------------------------------- /docs/_entries/functions/addnodebefore.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: addNodeBefore 3 | name: functions-addnodebefore 4 | --- 5 | 6 | **function addNodeBefore(newNodeInfo, existingNode);** 7 | 8 | Add a new node before this existing node. 9 | -------------------------------------------------------------------------------- /docs/_entries/functions/addparentnode.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: addParentNode 3 | name: functions-addparentnode 4 | --- 5 | 6 | **function addParentNode(newNodeInfo, existingNode);** 7 | 8 | Add a new node as parent of this existing node. 9 | 10 | {% highlight js %} 11 | var node1 = $('#tree1').tree('getNodeByName', 'node1'); 12 | $('#tree1').tree( 13 | 'addParentNode', 14 | { 15 | name: 'new_parent', 16 | id: 456 17 | }, 18 | node1 19 | ); 20 | {% endhighlight %} 21 | -------------------------------------------------------------------------------- /docs/_entries/functions/appendnode.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: appendNode 3 | name: functions-appendnode 4 | --- 5 | 6 | **function appendNode(newNodeInfo, parentNode);** 7 | 8 | Add a node to this parent node. If **parentNode** is empty, then the new node becomes a root node. 9 | 10 | {% highlight js %} 11 | var parentNode = $tree.tree('getNodeById', 123); 12 | 13 | $tree.tree( 14 | 'appendNode', 15 | { 16 | name: 'new_node', 17 | id: 456 18 | }, 19 | parentNode 20 | ); 21 | {% endhighlight %} 22 | 23 | To add a root node, leave *parent_node* empty: 24 | 25 | {% highlight js %} 26 | $tree.tree( 27 | 'appendNode', 28 | { 29 | name: 'new_node', 30 | id: 456 31 | } 32 | ); 33 | {% endhighlight %} 34 | 35 | It's also possible to append a subtree: 36 | 37 | {% highlight js %} 38 | $tree.tree( 39 | 'appendNode', 40 | { 41 | name: 'new_node', 42 | id: 456, 43 | children: [ 44 | { name: 'child1', id: 457 }, 45 | { name: 'child2', id: 458 } 46 | ] 47 | }, 48 | parentNode 49 | ); 50 | {% endhighlight %} 51 | -------------------------------------------------------------------------------- /docs/_entries/functions/closenode.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: closeNode 3 | name: functions-closenode 4 | --- 5 | 6 | **function closeNode(node);** 7 | 8 | **function closeNode(node, slide);** 9 | 10 | Close this node. The node must have child nodes. 11 | 12 | Parameter **slide**: close the node using a slide animation (default is true). 13 | 14 | {% highlight js %} 15 | var node = $tree.tree('getNodeById', 123); 16 | $tree.tree('closeNode', node); 17 | {% endhighlight %} 18 | 19 | To close the node without the slide animation, call with **slide** parameter is false. 20 | 21 | {% highlight js %} 22 | $tree.tree('closeNode', node, false); 23 | {% endhighlight %} 24 | -------------------------------------------------------------------------------- /docs/_entries/functions/destroy.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: destroy 3 | name: functions-destroy 4 | --- 5 | 6 | **function destroy();** 7 | 8 | Destroy the tree. This removes the dom elements and event bindings. 9 | 10 | {% highlight js %} 11 | $('#tree1').tree('destroy'); 12 | {% endhighlight %} 13 | -------------------------------------------------------------------------------- /docs/_entries/functions/getnodebycallback.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: getNodeByCallback 3 | name: functions-getnodebycallback 4 | --- 5 | 6 | **function getNodeByCallback(callback);** 7 | 8 | Get a tree node using a callback. The callback should return true if the node is found. 9 | 10 | {% highlight js %} 11 | var node = $('#tree1').tree( 12 | 'getNodeByCallback', 13 | function(node) { 14 | if (node.name == 'abc') { 15 | // Node is found; return true 16 | return true; 17 | } 18 | else { 19 | // Node not found; continue searching 20 | return false; 21 | } 22 | } 23 | ); 24 | {% endhighlight %} 25 | -------------------------------------------------------------------------------- /docs/_entries/functions/getnodebyhtmlelement.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: getNodeByHtmlElement 3 | name: functions-getnodebyhtmlelement 4 | --- 5 | 6 | **function getNodeByHtmlElement(htmlElement);** 7 | 8 | Get a tree node by an html element. The html htmlElement should be: 9 | 10 | * The `li` element for the node 11 | * Or, an element inside the `li`. For example the `span` for the title. 12 | 13 | {% highlight js %} 14 | var element = document.querySelector('#tree1 .jqtree-title'); 15 | 16 | var node = $('#tree1').tree('getNodeByHtmlElement', element); 17 | 18 | console.log(node); 19 | {% endhighlight %} 20 | 21 | The element can also be a jquery element: 22 | 23 | {% highlight js %} 24 | var $element = $('#tree1 .jqtree-title'); 25 | 26 | var node = $('#tree1').tree('getNodeByHtmlElement', $element); 27 | 28 | console.log(node); 29 | {% endhighlight %} 30 | -------------------------------------------------------------------------------- /docs/_entries/functions/getnodebyid.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: getNodeById 3 | name: functions-getnodebyid 4 | --- 5 | 6 | **function getNodeById(id);** 7 | 8 | Get a tree node by node-id. This assumes that you have given the nodes in the data a unique id. 9 | 10 | {% highlight js %} 11 | var $tree = $('#tree1'); 12 | var data = [ 13 | { id: 10, name: 'n1' }, 14 | { id: 11, name: 'n2' } 15 | ]; 16 | 17 | $tree.tree({ 18 | data: data 19 | }); 20 | var node = $tree.tree('getNodeById', 10); 21 | {% endhighlight %} 22 | -------------------------------------------------------------------------------- /docs/_entries/functions/getselectednode.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: getSelectedNode 3 | name: functions-getselectednode 4 | --- 5 | 6 | Get the selected node. Returns the row data or false. 7 | 8 | {% highlight js %} 9 | var node = $tree.tree('getSelectedNode'); 10 | {% endhighlight %} 11 | -------------------------------------------------------------------------------- /docs/_entries/functions/getstate.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: getState 3 | name: functions-getstate 4 | --- 5 | 6 | Get the state of tree: which nodes are open and which one is selected? 7 | 8 | Returns a javascript object that contains the ids of open nodes and selected nodes: 9 | 10 | {% highlight js %} 11 | { 12 | open_nodes: [1, 2, 3], 13 | selected_node: [4, 5, 6] 14 | } 15 | {% endhighlight %} 16 | 17 | If you want to use this function, then your tree data should include an **id** property for each node. 18 | 19 | You can use this function in combination with [setState](#functions-setstate) to save and restore the tree state. 20 | -------------------------------------------------------------------------------- /docs/_entries/functions/gettree.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: getTree 3 | name: functions-gettree 4 | --- 5 | 6 | **function getTree();** 7 | 8 | Get the root node of the tree. 9 | 10 | {% highlight js %} 11 | var treeData = $('#tree1').tree('getTree'); 12 | {% endhighlight %} 13 | -------------------------------------------------------------------------------- /docs/_entries/functions/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Functions 3 | name: functions 4 | section: true 5 | --- 6 | 7 | -------------------------------------------------------------------------------- /docs/_entries/functions/is-node-selected.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: isNodeSelected 3 | name: multiple-selection-is-node-selected 4 | --- 5 | 6 | Return if this node is selected. 7 | 8 | {% highlight js %} 9 | var node = $('#tree1').tree('getNodeById', 123); 10 | var isSelected = $('#tree1').tree('isNodeSelected', node); 11 | {% endhighlight %} 12 | -------------------------------------------------------------------------------- /docs/_entries/functions/isdragging.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: isDragging 3 | name: functions-isdragging 4 | --- 5 | 6 | **function isDragging();** 7 | 8 | Is currently a node being dragged for drag-and-drop? Returns `True` or `False`. 9 | 10 | {% highlight js %} 11 | const isDragging = $('#tree1').tree('isDragging'); 12 | {% endhighlight %} 13 | -------------------------------------------------------------------------------- /docs/_entries/functions/loaddata.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: loadData 3 | name: functions-loaddata 4 | --- 5 | 6 | **function loadData(data);** 7 | 8 | **function loadData(data, parentNode);** 9 | 10 | Load data in the tree. The data is array of nodes. 11 | 12 | You can **replace the whole tree** or you can **load a subtree**. 13 | 14 | {% highlight js %} 15 | // Assuming the tree exists 16 | var newData = [ 17 | { 18 | name: 'node1', 19 | children: [ 20 | { name: 'child1' }, 21 | { name: 'child2' } 22 | ] 23 | }, 24 | { 25 | name: 'node2', 26 | children: [ 27 | { name: 'child3' } 28 | ] 29 | } 30 | ]; 31 | $('#tree1').tree('loadData', newData); 32 | {% endhighlight %} 33 | 34 | Load a subtree: 35 | 36 | {% highlight js %} 37 | // Get node by id (this assumes that the nodes have an id) 38 | var node = $('#tree1').tree('getNodeById', 100); 39 | 40 | // Add new nodes 41 | var data = [ 42 | { name: 'new node' }, 43 | { name: 'another new node' } 44 | ]; 45 | $('#tree1').tree('loadData', data, node); 46 | {% endhighlight %} 47 | -------------------------------------------------------------------------------- /docs/_entries/functions/loaddatafromurl.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: loadDataFromUrl 3 | name: functions-loaddatafromurl 4 | --- 5 | 6 | **function loadDataFromUrl(url);** 7 | 8 | **function loadDataFromUrl(url, parentNode);** 9 | 10 | **function loadDataFromUrl(parentNode);** 11 | 12 | Load data in the tree from an url using ajax. You can **replace the whole tree** or you can **load a subtree**. 13 | 14 | {% highlight js %} 15 | $('#tree1').tree('loadDataFromUrl', '/category/tree/'); 16 | {% endhighlight %} 17 | 18 | Load a subtree: 19 | 20 | {% highlight js %} 21 | var node = $('#tree1').tree('getNodeById', 123); 22 | $('#tree1').tree('loadDataFromUrl', '/category/tree/123', node); 23 | {% endhighlight %} 24 | 25 | You can also omit the url. In this case jqTree will generate a url for you. This is very useful if you use the load-on-demand feature: 26 | 27 | {% highlight js %} 28 | var $tree = $('#tree1'); 29 | 30 | $tree.tree({ 31 | dataUrl: '/my_data/' 32 | }); 33 | 34 | var node = $tree.tree('getNodeById', 456); 35 | 36 | // jqTree will load data from /my_data/?node=456 37 | $tree.tree('loadDataFromUrl', node); 38 | {% endhighlight %} 39 | 40 | You can also add an **on_finished** callback parameter that will be called when the data is loaded: 41 | 42 | **function loadDataFromUrl(url, parentNode, onFinished);** 43 | 44 | **function loadDataFromUrl(parentNode, onFinished);** 45 | 46 | {% highlight js %} 47 | $('#tree1').tree( 48 | 'loadDataFromUrl', 49 | '/category/tree/123', 50 | null, 51 | function() { 52 | alert('data is loaded'); 53 | } 54 | ); 55 | {% endhighlight %} 56 | -------------------------------------------------------------------------------- /docs/_entries/functions/movedown.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: moveDown 3 | name: functions-movedown 4 | --- 5 | 6 | **function moveDown()** 7 | 8 | Select the next node. This does the same as the *down* key. -------------------------------------------------------------------------------- /docs/_entries/functions/movenode.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: moveNode 3 | name: functions-movenode 4 | --- 5 | 6 | **function moveNode(node, targetNode, position);** 7 | 8 | Move a node. Position can be 'before', 'after' or 'inside'. 9 | 10 | {% highlight js %} 11 | var node = $tree.tree('getNodeById', 1); 12 | var targetNode = $tree.tree('getNodeById', 2); 13 | 14 | $tree.tree('moveNode', node, targetNode, 'after'); 15 | {% endhighlight %} 16 | -------------------------------------------------------------------------------- /docs/_entries/functions/moveup.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: moveUp 3 | name: functions-moveup 4 | --- 5 | 6 | **function moveUp()** 7 | 8 | Select the previous node. This does the same as the *up* key. -------------------------------------------------------------------------------- /docs/_entries/functions/opennode.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: openNode 3 | name: functions-opennode 4 | --- 5 | 6 | **function openNode(node);** 7 | 8 | **function openNode(node, slide);** 9 | 10 | **function openNode(node, onFinished);** 11 | 12 | **function openNode(node, slide, onFinished);** 13 | 14 | Open this node. The node must have child nodes. 15 | 16 | Parameter **slide (optional)**: open the node using a slide animation (default is true). 17 | Parameter **onFinished (optional)**: callback when the node is opened; this also works for nodes that are loaded lazily 18 | 19 | {% highlight js %} 20 | // create tree 21 | var $tree = $('#tree1'); 22 | $tree.tree({ 23 | data: data 24 | }); 25 | 26 | var node = $tree.tree('getNodeById', 123); 27 | $tree.tree('openNode', node); 28 | {% endhighlight %} 29 | 30 | To open the node without the slide animation, call with **slide** parameter is false. 31 | 32 | {% highlight js %} 33 | $tree.tree('openNode', node, false); 34 | {% endhighlight %} 35 | 36 | Example with `on_finished` callback: 37 | 38 | {% highlight js %} 39 | function handleOpened(node) { 40 | console.log('openende node', node.name); 41 | } 42 | 43 | $tree.tree('openNode', node, handleOpened); 44 | {% endhighlight %} 45 | -------------------------------------------------------------------------------- /docs/_entries/functions/prependnode.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: prependNode 3 | name: functions-prependnode 4 | --- 5 | 6 | **function prependNode(newNodeInfo, parentNode);** 7 | 8 | Add a node to this parent node as the first child. If **parentNode** is empty, then the new node becomes a root node. 9 | 10 | {% highlight js %} 11 | var parentNode = $tree.tree('getNodeById', 123); 12 | 13 | $tree.tree( 14 | 'prependNode', 15 | { 16 | name: 'new_node', 17 | id: 456 18 | }, 19 | parentNode 20 | ); 21 | {% endhighlight %} 22 | -------------------------------------------------------------------------------- /docs/_entries/functions/refresh.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: refresh 3 | name: functions-refresh 4 | --- 5 | 6 | **function refresh();** 7 | 8 | Refresh the rendered nodes. In most cases you will not use this, because tree functions will rerender automatically. E.g. The functions `openNode` and `updateNode` rerender automatically. 9 | 10 | {% highlight js %} 11 | $('#tree1').tree('refresh'); 12 | {% endhighlight %} 13 | -------------------------------------------------------------------------------- /docs/_entries/functions/reload.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: reload 3 | name: functions-reload 4 | --- 5 | 6 | **function reload();** 7 | 8 | **function reload(onFinished);** 9 | 10 | Reload data from the server. 11 | 12 | * Call `onFinished` when the data is loaded. 13 | 14 | {% highlight js %} 15 | $('#tree1').tree('reload'); 16 | {% endhighlight %} 17 | 18 | {% highlight js %} 19 | $('#tree1').tree('reload', function() { 20 | console.log('data is loaded'); 21 | }); 22 | {% endhighlight %} 23 | -------------------------------------------------------------------------------- /docs/_entries/functions/removenode.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: removeNode 3 | name: functions-removenode 4 | --- 5 | 6 | **function removeNode(node);** 7 | 8 | Remove node from the tree. 9 | 10 | {% highlight js %} 11 | $('#tree1').tree('removeNode', node); 12 | {% endhighlight %} 13 | -------------------------------------------------------------------------------- /docs/_entries/functions/scrolltonode.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: scrollToNode 3 | name: functions-scrolltonode 4 | --- 5 | 6 | **function scrollToNode(node);** 7 | 8 | Scroll to this node. This is useful if the tree is in a container div and is scrollable. 9 | 10 | {% highlight js %} 11 | var node = $tree.tree('getNodeById', 1); 12 | $tree.tree('scrollToNode', node); 13 | {% endhighlight %} 14 | -------------------------------------------------------------------------------- /docs/_entries/functions/selectnode.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: selectNode 3 | name: functions-selectnode 4 | --- 5 | 6 | **function selectNode(node);** 7 | 8 | **function selectNode(null);** 9 | 10 | **function selectNode(node, { mustToggle, mustSetFocus });** 11 | 12 | Select this node. 13 | 14 | You can deselect the current node by calling **selectNode(null)**. 15 | 16 | {% highlight js %} 17 | // create tree 18 | const $tree = $('#tree1'); 19 | $tree.tree({ 20 | data: data, 21 | selectable: true 22 | }); 23 | 24 | const node = $tree.tree('getNodeById', 123); 25 | $tree.tree('selectNode', node); 26 | {% endhighlight %} 27 | 28 | **Options** 29 | 30 | * **mustSetFocus**: 31 | * **true (default)**: set the focus to the node; only do this on selection, not deselection 32 | * **false**: do not set the focus 33 | * **mustToggle**: 34 | * **true (default)**: toggle; deselected if selected and vice versa 35 | * **false**: select the node, never deselect 36 | 37 | {% highlight js %} 38 | const node = $tree.tree('getNodeById', 123); 39 | $tree.tree('selectNode', { mustSetFocus: false }); 40 | {% endhighlight %} 41 | 42 | {% highlight js %} 43 | const node = $tree.tree('getNodeById', 123); 44 | $tree.tree('selectNode', { mustToggle: false }); 45 | {% endhighlight %} 46 | -------------------------------------------------------------------------------- /docs/_entries/functions/setoption.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: setOption 3 | name: functions-setoption 4 | --- 5 | 6 | **function setOption(option, value);** 7 | 8 | Set a tree option. These are the same options that you can set when creating the tree. 9 | 10 | {% highlight js %} 11 | $('#tree1').tree('setOption', 'keyboardSupport', false); 12 | {% endhighlight %} 13 | -------------------------------------------------------------------------------- /docs/_entries/functions/setstate.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: setState 3 | name: functions-setstate 4 | --- 5 | 6 | Set the state of the tree: which nodes are open and which one is selected? 7 | 8 | See [getState](#functions-getstate) for more information. 9 | -------------------------------------------------------------------------------- /docs/_entries/functions/toggle.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: toggle 3 | name: functions-toggle 4 | --- 5 | 6 | **function toggle(node);** 7 | 8 | **function toggle(node, slide);** 9 | 10 | * slide: true / false 11 | 12 | Open or close the tree node. 13 | 14 | Default: toggle with slide animation: 15 | 16 | {% highlight js %} 17 | var node = $tree.tree('getNodeById', 123); 18 | $tree.tree('toggle', node); 19 | {% endhighlight %} 20 | 21 | Toggle without animation: 22 | 23 | {% highlight js %} 24 | $tree.tree('toggle', node, false); 25 | {% endhighlight %} 26 | -------------------------------------------------------------------------------- /docs/_entries/functions/tojson.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: toJson 3 | name: functions-tojson 4 | --- 5 | 6 | **function toJson();** 7 | 8 | Get the tree data as json. 9 | 10 | {% highlight js %} 11 | // Assuming the tree exists 12 | $('#tree1').tree('toJson'); 13 | {% endhighlight %} 14 | -------------------------------------------------------------------------------- /docs/_entries/functions/updatenode.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: updateNode 3 | name: functions-updatenode 4 | --- 5 | 6 | **function updateNode(node, name);** 7 | 8 | **function updateNode(node, data);** 9 | 10 | Update the title of a node. You can also update the data. 11 | 12 | Update the name: 13 | 14 | {% highlight js %} 15 | var node = $tree.tree('getNodeById', 123); 16 | 17 | $tree.tree('updateNode', node, 'new name'); 18 | {% endhighlight %} 19 | 20 | Update the data (including the name) 21 | 22 | {% highlight js %} 23 | var node = $tree.tree('getNodeById', 123); 24 | 25 | $tree.tree( 26 | 'updateNode', 27 | node, 28 | { 29 | name: 'new name', 30 | id: 1, 31 | otherProperty: 'abc' 32 | } 33 | ); 34 | {% endhighlight %} 35 | 36 | It is also possible to update the children. Note that this removes the existing children: 37 | 38 | {% highlight js %} 39 | $tree.tree( 40 | 'updateNode', 41 | node, 42 | { 43 | name: 'new name', 44 | id: 1, 45 | children: [ 46 | { name: 'child1', id: 2 } 47 | ] 48 | } 49 | ); 50 | {% endhighlight %} 51 | -------------------------------------------------------------------------------- /docs/_entries/general/demo.html: -------------------------------------------------------------------------------- 1 | --- 2 | title: Demo 3 | name: demo 4 | --- 5 | 6 | {% highlight js %} 7 | var data = [ 8 | { 9 | name: 'node1', id: 1, 10 | children: [ 11 | { name: 'child1', id: 2 }, 12 | { name: 'child2', id: 3 } 13 | ] 14 | }, 15 | { 16 | name: 'node2', id: 4, 17 | children: [ 18 | { name: 'child3', id: 5 } 19 | ] 20 | } 21 | ]; 22 | $('#tree1').tree({ 23 | data: data, 24 | autoOpen: true, 25 | dragAndDrop: true 26 | }); 27 | {% endhighlight %} 28 |
29 | -------------------------------------------------------------------------------- /docs/_entries/general/downloads.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Downloads 3 | name: downloads 4 | --- 5 | 6 | * All (version {{ site.jqtree_version }}): [jqTree.tar.gz](https://github.com/mbraak/jqTree/tarball/master) 7 | * Javascript: [tree.jquery.js](tree.jquery.js) 8 | * Css: [jqtree.css](jqtree.css) 9 | * Image: [jqtree-circle.png](jqtree-circle.png) 10 | -------------------------------------------------------------------------------- /docs/_entries/general/examples.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Examples 3 | name: examples 4 | --- 5 | 6 | {% for e in site.examples %} 7 | * [{{ e.title }}]({{ site.baseurl }}{{ e.url }}) 8 | {% endfor %} 9 | -------------------------------------------------------------------------------- /docs/_entries/general/features.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Features 3 | name: features 4 | --- 5 | 6 | - Create a tree from JSON data 7 | - Load data using ajax 8 | - Drag and drop 9 | - Saves the state 10 | - Keyboard support 11 | - Lazy loading 12 | - Works on all modern browsers 13 | - Written in Typescript 14 | 15 | The project is [hosted on github](https://github.com/mbraak/jqTree). 16 | -------------------------------------------------------------------------------- /docs/_entries/general/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: General 3 | name: general 4 | section: true 5 | hide_title: true 6 | --- 7 | 8 | -------------------------------------------------------------------------------- /docs/_entries/general/introduction.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Introduction 3 | name: introduction 4 | --- 5 | 6 | JqTree is a jQuery widget for displaying a **tree structure** in html. It supports **json data**, loading via 7 | **ajax** and **drag-and-drop**. 8 | 9 | [![NPM version](https://img.shields.io/npm/v/jqtree.svg)](https://www.npmjs.com/package/jqtree) 10 | -------------------------------------------------------------------------------- /docs/_entries/general/requirements.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Requirements 3 | name: requirements 4 | --- 5 | 6 | * [jQuery](http://jquery.com) 1.9+, 2.x or 3.x 7 | -------------------------------------------------------------------------------- /docs/_entries/general/tutorial.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Tutorial 3 | name: Tutorial 4 | --- 5 | 6 | Include [jQuery](http://code.jquery.com/jquery.min.js) 7 | 8 | {% highlight html %} 9 | 10 | {% endhighlight %} 11 | 12 | Include tree.jquery.js: 13 | 14 | {% highlight html %} 15 | 16 | {% endhighlight %} 17 | 18 | Include jqtree.css: 19 | 20 | {% highlight html %} 21 | 22 | {% endhighlight %} 23 | 24 | Create a div. 25 | 26 | {% highlight html %} 27 |
28 | {% endhighlight %} 29 | 30 | Create tree data. 31 | 32 | {% highlight js %} 33 | var data = [ 34 | { 35 | name: 'node1', 36 | children: [ 37 | { name: 'child1' }, 38 | { name: 'child2' } 39 | ] 40 | }, 41 | { 42 | name: 'node2', 43 | children: [ 44 | { name: 'child3' } 45 | ] 46 | } 47 | ]; 48 | {% endhighlight %} 49 | 50 | Create tree widget. 51 | 52 | {% highlight js %} 53 | $(function() { 54 | $('#tree1').tree({ 55 | data: data 56 | }); 57 | }); 58 | {% endhighlight %} 59 | 60 | Alternatively, get the data from the server. 61 | 62 | {% highlight js %} 63 | $.getJSON( 64 | '/some_url/', 65 | function(data) { 66 | $('#tree1').tree({ 67 | data: data 68 | }); 69 | } 70 | ); 71 | {% endhighlight %} 72 | -------------------------------------------------------------------------------- /docs/_entries/general/usecases.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Use cases 3 | name: usecases 4 | --- 5 | 6 | Use cases or implementations of JqTree 7 | 8 | ##### With AngularJS and FireBase 9 | * [https://github.com/romelgomez/jqtree-angularjs-firebase-example](https://github.com/romelgomez/jqtree-angularjs-firebase-example) 10 | 11 | ##### With CakePHP and OpenShift 12 | * Code: [https://github.com/romelgomez/jqtree-cakephp-openshift-example](https://github.com/romelgomez/jqtree-cakephp-openshift-example) 13 | 14 | ##### With Spring MVC and Google App Engine 15 | * Code: [https://github.com/romelgomez/jqtree-spring-mvc-gae-example](https://github.com/romelgomez/jqtree-spring-mvc-gae-example) 16 | -------------------------------------------------------------------------------- /docs/_entries/multiple_selection/add-to-selection.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: addToSelection 3 | name: multiple-selection-add-to-selection 4 | --- 5 | 6 | Add this node to the selection. Also set the focus to the node. 7 | 8 | **function addToSelection(node, mustSetFocus = true);** 9 | 10 | Parameter **mustSetFocus**: set the focus to the node (default true). 11 | 12 | {% highlight js %} 13 | var node = $('#tree1').tree('getNodeById', 123); 14 | $('#tree1').tree('addToSelection', node); 15 | {% endhighlight %} 16 | 17 | Without setting the focus: 18 | 19 | {% highlight js %} 20 | $('#tree1').tree('addToSelection', node, false); 21 | {% endhighlight %} 22 | -------------------------------------------------------------------------------- /docs/_entries/multiple_selection/get-selected-nodes.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: getSelectedNodes 3 | name: multiple-selection-get-selected-nodes 4 | --- 5 | 6 | Get the selected nodes. Return an array of nodes 7 | 8 | {% highlight js %} 9 | var node = $tree.tree('getSelectedNodes'); 10 | {% endhighlight %} 11 | -------------------------------------------------------------------------------- /docs/_entries/multiple_selection/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Multiple selection 3 | name: multiple-selection 4 | section: true 5 | --- 6 | 7 | Jqtree has some functions that can help you to implement multiple selection. See [Example 8 - multiple select](examples/08_multiple_select). 8 | 9 | In order for multiple selection to work, you must give the nodes an id. 10 | -------------------------------------------------------------------------------- /docs/_entries/multiple_selection/remove-from-selection.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: removeFromSelection 3 | name: multiple-selection-remove-from-selection 4 | --- 5 | 6 | Remove this node from the selection. 7 | 8 | {% highlight js %} 9 | var node = $('#tree1').tree('getNodeById', 123); 10 | $('#tree1').tree('removeFromSelection', node); 11 | {% endhighlight %} 12 | -------------------------------------------------------------------------------- /docs/_entries/node/children.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: children 3 | name: node-functions-children 4 | --- 5 | 6 | You can access the children of a node using the **children** property. 7 | 8 | {% highlight js %} 9 | for (var i=0; i < node.children.length; i++) { 10 | var child = node.children[i]; 11 | } 12 | {% endhighlight %} 13 | -------------------------------------------------------------------------------- /docs/_entries/node/getdata.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: getData 3 | name: node-functions-getdata 4 | --- 5 | 6 | **function getData(includeParent = false);** 7 | 8 | Get the subtree of this node. 9 | 10 | **includeParent** 11 | 12 | * **true**: include node and children 13 | * **false**: only include children (default) 14 | 15 | {% highlight js %} 16 | var data = node.getData(); 17 | {% endhighlight %} 18 | -------------------------------------------------------------------------------- /docs/_entries/node/getlevel.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: getLevel 3 | name: node-functions-getlevel 4 | --- 5 | 6 | Get the level of a node. The level is distance of a node to the root node. 7 | 8 | {% highlight js %} 9 | var node = $('#tree1').tree('getNodeById', 123); 10 | 11 | // result is e.g. 2 12 | var level = node.getLevel(); 13 | {% endhighlight %} 14 | -------------------------------------------------------------------------------- /docs/_entries/node/getnextnode.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: getNextNode 3 | name: node-functions-getnextnode 4 | --- 5 | 6 | Get the next node in the tree. This is the next sibling, if there is one. Or, if there is no next sibling, a node further down in the tree. 7 | 8 | - Returns a node or null. 9 | 10 | {% highlight js %} 11 | const nextNode = node.getNextNode(); 12 | {% endhighlight %} 13 | -------------------------------------------------------------------------------- /docs/_entries/node/getnextsibling.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: getNextSibling 3 | name: node-functions-getnextsibling 4 | --- 5 | 6 | Get the next sibling of this node. Returns a node or null. 7 | 8 | {% highlight js %} 9 | const nextSibling = node.getNextSibling(); 10 | {% endhighlight %} 11 | -------------------------------------------------------------------------------- /docs/_entries/node/getnextvisiblenode.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: getNextVisibleNode 3 | name: node-functions-getnextvisiblenode 4 | --- 5 | 6 | Get the next visible node in the tree. Does the same as using the _down_ key. 7 | 8 | This is the previous sibling, if there is one. Or, if there is no previous sibling, a node further up in the tree that is visible. 9 | 10 | - Returns a node or null. 11 | - A node is visible if all its parents are open. 12 | 13 | {% highlight js %} 14 | const nextNode = node.getNextVisibleNode(); 15 | {% endhighlight %} 16 | -------------------------------------------------------------------------------- /docs/_entries/node/getpreviousnode.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: getPreviousNode 3 | name: node-functions-getpreviousnode 4 | --- 5 | 6 | Return the previous node in the tree. This is the previous sibling, if there is one. Or, if there is no previous sibling, a node further up in the tree. 7 | 8 | - Returns a node or null. 9 | 10 | {% highlight js %} 11 | const previousNode = node.getPreviousNode(); 12 | {% endhighlight %} 13 | -------------------------------------------------------------------------------- /docs/_entries/node/getprevioussibling.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: getPreviousSibling 3 | name: node-functions-getprevioussibling 4 | --- 5 | 6 | Get the previous sibling of this node. Returns a node or null. 7 | 8 | {% highlight js %} 9 | const previousSibling = node.getPreviousSibling(); 10 | {% endhighlight %} 11 | -------------------------------------------------------------------------------- /docs/_entries/node/getpreviousvisiblenode.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: getPreviousVisibleNode 3 | name: node-functions-getpreviousvisiblenode 4 | --- 5 | 6 | Get the previous visible node in the tree. Does the same as using the _up_ key. 7 | 8 | This is the previous sibling, if there is one. Or, if there is no previous sibling, a node further up in the tree that is visible. 9 | 10 | - Returns a node or null. 11 | - A node is visible if all its parents are open. 12 | 13 | {% highlight js %} 14 | const previousNode = node.getPreviousVisibleNode(); 15 | {% endhighlight %} 16 | -------------------------------------------------------------------------------- /docs/_entries/node/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Node functions 3 | name: node-functions 4 | section: true 5 | --- 6 | 7 | You can access a node using for example [getNodeById](#functions-getnodebyid) function: 8 | 9 | {% highlight js %} 10 | var node = $('#tree1').tree('getNodeById', 123); 11 | {% endhighlight %} 12 | 13 | The Node object has the following properties and functions: 14 | -------------------------------------------------------------------------------- /docs/_entries/node/parent.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: parent 3 | name: node-functions-parent 4 | --- 5 | 6 | You can access the parent of a node using the **parent** property. 7 | 8 | {% highlight js %} 9 | var parentNode = node.parent; 10 | {% endhighlight %} 11 | -------------------------------------------------------------------------------- /docs/_entries/options/animationspeed.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: animationSpeed 3 | name: options-animationspeed 4 | --- 5 | 6 | Determine the speed of the slide animation. The value can be a number in millseconds, or a string like 'slow' or 'fast'. The default is 'fast'. 7 | 8 | -------------------------------------------------------------------------------- /docs/_entries/options/autoescape.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: autoEscape 3 | name: options-autoescape 4 | --- 5 | 6 | Determine if text is autoescaped. The default is true. 7 | -------------------------------------------------------------------------------- /docs/_entries/options/autoopen.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: autoOpen 3 | name: options-autoopen 4 | --- 5 | 6 | Open nodes initially. 7 | 8 | * **true**: open all nodes. 9 | * **false (default)**: do nothing 10 | * **n**: open n levels 11 | 12 | Open all nodes initially: 13 | 14 | {% highlight js %} 15 | $('#tree1').tree({ 16 | data: data, 17 | autoOpen: true 18 | }); 19 | {% endhighlight %} 20 | 21 | Open first level nodes: 22 | 23 | {% highlight js %} 24 | $('#tree1').tree({ 25 | data: data, 26 | autoOpen: 0 27 | }); 28 | {% endhighlight %} 29 | -------------------------------------------------------------------------------- /docs/_entries/options/buttonleft.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: buttonLeft 3 | name: options-buttonleft 4 | --- 5 | 6 | Set the position of the toggle button; can be `true` (left) or `false` (right). The default is `true`. 7 | 8 | {% highlight js %} 9 | $('#tree1').tree({ 10 | buttonLeft: false 11 | }); 12 | {% endhighlight %} 13 | -------------------------------------------------------------------------------- /docs/_entries/options/closedicon.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: closedIcon 3 | name: options-closedicon 4 | --- 5 | 6 | A character or symbol to display on closed nodes. The default is '&#x25ba;' (►) 7 | 8 | The value can be a: 9 | 10 | - **string**. E.g. a unicode character or a text. 11 | - The text is escaped. 12 | - **html element**. E.g. for an icon 13 | - **JQuery element**. Also for an icon 14 | 15 | {% highlight js %} 16 | // String 17 | $('#tree1').tree({ closedIcon: '+' }); 18 | 19 | // Html element 20 | const icon = document.createElement("span"); 21 | icon.className = "icon test"; 22 | $('#tree1').tree({ closedIcon: icon }); 23 | 24 | // JQuery element 25 | $('#tree1').tree({ closedIcon: $('') }); 26 | {% endhighlight %} 27 | -------------------------------------------------------------------------------- /docs/_entries/options/data-url.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: dataUrl 3 | name: options-data-url 4 | --- 5 | 6 | Load the node data from this url. 7 | 8 | {% highlight js %} 9 | $('#tree1').tree({ 10 | dataUrl: '/example_data.json' 11 | }); 12 | {% endhighlight %} 13 | 14 | You can also set the **data-url** attribute on the dom element: 15 | 16 | {% highlight html %} 17 |
18 | 21 | {% endhighlight %} 22 | 23 | You can set additional [jquery ajax options](http://api.jquery.com/jQuery.ajax/) in an object: 24 | 25 | {% highlight js %} 26 | $('#tree1').tree({ 27 | dataUrl: { 28 | url: '/example_data.json', 29 | headers: {'abc': 'def'} 30 | } 31 | }); 32 | {% endhighlight %} 33 | 34 | Or you can use a function: 35 | 36 | {% highlight js %} 37 | $('#tree1').tree({ 38 | dataUrl: function(node) { 39 | return { 40 | url: '/example_data.json', 41 | headers: {'abc': 'def'} 42 | }; 43 | } 44 | }); 45 | {% endhighlight %} 46 | -------------------------------------------------------------------------------- /docs/_entries/options/data.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: data 3 | name: options-data 4 | --- 5 | 6 | Define the contents of the tree. The data is a nested array of objects. This option is required. 7 | 8 | It looks like this: 9 | 10 | {% highlight js %} 11 | var data = [ 12 | { 13 | name: 'node1', 14 | children: [ 15 | { name: 'child1' }, 16 | { name: 'child2' } 17 | ] 18 | }, 19 | { 20 | name: 'node2', 21 | children: [ 22 | { name: 'child3' } 23 | ] 24 | } 25 | ]; 26 | $('#tree1').tree({data: data}); 27 | {% endhighlight %} 28 | 29 | * **name**: name of a node (required) 30 | * Note that you can also use `label` instead of `name` 31 | * **children**: array of child nodes (optional) 32 | * **id**: int or string (optional) 33 | * Must be an int or a string 34 | * Must be unique in the tree 35 | * The `id` property is required if you use the multiple selection feature 36 | 37 | You can also include other data in the objects. You can later access this data. 38 | 39 | For example, to add an id: 40 | 41 | {% highlight js %} 42 | { 43 | name: 'node1', 44 | id: 1 45 | } 46 | {% endhighlight %} 47 | -------------------------------------------------------------------------------- /docs/_entries/options/datafilter.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: dataFilter 3 | name: options-datafilter 4 | --- 5 | 6 | Process the tree data from the server. 7 | 8 | {% highlight js %} 9 | $('#tree1').tree({ 10 | dataUrl: '/my/data/', 11 | dataFilter: function(data) { 12 | // Example: 13 | // the server puts the tree data in 'my_tree_data' 14 | return data['my_tree_data']; 15 | } 16 | }); 17 | {% endhighlight %} 18 | -------------------------------------------------------------------------------- /docs/_entries/options/draganddrop.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: dragAndDrop 3 | name: options-draganddrop 4 | --- 5 | 6 | Turn on dragging and dropping of nodes. 7 | 8 | * **true**: turn on drag and drop 9 | * **false (default)**: do not allow drag and drop 10 | 11 | Example: turn on drag and drop. 12 | 13 | {% highlight js %} 14 | $('#tree1').tree({ 15 | data: data, 16 | dragAndDrop: true 17 | }); 18 | {% endhighlight %} 19 | -------------------------------------------------------------------------------- /docs/_entries/options/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Tree options 3 | name: tree-options 4 | section: true 5 | --- 6 | 7 | -------------------------------------------------------------------------------- /docs/_entries/options/keyboardsupport.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: keyboardSupport 3 | name: options-keyboardsupport 4 | --- 5 | 6 | Enable or disable keyboard support. Default is enabled. 7 | 8 | Example: disable keyboard support. 9 | 10 | {% highlight js %} 11 | $('#tree1').tree({ 12 | keyboardSupport: false 13 | }); 14 | {% endhighlight %} 15 | -------------------------------------------------------------------------------- /docs/_entries/options/oncanmove.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: onCanMove 3 | name: options-oncanmove 4 | --- 5 | 6 | You can override this function to determine if a node can be moved. 7 | 8 | {% highlight js %} 9 | $('#tree1').tree({ 10 | data: data, 11 | dragAndDrop: true, 12 | onCanMove: function(node) { 13 | if (! node.parent.parent) { 14 | // Example: Cannot move root node 15 | return false; 16 | } 17 | else { 18 | return true; 19 | } 20 | } 21 | }); 22 | {% endhighlight %} 23 | -------------------------------------------------------------------------------- /docs/_entries/options/oncanmoveto.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: onCanMoveTo 3 | name: options-oncanmoveto 4 | --- 5 | 6 | You can override this function to determine if a node can be moved to a certain position. 7 | 8 | {% highlight js %} 9 | $('#tree1').tree({ 10 | data: data, 11 | dragAndDrop: true, 12 | onCanMoveTo: function(movedNode, targetNode, position) { 13 | if (targetNode.isMenu) { 14 | // Example: can move inside menu, not before or after 15 | return (position == 'inside'); 16 | } 17 | else { 18 | return true; 19 | } 20 | } 21 | }); 22 | {% endhighlight %} 23 | -------------------------------------------------------------------------------- /docs/_entries/options/oncanselectnode.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: onCanSelectNode 3 | name: options-oncanselectnode 4 | --- 5 | 6 | You can set a function to override if a node can be selected. The function gets a node as parameter, and must return true or false. 7 | 8 | For this to work, the option 'selectable' must be 'true'. 9 | 10 | {% highlight js %} 11 | // Example: nodes with children cannot be selected 12 | $('#tree1').tree({ 13 | data: data, 14 | selectable: true 15 | onCanSelectNode: function(node) { 16 | if (node.children.length == 0) { 17 | // Nodes without children can be selected 18 | return true; 19 | } 20 | else { 21 | // Nodes with children cannot be selected 22 | return false; 23 | } 24 | } 25 | }); 26 | {% endhighlight %} 27 | -------------------------------------------------------------------------------- /docs/_entries/options/oncreateli.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: onCreateLi 3 | name: options-oncreateli 4 | --- 5 | 6 | The function is called for each created node. You can use this to define extra html. 7 | 8 | The function is called with the following parameters: 9 | 10 | * **node**: Node element 11 | * **$li**: Jquery li element 12 | * **isSelected**: is the node selected (true/false) 13 | 14 | {% highlight js %} 15 | $('#tree1').tree({ 16 | data: data, 17 | onCreateLi: function(node, $li, isSelected) { 18 | // Add 'icon' span before title 19 | $li.find('.jqtree-title').before(''); 20 | } 21 | }); 22 | {% endhighlight %} 23 | -------------------------------------------------------------------------------- /docs/_entries/options/ondragmove.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: onDragMove 3 | name: options-ondragmove 4 | --- 5 | 6 | Function that is called when a node is dragged **outside** the tree. This function is called while the node is being dragged. 7 | 8 | * Also see the ``onDragStop`` option. 9 | * The function signature is function(node, event); 10 | 11 | {% highlight js %} 12 | function handleMove(node: Node, e: JQueryEventObject) { 13 | // 14 | } 15 | 16 | $tree.tree({ 17 | dragAndDrop: true, 18 | onDragMove: handleMove, 19 | }); 20 | {% endhighlight %} 21 | -------------------------------------------------------------------------------- /docs/_entries/options/ondragstop.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: onDragStop 3 | name: options-ondragstop 4 | --- 5 | 6 | Function that is called when a node is dragged **outside** the tree. This function is called when user stops dragging. 7 | 8 | * Also see the ``onDragMove`` option. 9 | * The function signature is function(node, event); 10 | 11 | {% highlight js %} 12 | function handleStop(node: Node, e: JQueryEventObject) { 13 | // 14 | } 15 | 16 | $tree.tree({ 17 | dragAndDrop: true, 18 | onDragStop: handleMove, 19 | }); 20 | {% endhighlight %} 21 | -------------------------------------------------------------------------------- /docs/_entries/options/onismovehandle.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: onIsMoveHandle 3 | name: options-onismovehandle 4 | --- 5 | 6 | You can override this function to determine if a dom element can be used to move a node. 7 | 8 | {% highlight js %} 9 | $('#tree1').tree({ 10 | data: data, 11 | onIsMoveHandle: function($element) { 12 | // Only dom elements with 'jqtree-title' class can be used 13 | // as move handle. 14 | return ($element.is('.jqtree-title')); 15 | } 16 | }); 17 | {% endhighlight %} 18 | -------------------------------------------------------------------------------- /docs/_entries/options/onloadfailed.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: onLoadFailed 3 | name: options-onloadfailed 4 | --- 5 | 6 | When loading the data by ajax fails, then the option **onLoadFailed** is called. 7 | 8 | {% highlight js %} 9 | $('#tree1').tree({ 10 | dataUrl: '/my/data/', 11 | onLoadFailed: function(response) { 12 | // 13 | } 14 | }); 15 | {% endhighlight %} 16 | -------------------------------------------------------------------------------- /docs/_entries/options/onloading.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: onLoading 3 | name: options-onloading 4 | --- 5 | 6 | The onLoading parameter is called when the tree data is loading. This gives us the opportunity to display a loading signal. 7 | 8 | Callback looks like this: 9 | 10 | ```js 11 | function (isLoading, node, $el) 12 | ``` 13 | 14 | * **isLoading**: boolean 15 | * true: data is loading 16 | * false: data is loaded 17 | * **node**: 18 | * Node: if a node is loading 19 | * null: if the tree is loading 20 | * **$el**: 21 | * if a node is loading this is the `li` element 22 | * if the tree is loading is the `ul` element of the whole tree 23 | -------------------------------------------------------------------------------- /docs/_entries/options/openedicon.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: openedIcon 3 | name: options-openedicon 4 | --- 5 | 6 | A character or symbol to display on opened nodes. The default is '&#x25bc;' (▼) 7 | 8 | The value can be a: 9 | 10 | - **string**. E.g. a unicode character or a text. 11 | - The text is escaped. 12 | - **html element**. E.g. for an icon 13 | - **JQuery element**. Also for an icon 14 | 15 | {% highlight js %} 16 | // String 17 | $('#tree1').tree({ openedIcon: '-' }); 18 | 19 | // Html element 20 | const icon = document.createElement("span"); 21 | icon.className = "icon test"; 22 | $('#tree1').tree({ openedIcon: icon }); 23 | 24 | // JQuery element 25 | $('#tree1').tree({ openedIcon: $('') }); 26 | {% endhighlight %} 27 | -------------------------------------------------------------------------------- /docs/_entries/options/openfolderdelay.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: openFolderDelay 3 | name: options-openfolderdelay 4 | --- 5 | 6 | Set the delay for opening a folder during drag-and-drop. The delay is in milliseconds. The default is 500 ms. 7 | 8 | * Setting the option to `false` disables opening folders during drag-and-drop. 9 | 10 | {% highlight js %} 11 | $('#tree1').tree({ 12 | dataUrl: '/my/data/', 13 | openFolderDelay: 1000 14 | }); 15 | {% endhighlight %} 16 | -------------------------------------------------------------------------------- /docs/_entries/options/rtl.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: rtl 3 | name: options-rtl 4 | --- 5 | 6 | Set right-to-left support (true / false). Default is false. 7 | 8 | {% highlight js %} 9 | $('#tree1').tree({ 10 | rtl: true 11 | }); 12 | {% endhighlight %} 13 | 14 | You can also set the option using ``data-rtl``. 15 | 16 | {% highlight html %} 17 |
18 | {% endhighlight %} 19 | -------------------------------------------------------------------------------- /docs/_entries/options/savestate.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: saveState 3 | name: options-savestate 4 | --- 5 | 6 | Save and restore the state of the tree automatically. The state is saved in localstorage. 7 | 8 | For this to work, you should give each node in the tree data an id field: 9 | 10 | {% highlight js %} 11 | { 12 | name: 'node1', 13 | id: 123, 14 | children: [ 15 | { 16 | name: 'child1', 17 | id: 124 18 | } 19 | ] 20 | } 21 | {% endhighlight %} 22 | 23 | * **true**: save and restore state in localstorage 24 | * **false (default)**: do nothing 25 | * **string**: save state and use this name to store 26 | 27 | {% highlight js %} 28 | $('#tree1').tree({ 29 | data: data, 30 | saveState: true 31 | }); 32 | {% endhighlight %} 33 | 34 | Example: save state in key 'tree1': 35 | 36 | {% highlight js %} 37 | $('#tree1').tree({ 38 | data: data, 39 | saveState: 'tree1' 40 | }); 41 | {% endhighlight %} 42 | -------------------------------------------------------------------------------- /docs/_entries/options/selectable.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: selectable 3 | name: options-selectable 4 | --- 5 | 6 | Turn on selection of nodes. 7 | 8 | * **true (default)**: turn on selection of nodes 9 | * **false**: turn off selection of nodes 10 | 11 | Example: turn off selection of nodes. 12 | 13 | {% highlight js %} 14 | $('#tree1').tree({ 15 | data: data, 16 | selectable: false 17 | }); 18 | {% endhighlight %} 19 | -------------------------------------------------------------------------------- /docs/_entries/options/showemptyfolder.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: showEmptyFolder 3 | name: options-showemptyfolder 4 | --- 5 | 6 | * **true**: A node with empty children is considered a folder. Meaning: the node has a 'children' attribute, but it's an empty array. 7 | * Show folder icon 8 | * Folder can be opened and closed 9 | * **false (default)**: A node with empty children is considered a child node 10 | 11 | Example with option true: 12 | 13 | {% highlight js %} 14 | const data = [ 15 | { 16 | name: 'node1', 17 | id: 123, 18 | children: [] 19 | } 20 | ]; 21 | 22 | $('#tree1').tree({ 23 | data: data, 24 | showEmptyFolder: true 25 | }); 26 | {% endhighlight %} 27 | -------------------------------------------------------------------------------- /docs/_entries/options/slide.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: slide 3 | name: options-slide 4 | --- 5 | 6 | Turn slide animation on or off. Default is true. 7 | 8 | {% highlight js %} 9 | $('#tree1').tree({ 10 | slide: false 11 | }); 12 | {% endhighlight %} 13 | -------------------------------------------------------------------------------- /docs/_entries/options/start_dnd_delay.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: startDndDelay 3 | name: options-start-dnd-delay 4 | --- 5 | 6 | Sets the delay before drag-and-drop is started. The default is 300 milliseconds. 7 | 8 | {% highlight js %} 9 | $tree.tree({ 10 | dragAndDrop: true, 11 | startDndDelay: 3000, 12 | }); 13 | {% endhighlight %} 14 | -------------------------------------------------------------------------------- /docs/_entries/options/tabindex.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: tabIndex 3 | name: tab-index 4 | --- 5 | 6 | Set the tabindex of the tree. The default is `0`. 7 | 8 | Note that the tabindex is set to the selected node. 9 | 10 | {% highlight js %} 11 | $('#tree1').tree({ 12 | tabIndex: 5 13 | }); 14 | {% endhighlight %} 15 | -------------------------------------------------------------------------------- /docs/_entries/options/usecontextmenu.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: useContextMenu 3 | name: use-context-menu 4 | --- 5 | 6 | Bind the context menu event (true/false). 7 | 8 | **true** (default) 9 | 10 | A right mouse-click will trigger a [tree.contextmenu](#event-tree-contextmenu) event. This overrides the native contextmenu of the browser. 11 | 12 | **false** 13 | 14 | A right mouse-click will trigger the native contextmenu of the browser. 15 | -------------------------------------------------------------------------------- /docs/_examples/01_load_json_data.html: -------------------------------------------------------------------------------- 1 | --- 2 | title: Load json data in javascript tree 3 | js: examples/load_json_data.js 4 | --- 5 | 6 |

7 | « Documentation 8 | Example 2 » 9 |

10 | 11 |

Example 1 - load json data

12 | 13 |
14 | 15 |

16 | In this example we load the data using the data option. 17 | As you can see, the data is an array of objects. 18 |

19 |
    20 |
  • The name property defines the name of the node.
  • 21 |
  • The id is the unique id of the node.
  • 22 |
  • The children property is an array of nodes.
  • 23 |
24 | 25 | {% highlight js %} 26 | var data = [ 27 | { 28 | name: 'node1', id: 1, 29 | children: [ 30 | { name: 'child1', id: 2 }, 31 | { name: 'child2', id: 3 } 32 | ] 33 | }, 34 | { 35 | name: 'node2', id: 4, 36 | children: [ 37 | { name: 'child3', id: 5 } 38 | ] 39 | } 40 | ]; 41 | 42 | $('#tree1').tree({ 43 | data: data 44 | }); 45 | {% endhighlight %} 46 | -------------------------------------------------------------------------------- /docs/_examples/02_load_json_data_from_server.html: -------------------------------------------------------------------------------- 1 | --- 2 | title: Load json data from the server in javascript tree 3 | js: examples/load_json_data_from_server.js 4 | --- 5 | 6 |

7 | « Example 1 8 | Example 3 » 9 |

10 | 11 |

Example 2 - load json data from the server

12 | 13 |
14 | 15 |

16 | In this example we load the data from the server using the data-url property on the dom element. 17 |

18 | 19 |

html

20 | 21 | {% highlight html %} 22 |
23 | {% endhighlight %} 24 | 25 |

javascript

26 | 27 | {% highlight js %} 28 | $('#tree1').tree(); 29 | {% endhighlight %} 30 | -------------------------------------------------------------------------------- /docs/_examples/03_drag_and_drop.html: -------------------------------------------------------------------------------- 1 | --- 2 | title: Javascript tree with drag and drop 3 | js: examples/drag_and_drop.js 4 | --- 5 | 6 |

7 | « Example 2 8 | Example 4 » 9 |

10 | 11 |

Example 3 - Drag and drop

12 | 13 |
14 | 15 |

16 | Let's add drag-and-drop support by setting the option dragAndDrop to true. 17 | You can now drag tree nodes to another position. 18 |

19 | 20 |

21 | Other options: 22 |

23 | 24 |
    25 |
  • The option autoOpen is set to 0 to open the first level of nodes.
  • 26 |
27 | 28 |

html

29 | 30 | {% highlight html %} 31 |
32 | {% endhighlight %} 33 | 34 |

javascript

35 | 36 | {% highlight js %} 37 | var $tree = $('#tree1'); 38 | $tree.tree({ 39 | dragAndDrop: true, 40 | autoOpen: 0 41 | }); 42 | {% endhighlight %} 43 | -------------------------------------------------------------------------------- /docs/_examples/04_save_state.html: -------------------------------------------------------------------------------- 1 | --- 2 | title: Save the state in javascript tree 3 | js: examples/save_state.js 4 | --- 5 | 6 |

7 | « Example 3 8 | Example 5 » 9 |

10 | 11 |

Example 4 - Save the state

12 | 13 |
14 | 15 |

16 | If you set the option saveState to true, then jqtree remembers the tree state after a page reload. 17 |

18 |
    19 |
  • 20 | JqTree save the state into localStorage. 21 |
  • 22 |
23 | 24 |

html

25 | 26 | {% highlight html %} 27 |
28 | {% endhighlight %} 29 | 30 |

javascript

31 | 32 | {% highlight js %} 33 | $('#tree1').tree({ 34 | saveState: true 35 | }); 36 | {% endhighlight %} 37 | 38 |

39 | Giving the saveState a string value sets the storage key. The default key is 'tree'. 40 |

41 | 42 | {% highlight js %} 43 | $('#tree1').tree({ 44 | saveState: 'my-tree' 45 | }); 46 | {% endhighlight %} 47 | -------------------------------------------------------------------------------- /docs/_examples/05_load_on_demand.html: -------------------------------------------------------------------------------- 1 | --- 2 | title: Load nodes on demand from the server in javascript tree 3 | js: examples/load_on_demand.js 4 | --- 5 | 6 |

7 | « Example 4 8 | Example 6 » 9 |

10 | 11 |

Example 5 - Load nodes on demand from the server

12 | 13 | 14 |
15 | 16 |

17 | In this example, the data is loaded on demand from the server. 18 |
19 | To use load on demand, you must do the following: 20 |

21 | 22 |
    23 |
  • 24 | You must specify a data url. In this example this is done using the data-url 25 | html data attribute. 26 |
  • 27 |
  • 28 | Folders that must be loaded on demand must have the load_on_demand property. You can specify this in the data. 29 |
  • 30 |
  • 31 | In this example, the url /nodes/ returns the first level of data (Saurischia and Ornithischians). 32 |
  • 33 |
  • 34 | The url for the load on demand data is <data-url>?node=<node-id>. So, for node 23 this would be 35 | /nodes/?node=23. 36 |
  • 37 |
38 | 39 |

first level of data

40 | 41 | {% highlight js %} 42 | [ 43 | { 44 | "name": "Saurischia", 45 | "id": 1, 46 | "load_on_demand": true 47 | }, 48 | { 49 | "name": "Ornithischians", 50 | "id": 23, 51 | "load_on_demand": true 52 | } 53 | ] 54 | {% endhighlight %} 55 | 56 |

html

57 | 58 | {% highlight html %} 59 |
60 | {% endhighlight %} 61 | 62 |

javascript

63 | 64 | {% highlight js %} 65 | $('#tree1').tree({ 66 | dragAndDrop: true 67 | }); 68 | {% endhighlight %} 69 | -------------------------------------------------------------------------------- /docs/_examples/06_autoescape.html: -------------------------------------------------------------------------------- 1 | --- 2 | title: Javascript tree with autoescape 3 | js: examples/autoescape.js 4 | --- 5 | 6 |

7 | « Example 5 8 | Example 7 » 9 |

10 | 11 |

Example 6 - autoEscape

12 | 13 |

14 | You can put html in the node titles setting the autoEscape option to false. 15 |

16 | 17 |
18 | 19 |

html

20 | 21 | {% highlight html %} 22 |
23 | {% endhighlight %} 24 | 25 |

javascript

26 | 27 | {% highlight js %} 28 | var data = [ 29 | { 30 | name: 'examples', 31 | children: [ 32 | { name: 'Example 1' }, 33 | { name: 'Example 2' }, 34 | 'Example ' 35 | ] 36 | } 37 | ]; 38 | 39 | // set autoEscape to false 40 | $('#tree1').tree({ 41 | data: data, 42 | autoEscape: false, 43 | autoOpen: true 44 | }); 45 | {% endhighlight %} 46 | -------------------------------------------------------------------------------- /docs/_examples/07_autoscroll.html: -------------------------------------------------------------------------------- 1 | --- 2 | title: Javascript tree with autoscroll 3 | js: examples/autoscroll.js 4 | --- 5 | 6 |

7 | « Example 6 8 | Example 8 » 9 |

10 | 11 |

Example 7 - autoscroll

12 | 13 |
14 | 15 |

16 | This is an example of autoscroll. The tree will scroll automatically if you 17 | drag an item outside of the tree.
18 | Autoscroll will work automatically. There is no option for it. 19 |

20 | 21 |

html

22 | 23 | {% highlight html %} 24 |
25 |
26 |
27 | {% endhighlight %} 28 | 29 |

css

30 | 31 | {% highlight css %} 32 | #scroll-container { 33 | height: 200px; 34 | overflow-y: scroll; 35 | user-select: none; 36 | } 37 | {% endhighlight %} 38 | 39 |

js

40 | 41 | {% highlight js %} $('#tree1').tree({ data: ExampleData.exampleData }); {% 42 | endhighlight %} 43 | -------------------------------------------------------------------------------- /docs/_examples/08_multiple_select.html: -------------------------------------------------------------------------------- 1 | --- 2 | title: Javascript tree with multiple select 3 | js: examples/multiple_select.js 4 | --- 5 | 6 |

7 | « Example 7 8 | Example 9 » 9 |

10 | 11 |

Example 8 - multiple select

12 | 13 |

14 | This example implements multiple select using the following functions and 15 | events: 16 |

17 |
    18 |
  • addToSelection: add node to selections
  • 19 |
  • isNodeSelected: is this node selected?
  • 20 |
  • removeFromSelection: unselect this node
  • 21 |
  • 22 | tree.click event: this event is fired when a user 23 | clicks on a node 24 |
  • 25 |
26 | 27 |
28 | 29 |

html

30 | 31 | {% highlight html %} 32 |
33 | {% endhighlight %} 34 | 35 |

javascript

36 | 37 | {% highlight js %} 38 | var $tree = $('#tree1'); 39 | 40 | $tree.tree({ 41 | data: ExampleData.exampleData, 42 | autoOpen: true 43 | }); 44 | 45 | $tree.on( 'tree.click', function(e) { 46 | // Disable single selection 47 | e.preventDefault(); 48 | var selected_node = e.node; 49 | 50 | if (selected_node.id === undefined) { 51 | console.warn('The multiple selection functions require that nodes have an id'); 52 | } 53 | 54 | if ($tree.tree('isNodeSelected', selected_node)) { 55 | $tree.tree('removeFromSelection', selected_node); 56 | } else { 57 | $tree.tree('addToSelection', selected_node); 58 | } 59 | }); 60 | {% endhighlight %} 61 | -------------------------------------------------------------------------------- /docs/_examples/09_custom_html.html: -------------------------------------------------------------------------------- 1 | --- 2 | title: Javascript tree with custom html 3 | js: examples/custom_html.js 4 | --- 5 | 6 |

7 | « Example 8 8 | Example 10 » 9 |

10 | 11 |

Example 9 - custom html

12 | 13 | 19 |

20 | This example uses the onCreateLi option to create an edit 21 | link next to the tree node. 22 |

23 |
24 | 25 |

html

26 | 27 | {% highlight html %} 28 |
29 | {% endhighlight %} 30 | 31 |

javascript

32 | 33 | {% highlight js %} 34 | var $tree = $('#tree1'); 35 | 36 | $tree.tree({ 37 | data: ExampleData.exampleData, 38 | autoOpen: 1, 39 | onCreateLi: function(node, $li) { 40 | // Append a link to the jqtree-element div. 41 | // The link has an url '#node-[id]' and a data property 'node-id'. 42 | $li.find('.jqtree-element').append( 43 | 'edit' 44 | ); 45 | } 46 | }); 47 | 48 | // Handle a click on the edit link 49 | $tree.on( 'click', '.edit', function(e) { 50 | // Get the id from the 'node-id' data property 51 | var node_id = $(e.target).data('node-id'); 52 | 53 | // Get the node from the tree 54 | var node = $tree.tree('getNodeById', node_id); 55 | 56 | if (node) { 57 | // Display the node name 58 | alert(node.name); 59 | } 60 | } 61 | {% endhighlight %} 62 | -------------------------------------------------------------------------------- /docs/_examples/10_icon_buttons.html: -------------------------------------------------------------------------------- 1 | --- 2 | title: Use icon toggle buttons 3 | js: examples/icon_buttons.js 4 | --- 5 | 6 |

7 | « Example 9 8 | Example 11 » 9 |

10 | 11 |

Example 10 - use icon toggle buttons

12 | 13 |

14 | You can use the openedIcon and closedIcon options to use html for 15 | the toggle buttons. You can for example use Hero icons. 16 |

17 |
18 | 19 |

javascript

20 | 21 | {% highlight js %} 22 | $('#tree1').tree({ 23 | closedIcon: $( 24 | ` 26 | 27 | ` 28 | ), 29 | openedIcon: $( 30 | ` 32 | 33 | ` 34 | ) 35 | }); 36 | {% endhighlight %} 37 | -------------------------------------------------------------------------------- /docs/_examples/11_right-to-left.html: -------------------------------------------------------------------------------- 1 | --- 2 | title: Right-to-left support 3 | js: examples/right-to-left.js 4 | --- 5 | 6 |

7 | « Example 10 8 | Example 12 » 9 |

10 | 11 |

Example 11 - right-to-left support

12 | 13 |

14 | You can display the tree from right to left with the rtl option. 15 |

16 | 17 |
18 | 19 |

javascript

20 | 21 | {% highlight js %} 22 | $('#tree1').tree({ 23 | rtl: true 24 | }); 25 | {% endhighlight %} 26 | -------------------------------------------------------------------------------- /docs/_examples/12_button_on_right.html: -------------------------------------------------------------------------------- 1 | --- 2 | title: Button on right 3 | js: examples/button-on-right.js 4 | --- 5 | 6 |

7 | « Example 11 8 | Example 13 » 9 |

10 | 11 |

Example 12 - button on right

12 | 13 |

14 | You can put the toggle button on the right by setting the buttonLeft option to false. 15 |

16 | 17 |
18 | 19 |

javascript

20 | 21 | {% highlight js %} 22 | $('#tree1').tree({ 23 | buttonLeft: false, 24 | autoOpen: 0 25 | }); 26 | {% endhighlight %} 27 | -------------------------------------------------------------------------------- /docs/_examples/13_drag_outside.html: -------------------------------------------------------------------------------- 1 | --- 2 | title: Drag outside tree 3 | js: examples/drag-outside.js 4 | --- 5 | 6 |

7 | « Example 12 8 | Example 14 » 9 |

10 | 11 |

Example 13 - drag node outside tree

12 | 13 |
14 | 15 |
drag here (see the console)
16 | 17 |

javascript

18 | 19 | {% highlight js %} 20 | var targetCollisionDiv = $("#targetDiv"); 21 | 22 | function isOverTarget(e) { 23 | return ( 24 | e.clientX > targetCollisionDiv.position().left && 25 | e.clientX < 26 | targetCollisionDiv.position().left + 27 | targetCollisionDiv.width() && 28 | e.clientY > targetCollisionDiv.position().top && 29 | e.clientY < 30 | targetCollisionDiv.position().top + targetCollisionDiv.height() 31 | ); 32 | } 33 | 34 | function handleMove(node, e) { 35 | if (isOverTarget(e)) { 36 | console.log("the node is over the target div"); 37 | } 38 | } 39 | 40 | function handleStop(node, e) { 41 | console.log("stopped over target: ", isOverTarget(e)); 42 | } 43 | 44 | $('#tree1').tree({ 45 | onDragMove: handleMove, 46 | onDragStop: handleStop 47 | }); 48 | {% endhighlight %} 49 | -------------------------------------------------------------------------------- /docs/_examples/14_filter.html: -------------------------------------------------------------------------------- 1 | --- 2 | title: Filter 3 | js: examples/filter.js 4 | --- 5 | 6 |

7 | « Example 13 8 |

9 | 10 |

Example 14 - filter

11 | 12 |
13 | 14 | 15 |
16 |
17 | 18 |

javascript

19 | 20 | {% highlight js %} 21 | const $tree = $("#tree1"); 22 | 23 | let foundMatch = false; 24 | 25 | $tree.tree({ 26 | autoOpen: false, 27 | data: ExampleData.exampleData, 28 | useContextMenu: false, 29 | onCreateLi: (node, $el) => { 30 | if (foundMatch && !node.openForMatch && !node.parent.matches) { 31 | $el.addClass("hidden-node"); 32 | } 33 | 34 | if (node.matches) { 35 | $el.addClass("highlight-node"); 36 | } 37 | }, 38 | }); 39 | 40 | $("#search").on("click", () => { 41 | const searchTerm = $("#search-term").val().toLowerCase(); 42 | const tree = $tree.tree("getTree"); 43 | 44 | if (!searchTerm) { 45 | foundMatch = false; 46 | 47 | tree.iterate((node) => { 48 | node['openForMatch'] = false; 49 | node["matches"] = false; 50 | return true; 51 | }); 52 | 53 | $tree.tree("refresh"); 54 | return; 55 | } 56 | 57 | foundMatch = false; 58 | 59 | tree.iterate((node) => { 60 | const matches = node.name.toLowerCase().includes(searchTerm); 61 | node["openForMatch"] = matches; 62 | node["matches"] = matches; 63 | 64 | if (matches) { 65 | foundMatch = true; 66 | 67 | if (node.isFolder()) { 68 | node.is_open = true; 69 | } 70 | 71 | let parent = node.parent; 72 | while (parent) { 73 | parent["openForMatch"] = true; 74 | parent.is_open = true; 75 | parent = parent.parent; 76 | } 77 | } 78 | 79 | return true; 80 | }); 81 | 82 | $tree.tree("refresh"); 83 | }); 84 | {% endhighlight %} 85 | 86 |

html

87 | 88 | {% highlight html %} 89 | 90 | 91 | 92 | 93 |
94 | {% endhighlight %} 95 | 96 |

css

97 | 98 | {% highlight css %} 99 | .hidden-node { 100 | display: none; 101 | } 102 | 103 | .highlight-node > .jqtree-element > .jqtree-title { 104 | font-weight: bold; 105 | } 106 | 107 | #search-term { 108 | margin-bottom: 16px; 109 | margin-right: 8px; 110 | } 111 | {% endhighlight %} 112 | -------------------------------------------------------------------------------- /docs/_layouts/example.html: -------------------------------------------------------------------------------- 1 | --- 2 | layout: page 3 | --- 4 | 5 |
6 | {{ content }} 7 |
8 | -------------------------------------------------------------------------------- /docs/_layouts/page.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | {% if page.title %}{{ page.title }}{% else %}{{ site.title }}{% endif %} 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | {{ content }} 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | {% if page.js %} 25 | 26 | {% endif %} 27 | 28 | 29 | -------------------------------------------------------------------------------- /docs/copy_vendor_files: -------------------------------------------------------------------------------- 1 | mkdir -p static/vendor 2 | cp node_modules/jquery-mockjax/dist/jquery.mockjax.min.js static/vendor 3 | cp node_modules/jquery/dist/jquery.min.js static/vendor 4 | -------------------------------------------------------------------------------- /docs/documentation.css: -------------------------------------------------------------------------------- 1 | @import "tailwindcss"; 2 | 3 | @plugin "@tailwindcss/typography"; 4 | 5 | @theme { 6 | --container-8xl: 90rem; 7 | } 8 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | --- 2 | layout: page 3 | js: documentation.js 4 | --- 5 | 6 |
7 | 39 |
40 |
41 |

42 | jqTree is a jQuery widget for displaying a tree 43 |

44 |

45 | It supports json data, loading via ajax and drag-and-drop. 46 |

47 |
48 | Download jqTree 50 |
51 |
52 |
53 | {% for entry in site.entries %} 54 | {% if entry.hide_title %} 55 |
56 | {% elsif entry.section %} 57 |

{{ entry.title 58 | }}

59 | {% else %} 60 |

{{ entry.title }} 61 |

62 | {% endif %} 63 | {% if entry.output.size > 1 %} 64 |
65 | {{ entry.output }} 66 |
67 | {% endif %} 68 | {% endfor %} 69 |
70 |
71 |
72 | -------------------------------------------------------------------------------- /docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jqtree_documentation", 3 | "private": true, 4 | "scripts": { 5 | "jekyll-build": "bundle exec jekyll build", 6 | "jekyll-serve": "bundle exec jekyll serve", 7 | "build_docs_css": "pnpm tailwind && pnpm build_example_css && pnpm copy_jqtree && pnpm copy_vendor_files", 8 | "tailwind": "tailwindcss -m -i documentation.css -o static/documentation.css", 9 | "build_example_css": "postcss -o static/example.css static/example.postcss", 10 | "copy_jqtree": "cp ../tree.jquery.js . && cp ../jqtree.css .", 11 | "copy_vendor_files": "./copy_vendor_files" 12 | }, 13 | "devDependencies": { 14 | "@tailwindcss/cli": "^4.1.7", 15 | "@tailwindcss/postcss": "^4.1.7", 16 | "@tailwindcss/typography": "^0.5.16", 17 | "autoprefixer": "^10.4.21", 18 | "jquery": "^3.7.1", 19 | "jquery-mockjax": "2.7.0-beta.0", 20 | "postcss": "^8.5.3", 21 | "postcss-cli": "^11.0.1", 22 | "postcss-import": "^16.1.0", 23 | "postcss-load-config": "^6.0.1", 24 | "postcss-nested": "^7.0.2", 25 | "tailwindcss": "^4.1.7" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /docs/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [require("postcss-nested"), require("autoprefixer")], 3 | }; 4 | -------------------------------------------------------------------------------- /docs/static/documentation.js: -------------------------------------------------------------------------------- 1 | $(function () { 2 | // demo tree 3 | var data = [ 4 | { 5 | label: "node1", 6 | id: 1, 7 | children: [ 8 | { label: "child1", id: 2 }, 9 | { label: "child2", id: 3 }, 10 | ], 11 | }, 12 | { 13 | label: "node2", 14 | id: 4, 15 | children: [{ label: "child3", id: 5 }], 16 | }, 17 | ]; 18 | 19 | var $tree1 = $("#tree1"); 20 | 21 | $tree1.tree({ 22 | data: data, 23 | autoOpen: true, 24 | dragAndDrop: true, 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /docs/static/example.postcss: -------------------------------------------------------------------------------- 1 | #tree1 { 2 | background-color: #ccc; 3 | padding: 8px 16px; 4 | margin-bottom: 16px; 5 | 6 | &.block-style { 7 | background-color: #fafafa; 8 | } 9 | } 10 | 11 | .jqtree-tree .jqtree-loading > div .jqtree-title:after { 12 | content: url(spinner.gif); 13 | margin-left: 8px; 14 | } 15 | 16 | #tree1.jqtree-loading:after { 17 | content: url(spinner.gif); 18 | } 19 | 20 | #scroll-container { 21 | height: 200px; 22 | overflow-y: scroll; 23 | -ms-user-select: none; 24 | user-select: none; 25 | margin-bottom: 16px; 26 | } 27 | 28 | .block-style { 29 | ul.jqtree-tree { 30 | margin-left: 0; 31 | 32 | ul.jqtree_common { 33 | margin-left: 2em; 34 | } 35 | 36 | .jqtree-element { 37 | margin-bottom: 8px; 38 | background-color: #ddd; 39 | padding: 8px; 40 | 41 | .jqtree-title { 42 | margin-left: 0; 43 | } 44 | } 45 | 46 | li.jqtree-selected { 47 | > .jqtree-element, 48 | > .jqtree-element:hover { 49 | background-color: #97bdd6; 50 | text-shadow: none; 51 | } 52 | } 53 | } 54 | } 55 | 56 | .hidden-node { 57 | display: none; 58 | } 59 | 60 | .highlight-node > .jqtree-element > .jqtree-title { 61 | font-weight: bold; 62 | } 63 | 64 | .jqtree-tree { 65 | .jqtree-toggler { 66 | font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; 67 | align-self: center; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /docs/static/example_data.js: -------------------------------------------------------------------------------- 1 | const ExampleData = {}; 2 | 3 | ExampleData.exampleData = [ 4 | { 5 | name: "Saurischia", 6 | id: 1, 7 | children: [ 8 | { name: "Herrerasaurians", id: 2 }, 9 | { 10 | name: "Theropods", 11 | id: 3, 12 | children: [ 13 | { name: "Coelophysoids", id: 4 }, 14 | { name: "Ceratosaurians", id: 5 }, 15 | { name: "Spinosauroids", id: 6 }, 16 | { name: "Carnosaurians", id: 7 }, 17 | { 18 | name: "Coelurosaurians", 19 | id: 8, 20 | children: [ 21 | { name: "Tyrannosauroids", id: 9 }, 22 | { name: "Ornithomimosaurians", id: 10 }, 23 | { name: "Therizinosauroids", id: 11 }, 24 | { name: "Oviraptorosaurians", id: 12 }, 25 | { name: "Dromaeosaurids", id: 13 }, 26 | { name: "Troodontids", id: 14 }, 27 | { name: "Avialans", id: 15 }, 28 | ], 29 | }, 30 | ], 31 | }, 32 | { 33 | name: "Sauropodomorphs", 34 | id: 16, 35 | children: [ 36 | { name: "Prosauropods", id: 17 }, 37 | { 38 | name: "Sauropods", 39 | id: 18, 40 | children: [ 41 | { name: "Diplodocoids", id: 19 }, 42 | { 43 | name: "Macronarians", 44 | id: 20, 45 | children: [ 46 | { name: "Brachiosaurids", id: 21 }, 47 | { name: "Titanosaurians", id: 22 }, 48 | ], 49 | }, 50 | ], 51 | }, 52 | ], 53 | }, 54 | ], 55 | }, 56 | { 57 | name: "Ornithischians", 58 | id: 23, 59 | children: [ 60 | { name: "Heterodontosaurids", id: 24 }, 61 | { 62 | name: "Thyreophorans", 63 | id: 25, 64 | children: [ 65 | { name: "Ankylosaurians", id: 26 }, 66 | { name: "Stegosaurians", id: 27 }, 67 | ], 68 | }, 69 | { 70 | name: "Ornithopods", 71 | id: 28, 72 | children: [{ name: "Hadrosaurids", id: 29 }], 73 | }, 74 | { name: "Pachycephalosaurians", id: 30 }, 75 | { name: "Ceratopsians", id: 31 }, 76 | ], 77 | }, 78 | ]; 79 | 80 | ExampleData.getFirstLevelData = function (nodes) { 81 | if (!nodes) { 82 | nodes = ExampleData.exampleData; 83 | } 84 | 85 | const data = []; 86 | 87 | nodes.forEach(function (node) { 88 | const newNode = { id: node.id, name: node.name }; 89 | 90 | if (node.children) { 91 | newNode.load_on_demand = true; 92 | } 93 | 94 | data.push(newNode); 95 | }); 96 | 97 | return data; 98 | }; 99 | 100 | ExampleData.getChildrenOfNode = function (nodeId) { 101 | let result = null; 102 | 103 | function iterate(nodes) { 104 | nodes.forEach(function (node) { 105 | if (result) { 106 | return; 107 | } else { 108 | if (node.id == nodeId) { 109 | result = node; 110 | } 111 | 112 | if (node.children) { 113 | iterate(node.children); 114 | } 115 | } 116 | }); 117 | } 118 | 119 | iterate(ExampleData.exampleData); 120 | 121 | return ExampleData.getFirstLevelData(result.children); 122 | }; 123 | -------------------------------------------------------------------------------- /docs/static/examples/autoescape.js: -------------------------------------------------------------------------------- 1 | var data = [ 2 | { 3 | name: "examples", 4 | children: [ 5 | { name: 'Example 1' }, 6 | { name: 'Example 2' }, 7 | 'Example 3' 8 | ] 9 | } 10 | ]; 11 | 12 | // set autoEscape to false 13 | $("#tree1").tree({ 14 | data: data, 15 | autoEscape: false, 16 | autoOpen: true 17 | }); 18 | -------------------------------------------------------------------------------- /docs/static/examples/autoscroll.js: -------------------------------------------------------------------------------- 1 | var $tree = $("#tree1"); 2 | $tree.tree({ 3 | data: ExampleData.exampleData, 4 | dragAndDrop: true, 5 | autoOpen: true 6 | }); 7 | -------------------------------------------------------------------------------- /docs/static/examples/button-on-right.js: -------------------------------------------------------------------------------- 1 | $.mockjax({ 2 | url: "*", 3 | response: function(options) { 4 | this.responseText = ExampleData.exampleData; 5 | }, 6 | responseTime: 0 7 | }); 8 | 9 | $("#tree1").tree({ 10 | buttonLeft: false, 11 | autoOpen: 0, 12 | slide: true 13 | }); 14 | -------------------------------------------------------------------------------- /docs/static/examples/custom_html.js: -------------------------------------------------------------------------------- 1 | var $tree = $("#tree1"); 2 | 3 | $tree.tree({ 4 | data: ExampleData.exampleData, 5 | autoOpen: 1, 6 | onCreateLi: function (node, $li) { 7 | // Append a link to the jqtree-element div. 8 | // The link has an url '#node-[id]' and a data property 'node-id'. 9 | $li.find(".jqtree-element").append( 10 | `edit` 11 | ); 12 | }, 13 | }); 14 | 15 | // Handle a click on the edit link 16 | $tree.on("click", ".edit", function (e) { 17 | // Get the id from the 'node-id' data property 18 | var node_id = $(e.target).data("node-id"); 19 | 20 | // Get the node from the tree 21 | var node = $tree.tree("getNodeById", node_id); 22 | 23 | if (node) { 24 | // Display the node name 25 | alert(node.name); 26 | } 27 | }); 28 | -------------------------------------------------------------------------------- /docs/static/examples/drag-outside.js: -------------------------------------------------------------------------------- 1 | $.mockjax({ 2 | url: "*", 3 | response: function(options) { 4 | this.responseText = ExampleData.exampleData; 5 | }, 6 | responseTime: 0 7 | }); 8 | 9 | var targetCollisionDiv = $("#targetDiv"); 10 | 11 | function isOverTarget(e) { 12 | return ( 13 | e.clientX > targetCollisionDiv.position().left && 14 | e.clientX < 15 | targetCollisionDiv.position().left + 16 | targetCollisionDiv.width() && 17 | e.clientY > targetCollisionDiv.position().top && 18 | e.clientY < 19 | targetCollisionDiv.position().top + targetCollisionDiv.height() 20 | ); 21 | } 22 | 23 | function handleMove(node, e) { 24 | if (isOverTarget(e)) { 25 | console.log("the node is over the target div"); 26 | } 27 | } 28 | 29 | function handleStop(node, e) { 30 | console.log("stopped over target: ", isOverTarget(e)); 31 | } 32 | 33 | $("#tree1").tree({ 34 | dragAndDrop: true, 35 | onDragMove: handleMove, 36 | onDragStop: handleStop 37 | }); 38 | -------------------------------------------------------------------------------- /docs/static/examples/drag_and_drop.js: -------------------------------------------------------------------------------- 1 | $.mockjax({ 2 | url: "*", 3 | response: function(options) { 4 | this.responseText = ExampleData.exampleData; 5 | }, 6 | responseTime: 0 7 | }); 8 | 9 | var $tree = $("#tree1"); 10 | $tree.tree({ 11 | dragAndDrop: true, 12 | autoOpen: 0 13 | }); 14 | -------------------------------------------------------------------------------- /docs/static/examples/filter.js: -------------------------------------------------------------------------------- 1 | const $tree = $("#tree1"); 2 | 3 | let foundMatch = false; 4 | 5 | $tree.tree({ 6 | autoOpen: false, 7 | data: ExampleData.exampleData, 8 | useContextMenu: false, 9 | onCreateLi: (node, $el) => { 10 | if (foundMatch && !node.openForMatch && !node.parent.matches) { 11 | $el.addClass("hidden-node"); 12 | } 13 | 14 | if (node.matches) { 15 | $el.addClass("highlight-node"); 16 | } 17 | }, 18 | }); 19 | 20 | $("#search").on("click", () => { 21 | const searchTerm = $("#search-term").val().toLowerCase(); 22 | const tree = $tree.tree("getTree"); 23 | 24 | if (!searchTerm) { 25 | foundMatch = false; 26 | 27 | tree.iterate((node) => { 28 | node["openForMatch"] = false; 29 | node["matches"] = false; 30 | return true; 31 | }); 32 | 33 | $tree.tree("refresh"); 34 | return; 35 | } 36 | 37 | foundMatch = false; 38 | 39 | tree.iterate((node) => { 40 | const matches = node.name.toLowerCase().includes(searchTerm); 41 | node["openForMatch"] = matches; 42 | node["matches"] = matches; 43 | 44 | if (matches) { 45 | foundMatch = true; 46 | 47 | if (node.isFolder()) { 48 | node.is_open = true; 49 | } 50 | 51 | let parent = node.parent; 52 | while (parent) { 53 | parent["openForMatch"] = true; 54 | parent.is_open = true; 55 | parent = parent.parent; 56 | } 57 | } 58 | 59 | return true; 60 | }); 61 | 62 | $tree.tree("refresh"); 63 | }); 64 | -------------------------------------------------------------------------------- /docs/static/examples/icon_buttons.js: -------------------------------------------------------------------------------- 1 | $.mockjax({ 2 | url: "*", 3 | response: function () { 4 | this.responseText = ExampleData.exampleData; 5 | }, 6 | responseTime: 0, 7 | }); 8 | 9 | $("#tree1").tree({ 10 | closedIcon: $( 11 | ` 12 | 13 | `, 14 | ), 15 | openedIcon: $( 16 | ` 17 | 18 | `, 19 | ), 20 | }); 21 | -------------------------------------------------------------------------------- /docs/static/examples/load_json_data.js: -------------------------------------------------------------------------------- 1 | var data = [ 2 | { 3 | name: "node1", 4 | id: 1, 5 | children: [{ name: "child1", id: 2 }, { name: "child2", id: 3 }] 6 | }, 7 | { 8 | name: "node2", 9 | id: 4, 10 | children: [{ name: "child3", id: 5 }] 11 | } 12 | ]; 13 | 14 | $("#tree1").tree({ 15 | data: data 16 | }); 17 | -------------------------------------------------------------------------------- /docs/static/examples/load_json_data_from_server.js: -------------------------------------------------------------------------------- 1 | $.mockjax({ 2 | url: "*", 3 | response: function(options) { 4 | this.responseText = ExampleData.exampleData; 5 | }, 6 | responseTime: 0 7 | }); 8 | 9 | $("#tree1").tree(); 10 | -------------------------------------------------------------------------------- /docs/static/examples/load_on_demand.js: -------------------------------------------------------------------------------- 1 | $.mockjax({ 2 | url: "*", 3 | responseTime: 1000, 4 | response: function(options) { 5 | if (options.data && options.data.node) { 6 | this.responseText = ExampleData.getChildrenOfNode( 7 | options.data.node 8 | ); 9 | } else { 10 | this.responseText = ExampleData.getFirstLevelData(); 11 | } 12 | } 13 | }); 14 | 15 | var $tree = $("#tree1"); 16 | 17 | $tree.tree({ 18 | saveState: true 19 | }); 20 | -------------------------------------------------------------------------------- /docs/static/examples/multiple_select.js: -------------------------------------------------------------------------------- 1 | var $tree = $("#tree1"); 2 | $tree.tree({ 3 | data: ExampleData.exampleData, 4 | autoOpen: true 5 | }); 6 | $tree.on("tree.click", function(e) { 7 | // Disable single selection 8 | e.preventDefault(); 9 | 10 | var selected_node = e.node; 11 | 12 | if (selected_node.id == undefined) { 13 | console.log( 14 | "The multiple selection functions require that nodes have an id" 15 | ); 16 | } 17 | 18 | if ($tree.tree("isNodeSelected", selected_node)) { 19 | $tree.tree("removeFromSelection", selected_node); 20 | } else { 21 | $tree.tree("addToSelection", selected_node); 22 | } 23 | }); 24 | -------------------------------------------------------------------------------- /docs/static/examples/right-to-left.js: -------------------------------------------------------------------------------- 1 | $.mockjax({ 2 | url: "*", 3 | response: function(options) { 4 | this.responseText = ExampleData.exampleData; 5 | }, 6 | responseTime: 0 7 | }); 8 | 9 | $("#tree1").tree({ 10 | rtl: true 11 | }); 12 | -------------------------------------------------------------------------------- /docs/static/examples/save_state.js: -------------------------------------------------------------------------------- 1 | $.mockjax({ 2 | url: "*", 3 | response: function(options) { 4 | this.responseText = ExampleData.exampleData; 5 | }, 6 | responseTime: 0 7 | }); 8 | 9 | $("#tree1").tree({ 10 | saveState: true 11 | }); 12 | -------------------------------------------------------------------------------- /docs/static/monokai.css: -------------------------------------------------------------------------------- 1 | .highlight .hll { background-color: #49483e } 2 | .highlight { background: #272822; color: #f8f8f2 } 3 | .highlight .c { color: #75715e } /* Comment */ 4 | .highlight .err { color: #960050; background-color: #1e0010 } /* Error */ 5 | .highlight .k { color: #66d9ef } /* Keyword */ 6 | .highlight .l { color: #ae81ff } /* Literal */ 7 | .highlight .n { color: #f8f8f2 } /* Name */ 8 | .highlight .o { color: #f92672 } /* Operator */ 9 | .highlight .p { color: #f8f8f2 } /* Punctuation */ 10 | .highlight .ch { color: #75715e } /* Comment.Hashbang */ 11 | .highlight .cm { color: #75715e } /* Comment.Multiline */ 12 | .highlight .cp { color: #75715e } /* Comment.Preproc */ 13 | .highlight .cpf { color: #75715e } /* Comment.PreprocFile */ 14 | .highlight .c1 { color: #75715e } /* Comment.Single */ 15 | .highlight .cs { color: #75715e } /* Comment.Special */ 16 | .highlight .gd { color: #f92672 } /* Generic.Deleted */ 17 | .highlight .ge { font-style: italic } /* Generic.Emph */ 18 | .highlight .gi { color: #a6e22e } /* Generic.Inserted */ 19 | .highlight .gs { font-weight: bold } /* Generic.Strong */ 20 | .highlight .gu { color: #75715e } /* Generic.Subheading */ 21 | .highlight .kc { color: #66d9ef } /* Keyword.Constant */ 22 | .highlight .kd { color: #66d9ef } /* Keyword.Declaration */ 23 | .highlight .kn { color: #f92672 } /* Keyword.Namespace */ 24 | .highlight .kp { color: #66d9ef } /* Keyword.Pseudo */ 25 | .highlight .kr { color: #66d9ef } /* Keyword.Reserved */ 26 | .highlight .kt { color: #66d9ef } /* Keyword.Type */ 27 | .highlight .ld { color: #e6db74 } /* Literal.Date */ 28 | .highlight .m { color: #ae81ff } /* Literal.Number */ 29 | .highlight .s { color: #e6db74 } /* Literal.String */ 30 | .highlight .na { color: #a6e22e } /* Name.Attribute */ 31 | .highlight .nb { color: #f8f8f2 } /* Name.Builtin */ 32 | .highlight .nc { color: #a6e22e } /* Name.Class */ 33 | .highlight .no { color: #66d9ef } /* Name.Constant */ 34 | .highlight .nd { color: #a6e22e } /* Name.Decorator */ 35 | .highlight .ni { color: #f8f8f2 } /* Name.Entity */ 36 | .highlight .ne { color: #a6e22e } /* Name.Exception */ 37 | .highlight .nf { color: #a6e22e } /* Name.Function */ 38 | .highlight .nl { color: #f8f8f2 } /* Name.Label */ 39 | .highlight .nn { color: #f8f8f2 } /* Name.Namespace */ 40 | .highlight .nx { color: #a6e22e } /* Name.Other */ 41 | .highlight .py { color: #f8f8f2 } /* Name.Property */ 42 | .highlight .nt { color: #f92672 } /* Name.Tag */ 43 | .highlight .nv { color: #f8f8f2 } /* Name.Variable */ 44 | .highlight .ow { color: #f92672 } /* Operator.Word */ 45 | .highlight .w { color: #f8f8f2 } /* Text.Whitespace */ 46 | .highlight .mb { color: #ae81ff } /* Literal.Number.Bin */ 47 | .highlight .mf { color: #ae81ff } /* Literal.Number.Float */ 48 | .highlight .mh { color: #ae81ff } /* Literal.Number.Hex */ 49 | .highlight .mi { color: #ae81ff } /* Literal.Number.Integer */ 50 | .highlight .mo { color: #ae81ff } /* Literal.Number.Oct */ 51 | .highlight .sa { color: #e6db74 } /* Literal.String.Affix */ 52 | .highlight .sb { color: #e6db74 } /* Literal.String.Backtick */ 53 | .highlight .sc { color: #e6db74 } /* Literal.String.Char */ 54 | .highlight .dl { color: #e6db74 } /* Literal.String.Delimiter */ 55 | .highlight .sd { color: #e6db74 } /* Literal.String.Doc */ 56 | .highlight .s2 { color: #e6db74 } /* Literal.String.Double */ 57 | .highlight .se { color: #ae81ff } /* Literal.String.Escape */ 58 | .highlight .sh { color: #e6db74 } /* Literal.String.Heredoc */ 59 | .highlight .si { color: #e6db74 } /* Literal.String.Interpol */ 60 | .highlight .sx { color: #e6db74 } /* Literal.String.Other */ 61 | .highlight .sr { color: #e6db74 } /* Literal.String.Regex */ 62 | .highlight .s1 { color: #e6db74 } /* Literal.String.Single */ 63 | .highlight .ss { color: #e6db74 } /* Literal.String.Symbol */ 64 | .highlight .bp { color: #f8f8f2 } /* Name.Builtin.Pseudo */ 65 | .highlight .fm { color: #a6e22e } /* Name.Function.Magic */ 66 | .highlight .vc { color: #f8f8f2 } /* Name.Variable.Class */ 67 | .highlight .vg { color: #f8f8f2 } /* Name.Variable.Global */ 68 | .highlight .vi { color: #f8f8f2 } /* Name.Variable.Instance */ 69 | .highlight .vm { color: #f8f8f2 } /* Name.Variable.Magic */ 70 | .highlight .il { color: #ae81ff } /* Literal.Number.Integer.Long */ 71 | -------------------------------------------------------------------------------- /docs/static/spinner.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbraak/jqTree/cab1ca809d34c06ebc9ef41cb7f6530f44869cda/docs/static/spinner.gif -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import eslint from "@eslint/js"; 2 | import tseslint from "typescript-eslint"; 3 | import importPlugin from "eslint-plugin-import"; 4 | import jestPlugin from "eslint-plugin-jest"; 5 | import jestDomPlugin from "eslint-plugin-jest-dom"; 6 | import perfectionistPlugin from "eslint-plugin-perfectionist"; 7 | import playwrightPlugin from "eslint-plugin-playwright"; 8 | import testingLibraryPlugin from "eslint-plugin-testing-library"; 9 | 10 | export default [ 11 | eslint.configs.recommended, 12 | ...tseslint.configs.strictTypeChecked, 13 | ...tseslint.configs.stylisticTypeChecked, 14 | importPlugin.flatConfigs.recommended, 15 | importPlugin.flatConfigs.typescript, 16 | perfectionistPlugin.configs["recommended-natural"], 17 | { 18 | languageOptions: { 19 | parserOptions: { 20 | projectService: true, 21 | tsconfigRootDir: import.meta.dirname, 22 | }, 23 | }, 24 | rules: { 25 | "@typescript-eslint/explicit-function-return-type": "off", 26 | "@typescript-eslint/interface-name-prefix": "off", 27 | "@typescript-eslint/no-empty-object-type": "off", 28 | "@typescript-eslint/no-explicit-any": "off", 29 | "@typescript-eslint/no-use-before-define": "off", 30 | "@typescript-eslint/no-unused-vars": [ 31 | "error", 32 | { 33 | argsIgnorePattern: "^_", 34 | varsIgnorePattern: "^_", 35 | }, 36 | ], 37 | "@typescript-eslint/non-nullable-type-assertion-style": "off", 38 | "@typescript-eslint/prefer-includes": "off", 39 | "@typescript-eslint/triple-slash-reference": "off", 40 | "@typescript-eslint/prefer-string-starts-ends-with": "off", 41 | "@typescript-eslint/restrict-template-expressions": [ 42 | "error", 43 | { 44 | allowNumber: true, 45 | allowBoolean: true, 46 | allowAny: false, 47 | allowNullish: false, 48 | }, 49 | ], 50 | "@typescript-eslint/unified-signatures": "off", 51 | }, 52 | }, 53 | { 54 | files: ["src/test/**/*.ts"], 55 | ...jestPlugin.configs["flat/all"], 56 | }, 57 | { 58 | files: ["src/test/**/*.ts"], 59 | rules: { 60 | "jest/no-conditional-in-test": "off", 61 | "jest/no-duplicate-hooks": "off", 62 | "jest/no-hooks": "off", 63 | "jest/no-identical-title": "off", 64 | "jest/prefer-expect-assertions": "off", 65 | "jest/prefer-importing-jest-globals": [ 66 | "error", 67 | { types: ["jest"] }, 68 | ], 69 | "jest/prefer-lowercase-title": "off", 70 | "jest/require-hook": "off", 71 | }, 72 | }, 73 | { 74 | files: ["src/test/**/*.ts"], 75 | ...testingLibraryPlugin.configs["flat/dom"], 76 | }, 77 | { 78 | files: ["src/test/**/*.ts"], 79 | ...jestDomPlugin.configs["flat/recommended"], 80 | }, 81 | { 82 | files: ["src/playwright/**/*.ts"], 83 | ...playwrightPlugin.configs["flat/recommended"], 84 | }, 85 | ]; 86 | -------------------------------------------------------------------------------- /jqtree-circle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbraak/jqTree/cab1ca809d34c06ebc9ef41cb7f6530f44869cda/jqtree-circle.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jqtree", 3 | "version": "1.8.10", 4 | "description": "Tree widget for jQuery", 5 | "keywords": [ 6 | "jquery-plugin", 7 | "tree" 8 | ], 9 | "license": "Apache-2.0", 10 | "browser": "./tree.jquery.js", 11 | "main": "./tree.jquery.js", 12 | "types": "./src/tree.jquery.d.ts", 13 | "repository": { 14 | "type": "git", 15 | "url": "https://github.com/mbraak/jqtree" 16 | }, 17 | "scripts": { 18 | "ci": "pnpm lint && pnpm tsc && pnpm test", 19 | "jest": "jest --coverage --no-cache --verbose --config ./config/jest.config.js", 20 | "jest-watch": "jest --watch --config ./config/jest.config.js", 21 | "lint": "eslint src", 22 | "production": "./config/production", 23 | "devserver": "SERVE=true rollup --config config/rollup.config.mjs --watch", 24 | "devserver-with-coverage": "COVERAGE=true SERVE=true rollup --config config/rollup.config.mjs", 25 | "build-with-coverage": "COVERAGE=true rollup --config config/rollup.config.mjs", 26 | "prettier": "prettier src/*.ts --write --tab-width 4", 27 | "tsc": "tsc --noEmit --project tsconfig.json", 28 | "playwright": "pnpm build-with-coverage && playwright test --config config/playwright.config.js", 29 | "test": "pnpm jest && pnpm playwright" 30 | }, 31 | "browserslist": [ 32 | "defaults" 33 | ], 34 | "peerDependencies": { 35 | "jquery": "^3" 36 | }, 37 | "devDependencies": { 38 | "@babel/cli": "^7.27.2", 39 | "@babel/core": "^7.27.1", 40 | "@babel/preset-env": "^7.27.2", 41 | "@babel/preset-typescript": "^7.27.1", 42 | "@eslint/js": "^9.27.0", 43 | "@jest/globals": "^29.7.0", 44 | "@playwright/test": "^1.52.0", 45 | "@rollup/plugin-babel": "^6.0.4", 46 | "@rollup/plugin-node-resolve": "^16.0.1", 47 | "@rollup/plugin-terser": "^0.4.4", 48 | "@testing-library/dom": "^10.4.0", 49 | "@testing-library/jest-dom": "^6.6.3", 50 | "@testing-library/user-event": "^14.6.1", 51 | "@types/debug": "^4.1.12", 52 | "@types/jest": "^29.5.14", 53 | "@types/jest-axe": "^3.5.9", 54 | "@types/jquery": "^3.5.32", 55 | "@types/node": "^22.15.21", 56 | "autoprefixer": "^10.4.21", 57 | "babel-jest": "^29.7.0", 58 | "babel-plugin-istanbul": "^7.0.0", 59 | "eslint": "^9.27.0", 60 | "eslint-plugin-import": "^2.31.0", 61 | "eslint-plugin-jest": "^28.11.0", 62 | "eslint-plugin-jest-dom": "^5.5.0", 63 | "eslint-plugin-perfectionist": "^4.13.0", 64 | "eslint-plugin-playwright": "^2.2.0", 65 | "eslint-plugin-testing-library": "^7.2.1", 66 | "givens": "^1.3.9", 67 | "jest": "^29.7.0", 68 | "jest-axe": "^10.0.0", 69 | "jest-extended": "^5.0.3", 70 | "jest-fixed-jsdom": "^0.0.9", 71 | "jsdom-testing-mocks": "^1.13.1", 72 | "jsonfile": "^6.1.0", 73 | "lodash": "^4.17.21", 74 | "msw": "^2.8.4", 75 | "postcss": "^8.5.3", 76 | "postcss-cli": "^11.0.1", 77 | "postcss-import": "^16.1.0", 78 | "postcss-load-config": "^6.0.1", 79 | "postcss-nested": "^7.0.2", 80 | "prettier": "^3.5.3", 81 | "rollup": "^4.41.1", 82 | "rollup-plugin-serve": "^3.0.0", 83 | "tslib": "^2.8.1", 84 | "typescript": "^5.8.3", 85 | "typescript-eslint": "^8.32.1" 86 | }, 87 | "pnpm": { 88 | "onlyBuiltDependencies": [ 89 | "msw" 90 | ] 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbraak/jqTree/cab1ca809d34c06ebc9ef41cb7f6530f44869cda/screenshot.png -------------------------------------------------------------------------------- /src/dragAndDropHandler/binarySearch.ts: -------------------------------------------------------------------------------- 1 | function binarySearch(items: T[], compareFn: (a: T) => number): null | T { 2 | let low = 0; 3 | let high = items.length; 4 | 5 | while (low < high) { 6 | const mid = (low + high) >> 1; 7 | const item = items[mid]; 8 | 9 | if (item === undefined) { 10 | return null; 11 | } 12 | 13 | const compareResult = compareFn(item); 14 | 15 | if (compareResult > 0) { 16 | high = mid; 17 | } else if (compareResult < 0) { 18 | low = mid + 1; 19 | } else { 20 | return item; 21 | } 22 | } 23 | 24 | return null; 25 | } 26 | 27 | export default binarySearch; 28 | -------------------------------------------------------------------------------- /src/dragAndDropHandler/dragElement.ts: -------------------------------------------------------------------------------- 1 | interface DragElementParams { 2 | autoEscape: boolean; 3 | nodeName: string; 4 | offsetX: number; 5 | offsetY: number; 6 | treeElement: HTMLElement; 7 | } 8 | 9 | class DragElement { 10 | private element: HTMLElement; 11 | private offsetX: number; 12 | private offsetY: number; 13 | 14 | constructor({ 15 | autoEscape, 16 | nodeName, 17 | offsetX, 18 | offsetY, 19 | treeElement, 20 | }: DragElementParams) { 21 | this.offsetX = offsetX; 22 | this.offsetY = offsetY; 23 | 24 | this.element = this.createElement(nodeName, autoEscape); 25 | 26 | treeElement.appendChild(this.element); 27 | } 28 | 29 | public move(pageX: number, pageY: number): void { 30 | this.element.style.left = `${pageX - this.offsetX}px`; 31 | this.element.style.top = `${pageY - this.offsetY}px`; 32 | } 33 | 34 | public remove(): void { 35 | this.element.remove(); 36 | } 37 | 38 | private createElement(nodeName: string, autoEscape: boolean) { 39 | const element = document.createElement("span"); 40 | element.classList.add("jqtree-title", "jqtree-dragging"); 41 | 42 | if (autoEscape) { 43 | element.textContent = nodeName; 44 | } else { 45 | element.innerHTML = nodeName; 46 | } 47 | 48 | element.style.position = "absolute"; 49 | 50 | return element; 51 | } 52 | } 53 | 54 | export default DragElement; 55 | -------------------------------------------------------------------------------- /src/dragAndDropHandler/iterateVisibleNodes.ts: -------------------------------------------------------------------------------- 1 | import { Node } from "../node"; 2 | 3 | interface Options { 4 | handleAfterOpenFolder: (node: Node, nextNode: Node | null) => void; 5 | handleClosedFolder: ( 6 | node: Node, 7 | nextNode: Node | null, 8 | element: HTMLElement, 9 | ) => void; 10 | handleFirstNode: (node: Node) => void; 11 | handleNode: ( 12 | node: Node, 13 | nextNode: Node | null, 14 | element: HTMLElement, 15 | ) => void; 16 | 17 | /* 18 | override 19 | return 20 | - true: continue iterating 21 | - false: stop iterating 22 | */ 23 | handleOpenFolder: (node: Node, element: HTMLElement) => boolean; 24 | } 25 | 26 | const iterateVisibleNodes = ( 27 | tree: Node, 28 | { 29 | handleAfterOpenFolder, 30 | handleClosedFolder, 31 | handleFirstNode, 32 | handleNode, 33 | handleOpenFolder, 34 | }: Options, 35 | ) => { 36 | let isFirstNode = true; 37 | 38 | const iterate = (node: Node, nextNode: Node | null): void => { 39 | let mustIterateInside = 40 | (node.is_open || !node.element) && node.hasChildren(); 41 | 42 | let element: HTMLElement | null = null; 43 | 44 | // Is the element visible? 45 | if (node.element?.offsetParent) { 46 | element = node.element; 47 | 48 | if (isFirstNode) { 49 | handleFirstNode(node); 50 | isFirstNode = false; 51 | } 52 | 53 | if (!node.hasChildren()) { 54 | handleNode(node, nextNode, node.element); 55 | } else if (node.is_open) { 56 | if (!handleOpenFolder(node, node.element)) { 57 | mustIterateInside = false; 58 | } 59 | } else { 60 | handleClosedFolder(node, nextNode, element); 61 | } 62 | } 63 | 64 | if (mustIterateInside) { 65 | const childrenLength = node.children.length; 66 | node.children.forEach((_, i) => { 67 | const child = node.children[i]; 68 | 69 | if (child) { 70 | if (i === childrenLength - 1) { 71 | iterate(child, null); 72 | } else { 73 | const nextChild = node.children[i + 1]; 74 | 75 | if (nextChild) { 76 | iterate(child, nextChild); 77 | } 78 | } 79 | } 80 | }); 81 | 82 | if (node.is_open && element) { 83 | handleAfterOpenFolder(node, nextNode); 84 | } 85 | } 86 | }; 87 | 88 | iterate(tree, null); 89 | }; 90 | 91 | export default iterateVisibleNodes; 92 | -------------------------------------------------------------------------------- /src/dragAndDropHandler/types.ts: -------------------------------------------------------------------------------- 1 | import { Node, Position } from "../node"; 2 | 3 | export interface DropHint { 4 | remove: () => void; 5 | } 6 | 7 | export interface HitArea { 8 | bottom: number; 9 | node: Node; 10 | position: Position; 11 | top: number; 12 | } 13 | -------------------------------------------------------------------------------- /src/header.txt: -------------------------------------------------------------------------------- 1 | JqTree <%= version %> 2 | 3 | Copyright <%= year %> Marco Braak 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | @license 17 | -------------------------------------------------------------------------------- /src/jqtreeMethodTypes.ts: -------------------------------------------------------------------------------- 1 | import { Node } from "./node"; 2 | 3 | export type AddToSelection = (node: Node) => void; 4 | 5 | export type CloseNode = (node: Node) => void; 6 | 7 | export type GetNodeById = (nodeId: NodeId) => Node | null; 8 | 9 | export type GetScrollLeft = () => number; 10 | 11 | export type GetSelectedNode = () => false | Node; 12 | 13 | export type GetSelectedNodes = () => Node[]; 14 | 15 | export type GetTree = () => Node | null; 16 | 17 | export type IsFocusOnTree = () => boolean; 18 | 19 | export type IsNodeSelected = (node: Node) => boolean; 20 | 21 | export type LoadData = (data: NodeData[], parentNode: Node | null) => void; 22 | 23 | export type OnFinishOpenNode = (node: Node) => void; 24 | 25 | export type OpenNode = ( 26 | node: Node, 27 | slide?: boolean, 28 | onFinished?: OnFinishOpenNode, 29 | ) => void; 30 | 31 | export type RefreshElements = (fromNode: Node | null) => void; 32 | 33 | export type RemoveFromSelection = (node: Node) => void; 34 | 35 | export type SelectNode = (node: Node) => void; 36 | 37 | export type TriggerEvent = ( 38 | eventName: string, 39 | values?: Record, 40 | ) => JQuery.Event; 41 | -------------------------------------------------------------------------------- /src/jqtreeOptions.ts: -------------------------------------------------------------------------------- 1 | import { Node } from "./node"; 2 | 3 | export type DataFilter = (data: unknown) => NodeData[]; 4 | 5 | export type DataUrl = DataUrlFunction | JQuery.AjaxSettings | string; 6 | 7 | export type DragMethod = (node: Node, event: Event | Touch) => void; 8 | 9 | export type IconElement = HTMLElement | JQuery | string; 10 | 11 | export interface JQTreeOptions { 12 | animationSpeed: JQuery.Duration; 13 | autoEscape: boolean; 14 | autoOpen: boolean | number; 15 | buttonLeft: boolean; 16 | closedIcon?: IconElement; 17 | data?: NodeData[]; 18 | dataFilter?: DataFilter; 19 | dataUrl?: DataUrl; 20 | dragAndDrop: boolean; 21 | keyboardSupport: boolean; 22 | nodeClass: typeof Node; 23 | onCanMove?: OnCanMove; 24 | onCanMoveTo?: OnCanMoveTo; 25 | onCanSelectNode?: (node: Node) => boolean; 26 | onCreateLi?: OnCreateLi; 27 | onDragMove?: DragMethod; 28 | onDragStop?: DragMethod; 29 | onGetStateFromStorage?: OnGetStateFromStorage; 30 | onIsMoveHandle?: OnIsMoveHandle; 31 | onLoadFailed?: OnLoadFailed; 32 | onLoading?: OnLoading; 33 | onSetStateFromStorage?: OnSetStateFromStorage; 34 | openedIcon?: IconElement; 35 | openFolderDelay: false | number; 36 | rtl?: boolean; 37 | saveState: boolean | string; 38 | selectable: boolean; 39 | showEmptyFolder: boolean; 40 | slide: boolean; 41 | startDndDelay?: number; 42 | tabIndex?: number; 43 | useContextMenu: boolean; 44 | } 45 | 46 | export type OnCanMove = ((node: Node) => boolean) | undefined; 47 | 48 | export type OnCanMoveTo = ( 49 | node: Node, 50 | targetNode: Node, 51 | positionName: string, 52 | ) => boolean; 53 | 54 | export type OnCreateLi = (node: Node, el: JQuery, isSelected: boolean) => void; 55 | 56 | export type OnGetStateFromStorage = (() => string) | undefined; 57 | 58 | export type OnIsMoveHandle = (el: JQuery) => boolean; 59 | 60 | export type OnLoadFailed = (response: JQuery.jqXHR) => void; 61 | 62 | export type OnLoading = ( 63 | isLoading: boolean, 64 | node: Node | null, 65 | $el: JQuery, 66 | ) => void; 67 | 68 | export type OnSetStateFromStorage = ((data: string) => void) | undefined; 69 | 70 | type DataUrlFunction = (node: Node | null) => JQuery.AjaxSettings; 71 | -------------------------------------------------------------------------------- /src/mouseUtils.ts: -------------------------------------------------------------------------------- 1 | export interface PositionInfo { 2 | originalEvent: Event; 3 | pageX: number; 4 | pageY: number; 5 | target: HTMLElement; 6 | } 7 | 8 | export const getPositionInfoFromMouseEvent = (e: MouseEvent): PositionInfo => ({ 9 | originalEvent: e, 10 | pageX: e.pageX, 11 | pageY: e.pageY, 12 | target: e.target as HTMLElement, 13 | }); 14 | 15 | export const getPositionInfoFromTouch = ( 16 | touch: Touch, 17 | e: TouchEvent, 18 | ): PositionInfo => ({ 19 | originalEvent: e, 20 | pageX: touch.pageX, 21 | pageY: touch.pageY, 22 | target: touch.target as HTMLElement, 23 | }); 24 | -------------------------------------------------------------------------------- /src/nodeElement/borderDropHint.ts: -------------------------------------------------------------------------------- 1 | import { DropHint } from "../dragAndDropHandler/types"; 2 | 3 | class BorderDropHint implements DropHint { 4 | private hint?: HTMLElement; 5 | 6 | constructor(element: HTMLElement, scrollLeft: number) { 7 | const div = element.querySelector(":scope > .jqtree-element"); 8 | 9 | if (!div) { 10 | this.hint = undefined; 11 | return; 12 | } 13 | 14 | const width = Math.max(element.offsetWidth + scrollLeft - 4, 0); 15 | const height = Math.max(element.clientHeight - 4, 0); 16 | 17 | const hint = document.createElement("span"); 18 | hint.className = "jqtree-border"; 19 | hint.style.width = `${width}px`; 20 | hint.style.height = `${height}px`; 21 | 22 | this.hint = hint; 23 | 24 | div.append(this.hint); 25 | } 26 | 27 | public remove(): void { 28 | this.hint?.remove(); 29 | } 30 | } 31 | 32 | export default BorderDropHint; 33 | -------------------------------------------------------------------------------- /src/nodeElement/folderElement.ts: -------------------------------------------------------------------------------- 1 | import { OnFinishOpenNode, TriggerEvent } from "../jqtreeMethodTypes"; 2 | import { Position } from "../node"; 3 | import NodeElement, { NodeElementParams } from "./index"; 4 | 5 | interface FolderElementParams extends NodeElementParams { 6 | closedIconElement?: HTMLElement | Text; 7 | openedIconElement?: HTMLElement | Text; 8 | triggerEvent: TriggerEvent; 9 | } 10 | 11 | class FolderElement extends NodeElement { 12 | private closedIconElement?: HTMLElement | Text; 13 | private openedIconElement?: HTMLElement | Text; 14 | private triggerEvent: TriggerEvent; 15 | 16 | constructor({ 17 | closedIconElement, 18 | getScrollLeft, 19 | node, 20 | openedIconElement, 21 | tabIndex, 22 | treeElement, 23 | triggerEvent, 24 | }: FolderElementParams) { 25 | super({ 26 | getScrollLeft, 27 | node, 28 | tabIndex, 29 | treeElement, 30 | }); 31 | 32 | this.closedIconElement = closedIconElement; 33 | this.openedIconElement = openedIconElement; 34 | this.triggerEvent = triggerEvent; 35 | } 36 | 37 | public close(slide: boolean, animationSpeed: JQuery.Duration): void { 38 | if (!this.node.is_open) { 39 | return; 40 | } 41 | 42 | this.node.is_open = false; 43 | 44 | const button = this.getButton(); 45 | button.classList.add("jqtree-closed"); 46 | button.innerHTML = ""; 47 | 48 | const closedIconElement = this.closedIconElement; 49 | 50 | if (closedIconElement) { 51 | const icon = closedIconElement.cloneNode(true); 52 | button.appendChild(icon); 53 | } 54 | 55 | const doClose = (): void => { 56 | this.element.classList.add("jqtree-closed"); 57 | 58 | const titleSpan = this.getTitleSpan(); 59 | titleSpan.setAttribute("aria-expanded", "false"); 60 | 61 | this.triggerEvent("tree.close", { 62 | node: this.node, 63 | }); 64 | }; 65 | 66 | if (slide) { 67 | jQuery(this.getUl()).slideUp(animationSpeed, doClose); 68 | } else { 69 | jQuery(this.getUl()).hide(); 70 | doClose(); 71 | } 72 | } 73 | 74 | public open( 75 | onFinished: OnFinishOpenNode | undefined, 76 | slide: boolean, 77 | animationSpeed: JQuery.Duration, 78 | ): void { 79 | if (this.node.is_open) { 80 | return; 81 | } 82 | 83 | this.node.is_open = true; 84 | 85 | const button = this.getButton(); 86 | button.classList.remove("jqtree-closed"); 87 | button.innerHTML = ""; 88 | 89 | const openedIconElement = this.openedIconElement; 90 | 91 | if (openedIconElement) { 92 | const icon = openedIconElement.cloneNode(true); 93 | button.appendChild(icon); 94 | } 95 | 96 | const doOpen = (): void => { 97 | this.element.classList.remove("jqtree-closed"); 98 | 99 | const titleSpan = this.getTitleSpan(); 100 | titleSpan.setAttribute("aria-expanded", "true"); 101 | 102 | if (onFinished) { 103 | onFinished(this.node); 104 | } 105 | 106 | this.triggerEvent("tree.open", { 107 | node: this.node, 108 | }); 109 | }; 110 | 111 | if (slide) { 112 | jQuery(this.getUl()).slideDown(animationSpeed, doOpen); 113 | } else { 114 | jQuery(this.getUl()).show(); 115 | doOpen(); 116 | } 117 | } 118 | 119 | protected mustShowBorderDropHint(position: Position): boolean { 120 | return !this.node.is_open && position === "inside"; 121 | } 122 | 123 | private getButton(): HTMLLinkElement { 124 | return this.element.querySelector( 125 | ":scope > .jqtree-element > a.jqtree-toggler", 126 | ) as HTMLLinkElement; 127 | } 128 | } 129 | 130 | export default FolderElement; 131 | -------------------------------------------------------------------------------- /src/nodeElement/ghostDropHint.ts: -------------------------------------------------------------------------------- 1 | import { DropHint } from "../dragAndDropHandler/types"; 2 | import { Node, Position } from "../node"; 3 | 4 | class GhostDropHint implements DropHint { 5 | private element: HTMLElement; 6 | private ghost: HTMLElement; 7 | private node: Node; 8 | 9 | constructor(node: Node, element: HTMLElement, position: Position) { 10 | this.element = element; 11 | this.node = node; 12 | this.ghost = this.createGhostElement(); 13 | 14 | switch (position) { 15 | case "after": 16 | this.moveAfter(); 17 | break; 18 | 19 | case "before": 20 | this.moveBefore(); 21 | break; 22 | 23 | case "inside": { 24 | if (node.isFolder() && node.is_open) { 25 | this.moveInsideOpenFolder(); 26 | } else { 27 | this.moveInside(); 28 | } 29 | } 30 | } 31 | } 32 | 33 | public remove(): void { 34 | this.ghost.remove(); 35 | } 36 | 37 | private createGhostElement() { 38 | const ghost = document.createElement("li"); 39 | ghost.className = "jqtree_common jqtree-ghost"; 40 | 41 | const circleSpan = document.createElement("span"); 42 | circleSpan.className = "jqtree_common jqtree-circle"; 43 | ghost.append(circleSpan); 44 | 45 | const lineSpan = document.createElement("span"); 46 | lineSpan.className = "jqtree_common jqtree-line"; 47 | ghost.append(lineSpan); 48 | 49 | return ghost; 50 | } 51 | 52 | private moveAfter(): void { 53 | this.element.after(this.ghost); 54 | } 55 | 56 | private moveBefore(): void { 57 | this.element.before(this.ghost); 58 | } 59 | 60 | private moveInside(): void { 61 | this.element.after(this.ghost); 62 | this.ghost.classList.add("jqtree-inside"); 63 | } 64 | 65 | private moveInsideOpenFolder(): void { 66 | const childElement = this.node.children[0]?.element; 67 | 68 | if (childElement) { 69 | childElement.before(this.ghost); 70 | } 71 | } 72 | } 73 | 74 | export default GhostDropHint; 75 | -------------------------------------------------------------------------------- /src/nodeElement/index.ts: -------------------------------------------------------------------------------- 1 | import { DropHint } from "../dragAndDropHandler/types"; 2 | import { GetScrollLeft } from "../jqtreeMethodTypes"; 3 | import { Node, Position } from "../node"; 4 | import BorderDropHint from "./borderDropHint"; 5 | import GhostDropHint from "./ghostDropHint"; 6 | 7 | export interface NodeElementParams { 8 | getScrollLeft: GetScrollLeft; 9 | node: Node; 10 | tabIndex?: number; 11 | treeElement: HTMLElement; 12 | } 13 | 14 | class NodeElement { 15 | public element: HTMLElement; 16 | public node: Node; 17 | private getScrollLeft: GetScrollLeft; 18 | private tabIndex?: number; 19 | private treeElement: HTMLElement; 20 | 21 | constructor({ 22 | getScrollLeft, 23 | node, 24 | tabIndex, 25 | treeElement, 26 | }: NodeElementParams) { 27 | this.getScrollLeft = getScrollLeft; 28 | this.tabIndex = tabIndex; 29 | this.treeElement = treeElement; 30 | 31 | this.init(node); 32 | } 33 | 34 | public addDropHint(position: Position): DropHint { 35 | if (this.mustShowBorderDropHint(position)) { 36 | return new BorderDropHint(this.element, this.getScrollLeft()); 37 | } else { 38 | return new GhostDropHint(this.node, this.element, position); 39 | } 40 | } 41 | 42 | public deselect(): void { 43 | this.element.classList.remove("jqtree-selected"); 44 | 45 | const titleSpan = this.getTitleSpan(); 46 | titleSpan.removeAttribute("tabindex"); 47 | titleSpan.setAttribute("aria-selected", "false"); 48 | 49 | titleSpan.blur(); 50 | } 51 | 52 | public init(node: Node): void { 53 | this.node = node; 54 | 55 | node.element ??= this.treeElement; 56 | 57 | this.element = node.element; 58 | } 59 | 60 | public select(mustSetFocus: boolean): void { 61 | this.element.classList.add("jqtree-selected"); 62 | 63 | const titleSpan = this.getTitleSpan(); 64 | const tabIndex = this.tabIndex; 65 | 66 | // Check for null or undefined 67 | if (tabIndex != null) { 68 | titleSpan.setAttribute("tabindex", tabIndex.toString()); 69 | } 70 | 71 | titleSpan.setAttribute("aria-selected", "true"); 72 | 73 | if (mustSetFocus) { 74 | titleSpan.focus(); 75 | } 76 | } 77 | 78 | protected getTitleSpan(): HTMLSpanElement { 79 | return this.element.querySelector( 80 | ":scope > .jqtree-element > span.jqtree-title", 81 | ) as HTMLSpanElement; 82 | } 83 | 84 | protected getUl(): HTMLUListElement { 85 | return this.element.querySelector(":scope > ul") as HTMLUListElement; 86 | } 87 | 88 | protected mustShowBorderDropHint(position: Position): boolean { 89 | return position === "inside"; 90 | } 91 | } 92 | 93 | export default NodeElement; 94 | -------------------------------------------------------------------------------- /src/nodeUtils.ts: -------------------------------------------------------------------------------- 1 | interface NodeRecordWithChildren extends NodeRecord { 2 | children: NodeData[]; 3 | } 4 | 5 | export const isNodeRecordWithChildren = ( 6 | data: NodeData, 7 | ): data is NodeRecordWithChildren => 8 | typeof data === "object" && 9 | "children" in data && 10 | data.children instanceof Array; 11 | -------------------------------------------------------------------------------- /src/playwright/coverage.ts: -------------------------------------------------------------------------------- 1 | import { BrowserContext } from "@playwright/test"; 2 | import crypto from "crypto"; 3 | import fs from "fs"; 4 | import path from "path"; 5 | 6 | const istanbulCLIOutput = path.join(process.cwd(), ".nyc_output"); 7 | 8 | const generateUUID = () => crypto.randomBytes(16).toString("hex"); 9 | 10 | export const initCoverage = async (context: BrowserContext) => { 11 | await fs.promises.mkdir(istanbulCLIOutput, { recursive: true }); 12 | 13 | await context.exposeFunction( 14 | "collectIstanbulCoverage", 15 | (coverageJSON: string) => { 16 | if (coverageJSON) { 17 | const filename = path.join( 18 | istanbulCLIOutput, 19 | `playwright_coverage_${generateUUID()}.json`, 20 | ); 21 | fs.writeFileSync(filename, coverageJSON); 22 | } 23 | }, 24 | ); 25 | }; 26 | 27 | export const saveCoverage = async (context: BrowserContext) => { 28 | for (const page of context.pages()) { 29 | await page.evaluate(() => { 30 | // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment 31 | const anyWindow = window as any; 32 | // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access 33 | const coverageData = anyWindow.__coverage__; 34 | // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access 35 | anyWindow.collectIstanbulCoverage(JSON.stringify(coverageData)); 36 | }); 37 | } 38 | }; 39 | -------------------------------------------------------------------------------- /src/playwright/playwright.test.ts-snapshots/with-dragAndDrop-moves-a-node-1-Chromium-darwin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbraak/jqTree/cab1ca809d34c06ebc9ef41cb7f6530f44869cda/src/playwright/playwright.test.ts-snapshots/with-dragAndDrop-moves-a-node-1-Chromium-darwin.png -------------------------------------------------------------------------------- /src/playwright/playwright.test.ts-snapshots/with-dragAndDrop-moves-a-node-1-Chromium-linux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbraak/jqTree/cab1ca809d34c06ebc9ef41cb7f6530f44869cda/src/playwright/playwright.test.ts-snapshots/with-dragAndDrop-moves-a-node-1-Chromium-linux.png -------------------------------------------------------------------------------- /src/playwright/playwright.test.ts-snapshots/without-dragAndDrop-displays-a-tree-1-Chromium-darwin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbraak/jqTree/cab1ca809d34c06ebc9ef41cb7f6530f44869cda/src/playwright/playwright.test.ts-snapshots/without-dragAndDrop-displays-a-tree-1-Chromium-darwin.png -------------------------------------------------------------------------------- /src/playwright/playwright.test.ts-snapshots/without-dragAndDrop-displays-a-tree-1-Chromium-linux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbraak/jqTree/cab1ca809d34c06ebc9ef41cb7f6530f44869cda/src/playwright/playwright.test.ts-snapshots/without-dragAndDrop-displays-a-tree-1-Chromium-linux.png -------------------------------------------------------------------------------- /src/playwright/playwright.test.ts-snapshots/without-dragAndDrop-selects-a-node-1-Chromium-darwin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbraak/jqTree/cab1ca809d34c06ebc9ef41cb7f6530f44869cda/src/playwright/playwright.test.ts-snapshots/without-dragAndDrop-selects-a-node-1-Chromium-darwin.png -------------------------------------------------------------------------------- /src/playwright/playwright.test.ts-snapshots/without-dragAndDrop-selects-a-node-1-Chromium-linux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbraak/jqTree/cab1ca809d34c06ebc9ef41cb7f6530f44869cda/src/playwright/playwright.test.ts-snapshots/without-dragAndDrop-selects-a-node-1-Chromium-linux.png -------------------------------------------------------------------------------- /src/scrollHandler.ts: -------------------------------------------------------------------------------- 1 | import { PositionInfo } from "./mouseUtils"; 2 | import createScrollParent from "./scrollHandler/createScrollParent"; 3 | import { ScrollParent } from "./scrollHandler/scrollParent"; 4 | 5 | interface ScrollHandlerParams { 6 | refreshHitAreas: () => void; 7 | treeElement: HTMLElement; 8 | } 9 | 10 | export default class ScrollHandler { 11 | private refreshHitAreas: () => void; 12 | private scrollParent?: ScrollParent; 13 | private treeElement: HTMLElement; 14 | 15 | constructor({ refreshHitAreas, treeElement }: ScrollHandlerParams) { 16 | this.refreshHitAreas = refreshHitAreas; 17 | this.scrollParent = undefined; 18 | this.treeElement = treeElement; 19 | } 20 | 21 | public checkScrolling(positionInfo: PositionInfo): void { 22 | this.checkVerticalScrolling(positionInfo); 23 | this.checkHorizontalScrolling(positionInfo); 24 | } 25 | 26 | public getScrollLeft(): number { 27 | return this.getScrollParent().getScrollLeft(); 28 | } 29 | 30 | public scrollToY(top: number): void { 31 | this.getScrollParent().scrollToY(top); 32 | } 33 | 34 | public stopScrolling() { 35 | this.getScrollParent().stopScrolling(); 36 | } 37 | 38 | private checkHorizontalScrolling(positionInfo: PositionInfo): void { 39 | this.getScrollParent().checkHorizontalScrolling(positionInfo.pageX); 40 | } 41 | 42 | private checkVerticalScrolling(positionInfo: PositionInfo): void { 43 | this.getScrollParent().checkVerticalScrolling(positionInfo.pageY); 44 | } 45 | 46 | private getScrollParent(): ScrollParent { 47 | this.scrollParent ??= createScrollParent( 48 | this.treeElement, 49 | this.refreshHitAreas, 50 | ); 51 | 52 | return this.scrollParent; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/scrollHandler/containerScrollParent.ts: -------------------------------------------------------------------------------- 1 | import { getElementPosition, getOffsetTop } from "../util"; 2 | import { 3 | HorizontalScrollDirection, 4 | ScrollParent, 5 | VerticalScrollDirection, 6 | } from "./scrollParent"; 7 | 8 | export default class ContainerScrollParent extends ScrollParent { 9 | private scrollParentBottom?: number; 10 | private scrollParentTop?: number; 11 | 12 | public stopScrolling() { 13 | super.stopScrolling(); 14 | 15 | this.horizontalScrollDirection = undefined; 16 | this.verticalScrollDirection = undefined; 17 | } 18 | 19 | protected getNewHorizontalScrollDirection( 20 | pageX: number, 21 | ): HorizontalScrollDirection | undefined { 22 | const scrollParentOffset = getElementPosition(this.container); 23 | const containerWidth = this.container.getBoundingClientRect().width; 24 | 25 | const rightEdge = scrollParentOffset.left + containerWidth; 26 | const leftEdge = scrollParentOffset.left; 27 | const isNearRightEdge = pageX > rightEdge - 20; 28 | const isNearLeftEdge = pageX < leftEdge + 20; 29 | 30 | if (isNearRightEdge) { 31 | return "right"; 32 | } else if (isNearLeftEdge) { 33 | return "left"; 34 | } 35 | 36 | return undefined; 37 | } 38 | 39 | protected getNewVerticalScrollDirection( 40 | pageY: number, 41 | ): undefined | VerticalScrollDirection { 42 | if (pageY < this.getScrollParentTop()) { 43 | return "top"; 44 | } 45 | 46 | if (pageY > this.getScrollParentBottom()) { 47 | return "bottom"; 48 | } 49 | 50 | return undefined; 51 | } 52 | 53 | private getScrollParentBottom() { 54 | if (this.scrollParentBottom == null) { 55 | const containerHeight = 56 | this.container.getBoundingClientRect().height; 57 | this.scrollParentBottom = 58 | this.getScrollParentTop() + containerHeight; 59 | } 60 | 61 | return this.scrollParentBottom; 62 | } 63 | 64 | private getScrollParentTop() { 65 | this.scrollParentTop ??= getOffsetTop(this.container); 66 | 67 | return this.scrollParentTop; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/scrollHandler/createScrollParent.ts: -------------------------------------------------------------------------------- 1 | import type { ScrollParent } from "./scrollParent"; 2 | 3 | import ContainerScrollParent from "./containerScrollParent"; 4 | import DocumentScrollParent from "./documentScrollParent"; 5 | 6 | const isOverflow = (overflowValue: string) => 7 | overflowValue === "auto" || overflowValue === "scroll"; 8 | 9 | const hasOverFlow = (element: HTMLElement): boolean => { 10 | const style = getComputedStyle(element); 11 | 12 | return isOverflow(style.overflowX) || isOverflow(style.overflowY); 13 | }; 14 | 15 | const getParentWithOverflow = ( 16 | treeElement: HTMLElement, 17 | ): HTMLElement | null => { 18 | if (hasOverFlow(treeElement)) { 19 | return treeElement; 20 | } 21 | 22 | let parent = treeElement.parentElement; 23 | 24 | while (parent) { 25 | if (hasOverFlow(parent)) { 26 | return parent; 27 | } 28 | 29 | parent = parent.parentElement; 30 | } 31 | 32 | return null; 33 | }; 34 | 35 | const createScrollParent = ( 36 | treeElement: HTMLElement, 37 | refreshHitAreas: () => void, 38 | ): ScrollParent => { 39 | const container = getParentWithOverflow(treeElement); 40 | 41 | if (container && container.tagName !== "HTML") { 42 | return new ContainerScrollParent({ 43 | container, 44 | refreshHitAreas, 45 | }); 46 | } else { 47 | return new DocumentScrollParent({ refreshHitAreas, treeElement }); 48 | } 49 | }; 50 | 51 | export default createScrollParent; 52 | -------------------------------------------------------------------------------- /src/scrollHandler/documentScrollParent.ts: -------------------------------------------------------------------------------- 1 | import { getOffsetTop } from "../util"; 2 | import { 3 | HorizontalScrollDirection, 4 | ScrollParent, 5 | VerticalScrollDirection, 6 | } from "./scrollParent"; 7 | 8 | interface Params { 9 | refreshHitAreas: () => void; 10 | treeElement: HTMLElement; 11 | } 12 | 13 | export default class DocumentScrollParent extends ScrollParent { 14 | private documentScrollHeight?: number; 15 | private documentScrollWidth?: number; 16 | private treeElement: HTMLElement; 17 | 18 | constructor({ refreshHitAreas, treeElement }: Params) { 19 | super({ container: document.documentElement, refreshHitAreas }); 20 | 21 | this.treeElement = treeElement; 22 | } 23 | 24 | public scrollToY(top: number): void { 25 | const treeTop = getOffsetTop(this.treeElement); 26 | 27 | super.scrollToY(top + treeTop); 28 | } 29 | 30 | public stopScrolling() { 31 | super.stopScrolling(); 32 | 33 | this.documentScrollHeight = undefined; 34 | this.documentScrollWidth = undefined; 35 | } 36 | 37 | protected getNewHorizontalScrollDirection( 38 | pageX: number, 39 | ): HorizontalScrollDirection | undefined { 40 | const scrollLeft = this.container.scrollLeft; 41 | const windowWidth = window.innerWidth; 42 | 43 | const isNearRightEdge = pageX > windowWidth - 20; 44 | const isNearLeftEdge = pageX - scrollLeft < 20; 45 | 46 | if (isNearRightEdge && this.canScrollRight()) { 47 | return "right"; 48 | } 49 | 50 | if (isNearLeftEdge) { 51 | return "left"; 52 | } 53 | 54 | return undefined; 55 | } 56 | 57 | protected getNewVerticalScrollDirection( 58 | pageY: number, 59 | ): undefined | VerticalScrollDirection { 60 | const scrollTop = this.container.scrollTop; 61 | const distanceTop = pageY - scrollTop; 62 | 63 | if (distanceTop < 20) { 64 | return "top"; 65 | } 66 | 67 | const windowHeight = window.innerHeight; 68 | 69 | if (windowHeight - (pageY - scrollTop) < 20 && this.canScrollDown()) { 70 | return "bottom"; 71 | } 72 | 73 | return undefined; 74 | } 75 | 76 | private canScrollDown() { 77 | return ( 78 | this.container.scrollTop + this.container.clientHeight < 79 | this.getDocumentScrollHeight() 80 | ); 81 | } 82 | 83 | private canScrollRight() { 84 | return ( 85 | this.container.scrollLeft + this.container.clientWidth < 86 | this.getDocumentScrollWidth() 87 | ); 88 | } 89 | 90 | private getDocumentScrollHeight() { 91 | // Store the original scroll height because the scroll height can increase when the drag element is moved beyond the scroll height. 92 | this.documentScrollHeight ??= this.container.scrollHeight; 93 | 94 | return this.documentScrollHeight; 95 | } 96 | 97 | private getDocumentScrollWidth() { 98 | // Store the original scroll width because the scroll width can increase when the drag element is moved beyond the scroll width. 99 | this.documentScrollWidth ??= this.container.scrollWidth; 100 | 101 | return this.documentScrollWidth; 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/scrollHandler/scrollParent.ts: -------------------------------------------------------------------------------- 1 | export type HorizontalScrollDirection = "left" | "right"; 2 | export type VerticalScrollDirection = "bottom" | "top"; 3 | 4 | interface ConstructorParams { 5 | container: HTMLElement; 6 | refreshHitAreas: () => void; 7 | } 8 | 9 | export abstract class ScrollParent { 10 | protected container: HTMLElement; 11 | protected horizontalScrollDirection?: HorizontalScrollDirection; 12 | protected horizontalScrollTimeout?: number; 13 | 14 | protected refreshHitAreas: () => void; 15 | protected verticalScrollDirection?: VerticalScrollDirection; 16 | protected verticalScrollTimeout?: number; 17 | 18 | constructor({ container, refreshHitAreas }: ConstructorParams) { 19 | this.container = container; 20 | this.refreshHitAreas = refreshHitAreas; 21 | } 22 | 23 | public checkHorizontalScrolling(pageX: number): void { 24 | const newHorizontalScrollDirection = 25 | this.getNewHorizontalScrollDirection(pageX); 26 | 27 | if (this.horizontalScrollDirection !== newHorizontalScrollDirection) { 28 | this.horizontalScrollDirection = newHorizontalScrollDirection; 29 | 30 | if (this.horizontalScrollTimeout != null) { 31 | window.clearTimeout(this.horizontalScrollTimeout); 32 | } 33 | 34 | if (newHorizontalScrollDirection) { 35 | this.horizontalScrollTimeout = window.setTimeout( 36 | this.scrollHorizontally.bind(this), 37 | 40, 38 | ); 39 | } 40 | } 41 | } 42 | 43 | public checkVerticalScrolling(pageY: number) { 44 | const newVerticalScrollDirection = 45 | this.getNewVerticalScrollDirection(pageY); 46 | 47 | if (this.verticalScrollDirection !== newVerticalScrollDirection) { 48 | this.verticalScrollDirection = newVerticalScrollDirection; 49 | 50 | if (this.verticalScrollTimeout != null) { 51 | window.clearTimeout(this.verticalScrollTimeout); 52 | this.verticalScrollTimeout = undefined; 53 | } 54 | 55 | if (newVerticalScrollDirection) { 56 | this.verticalScrollTimeout = window.setTimeout( 57 | this.scrollVertically.bind(this), 58 | 40, 59 | ); 60 | } 61 | } 62 | } 63 | 64 | public getScrollLeft(): number { 65 | return this.container.scrollLeft; 66 | } 67 | 68 | public scrollToY(top: number): void { 69 | this.container.scrollTop = top; 70 | } 71 | 72 | public stopScrolling() { 73 | this.horizontalScrollDirection = undefined; 74 | this.verticalScrollDirection = undefined; 75 | } 76 | 77 | protected abstract getNewHorizontalScrollDirection( 78 | pageX: number, 79 | ): HorizontalScrollDirection | undefined; 80 | protected abstract getNewVerticalScrollDirection( 81 | pageY: number, 82 | ): undefined | VerticalScrollDirection; 83 | 84 | protected scrollHorizontally() { 85 | if (!this.horizontalScrollDirection) { 86 | return; 87 | } 88 | 89 | const distance = this.horizontalScrollDirection === "left" ? -20 : 20; 90 | this.container.scrollBy({ 91 | behavior: "instant", 92 | left: distance, 93 | top: 0, 94 | }); 95 | 96 | this.refreshHitAreas(); 97 | 98 | setTimeout(this.scrollHorizontally.bind(this), 40); 99 | } 100 | 101 | protected scrollVertically() { 102 | if (!this.verticalScrollDirection) { 103 | return; 104 | } 105 | 106 | const distance = this.verticalScrollDirection === "top" ? -20 : 20; 107 | this.container.scrollBy({ 108 | behavior: "instant", 109 | left: 0, 110 | top: distance, 111 | }); 112 | 113 | this.refreshHitAreas(); 114 | 115 | setTimeout(this.scrollVertically.bind(this), 40); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/selectNodeHandler.ts: -------------------------------------------------------------------------------- 1 | import { GetNodeById } from "./jqtreeMethodTypes"; 2 | import { Node } from "./node"; 3 | 4 | interface SelectNodeHandlerParameters { 5 | getNodeById: GetNodeById; 6 | } 7 | 8 | export default class SelectNodeHandler { 9 | private getNodeById: GetNodeById; 10 | private selectedNodes: Set; 11 | private selectedSingleNode: Node | null; 12 | 13 | constructor({ getNodeById }: SelectNodeHandlerParameters) { 14 | this.getNodeById = getNodeById; 15 | this.selectedNodes = new Set(); 16 | this.clear(); 17 | } 18 | 19 | public addToSelection(node: Node): void { 20 | if (node.id != null) { 21 | this.selectedNodes.add(node.id); 22 | } else { 23 | this.selectedSingleNode = node; 24 | } 25 | } 26 | 27 | public clear(): void { 28 | this.selectedNodes.clear(); 29 | this.selectedSingleNode = null; 30 | } 31 | 32 | public getSelectedNode(): false | Node { 33 | const selectedNodes = this.getSelectedNodes(); 34 | 35 | if (selectedNodes.length) { 36 | return selectedNodes[0] ?? false; 37 | } else { 38 | return false; 39 | } 40 | } 41 | 42 | public getSelectedNodes(): Node[] { 43 | if (this.selectedSingleNode) { 44 | return [this.selectedSingleNode]; 45 | } else { 46 | const selectedNodes: Node[] = []; 47 | 48 | this.selectedNodes.forEach((id) => { 49 | const node = this.getNodeById(id); 50 | if (node) { 51 | selectedNodes.push(node); 52 | } 53 | }); 54 | 55 | return selectedNodes; 56 | } 57 | } 58 | 59 | public getSelectedNodesUnder(parent: Node): Node[] { 60 | if (this.selectedSingleNode) { 61 | if (parent.isParentOf(this.selectedSingleNode)) { 62 | return [this.selectedSingleNode]; 63 | } else { 64 | return []; 65 | } 66 | } else { 67 | const selectedNodes: Node[] = []; 68 | 69 | this.selectedNodes.forEach((id) => { 70 | const node = this.getNodeById(id); 71 | if (node && parent.isParentOf(node)) { 72 | selectedNodes.push(node); 73 | } 74 | }); 75 | 76 | return selectedNodes; 77 | } 78 | } 79 | 80 | public isNodeSelected(node: Node): boolean { 81 | if (node.id != null) { 82 | return this.selectedNodes.has(node.id); 83 | } else if (this.selectedSingleNode) { 84 | return this.selectedSingleNode.element === node.element; 85 | } else { 86 | return false; 87 | } 88 | } 89 | 90 | public removeFromSelection(node: Node, includeChildren = false): void { 91 | if (node.id == null) { 92 | if ( 93 | this.selectedSingleNode && 94 | node.element === this.selectedSingleNode.element 95 | ) { 96 | this.selectedSingleNode = null; 97 | } 98 | } else { 99 | this.selectedNodes.delete(node.id); 100 | 101 | if (includeChildren) { 102 | node.iterate(() => { 103 | if (node.id != null) { 104 | this.selectedNodes.delete(node.id); 105 | } 106 | return true; 107 | }); 108 | } 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/test/dataLoader.test.ts: -------------------------------------------------------------------------------- 1 | import { jest } from "@jest/globals"; 2 | import { waitFor } from "@testing-library/dom"; 3 | import { http, HttpResponse } from "msw"; 4 | import { setupServer } from "msw/node"; 5 | 6 | import DataLoader from "../dataLoader"; 7 | import { TriggerEvent } from "../jqtreeMethodTypes"; 8 | 9 | describe("loadFromUrl", () => { 10 | const server = setupServer(); 11 | 12 | beforeAll(() => { 13 | server.listen(); 14 | }); 15 | 16 | afterEach(() => { 17 | server.resetHandlers(); 18 | }); 19 | 20 | afterAll(() => { 21 | server.close(); 22 | }); 23 | 24 | it("does nothing when urlInfo is empty", () => { 25 | const loadData = () => null; 26 | const treeElement = document.createElement("div"); 27 | const triggerEvent = jest.fn(); 28 | 29 | const dataLoader = new DataLoader({ 30 | loadData, 31 | treeElement, 32 | triggerEvent, 33 | }); 34 | 35 | dataLoader.loadFromUrl(null, null, null); 36 | 37 | expect(triggerEvent).not.toHaveBeenCalled(); 38 | }); 39 | 40 | it("parses json when the response is a string", async () => { 41 | server.use( 42 | http.get( 43 | "/test", 44 | () => 45 | new HttpResponse('{ "key1": "value1" }', { 46 | headers: { 47 | "Content-Type": "text/plain", 48 | }, 49 | }), 50 | {}, 51 | ), 52 | ); 53 | 54 | const loadData = jest.fn(); 55 | const treeElement = document.createElement("div"); 56 | const triggerEvent = jest.fn(); 57 | 58 | const dataLoader = new DataLoader({ 59 | loadData, 60 | treeElement, 61 | triggerEvent, 62 | }); 63 | dataLoader.loadFromUrl({ dataType: "text", url: "/test" }, null, null); 64 | 65 | await waitFor(() => { 66 | expect(loadData).toHaveBeenCalledWith({ key1: "value1" }, null); 67 | }); 68 | }); 69 | }); 70 | -------------------------------------------------------------------------------- /src/test/dragAndDropHandler/binarySearch.test.ts: -------------------------------------------------------------------------------- 1 | import binarySearch from "../../dragAndDropHandler/binarySearch"; 2 | 3 | describe("binarySearch", () => { 4 | it("returns null when the array is empty", () => { 5 | const compareFn = (_item: number) => 0; 6 | 7 | const result = binarySearch([], compareFn); 8 | 9 | expect(result).toBeNull(); 10 | }); 11 | 12 | it("finds a value", () => { 13 | const compareFn = (item: number) => item - 5; 14 | 15 | const result = binarySearch([1, 5, 7, 9], compareFn); 16 | 17 | expect(result).toBe(5); 18 | }); 19 | 20 | it("returns null when the value doesn't exist", () => { 21 | const compareFn = (item: number) => item - 6; 22 | 23 | const result = binarySearch([1, 5, 7, 9], compareFn); 24 | 25 | expect(result).toBeNull(); 26 | }); 27 | 28 | it("handles undefined values in the array", () => { 29 | const compareFn = (item: number) => item - 6; 30 | const array = [1, 5, 7, 9]; 31 | (array as any)[1] = undefined; // eslint-disable-line @typescript-eslint/no-unsafe-member-access 32 | 33 | const result = binarySearch(array, compareFn); 34 | 35 | expect(result).toBeNull(); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /src/test/dragAndDropHandler/dragElement.test.ts: -------------------------------------------------------------------------------- 1 | import DragElement from "../../dragAndDropHandler/dragElement"; 2 | 3 | describe("DragElement", () => { 4 | it("creates an element with autoEscape is true", () => { 5 | const treeElement = document.createElement("div"); 6 | 7 | new DragElement({ 8 | autoEscape: true, 9 | nodeName: "abc & def", 10 | offsetX: 0, 11 | offsetY: 0, 12 | treeElement, 13 | }); 14 | 15 | expect(treeElement.children).toHaveLength(1); 16 | 17 | const childElement = treeElement.children[0]; 18 | 19 | expect(childElement).toHaveClass("jqtree-title"); 20 | expect(childElement).toHaveClass("jqtree-dragging"); 21 | expect(childElement).toHaveTextContent("abc & def"); 22 | }); 23 | 24 | it("creates an element with autoEscape is false", () => { 25 | const treeElement = document.createElement("div"); 26 | 27 | new DragElement({ 28 | autoEscape: false, 29 | nodeName: "abc & def", 30 | offsetX: 0, 31 | offsetY: 0, 32 | treeElement, 33 | }); 34 | 35 | expect(treeElement.children).toHaveLength(1); 36 | 37 | const childElement = treeElement.children[0]; 38 | 39 | expect(childElement).toHaveTextContent("abc & def"); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /src/test/jqTree/accessibility.test.ts: -------------------------------------------------------------------------------- 1 | import { axe, toHaveNoViolations } from "jest-axe"; 2 | 3 | import "../../tree.jquery"; 4 | import exampleData from "../support/exampleData"; 5 | 6 | expect.extend(toHaveNoViolations); 7 | 8 | describe("accessibility", () => { 9 | beforeEach(() => { 10 | $("body").append('
'); 11 | }); 12 | 13 | afterEach(() => { 14 | const $tree = $("#tree1"); 15 | $tree.tree("destroy"); 16 | $tree.remove(); 17 | }); 18 | 19 | it("has an accessible ui", async () => { 20 | const $tree = $("#tree1"); 21 | $tree.tree({ 22 | data: exampleData, 23 | }); 24 | const element = $tree.get()[0] as HTMLElement; 25 | 26 | await expect(axe(element)).resolves.toHaveNoViolations(); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /src/test/jqTree/create.test.ts: -------------------------------------------------------------------------------- 1 | import getGiven from "givens"; 2 | 3 | import "../../tree.jquery"; 4 | import exampleData from "../support/exampleData"; 5 | 6 | describe("create with data", () => { 7 | beforeEach(() => { 8 | $("body").append('
'); 9 | }); 10 | 11 | afterEach(() => { 12 | const $tree = $("#tree1"); 13 | $tree.tree("destroy"); 14 | $tree.remove(); 15 | }); 16 | 17 | interface Vars { 18 | $tree: JQuery; 19 | } 20 | 21 | const given = getGiven(); 22 | given("$tree", () => $("#tree1")); 23 | 24 | beforeEach(() => { 25 | given.$tree.tree({ 26 | data: exampleData, 27 | }); 28 | }); 29 | 30 | it("creates a tree", () => { 31 | expect(given.$tree).toHaveTreeStructure([ 32 | expect.objectContaining({ 33 | children: [ 34 | expect.objectContaining({ name: "child1" }), 35 | expect.objectContaining({ name: "child2" }), 36 | ], 37 | name: "node1", 38 | open: false, 39 | selected: false, 40 | }), 41 | expect.objectContaining({ 42 | children: [ 43 | expect.objectContaining({ 44 | children: [expect.objectContaining({ name: "child3" })], 45 | name: "node3", 46 | open: false, 47 | }), 48 | ], 49 | name: "node2", 50 | open: false, 51 | selected: false, 52 | }), 53 | ]); 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /src/test/jqTree/mouse.test.ts: -------------------------------------------------------------------------------- 1 | import { userEvent } from "@testing-library/user-event"; 2 | 3 | import "../../tree.jquery"; 4 | import exampleData from "../support/exampleData"; 5 | import { titleSpan, togglerLink } from "../support/testUtil"; 6 | 7 | describe("mouse", () => { 8 | beforeEach(() => { 9 | $("body").append('
'); 10 | }); 11 | 12 | afterEach(() => { 13 | const $tree = $("#tree1"); 14 | $tree.tree("destroy"); 15 | $tree.remove(); 16 | }); 17 | 18 | it("selects a node and sets the focus when it is clicked", async () => { 19 | const $tree = $("#tree1"); 20 | $tree.tree({ data: exampleData }); 21 | 22 | const node = $tree.tree("getNodeByNameMustExist", "node1"); 23 | 24 | expect(node.element).not.toBeSelected(); 25 | expect(node.element).not.toBeFocused(); 26 | 27 | await userEvent.click(titleSpan(node.element as HTMLElement)); 28 | 29 | expect(node.element).toBeSelected(); 30 | }); 31 | 32 | it("deselects when a selected node is clicked", async () => { 33 | const $tree = $("#tree1"); 34 | $tree.tree({ data: exampleData }); 35 | 36 | const node = $tree.tree("getNodeByNameMustExist", "node1"); 37 | $tree.tree("selectNode", node); 38 | 39 | expect(node.element).toBeSelected(); 40 | 41 | await userEvent.click(titleSpan(node.element as HTMLElement)); 42 | 43 | expect(node.element).not.toBeSelected(); 44 | }); 45 | 46 | it("opens a node when the toggle button is clicked", async () => { 47 | const $tree = $("#tree1"); 48 | $tree.tree({ data: exampleData }); 49 | 50 | const node = $tree.tree("getNodeByNameMustExist", "node1"); 51 | 52 | expect(node.element).not.toBeOpen(); 53 | 54 | await userEvent.click(togglerLink(node.element as HTMLElement)); 55 | 56 | expect(node.element).toBeOpen(); 57 | }); 58 | 59 | it("doesn't select a node when it is opened", async () => { 60 | const $tree = $("#tree1"); 61 | $tree.tree({ data: exampleData }); 62 | 63 | const node = $tree.tree("getNodeByNameMustExist", "node1"); 64 | 65 | expect(node.element).not.toBeSelected(); 66 | expect(node.element).not.toBeOpen(); 67 | 68 | await userEvent.click(togglerLink(node.element as HTMLElement)); 69 | 70 | expect(node.element).not.toBeSelected(); 71 | expect(node.element).toBeOpen(); 72 | }); 73 | 74 | it("keeps it selected when a selected node is opened", async () => { 75 | const $tree = $("#tree1"); 76 | $tree.tree({ data: exampleData }); 77 | 78 | const node = $tree.tree("getNodeByNameMustExist", "node1"); 79 | $tree.tree("selectNode", node); 80 | 81 | expect(node.element).toBeSelected(); 82 | expect(node.element).not.toBeOpen(); 83 | 84 | await userEvent.click(togglerLink(node.element as HTMLElement)); 85 | 86 | expect(node.element).toBeSelected(); 87 | expect(node.element).toBeOpen(); 88 | }); 89 | }); 90 | -------------------------------------------------------------------------------- /src/test/nodeElement/borderDropHint.test.ts: -------------------------------------------------------------------------------- 1 | import BorderDropHint from "../../nodeElement/borderDropHint"; 2 | 3 | describe("BorderDropHint", () => { 4 | it("creates an element", () => { 5 | const element = document.createElement("div"); 6 | 7 | const jqTreeElement = document.createElement("div"); 8 | jqTreeElement.classList.add("jqtree-element"); 9 | element.append(jqTreeElement); 10 | 11 | new BorderDropHint(element, 0); 12 | 13 | expect(jqTreeElement.children).toHaveLength(1); 14 | expect(jqTreeElement.children[0]).toHaveClass("jqtree-border"); 15 | }); 16 | 17 | it("doesn't create an element if the node doesn't have a jqtree-element child", () => { 18 | const element = document.createElement("div"); 19 | 20 | new BorderDropHint(element, 0); 21 | 22 | expect(element.children).toBeEmpty(); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /src/test/nodeElement/ghostDropHint.test.ts: -------------------------------------------------------------------------------- 1 | import { Node } from "../../node"; 2 | import GhostDropHint from "../../nodeElement/ghostDropHint"; 3 | 4 | describe("GhostDropHint", () => { 5 | beforeEach(() => { 6 | document.body.innerHTML = ""; 7 | }); 8 | 9 | it("creates a hint element after the node element when the position is After", () => { 10 | const nodeElement = document.createElement("div"); 11 | document.body.append(nodeElement); 12 | 13 | const node = new Node(); 14 | new GhostDropHint(node, nodeElement, "after"); 15 | 16 | expect(nodeElement.nextSibling).toHaveClass("jqtree-ghost"); 17 | expect(nodeElement.previousSibling).toBeNull(); 18 | expect(nodeElement.children).toBeEmpty(); 19 | }); 20 | 21 | it("creates a hint element after the node element when the position is Before", () => { 22 | const nodeElement = document.createElement("div"); 23 | document.body.append(nodeElement); 24 | 25 | const node = new Node(); 26 | new GhostDropHint(node, nodeElement, "before"); 27 | 28 | expect(nodeElement.previousSibling).toHaveClass("jqtree-ghost"); 29 | expect(nodeElement.nextSibling).toBeNull(); 30 | expect(nodeElement.children).toBeEmpty(); 31 | }); 32 | 33 | it("creates a hint element after the node element when the position is Inside and the node is an open folder", () => { 34 | const nodeElement = document.createElement("div"); 35 | document.body.append(nodeElement); 36 | 37 | const childElement = document.createElement("div"); 38 | nodeElement.append(childElement); 39 | 40 | const node = new Node({ is_open: true }); 41 | const childNode = new Node(); 42 | childNode.element = childElement; 43 | node.addChild(childNode); 44 | 45 | new GhostDropHint(node, nodeElement, "inside"); 46 | 47 | expect(nodeElement.previousSibling).toBeNull(); 48 | expect(nodeElement.nextSibling).toBeNull(); 49 | expect(nodeElement.children).toHaveLength(2); 50 | expect(nodeElement.children[0]).toHaveClass("jqtree-ghost"); 51 | }); 52 | 53 | it("creates a hint element after the node element when the position is Inside and the node is a closed folder", () => { 54 | const nodeElement = document.createElement("div"); 55 | document.body.append(nodeElement); 56 | 57 | const node = new Node(); 58 | node.addChild(new Node()); 59 | 60 | new GhostDropHint(node, nodeElement, "inside"); 61 | 62 | expect(nodeElement.nextSibling).toHaveClass("jqtree-ghost"); 63 | expect(nodeElement.previousSibling).toBeNull(); 64 | expect(nodeElement.children).toBeEmpty(); 65 | }); 66 | }); 67 | -------------------------------------------------------------------------------- /src/test/nodeElement/index.test.ts: -------------------------------------------------------------------------------- 1 | import { Node } from "../../node"; 2 | import NodeElement from "../../nodeElement"; 3 | 4 | describe("NodeElement", () => { 5 | it("sets the element to the element of the node", () => { 6 | const treeElement = document.createElement("div"); 7 | document.body.append(treeElement); 8 | 9 | const element = document.createElement("div"); 10 | document.body.append(element); 11 | 12 | const node = new Node(); 13 | node.element = element; 14 | 15 | const getScrollLeft = () => 0; 16 | 17 | const nodeElement = new NodeElement({ 18 | getScrollLeft, 19 | node, 20 | treeElement, 21 | }); 22 | 23 | expect(nodeElement.element).toStrictEqual(element); 24 | }); 25 | 26 | it("sets the element to the tree element when the node doesn't have an element", () => { 27 | const treeElement = document.createElement("div"); 28 | document.body.append(treeElement); 29 | 30 | const node = new Node(); 31 | const getScrollLeft = () => 0; 32 | 33 | const nodeElement = new NodeElement({ 34 | getScrollLeft, 35 | node, 36 | treeElement, 37 | }); 38 | 39 | expect(nodeElement.element).toStrictEqual(treeElement); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /src/test/nodeUtils.test.ts: -------------------------------------------------------------------------------- 1 | import { isNodeRecordWithChildren } from "../nodeUtils"; 2 | 3 | describe("isNodeRecordWithChildren", () => { 4 | it("returns true when the data is an object with the children attribute of type array", () => { 5 | const data = { 6 | children: [], 7 | }; 8 | 9 | expect(isNodeRecordWithChildren(data)).toBe(true); 10 | }); 11 | 12 | it("returns when the data is an object without the children attribute", () => { 13 | const data = { name: "test" }; 14 | 15 | expect(isNodeRecordWithChildren(data)).toBe(false); 16 | }); 17 | 18 | it("returns when the data is a string", () => { 19 | expect(isNodeRecordWithChildren("test")).toBe(false); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /src/test/scrollHandler/documentScrollParent.test.ts: -------------------------------------------------------------------------------- 1 | import { afterEach, describe, expect, it, jest } from "@jest/globals"; 2 | 3 | import DocumentScrollParent from "../../scrollHandler/documentScrollParent"; 4 | 5 | describe("checkHorizontalScrolling", () => { 6 | afterEach(() => { 7 | jest.useRealTimers(); 8 | }); 9 | 10 | it("scrolls to the left when pageX is near the left edge", () => { 11 | jest.useFakeTimers(); 12 | const scrollBy = jest.fn(); 13 | document.documentElement.scrollBy = scrollBy; 14 | 15 | const refreshHitAreas = jest.fn(); 16 | const treeElement = document.createElement("div"); 17 | 18 | const documentScrollParent = new DocumentScrollParent({ 19 | refreshHitAreas, 20 | treeElement, 21 | }); 22 | 23 | documentScrollParent.checkHorizontalScrolling(10); 24 | 25 | expect(scrollBy).not.toHaveBeenCalled(); 26 | 27 | jest.advanceTimersByTime(50); 28 | 29 | expect(scrollBy).toHaveBeenCalledWith({ 30 | behavior: "instant", 31 | left: -20, 32 | top: 0, 33 | }); 34 | }); 35 | 36 | it("stops scrolling when pageX is moved from the left edge", () => { 37 | jest.useFakeTimers(); 38 | const scrollBy = jest.fn(); 39 | document.documentElement.scrollBy = scrollBy; 40 | 41 | const refreshHitAreas = jest.fn(); 42 | const treeElement = document.createElement("div"); 43 | 44 | const documentScrollParent = new DocumentScrollParent({ 45 | refreshHitAreas, 46 | treeElement, 47 | }); 48 | 49 | documentScrollParent.checkHorizontalScrolling(10); 50 | 51 | expect(scrollBy).not.toHaveBeenCalled(); 52 | 53 | jest.advanceTimersByTime(50); 54 | 55 | expect(scrollBy).toHaveBeenCalledWith({ 56 | behavior: "instant", 57 | left: -20, 58 | top: 0, 59 | }); 60 | 61 | documentScrollParent.checkHorizontalScrolling(100); 62 | jest.advanceTimersByTime(50); 63 | 64 | expect(scrollBy).toHaveBeenCalledTimes(1); 65 | }); 66 | }); 67 | 68 | describe("checkVerticalScrolling", () => { 69 | it("scrolls to the top when pageY is near the top edge", () => { 70 | jest.useFakeTimers(); 71 | const scrollBy = jest.fn(); 72 | document.documentElement.scrollBy = scrollBy; 73 | 74 | const refreshHitAreas = jest.fn(); 75 | const treeElement = document.createElement("div"); 76 | 77 | const documentScrollParent = new DocumentScrollParent({ 78 | refreshHitAreas, 79 | treeElement, 80 | }); 81 | 82 | documentScrollParent.checkVerticalScrolling(10); 83 | 84 | expect(scrollBy).not.toHaveBeenCalled(); 85 | 86 | jest.advanceTimersByTime(50); 87 | 88 | expect(scrollBy).toHaveBeenCalledWith({ 89 | behavior: "instant", 90 | left: 0, 91 | top: -20, 92 | }); 93 | }); 94 | 95 | it("stops scrolling when pageX is moved from the top edge", () => { 96 | jest.useFakeTimers(); 97 | const scrollBy = jest.fn(); 98 | document.documentElement.scrollBy = scrollBy; 99 | 100 | const refreshHitAreas = jest.fn(); 101 | const treeElement = document.createElement("div"); 102 | 103 | const documentScrollParent = new DocumentScrollParent({ 104 | refreshHitAreas, 105 | treeElement, 106 | }); 107 | 108 | documentScrollParent.checkVerticalScrolling(10); 109 | 110 | expect(scrollBy).not.toHaveBeenCalled(); 111 | 112 | jest.advanceTimersByTime(50); 113 | 114 | expect(scrollBy).toHaveBeenCalledWith({ 115 | behavior: "instant", 116 | left: 0, 117 | top: -20, 118 | }); 119 | 120 | documentScrollParent.checkVerticalScrolling(100); 121 | jest.advanceTimersByTime(50); 122 | 123 | expect(scrollBy).toHaveBeenCalledTimes(1); 124 | }); 125 | }); 126 | -------------------------------------------------------------------------------- /src/test/selectNodeHandler.test.ts: -------------------------------------------------------------------------------- 1 | import { Node } from "../node"; 2 | import SelectNodeHandler from "../selectNodeHandler"; 3 | 4 | describe("getSelectedNodesUnder", () => { 5 | it("returns the nodes when the nodes have an id", () => { 6 | const node = new Node({ id: 1 }); 7 | 8 | const child = new Node({ id: 2 }); 9 | node.addChild(child); 10 | 11 | const nodeMap = new Map(); 12 | nodeMap.set(1, node); 13 | nodeMap.set(2, child); 14 | 15 | const getNodeById = (id: NodeId) => nodeMap.get(id) ?? null; 16 | 17 | const selectNodeHandler = new SelectNodeHandler({ getNodeById }); 18 | selectNodeHandler.addToSelection(child); 19 | 20 | expect( 21 | selectNodeHandler.getSelectedNodesUnder(node), 22 | ).toIncludeAllMembers([child]); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /src/test/support/exampleData.ts: -------------------------------------------------------------------------------- 1 | const exampleData = [ 2 | { 3 | children: [ 4 | { id: 125, intProperty: 2, name: "child1" }, 5 | { id: 126, name: "child2" }, 6 | ], 7 | id: 123, // extra data 8 | intProperty: 1, 9 | name: "node1", 10 | strProperty: "1", 11 | }, 12 | { 13 | children: [ 14 | { children: [{ id: 128, name: "child3" }], id: 127, name: "node3" }, 15 | ], 16 | id: 124, 17 | intProperty: 3, 18 | name: "node2", 19 | strProperty: "3", 20 | }, 21 | ]; 22 | 23 | export default exampleData; 24 | -------------------------------------------------------------------------------- /src/test/support/jqTreeMatchers.ts: -------------------------------------------------------------------------------- 1 | import { titleSpan } from "./testUtil"; 2 | import treeStructure from "./treeStructure"; 3 | 4 | const assertJqTreeFolder = (el: HTMLElement) => { 5 | /* istanbul ignore if */ 6 | if (!el.classList.contains("jqtree-folder")) { 7 | throw new Error("Node is not a folder"); 8 | } 9 | }; 10 | 11 | expect.extend({ 12 | toBeClosed(el: HTMLElement) { 13 | assertJqTreeFolder(el); 14 | 15 | /* istanbul ignore next */ 16 | return { 17 | message: () => "The node is open", 18 | pass: el.classList.contains("jqtree-closed"), 19 | }; 20 | }, 21 | toBeFocused(el: HTMLElement) { 22 | /* istanbul ignore next */ 23 | return { 24 | message: () => "The is node is not focused", 25 | pass: document.activeElement === titleSpan(el), 26 | }; 27 | }, 28 | toBeOpen(el: HTMLElement) { 29 | assertJqTreeFolder(el); 30 | 31 | /* istanbul ignore next */ 32 | return { 33 | message: () => "The node is closed", 34 | pass: !el.classList.contains("jqtree-closed"), 35 | }; 36 | }, 37 | toBeSelected(el: HTMLElement) { 38 | /* istanbul ignore next */ 39 | return { 40 | message: () => "The node is not selected", 41 | pass: el.classList.contains("jqtree-selected"), 42 | }; 43 | }, 44 | toHaveTreeStructure( 45 | $el: JQuery, 46 | expectedStructure: JQTreeMatchers.TreeStructure, 47 | ) { 48 | const el = $el.get(0) as HTMLElement; 49 | const receivedStructure = treeStructure(el); 50 | 51 | /* istanbul ignore next */ 52 | return { 53 | message: () => 54 | this.utils.printDiffOrStringify( 55 | expectedStructure, 56 | receivedStructure, 57 | "expected", 58 | "received", 59 | true, 60 | ), 61 | pass: this.equals(receivedStructure, expectedStructure), 62 | }; 63 | }, 64 | }); 65 | -------------------------------------------------------------------------------- /src/test/support/matchers.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | declare namespace JQTreeMatchers { 4 | export type TreeNode = TreeChild | TreeFolder; 5 | 6 | export type TreeStructure = TreeNode[]; 7 | 8 | interface TreeChild { 9 | name: string; 10 | nodeType: "child"; 11 | selected: boolean; 12 | } 13 | interface TreeFolder { 14 | children: TreeNode[]; 15 | name: string; 16 | nodeType: "folder"; 17 | open: boolean; 18 | selected: boolean; 19 | } 20 | } 21 | 22 | declare namespace jest { 23 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 24 | interface Matchers { 25 | toBeClosed(): boolean; 26 | toBeFocused(): boolean; 27 | toBeOpen(): boolean; 28 | toBeSelected(): boolean; 29 | toHaveTreeStructure(treeStructure: TreeStructure): boolean; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/test/support/setupTests.ts: -------------------------------------------------------------------------------- 1 | import "@testing-library/jest-dom"; 2 | import jQuery from "jquery"; 3 | 4 | import "./jqTreeMatchers"; 5 | 6 | declare global { 7 | interface Window { 8 | $: JQueryStatic; 9 | jQuery: JQueryStatic; 10 | TransformStream: any; 11 | } 12 | } 13 | 14 | window.$ = jQuery; 15 | window.jQuery = jQuery; 16 | -------------------------------------------------------------------------------- /src/test/support/testUtil.ts: -------------------------------------------------------------------------------- 1 | import { jest } from "@jest/globals"; 2 | import { mockElementBoundingClientRect } from "jsdom-testing-mocks"; 3 | 4 | import { Node } from "../../node"; 5 | 6 | interface Rect { 7 | height: number; 8 | width: number; 9 | x: number; 10 | y: number; 11 | } 12 | 13 | export const getChilden = ( 14 | el: HTMLElement, 15 | nodeName: string, 16 | className: string, 17 | ) => { 18 | const result: HTMLElement[] = []; 19 | 20 | for (const child of el.children) { 21 | if ( 22 | child.nodeName === nodeName.toUpperCase() && 23 | child.classList.contains(className) 24 | ) { 25 | result.push(child as HTMLElement); 26 | } 27 | } 28 | 29 | return result; 30 | }; 31 | 32 | export const singleChild = ( 33 | el: HTMLElement, 34 | nodeName: string, 35 | className: string, 36 | ) => { 37 | const children = getChilden(el, nodeName, className); 38 | 39 | /* istanbul ignore if */ 40 | if (children.length !== 1) { 41 | throw new Error( 42 | `Expected single child, got ${el.children.length} for ${nodeName} ${className}`, 43 | ); 44 | } 45 | 46 | return children[0] as HTMLElement; 47 | }; 48 | 49 | export const titleSpan = (liNode: HTMLElement): HTMLElement => 50 | singleChild(nodeElement(liNode), "span", "jqtree-title"); 51 | 52 | export const togglerLink = (liNode: HTMLElement): HTMLElement => 53 | singleChild(nodeElement(liNode), "a", "jqtree-toggler"); 54 | 55 | const nodeElement = (liNode: HTMLElement): HTMLElement => 56 | singleChild(liNode, "div", "jqtree-element"); 57 | 58 | const mockLayout = (element: HTMLElement, rect: Rect) => { 59 | jest.spyOn(element, "clientHeight", "get").mockReturnValue(rect.height); 60 | jest.spyOn(element, "clientWidth", "get").mockReturnValue(rect.width); 61 | jest.spyOn(element, "offsetParent", "get").mockReturnValue( 62 | element.parentElement, 63 | ); 64 | 65 | mockElementBoundingClientRect(element, rect); 66 | }; 67 | 68 | export const generateHtmlElementsForTree = (tree: Node) => { 69 | let y = 0; 70 | 71 | const createNodeElement = (node: Node) => { 72 | const isTree = node.tree === node; 73 | 74 | if (isTree) { 75 | const element = document.createElement("ul"); 76 | element.className = "jqtree-tree"; 77 | return element; 78 | } else { 79 | const li = document.createElement("li"); 80 | 81 | if (node.isFolder()) { 82 | li.className = "jqtree-folder"; 83 | 84 | if (!node.is_open) { 85 | li.classList.add("jqtree-closed"); 86 | } 87 | } 88 | 89 | return li; 90 | } 91 | }; 92 | 93 | function generateHtmlElementsForNode( 94 | node: Node, 95 | parentElement: HTMLElement, 96 | x: number, 97 | ) { 98 | const isTree = node.tree === node; 99 | const nodeElement = createNodeElement(node); 100 | 101 | parentElement.append(nodeElement); 102 | 103 | if (!isTree) { 104 | const divElement = document.createElement("div"); 105 | divElement.className = "jqtree-element"; 106 | nodeElement.append(divElement); 107 | 108 | mockLayout(nodeElement, { height: 20, width: 100 - x, x, y }); 109 | node.element = nodeElement; 110 | y += 20; 111 | } 112 | 113 | if (node.hasChildren() && (node.is_open || isTree)) { 114 | for (const child of node.children) { 115 | generateHtmlElementsForNode( 116 | child, 117 | nodeElement, 118 | isTree ? x : x + 10, 119 | ); 120 | } 121 | } 122 | 123 | return nodeElement; 124 | } 125 | 126 | const treeElement = generateHtmlElementsForNode(tree, document.body, 0); 127 | mockLayout(treeElement, { height: y, width: 100, x: 0, y: 0 }); 128 | 129 | return treeElement; 130 | }; 131 | -------------------------------------------------------------------------------- /src/test/support/treeStructure.ts: -------------------------------------------------------------------------------- 1 | import { getChilden, singleChild } from "./testUtil"; 2 | 3 | const getTreeNode = (li: HTMLElement): JQTreeMatchers.TreeNode => { 4 | const div = singleChild(li, "div", "jqtree-element"); 5 | const span = singleChild(div, "span", "jqtree-title"); 6 | const name = span.innerHTML; 7 | const selected = li.classList.contains("jqtree-selected"); 8 | 9 | if (li.classList.contains("jqtree-folder")) { 10 | const ulChildren = getChilden(li, "ul", "jqtree_common"); 11 | 12 | const children = 13 | ulChildren.length === 1 14 | ? getChildNodes(ulChildren[0] as HTMLElement) 15 | : []; 16 | 17 | return { 18 | children, 19 | name, 20 | nodeType: "folder", 21 | open: !li.classList.contains("jqtree-closed"), 22 | selected, 23 | }; 24 | } else { 25 | return { 26 | name, 27 | nodeType: "child", 28 | selected, 29 | }; 30 | } 31 | }; 32 | 33 | const getChildNodes = (ul: HTMLElement) => 34 | getChilden(ul, "li", "jqtree_common").map((li) => getTreeNode(li)); 35 | 36 | const treeStructure = (el: HTMLElement): JQTreeMatchers.TreeStructure => { 37 | return getChildNodes(singleChild(el, "ul", "jqtree-tree")); 38 | }; 39 | 40 | export default treeStructure; 41 | -------------------------------------------------------------------------------- /src/test/util.test.ts: -------------------------------------------------------------------------------- 1 | import { getBoolString, isFunction, isInt } from "../util"; 2 | 3 | describe("getBoolString", () => { 4 | it("returns true or false", () => { 5 | expect(getBoolString(true)).toBe("true"); 6 | expect(getBoolString(false)).toBe("false"); 7 | expect(getBoolString(1)).toBe("true"); 8 | expect(getBoolString(null)).toBe("false"); 9 | }); 10 | }); 11 | 12 | describe("isFunction", () => { 13 | it("returns a boolean", () => { 14 | expect(isFunction(isInt)).toBe(true); 15 | expect(isFunction("isInt")).toBe(false); 16 | }); 17 | }); 18 | 19 | describe("isInt", () => { 20 | it("returns a boolean", () => { 21 | expect(isInt(10)).toBe(true); 22 | expect(isInt(0)).toBe(true); 23 | expect(isInt(-1)).toBe(true); 24 | expect(isInt("1")).toBe(false); 25 | expect(isInt(null)).toBe(false); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /src/typings.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /src/util.ts: -------------------------------------------------------------------------------- 1 | export const isInt = (n: unknown): boolean => 2 | typeof n === "number" && n % 1 === 0; 3 | 4 | export const isFunction = (v: unknown): boolean => typeof v === "function"; 5 | 6 | export const getBoolString = (value: unknown): string => 7 | value ? "true" : "false"; 8 | 9 | export const getOffsetTop = (element: HTMLElement) => 10 | getElementPosition(element).top; 11 | 12 | export const getElementPosition = (element: HTMLElement) => { 13 | const rect = element.getBoundingClientRect(); 14 | 15 | return { 16 | left: rect.x + window.scrollX, 17 | top: rect.y + window.scrollY, 18 | }; 19 | }; 20 | -------------------------------------------------------------------------------- /src/version.ts: -------------------------------------------------------------------------------- 1 | const version = "1.8.10"; 2 | 3 | export default version; 4 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "exclude": ["_site", "node_modules"], 4 | "include": ["src/**/*"], 5 | "compilerOptions": { 6 | "erasableSyntaxOnly": true, 7 | "esModuleInterop": true, 8 | "lib": ["ES2022", "DOM", "DOM.Iterable"], 9 | "module": "ESNext", 10 | "moduleResolution": "bundler", 11 | "noEmit": true, 12 | "noFallthroughCasesInSwitch": true, 13 | "noImplicitAny": true, 14 | "noImplicitReturns": true, 15 | "noImplicitThis": true, 16 | "noUncheckedIndexedAccess": true, 17 | "noUnusedLocals": true, 18 | "noUnusedParameters": true, 19 | "rootDir": "src", 20 | "skipLibCheck": true, 21 | "strictNullChecks": true, 22 | "strictPropertyInitialization": false, 23 | "strict": true, 24 | "target": "ES2022", 25 | } 26 | } 27 | --------------------------------------------------------------------------------