/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 |
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 |
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 | [](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 '►' (►)
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 '▼' (▼)
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 |
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 | Search
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 | Search
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 |
9 |
10 |
11 | {% assign level = 0 %}
12 |
13 | {% for entry in site.entries %}
14 | {% if entry.section %}
15 | {% if level == 1 %}
16 |
17 |
18 | {% endif %}
19 |
20 | {{
21 | entry.title }}
22 |
23 | {% assign level = 1 %}
24 | {% else %}
25 |
26 | {{ entry.title }}
28 |
29 | {% endif %}
30 | {% endfor %}
31 |
32 | {% if level == 1 %}
33 |
34 |
35 | {% endif %}
36 |
37 |
38 |
39 |
40 |
41 |
44 |
45 | It supports json data, loading via ajax and drag-and-drop.
46 |
47 |
51 |
52 |
53 | {% for entry in site.entries %}
54 | {% if entry.hide_title %}
55 |
56 | {% elsif entry.section %}
57 |
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 |
--------------------------------------------------------------------------------