├── .github └── stale.yml ├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── bower.json ├── jstreegrid.js ├── package.json ├── publish ├── tests ├── README.md ├── components │ ├── api-hide-show-test.html │ ├── api-select-test.html │ ├── chrome-header-test.html │ ├── click-sort-test.html │ ├── clicked-on-show.html │ ├── dnd-test.html │ ├── dynamic-root-node-test.html │ ├── fixed-widths-resize-test.html │ ├── fnvalue-test.html │ ├── grid-api-test.html │ ├── large-dataset-test.html │ ├── minwidth-maxwidth-test.html │ ├── odd-named-id-test.html │ ├── rename-dynamic-node-test.html │ ├── rename_id_test.html │ ├── render_cell-event-test.html │ ├── resize-event-test.html │ ├── search-test.html │ ├── select-child-test.html │ ├── select-prevent-test.html │ ├── sort-without-resizable-test.html │ └── widths-test.html ├── test-list.json └── test-master.html └── treegrid.html /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Configuration for probot-stale - https://github.com/probot/stale 2 | 3 | # Number of days of inactivity before an Issue or Pull Request becomes stale 4 | daysUntilStale: 60 5 | 6 | # Number of days of inactivity before an Issue or Pull Request with the stale label is closed. 7 | # Set to false to disable. If disabled, issues still need to be closed manually, but will remain marked as stale. 8 | daysUntilClose: 7 9 | 10 | # Issues or Pull Requests with these labels will never be considered stale. Set to `[]` to disable 11 | exemptLabels: 12 | - pinned 13 | - "[Status] Maybe Later" 14 | 15 | # Set to true to ignore issues in a project (defaults to false) 16 | exemptProjects: false 17 | 18 | # Set to true to ignore issues in a milestone (defaults to false) 19 | exemptMilestones: false 20 | 21 | # Set to true to ignore issues with an assignee (defaults to false) 22 | exemptAssignees: false 23 | 24 | # Label to use when marking as stale 25 | staleLabel: wontfix 26 | 27 | # Comment to post when marking as stale. Set to `false` to disable 28 | markComment: > 29 | This issue has been automatically marked as stale because it has not had 30 | recent activity. It will be closed if no further activity occurs. Thank you 31 | for your contributions. 32 | 33 | # Comment to post when removing the stale label. 34 | # unmarkComment: > 35 | # Your comment here. 36 | 37 | # Comment to post when closing a stale Issue or Pull Request. 38 | # closeComment: > 39 | # Your comment here. 40 | 41 | # Limit the number of actions per hour, from 1-30. Default is 30 42 | limitPerRun: 30 43 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | lib/** 2 | oneoff/** 3 | /node_modules -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | lib/** 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Avi Deitcher 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # jsTreeGrid 2 | [![CDNJS](https://img.shields.io/cdnjs/v/jstreegrid.svg)](https://cdnjs.com/libraries/jstreegrid) 3 | 4 | UNSUPPORTED: You are welcome to find issues, and submit Pull Requests to fix. Beyond that, there really isn't time to support it. 5 | 6 | ## Overview 7 | Plugin for the jstree www.jstree.com tree component that provides a grid extension to the tree. 8 | 9 | Allows any number of columns, and can use any property of the node to display data in the grid 10 | 11 | For significant changes to v3 compared to v1, see the end of this document. 12 | 13 | **Note:** treegrid may have issues when using theme autoloading as in: 14 | 15 | ````JavaScript 16 | core: { 17 | themes: {url: 'http://some.url/theme'} 18 | } 19 | ```` 20 | 21 | In any case, theme autoloading is strongly discouraged in jstree v3. 22 | 23 | ## License and Support 24 | jstree-grid is released under the very permissive MIT License, albeit with no warranty or guarantee. See the [MIT License](./LICENSE) for full details. Use it as you will, and use it well. 25 | 26 | If you need commercial support, general assistance or have any questions, please contact [info@atomicinc.com](mailto:info@atomicinc.com). 27 | 28 | jstree-grid can be made a native component for Angular, Angular2, Polymer, Aurelia, and other frameworks. If you have commercial need of a tree grid those or any other framework, please contact [info@atomicinc.com](mailto:info@atomicinc.com). 29 | 30 | 31 | 32 | ## Usage 33 | 34 | 1. Include jquery (>= 1.4.2) and jstree in your page, as usual 35 | 2. Inclue jstree **>= 3.3.0** 36 | 3. Include jstreegrid.js v3 or later 37 | 4. Include grid as a plugin 38 | 5. Include relevant parameters. 39 | 40 | ````HTML 41 | 42 | 43 | ```` 44 | 45 | 46 | ````JavaScript 47 | $("div#id").jstree({ 48 | // include grid as a plugin 49 | plugins: ["core","ui",...,"grid"], 50 | // include relevant parameters 51 | grid: { 52 | columns: [{},{},...,{}], 53 | width: 25 54 | }, 55 | core: { 56 | data: [...] 57 | } 58 | }); 59 | ```` 60 | 61 | As of 3.0.0-beta5, jstree-grid supports AMD, thanks https://github.com/jochenberger 62 | 63 | As of 3.1.0-beta1, jstree-grid uses a wrapping table, rather than inserted `div`s in the tree. This does a much better job with widths, alignment, editing, etc. etc. 64 | 65 | As of 3.10.2, jstree-grid supports CommonJS, thanks to https://github.com/YarnSeemannsgarn 66 | 67 | ### Structure 68 | 69 | The grid is built by adding divs `
` to each `
  • ` entry for a row in the tree. Inside the `
    ` is a `` with the data. 70 | Thus, an entry is likely to look like 71 | 72 | ````HTML 73 |
    $5.00
    74 | ```` 75 | 76 | We use the div to control the entire height and width, and the span to get access to the actual data itself. 77 | 78 | ### Options 79 | 80 | #### The options are as follows: 81 | 82 | * `width`: width for the entire jstree-grid. If no width is given, automatically fills the entire viewport (`width: 100%;`) 83 | * `height`: height for the entire jstree-grid. If no height is given, height will reflect the amount of content. 84 | * `fixedHeader`: true/false. If true, then when the tree is scrolled the column headers will remain visible. Defaults to true. 85 | * `columnWidth`: default width for a column for which no width is given. If no width is given, the default is `auto`. 86 | * `columns`: an array of columns to create, on order. Each entry is an object with the following parameters: 87 | * `tree`: boolean, whether the jstree should be placed in this column. Only the first `true` is accepted. If no column is set to `tree:true`, then the first column is used. 88 | * `width`: width of the column in pixels. If no width is given, the default is `auto` **except for the last column**. In the last column, if no width is given, it is treated as 'auto' and fills the entire rest of the grid to the right. 89 | * `minWidth`: Minimum width of the column when width is set to `auto`. Does **not** limit manual column resizing. 90 | * `maxWidth`: Maximum width of the column when width is set to `auto`. Does **not** limit manual column resizing. 91 | * `header`: string to use as a header for the column. 92 | * `headerClass`: a CSS class to add to the header cell in this column 93 | * `headerTitle`: a title to add to the header cell in this column, shown as a tooltip 94 | * `columnClass`: a CSS class to add to the header cell and the column cell 95 | * `cellClass`: a CSS class to add to each cell in this column (except for the header) - added to the `` 96 | * `wideCellClass`: a CSS class to add to each cell in this column (except for the header) - added to the `
    ` 97 | * `value`: the attribute on the node to use as the value for this cell - entered as the `` text. Can be a string or a function. 98 | * `valueClass`: the attribute on the node to use as a class on this cell - added to the `` 99 | * `valueClassPrefix`: a prefix to add to the valueClass to use as a class on this cell 100 | * `wideValueClass`: the attribute on the node to use as a class on this cell - added to the `
    ` 101 | * `wideValueClassPrefix`: a prefix to add to the wideValueClass to use as a class on this cell 102 | * `search_callback`: sets the search callback function used when using the `searchColumn` function. The function is passed with four arguments: the `search string`, the `column value`, the `jstree node` corresponding to the column and the `column`. returns true for a match and false if it doesn't match. If not set a default string search function is used (this is similar to [https://www.jstree.com/api/#/?q=search&f=$.jstree.defaults.search.search_callback](https://www.jstree.com/api/#/?q=search&f=$.jstree.defaults.search.search_callback)) 103 | * `resizable`: true/false if the columns should be resizable. Defaults to false. 104 | * `draggable`: true/false if the columns should be draggable (requires jQuery UI with sortable plugin). Defaults to false. 105 | * `stateful`: true/false. If true, then whenever a column width is resized, it will store it in html5 localStorage, if available. Defaults to false. 106 | * `headerAsTitle`: true/false. If true, the header is used as `headerTitle`. If false, no title is added, but in case the option `headerTitle` is set, a title is added. Defaults to false. 107 | * `contextmenu`: true/false whether or not a context menu for editing the cells should be shown on right-click. Defaults to false. 108 | * `gridcontextmenu`: function to create context menu items; see context menu of jstree for format. In addition, if `false` or not set, no special context menu. If `true`, creates a default menu to edit the cell entry. 109 | * `caseInsensitive`: true/false whether or not the column sort should be case insensitive. Default value is false (the default sort is case sensitive) 110 | 111 | The reason for both `valueClass` and `wideValueClass` is to give you the ability to control both the narrow part of the text, and the entire width of the cell. For example, if the cell is 56px wide, but the text in it is "OK" and thus only 20px wide. 112 | Suppose you have a class "important" which backgrounds in red, and a class "clickable" which changes the cursor to a pointer. 113 | You want the entire width of the cell to be red, but just the word "OK" to be clickable. 114 | You would ensure that "clickable" is applied to the span, but important to the div. 115 | 116 | Value is one of: 117 | 118 | * the name of the property of the node data whose content will be used; you can choose which once for the entire grid. 119 | * a function, which will be passed the node's data given by `tree.get_node(node)` for the individual tree item. If you want your custom data, access it via `node.data` 120 | 121 | Thus, if you have a node whose data is given by: 122 | 123 | ````JavaScript 124 | {text: "My Node", data: {price: "$10"}} 125 | ```` 126 | 127 | 128 | and we want the price value ($10) to be in column 1, then we have a config of: 129 | 130 | ````JavaScript 131 | grid: { 132 | columns: [ 133 | {width: 50, header: "Nodes"}, 134 | {width: 30, header: "Price", value: "price"} 135 | ] 136 | } 137 | ```` 138 | 139 | Or, in a function: 140 | 141 | ````JavaScript 142 | grid: { 143 | columns: [ 144 | {width: 50, header: "Nodes"}, 145 | {width: 30, header: "Price", value: function(node){return(node.data.price);}} 146 | ] 147 | } 148 | ```` 149 | 150 | Using a function allows you to calculate things, or make conditions: 151 | 152 | ````JavaScript 153 | grid: { 154 | columns: [ 155 | {width: 50, header: "Nodes"}, 156 | {width: 30, header: "Price", value: function(node){return("$"+(node.data.price*2));}} 157 | ] 158 | } 159 | ```` 160 | 161 | 162 | 163 | Thanks to ChrisRaven for the metadata support (deprecated as of v3), resizable columns, and cell click events. 164 | 165 | ValueClass is the name of the attribute that will be added as a class to this cell. Thus, if you have a node whose data is given by: 166 | 167 | ````JavaScript 168 | {text: "My Node", data: {price: "$10", scale: "expensive"}} 169 | ```` 170 | 171 | and we want the cell to have the class "expensive", then we have a config of: 172 | 173 | ````JavaScript 174 | grid: { 175 | columns: [ 176 | {width: 50, header: "Nodes"}, 177 | {width: 30, header: "Price", value:"price", valueClass: "scale"} 178 | ] 179 | } 180 | ```` 181 | 182 | The result would be: 183 | 184 | ````HTML 185 |
    $10
    186 | ```` 187 | 188 | Conversely, if we want it to be "price-expensive", we would have a config of: 189 | 190 | ````JavaScript 191 | grid: { 192 | columns: [ 193 | {width: 50, header: "Nodes"}, 194 | {width: 30, header: "Price", value: "price", valueClass: "scale", valueClassPrefix: "price-"} 195 | ] 196 | } 197 | ```` 198 | 199 | The result would be: 200 | 201 | ````HTML 202 |
    $10
    203 | ```` 204 | 205 | You can add a tooltip to each element in the grid by adding the name of it to the title, with the HTML stripped out. For example: 206 | 207 | ````JavaScript 208 | grid: { 209 | columns: [ 210 | {width: 50, header: "Nodes"}, 211 | {width: 30, header: "Price", value: "price", title: "price"} 212 | ] 213 | } 214 | ```` 215 | 216 | The result would be: 217 | 218 | ````HTML 219 |
    $10
    220 | ```` 221 | 222 | This includes the actual tree node, not just the added grid cells: 223 | 224 | ````JavaScript 225 | grid: { 226 | columns: [ 227 | {width: 50, header: "Nodes", title:"price"}, 228 | {width: 30, header: "Price", value: "price", title: "price"} 229 | ] 230 | } 231 | ```` 232 | 233 | You also can have the contents of the tree node (value of tree.get_text()) itself made into the tooltip by using the special title keyword 234 | "_DATA_". 235 | 236 | ````JavaScript 237 | grid: { 238 | columns: [ 239 | {width: 50, header: "Nodes", title:"_DATA_"}, 240 | {width: 30, header: "Price", value: "price", title: "price"} 241 | ] 242 | } 243 | ```` 244 | 245 | Finally, you can change a node contents on the fly using "change_node.jstree". You change the `data` attribute of the node, then trigger the event, 246 | for example: 247 | 248 | ````JavaScript 249 | var tree = $("#jstree").jstree(true), node = tree.get_node("my_node"); 250 | node.data.value = 25; 251 | tree.trigger("change_node.jstree",node); 252 | ```` 253 | 254 | ### HTML 255 | 256 | Note that the data in each cell is treated as HTML content for the span, rather than raw text. You can use HTML in any cell, except for the 257 | base tree node cell, which follows jstree rules. 258 | 259 | ### Heights 260 | The height of the entire div in which the tree is rendered is given by you. If you wish the tree to have a max-height of 40px, you need to set it as part of the standard HTML/CSS. 261 | 262 | ````HTML 263 | 268 |
    269 | ```` 270 | 271 | In the above example, the tree itself, but *not* the headers, will be limited to 40px, not by jstree or jstreegrid, but by straight CSS. However, jstreegrid will structure the tree and header in such a way that if the total tree is greater than 40px in height, then the tree will have a vertical scrollbar, but the header will remain fixed. 272 | 273 | 274 | ### Themeroller 275 | The themeroller plugin for jstree is supported as of tag 0.9.1 (29 November 2011). Thanks to ChrisRaven for the support. 276 | 277 | 278 | ### API 279 | The following methods can be called on the jstree: 280 | 281 | * `grid_hide_column(column)`: Hide column number `column`. Column numbers begin with `0`. If the column already is hidden, has no effect. 282 | * `grid_show_column(column)`: Show column number `column`. Column numbers begin with `0`. If the column already is shown, has no effect. 283 | * `search(str)`: the [jstree search](https://www.jstree.com/api/#/?q=search) has been extended to search the entire grid. to search a specific column use `searchColumn`. 284 | * `searchColumn(searchObject)`: Search one or more columns for specific strings. `searchObject` is an object with the column number as key, beginning with `0`, and the searchString as value. Multiple column values are joined together with logical `AND`. Uses the jstree search settings to hide/show nodes. Examples: 285 | * `searchColumn({0:'My Node'})`: search for rows wherein the first column has text `'My Node'` 286 | * `searchColumn({0:'My Node', 1:'$10'})`: search for rows wherein the first column has text `'My Node'` *AND* second column has text `'$10'` 287 | 288 | ### Events 289 | * `loaded.jstree`: When the tree is done loading, as usual, it fires a "loaded.jstree" event on the div to which you added jstree. jsTreeGrid uses this event to start its own load process. 290 | * `loaded_grid.jstree`: When jsTreeGrid is done, it fires a "loaded_grid.jstree" event on the same div. If you need to run some 291 | code after the jsTreeGrid is done loading, just listen for that event. An example is in the treegrid.HTML sample page. 292 | * `select_cell.jstree-grid`: If you click in any individual cell, the jstreegrid will fire a "select_cell.jstree_grid" event on the tree. See below for "cell selection". 293 | * `update_cell.jstree-grid`: If you right-click a cell and edit it, when the edit is complete, and if the value has changed, the jstreegrid will fire a `update_cell.jstree-grid` event on the jstree. 294 | * `render_cell.jstree-grid`: Called each time a cell is rendered. Is not a one-time call, as cells are rendered when their container node is opened and destroyed each time is closed, so you can get multiple `render_cell.jstree-grid` calls for the same cell. 295 | * `resize_column.jstree-grid`: When a column is resized, whether from dragging the resizer or double-clicking it, this event will be fired. 296 | 297 | #### Cell Selection 298 | When you select a cell by clicking on it, two things happen: 299 | 300 | 1. jstree-grid fires off a `select_cell.jstree-grid` event on the tree, to which you can listen. 301 | 2. jstree-grid selects the row, leading in turn to the normal jstree row selection event handlers. 302 | 303 | If you wish to prevent the second part - entire row selection by the jstree - for example, if you have input HTML rendered in the cell and do not want to lose input focus, you can prevent the jstree selection. 304 | 305 | To do so, create a handler for the `select_cell.jstree-grid` event and prevent its handling: 306 | 307 | ``` js 308 | mytree.on("select_cell.jstree-grid", function(event, data) { 309 | if (/* some logic, if you want */) { 310 | event.preventDefault(); 311 | } 312 | }); 313 | ``` 314 | 315 | If you still want to prevent the automatic selection of the row, but do it yourself, you can do it manually: 316 | 317 | ``` js 318 | mytree.on("select_cell.jstree-grid", function(event, data) { 319 | event.preventDefault(); 320 | mytree.jstree("deselect_all"); 321 | mytree.jstree("select_node", data.node); 322 | }); 323 | ``` 324 | 325 | 326 | The signature for the select_cell.jstree-grid handler is: 327 | 328 | ````JavaScript 329 | function(event,{value:value,header:header,node:node,grid:grid,sourceName:sourceName}) 330 | ```` 331 | 332 | where: 333 | 334 | * value: value of the data element that rendered this cell 335 | * column: header for the column 336 | * node: reference to the <li> element in the tree that starts the row with the clicked cell 337 | * grid: reference to the <div> element in the grid that was clicked 338 | * sourceName: name of the element in the original data that contained this value, as provided by the config in the columns "value" for this column 339 | 340 | 341 | #### Cell Update 342 | 343 | The signature for the update_cell.jstree-grid handler is: 344 | 345 | ````JavaScript 346 | function(event,{node:node, col:column, value:newvalue,old:oldvalue}) 347 | ```` 348 | 349 | where: 350 | 351 | * node: the edited node in the data structure 352 | * col: name of the column, e.g. "price" 353 | * value: new value for the cell 354 | * old: the original value before it was changed 355 | 356 | 357 | The signature for the resize_column.jstree-grid handler is: 358 | 359 | ````JavaScript 360 | function(event,columnNumber,newWidth) 361 | ```` 362 | 363 | where: 364 | 365 | * columnNumber: the number of the column being resized, with the first column being 0 366 | * newWidth: the new width of the column in pixels 367 | 368 | #### Cell Render 369 | 370 | The signature for the render_cell.jstree-grid handler is: 371 | 372 | ````JavaScript 373 | function(event,{value:cellvalue, column:columnheader, node:node, sourceName:datakeyname}) 374 | ```` 375 | 376 | where: 377 | 378 | * value: The value of the cell 379 | * column: The column header 380 | * node: Reference to the <li> element in the tree where this cell is rendered 381 | * sourceName: The key name in the data structure that gives the value to this cell 382 | 383 | ### Width 384 | 385 | The width of the table and its columns can change depending on the selected options. As described above, there are three options that affect the width: 386 | 387 | * `width`: in the root of the config, determines the width **for the entire jstree-grid**. If you set this `width` to `200`, the entire jstree-grid will occupy a fixed `200px` of screen width. The default is `100%`, i.e. fill the enclosing element or the viewport. 388 | * `columnWidth`: in the root of the config, determines the default width of a column for which a width is not provided. 389 | * `width`: in a particular column, determines the width of that individual column. 390 | 391 | A number of combinations and permutations is possible using the above. 392 | 393 | #### fixed `width` defined for entire grid 394 | If a fixed `width` is not defined for the entire grid, the behaviour depends on what happens in each column. In all cases, the entire jstree-grid will be the given fixed width. 395 | 396 | * If all columns have a fixed width defined, the columns are given those widths. 397 | * If the sum of all columns is *greater* than the width defined on the entire grid, the second column onwards will be inside a scrolling element. 398 | * If the sum of all columns is *less* than the width defined on the entire grid, the gap will be filled by whitespace after the last column. 399 | * If one or more columns is defined as `auto`, these will automatically be resized to fill to the entire grid. 400 | * If one or more columns does not have a `width` defined, it will be treated as the root value of `columnWidth`, else as `auto` 401 | 402 | Here are examples: 403 | 404 | ````JavaScript 405 | // all columns and grid defined widths, will have blank space after 3rd column 406 | { 407 | "width": "500px", 408 | "columns": [ 409 | {"width": "100px"}, 410 | {"width": "100px"}, 411 | {"width": "150px"} 412 | ] 413 | } 414 | 415 | // all columns and grid defined widths, will scroll 2nd through last column 416 | { 417 | "width": "500px", 418 | "columns": [ 419 | {"width": "200px"}, 420 | {"width": "200px"}, 421 | {"width": "400px"} 422 | ] 423 | } 424 | 425 | // grid defined width, some columns defined, some "auto", will automatically fill grid, no scrolling or whitespace 426 | { 427 | "width": "500px", 428 | "columns": [ 429 | {"width": "200px"}, 430 | {"width": "200px"}, 431 | {"width": "auto"} 432 | ] 433 | } 434 | 435 | // grid defined width, some columns defined, some not, columnWidth fixed, will scroll 436 | // since 200 + 200 + 120 = 520 > 500 437 | // col0 + col1 + col2(from default) = 520 > grid width 438 | { 439 | "width": "500px", 440 | "columnWidth": "120px", 441 | "columns": [ 442 | {"width": "200px"}, 443 | {"width": "200px"}, 444 | {} 445 | ] 446 | } 447 | 448 | // grid defined width, some columns defined, some not, columnWidth auto or not defined, will fill 449 | // since col2 inherits columnWidth = "auto", will default to filling at 100px 450 | { 451 | "width": "500px", 452 | "columns": [ 453 | {"width": "200px"}, 454 | {"width": "200px"}, 455 | {} 456 | ] 457 | } 458 | ```` 459 | 460 | #### fixed `width` not defined for entire grid 461 | If a fixed `width` is not defined for the entire grid, then the grid fills the entire containing element or viewport, i.e. it is treated as `width:100%;`. In that case, the widths of the columns are as follows: 462 | 463 | * If all columns have a fixed width defined, the columns are given those widths. 464 | * If the sum of all columns is *greater* than the calculated width of the entire grid, the second column onwards will be inside a scrolling element. 465 | * If the sum of all columns is *less* than the calculated width of the entire grid, the gap will be filled by whitespace after the last column. 466 | * If one or more columns is defined as `auto`, these will automatically be resized to fill to the entire grid. 467 | * If one or more columns does not have a `width` defined, it will be treated as the root value of `columnWidth`, else as `auto` 468 | 469 | In other words, the behaviour of columns does *not* change based on the `width` definition of the entire grid. It only affects the defined or calculated width of the grid, which in turn affects if the columns scroll (columns > grid), have whitespace (columns < grid), or fill (one or more columns is `auto`) 470 | 471 | 472 | ### V3 Changes 473 | jsTree v3 has created significant non-backwards-compatible changes to jsTree. To make jsTreeGrid compatible with jsTree3, jsTreeGrid v3 has changed as well, and is no longer backwards compatible. However, the changes required to support v3 are minimal. 474 | 475 | This section lists significant changes between pre-v3 and v3. 476 | 477 | * jstree v3 no longer stores its data in the DOM, rather inside JS. As such, jsTreeGrid no longer stores any data in the DOM. For example, in pre-v3 jstree, `attr` on a node's source data would store the actual data on the DOM as attributes using `jQuery.attr()`. This is no longer true in v3. jsTreeGrid similarly no longer looks for its source data on the DOM. All data to be passed to the grid should be stored in the `data` property of the node's JSON source. 478 | * `metadata` is no longer an option, since it is no longer necessary to use it to avoid storing data on the DOM. 479 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jstreegrid", 3 | "description": "grid plugin for jstree", 4 | "main": "jstreegrid.js", 5 | "authors": [ 6 | { 7 | "name": "Avi Deitcher", 8 | "url": "https://github.com/deitch" 9 | } 10 | ], 11 | "license": "MIT", 12 | "keywords": [ 13 | "jquery", 14 | "jstree", 15 | "grid", 16 | "javascript" 17 | ], 18 | "homepage": "https://github.com/deitch/jstree-grid", 19 | "ignore": [ 20 | "**/.*", 21 | "node_modules", 22 | "bower_components", 23 | "test", 24 | "tests" 25 | ], 26 | "dependencies": { 27 | "jstree": "^3.3.1", 28 | "jquery": "^1.12.4" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /jstreegrid.js: -------------------------------------------------------------------------------- 1 | /* 2 | * http://github.com/deitch/jstree-grid 3 | * 4 | * This plugin handles adding a grid to a tree to display additional data 5 | * 6 | * Licensed under the MIT license: 7 | * http://www.opensource.org/licenses/mit-license.php 8 | * 9 | * Works only with jstree version >= 3.3.0 10 | * 11 | * $Date: 2017-04-19 $ 12 | * $Revision: 3.8.2 $ 13 | */ 14 | 15 | /*jslint nomen:true */ 16 | /*jshint unused:vars */ 17 | /*global console, navigator, document, jQuery, define, localStorage */ 18 | 19 | /* AMD support added by jochenberger per https://github.com/deitch/jstree-grid/pull/49 20 | * 21 | */ 22 | (function (factory) { 23 | if (typeof define === 'function' && define.amd) { 24 | // AMD. Register as an anonymous module. 25 | define(['jquery', 'jstree'], factory); 26 | } else if(typeof module !== 'undefined' && module.exports) { 27 | module.exports = factory(require('jquery')); 28 | } else { 29 | // Browser globals 30 | factory(jQuery); 31 | } 32 | }(function ($) { 33 | var BLANKRE = /^\s*$/g, 34 | IDREGEX = /[\\:&!^|()\[\]<>@*'+~#";,= \/${}%]/g, escapeId = function (id) { 35 | return (id||"").replace(IDREGEX,'\\$&'); 36 | }, NODE_DATA_ATTR = "data-jstreegrid", COL_DATA_ATTR = "data-jstreegrid-column", 37 | SEARCHCLASS = "jstree-search", 38 | SPECIAL_TITLE = "_DATA_", LEVELINDENT = 24, styled = false, 39 | MINCOLWIDTH = 10, 40 | generateCellId = function (tree,id) { 41 | return("jstree_"+tree+"_grid_"+escapeId(id)+"_col"); 42 | }, 43 | getIds = function (nodes) { 44 | return $.makeArray(nodes.map(function(){return this.id;})); 45 | }, 46 | findDataCell = function (uniq, ids, col, scope) { 47 | if (scope == undefined) { scope = $(); }; 48 | if (ids === null || ids === undefined || ids.length === 0) { 49 | return scope; 50 | } 51 | var ret = $(), columns = [].concat(col), cellId; 52 | if (typeof (ids) === "string") { 53 | cellId = generateCellId(uniq,ids); 54 | ret = columns.map(function (col) { 55 | return "#"+cellId+col; 56 | }).join(", "); 57 | } else { 58 | ret = [] 59 | ids.forEach(function (elm,i) { 60 | var cellId = generateCellId(uniq,elm); 61 | ret = ret.concat(columns.map(function (col) { 62 | return "#"+cellId+col; 63 | })); 64 | }); 65 | ret = ret.join(", "); 66 | } 67 | return columns.length ==1 ? scope.find(ret) : $(ret); 68 | }, 69 | isClickedSep = false, toResize = null, oldMouseX = 0, newMouseX = 0, 70 | 71 | /*jslint regexp:true */ 72 | htmlstripre = /<\/?[^>]+>/gi, 73 | /*jslint regexp:false */ 74 | 75 | getIndent = function(node,tree) { 76 | var div, i, li, width; 77 | 78 | // did we already save it for this tree? 79 | tree._gridSettings = tree._gridSettings || {}; 80 | if (tree._gridSettings.indent > 0) { 81 | width = tree._gridSettings.indent; 82 | } else { 83 | // create a new div on the DOM but not visible on the page 84 | div = $("
    "); 85 | i = node.prev("i"); 86 | li = i.parent(); 87 | // add to that div all of the classes on the tree root 88 | div.addClass(tree.get_node("#",true).attr("class")); 89 | 90 | // move the li to the temporary div root 91 | li.appendTo(div); 92 | 93 | // attach to the body quickly 94 | div.appendTo($("body")); 95 | 96 | // get the width 97 | width = i.width() || LEVELINDENT; 98 | 99 | // detach the li from the new div and destroy the new div 100 | li.detach(); 101 | div.remove(); 102 | 103 | // save it for the future 104 | tree._gridSettings.indent = width; 105 | } 106 | 107 | 108 | return(width); 109 | 110 | }, 111 | 112 | copyData = function (fromtree,from,totree,to,recurse) { 113 | var i, j; 114 | to.data = $.extend(true, {}, from.data); 115 | if (from && from.children_d && recurse) { 116 | for(i = 0, j = from.children_d.length; i < j; i++) { 117 | copyData(fromtree,fromtree.get_node(from.children_d[i]),totree,totree.get_node(to.children_d[i]),recurse); 118 | } 119 | } 120 | }, 121 | 122 | findLastClosedNode = function (tree,id) { 123 | // first get our node 124 | var ret, node = tree.get_node(id), children = node.children; 125 | // is it closed? 126 | if (!children || children.length <= 0 || !node.state.opened) { 127 | ret = id; 128 | } else { 129 | ret = findLastClosedNode(tree,children[children.length-1]); 130 | } 131 | return(ret); 132 | }, 133 | 134 | renderAWidth = function(node,tree) { 135 | var depth, width, 136 | fullWidth = parseInt(tree.settings.grid.columns[0].width,10) + parseInt(tree._gridSettings.treeWidthDiff,10); 137 | // need to use a selector in jquery 1.4.4+ 138 | depth = tree.get_node(node).parents.length; 139 | width = fullWidth - depth*getIndent(node,tree); 140 | // the following line is no longer needed, since we are doing this inside a 141 | //a.css({"vertical-align": "top", "overflow":"hidden"}); 142 | return(fullWidth); 143 | }, 144 | renderATitle = function(node,t,tree) { 145 | var a = node.hasClass("jstree-anchor") ? node : node.children("[class~='jstree-anchor']"), title, col = tree.settings.grid.columns[0]; 146 | // get the title 147 | title = ""; 148 | if (col.title) { 149 | if (col.title === SPECIAL_TITLE) { 150 | title = tree.get_text(t); 151 | } else if (t.attr(col.title)) { 152 | title = t.attr(col.title); 153 | } 154 | } 155 | // strip out HTML 156 | title = title.replace(htmlstripre, ''); 157 | if (title) { 158 | a.attr("title",title); 159 | } 160 | }, 161 | getCellData = function (value,data) { 162 | var val; 163 | // get the contents of the cell - value could be a string or a function 164 | if (value !== undefined && value !== null) { 165 | if (typeof(value) === "function") { 166 | val = value(data); 167 | } else if (data.data !== null && data.data !== undefined && data.data[value] !== undefined) { 168 | val = data.data[value]; 169 | } else { 170 | val = ""; 171 | } 172 | } else { 173 | val = ""; 174 | } 175 | return val; 176 | }; 177 | 178 | $.jstree.defaults.grid = { 179 | width: 'auto' 180 | }; 181 | 182 | $.jstree.plugins.grid = function(options,parent) { 183 | this._initialize = function () { 184 | if (!this._initialized) { 185 | var s = this.settings.grid || {}, styles, container = this.element, i, 186 | gs = this._gridSettings = { 187 | columns : s.columns || [], 188 | treeClass : "jstree-grid-col-0", 189 | context: s.contextmenu || false, 190 | columnWidth : s.columnWidth, 191 | defaultConf : {"*display":"inline","*+display":"inline"}, 192 | isThemeroller : !!this._data.themeroller, 193 | treeWidthDiff : 0, 194 | resizable : s.resizable, 195 | draggable : s.draggable, 196 | stateful: s.stateful, 197 | headerAsTitle: s.headerAsTitle || false, 198 | indent: 0, 199 | sortOrder: 'text', 200 | sortAsc: true, 201 | caseInsensitive: s.caseInsensitive, 202 | fixedHeader: s.fixedHeader !== false, 203 | width: s.width, 204 | height: s.height, 205 | gridcontextmenu : s.gridcontextmenu, 206 | treecol: 0, 207 | gridcols: [] 208 | }, cols = gs.columns, treecol = 0, columnSearch = false; 209 | if(gs.gridcontextmenu === true) { 210 | gs.gridcontextmenu = function (grid,tree,node,val,col,t,target) { 211 | return { 212 | "edit": { 213 | label: "Edit", 214 | "action": function (data) { 215 | var obj = t.get_node(node); 216 | grid._edit(obj,col,target); 217 | } 218 | } 219 | } 220 | } 221 | } else if (gs.gridcontextmenu === false) { 222 | gs.gridcontextmenu = false; 223 | } 224 | // find which column our tree shuld go in 225 | for (var i = 0, len = s.columns.length;i'+styles.join("\n")+'').appendTo("head"); 269 | } 270 | this.gridWrapper = $("
    ").addClass("jstree-grid-wrapper").insertAfter(container); 271 | this.midWrapper = $("
    ").addClass("jstree-grid-midwrapper").appendTo(this.gridWrapper); 272 | // set the wrapper width 273 | if (s.width) { 274 | this.gridWrapper.width(s.width); 275 | } 276 | if (s.height) { 277 | this.gridWrapper.height(s.height); 278 | } 279 | // create the data columns 280 | for (var i = 0, len = cols.length;i
    ").addClass("jstree-default jstree-grid-column jstree-grid-column-"+i+" jstree-grid-column-root-"+this.rootid).appendTo(this.midWrapper); 283 | } 284 | this.midWrapper.children("div:eq("+treecol+")").append(container); 285 | container.addClass("jstree-grid-cell"); 286 | 287 | //move header with scroll 288 | if (gs.fixedHeader) { 289 | this.gridWrapper.scroll(function() { 290 | $(this).find('.jstree-grid-header').css('top', $(this).scrollTop()); 291 | }); 292 | } 293 | 294 | // copy original sort function 295 | var defaultSort = $.proxy(this.settings.sort, this); 296 | 297 | // override sort function 298 | this.settings.sort = function (a, b) { 299 | var bigger, colrefs = this.colrefs; 300 | 301 | if (gs.sortOrder==='text') { 302 | var caseInsensitiveSort = this.get_text(a).toLowerCase().localeCompare(this.get_text(b).toLowerCase()); 303 | bigger = gs.caseInsensitive ? (caseInsensitiveSort === 1) : (defaultSort(a, b) === 1); 304 | } else { 305 | // gs.sortOrder just refers to the unique random name for this column 306 | // we need to get the correct value 307 | var nodeA = this.get_node(a), nodeB = this.get_node(b), 308 | value = colrefs[gs.sortOrder].value, 309 | valueA = typeof(value) === 'function' ? value(nodeA) : nodeA.data[value], 310 | valueB = typeof(value) === 'function' ? value(nodeB) : nodeB.data[value]; 311 | if(typeof(valueA) && typeof(valueB) !== 'undefined') { 312 | bigger = gs.caseInsensitive ? valueA.toLowerCase() > valueB.toLowerCase(): valueA > valueB ; 313 | } 314 | } 315 | 316 | if (!gs.sortAsc) 317 | bigger = !bigger; 318 | 319 | return bigger ? 1 : -1; 320 | }; 321 | 322 | // sortable columns when jQuery UI is available 323 | if (gs.draggable) { 324 | if (!$.ui || !$.ui.sortable) { 325 | console.warn('[jstree-grid] draggable option requires jQuery UI'); 326 | } else { 327 | var from, to; 328 | 329 | $(this.midWrapper).sortable({ 330 | axis: "x", 331 | handle: ".jstree-grid-header", 332 | cancel: ".jstree-grid-separator", 333 | start: function (event, ui) { 334 | from = ui.item.index(); 335 | }, 336 | stop: function (event, ui) { 337 | to = ui.item.index(); 338 | gs.columns.splice(to, 0, gs.columns.splice(from, 1)[0]); 339 | } 340 | }); 341 | } 342 | } 343 | 344 | //public function. validate searchObject keys, set columnSearch flag, calls jstree search and reset columnSearch flag 345 | this.searchColumn = function (searchObj) { 346 | var validatedSearchObj = {}; 347 | 348 | if(typeof searchObj == 'object') { 349 | for(var columnIndex in searchObj) { 350 | if(searchObj.hasOwnProperty(columnIndex)) { 351 | // keys should be the index of a column. This means the following: 352 | // only integers and smaller than the number of columns and bigger or equal to 0 353 | // (possilbe idea for in the future: ability to set key as a more human readable term like the column header and then map it here to an index) 354 | if (columnIndex % 1 === 0 && columnIndex < cols.length && columnIndex >= 0) { 355 | validatedSearchObj[columnIndex] = searchObj[columnIndex]; 356 | } 357 | } 358 | } 359 | } 360 | columnSearch = validatedSearchObj; 361 | 362 | if(Object.keys(validatedSearchObj).length !== 0){ 363 | //the search string doesn't matter. we'll use the search string in the columnSearch object! 364 | this.search('someValue'); 365 | } else { // nothing to search so reset jstree's search by passing an empty string 366 | this.search(''); 367 | } 368 | columnSearch = false; 369 | } 370 | 371 | 372 | // set default search for each column with no user defined search function (used when doing a columnSearch) 373 | for (var i = 0, len = cols.length; idiv.jstree-grid-cell-root-'+this.rootid+' {line-height: '+anchorHeight+'px; height: '+anchorHeight+'px;}').appendTo("head"); 498 | 499 | // add container classes to the wrapper - EXCEPT those that are added by jstree, i.e. "jstree" and "jstree-*" 500 | q = cls.split(/\s+/).map(function(i){ 501 | var match = i.match(/^jstree(-|$)/); 502 | return (match ? "" : i); 503 | }); 504 | this.gridWrapper.addClass(q.join(" ")); 505 | 506 | },this)) 507 | .on("move_node.jstree",$.proxy(function(e,data){ 508 | var node = data.new_instance.element; 509 | //renderAWidth(node,this); 510 | // check all the children, because we could drag a tree over 511 | node.find("li > a").each($.proxy(function(i,elm){ 512 | //renderAWidth($(elm),this); 513 | },this)); 514 | 515 | },this)) 516 | .on("hover_node.jstree",$.proxy(function(node,selected,event){ 517 | var id = selected.node.id; 518 | if (this._hover_node !== null && this._hover_node !== undefined) { 519 | findDataCell(this.uniq,this._hover_node,this._gridSettings.gridcols).removeClass("jstree-hovered"); 520 | } 521 | this._hover_node = id; 522 | findDataCell(this.uniq,id,this._gridSettings.gridcols).addClass("jstree-hovered"); 523 | },this)) 524 | .on("dehover_node.jstree",$.proxy(function(node,selected,event){ 525 | var id = selected.node.id; 526 | this._hover_node = null; 527 | findDataCell(this.uniq,id,this._gridSettings.gridcols).removeClass("jstree-hovered"); 528 | },this)) 529 | .on("select_node.jstree",$.proxy(function(node,selected,event){ 530 | var id = selected.node.id; 531 | findDataCell(this.uniq,id,this._gridSettings.gridcols).addClass("jstree-clicked"); 532 | this.get_node(selected.node.id,true).children("div.jstree-grid-cell").addClass("jstree-clicked"); 533 | },this)) 534 | .on("deselect_node.jstree",$.proxy(function(node,selected,event){ 535 | var id = selected.node.id; 536 | findDataCell(this.uniq,id,this._gridSettings.gridcols).removeClass("jstree-clicked"); 537 | },this)) 538 | .on("deselect_all.jstree",$.proxy(function(node,selected,event){ 539 | // get all of the ids that were unselected 540 | var ids = selected.node || [], i; 541 | findDataCell(this.uniq,ids,this._gridSettings.gridcols).removeClass("jstree-clicked"); 542 | },this)) 543 | .on("search.jstree", $.proxy(function (e, data) { 544 | // search sometimes filters, so we need to hide all of the appropriate grid cells as well, and show only the matches 545 | var grid = this.gridWrapper, that = this, nodesToShow, startTime = new Date().getTime(), 546 | ids = getIds(data.nodes.filter(".jstree-node")), endTime; 547 | this.holdingCells = {}; 548 | if (data.nodes.length) { 549 | var id = _guid(); 550 | // save the cells we will hide 551 | var cells = grid.find('div.jstree-grid-cell-regular'); 552 | this._detachColumns(id); 553 | if(this._data.search.som) { 554 | // create the list of nodes we want to look at 555 | if(this._data.search.smc) { 556 | nodesToShow = data.nodes.add(data.nodes.find('.jstree-node')); 557 | } 558 | nodesToShow = (nodesToShow || data.nodes).add(data.nodes.parentsUntil(".jstree")); 559 | 560 | // hide all of the grid cells 561 | cells.hide(); 562 | // show only those that match 563 | nodesToShow.filter(".jstree-node").each(function (i,node) { 564 | var id = node.id; 565 | if (id) { 566 | that._prepare_grid(node); 567 | for (var i = 0, len = that._gridSettings.gridcols.length; i < len; i++) { 568 | if (i === that._gridSettings.treecol) { continue; } 569 | findDataCell(that.uniq, id, that._gridSettings.gridcols[i], $(that._domManipulation.columns[i])).show(); 570 | } 571 | } 572 | }); 573 | } 574 | 575 | for (var i = 0, len = this._gridSettings.gridcols.length; i < len; i++) { 576 | if (i === this._gridSettings.treecol) { continue; } 577 | findDataCell(that.uniq, ids, this._gridSettings.gridcols[i], $(this._domManipulation.columns[i])).addClass(SEARCHCLASS); 578 | } 579 | this._reattachColumns(id); 580 | endTime = new Date().getTime(); 581 | this.element.trigger("search-complete.jstree-grid", [{time:endTime-startTime}]); 582 | } 583 | return true; 584 | }, this)) 585 | .on("clear_search.jstree", $.proxy(function (e, data) { 586 | // search has been cleared, so we need to show all rows 587 | var grid = this.gridWrapper, ids = getIds(data.nodes.filter(".jstree-node")); 588 | grid.find('div.jstree-grid-cell').show(); 589 | findDataCell(this.uniq,ids,this._gridSettings.gridcols).removeClass(SEARCHCLASS); 590 | return true; 591 | }, this)) 592 | .on("copy_node.jstree", function (e, data) { 593 | var newtree = data.new_instance, oldtree = data.old_instance, obj = newtree.get_node(data.node,true); 594 | copyData(oldtree, data.original, newtree, data.node, true); 595 | newtree._detachColumns(obj.id); 596 | newtree._prepare_grid(obj); 597 | newtree._reattachColumns(obj.id); 598 | return true; 599 | }) 600 | .on("show_ellipsis.jstree", $.proxy(function (e, data) { 601 | this.gridWrapper.find(".jstree-grid-cell").add(".jstree-grid-header", this.gridWrapper).addClass("jstree-grid-ellipsis"); 602 | return true; 603 | }, this)) 604 | .on("hide_ellipsis.jstree", $.proxy(function (e, data) { 605 | this.gridWrapper.find(".jstree-grid-cell").add(".jstree-grid-header", this.gridWrapper).removeClass("jstree-grid-ellipsis"); 606 | return true; 607 | }, this)) 608 | .on("enable_node.jstree", $.proxy(function (e, data) { 609 | var id = data.node.id; 610 | findDataCell(this.uniq,id,this._gridSettings.gridcols).removeClass("jstree-disabled"); 611 | this.get_node(data.node.id,true).children("div.jstree-grid-cell").removeClass("jstree-disabled"); 612 | }, this)) 613 | .on("disable_node.jstree", $.proxy(function (e, data) { 614 | var id = data.node.id; 615 | findDataCell(this.uniq,id,this._gridSettings.gridcols).addClass("jstree-disabled"); 616 | this.get_node(data.node.id,true).children("div.jstree-grid-cell").addClass("jstree-disabled"); 617 | }, this)) 618 | ; 619 | if (this._gridSettings.isThemeroller) { 620 | this.element 621 | .on("select_node.jstree",$.proxy(function(e,data){ 622 | data.rslt.obj.children("[class~='jstree-anchor']").nextAll("div").addClass("ui-state-active"); 623 | },this)) 624 | .on("deselect_node.jstree deselect_all.jstree",$.proxy(function(e,data){ 625 | data.rslt.obj.children("[class~='jstree-anchor']").nextAll("div").removeClass("ui-state-active"); 626 | },this)) 627 | .on("hover_node.jstree",$.proxy(function(e,data){ 628 | data.rslt.obj.children("[class~='jstree-anchor']").nextAll("div").addClass("ui-state-hover"); 629 | },this)) 630 | .on("dehover_node.jstree",$.proxy(function(e,data){ 631 | data.rslt.obj.children("[class~='jstree-anchor']").nextAll("div").removeClass("ui-state-hover"); 632 | },this)); 633 | } 634 | 635 | if (this._gridSettings.stateful) { 636 | this.element 637 | .on("resize_column.jstree-grid",$.proxy(function(e,col,width){ 638 | localStorage['jstree-root-'+this.rootid+'-column-'+col] = width; 639 | },this)); 640 | } 641 | }; 642 | 643 | // tear down the tree entirely 644 | this.teardown = function() { 645 | var gw = this.gridWrapper, container = this.element, gridparent = gw.parent(); 646 | container.detach(); 647 | gw.remove(); 648 | gridparent.append(container); 649 | parent.teardown.call(this); 650 | }; 651 | // clean the grid in case of redraw or refresh entire tree 652 | this._clean_grid = function (target,id) { 653 | var grid = this.gridWrapper; 654 | if (target) { 655 | findDataCell(this.uniq,id,this._gridSettings.gridcols).remove(); 656 | } else { 657 | // get all of the `div` children in all of the `td` in dataRow except for :first (that is the tree itself) and remove 658 | grid.find("div.jstree-grid-cell-regular").remove(); 659 | } 660 | }; 661 | // prepare the headers 662 | this._prepare_headers = function() { 663 | var header, i, col, _this = this, gs = this._gridSettings,cols = gs.columns || [], width, defaultWidth = gs.columnWidth, resizable = gs.resizable || false, 664 | cl, ccl, val, name, last, tr = gs.isThemeroller, classAdd = (tr?"themeroller":"regular"), puller, 665 | hasHeaders = false, gridparent = this.gridparent, rootid = this.rootid, 666 | conf = gs.defaultConf, coluuid, 667 | borPadWidth = 0, totalWidth = 0; 668 | 669 | // save the original parent so we can reparent on destroy 670 | this.parent = gridparent; 671 | 672 | // save the references to columns by unique ID 673 | this.colrefs = {}; 674 | 675 | 676 | // create the headers 677 | for (var i = 0, len = cols.length;i"); 679 | //col.appendTo(colgroup); 680 | cl = cols[i].headerClass || ""; 681 | ccl = cols[i].columnClass || ""; 682 | val = cols[i].header || ""; 683 | headerTitle = cols[i].headerTitle || ""; 684 | do { 685 | coluuid = String(Math.floor(Math.random()*10000)); 686 | } while(this.colrefs[coluuid] !== undefined); 687 | // create a unique name for this column 688 | name = cols[i].value ? coluuid : "text"; 689 | this.colrefs[name] = cols[i]; 690 | 691 | if (val) {hasHeaders = true;} 692 | if(gs.stateful && localStorage['jstree-root-'+rootid+'-column-'+i]) 693 | width = localStorage['jstree-root-'+rootid+'-column-'+i]; 694 | else 695 | width = cols[i].width || defaultWidth; 696 | 697 | var minWidth = cols[i].minWidth || width; 698 | var maxWidth = cols[i].maxWidth || width; 699 | 700 | // we only deal with borders if width is not auto and not percentages 701 | borPadWidth = tr ? 1+6 : 2+8; // account for the borders and padding 702 | if (width !== 'auto' && typeof(width) !== "string") { 703 | width -= borPadWidth; 704 | } 705 | col = this.midWrapper.children("div.jstree-grid-column-"+i); 706 | last = $("
    ").css(conf).addClass("jstree-grid-div-"+this.uniq+"-"+i+" "+(tr?"ui-widget-header ":"")+" jstree-grid-header jstree-grid-header-cell jstree-grid-header-"+classAdd+" "+cl+" "+ccl).html(val); 707 | last.addClass((tr?"ui-widget-header ":"")+"jstree-grid-header jstree-grid-header-"+classAdd); 708 | if (this.settings.core.themes.ellipsis === true){ 709 | last.addClass('jstree-grid-ellipsis'); 710 | } 711 | 712 | // add title and strip out HTML 713 | var title = gs.headerAsTitle && !headerTitle ? val : (headerTitle ? headerTitle : ""); 714 | title = title.replace(htmlstripre, ''); 715 | if (title) { 716 | last.attr("title",title); 717 | } 718 | 719 | last.prependTo(col); 720 | last.attr(COL_DATA_ATTR, name); 721 | totalWidth += last.outerWidth(); 722 | puller = $("
     
    ").appendTo(last); 723 | col.width(width); 724 | col.css("min-width", minWidth); 725 | col.css("max-width", maxWidth); 726 | } 727 | 728 | last.addClass((tr?"ui-widget-header ":"")+"jstree-grid-header jstree-grid-header-last jstree-grid-header-"+classAdd); 729 | // if there is no width given for the last column, do it via automatic 730 | if (cols[cols.length-1].width === undefined) { 731 | totalWidth -= width; 732 | col.css({width:"auto"}); 733 | last.addClass("jstree-grid-width-auto").next(".jstree-grid-separator").remove(); 734 | } 735 | if (hasHeaders) { 736 | // save the offset of the div from the body 737 | //gs.divOffset = header.parent().offset().left; 738 | gs.header = header; 739 | } else { 740 | $("div.jstree-grid-header").hide(); 741 | } 742 | 743 | if (!this.bound && resizable) { 744 | this.bound = true; 745 | $(document).mouseup(function () { 746 | var ref, cols, width, headers, currentTree, colNum; 747 | if (isClickedSep) { 748 | colNum = toResize.prevAll(".jstree-grid-column").length; 749 | currentTree = toResize.closest(".jstree-grid-wrapper").find(".jstree"); 750 | ref = $.jstree.reference(currentTree); 751 | cols = ref.settings.grid.columns; 752 | headers = toResize.parent().children("div.jstree-grid-column"); 753 | if (isNaN(colNum) || colNum < 0) { ref._gridSettings.treeWidthDiff = currentTree.find("ins:eq(0)").width() + currentTree.find("[class~='jstree-anchor']:eq(0)").width() - ref._gridSettings.columns[0].width; } 754 | width = ref._gridSettings.columns[colNum].width = parseFloat(toResize.css("width")); 755 | isClickedSep = false; 756 | toResize = null; 757 | 758 | currentTree.trigger("resize_column.jstree-grid", [colNum,width]); 759 | } 760 | }).mousemove(function (e) { 761 | if (isClickedSep) { 762 | newMouseX = e.pageX; 763 | var diff = newMouseX - oldMouseX, 764 | oldPrevHeaderInner, 765 | oldPrevColWidth, newPrevColWidth; 766 | 767 | if (diff !== 0){ 768 | oldPrevHeaderInner = toResize.width(); 769 | oldPrevColWidth = parseFloat(toResize.css("width")); 770 | 771 | // handle a Chrome issue with columns set to auto 772 | // thanks to Brabus https://github.com/side-by-side 773 | if (!oldPrevColWidth) {oldPrevColWidth = toResize.innerWidth();} 774 | 775 | // make sure that diff cannot be beyond the left/right limits 776 | diff = diff < 0 ? Math.max(diff,-oldPrevHeaderInner) : diff; 777 | newPrevColWidth = oldPrevColWidth+diff; 778 | 779 | // only do this if we are not shrinking past 0 on left - and limit it to that amount 780 | if ((diff > 0 || oldPrevHeaderInner > 0) && newPrevColWidth > MINCOLWIDTH) { 781 | toResize.width(newPrevColWidth+"px"); 782 | toResize.css("min-width",newPrevColWidth+"px"); 783 | toResize.css("max-width",newPrevColWidth+"px"); 784 | oldMouseX = newMouseX; 785 | } 786 | } 787 | } 788 | }); 789 | this.gridWrapper.on("selectstart", ".jstree-grid-resizable-separator", function () { 790 | return false; 791 | }).on("mousedown", ".jstree-grid-resizable-separator", function (e) { 792 | isClickedSep = true; 793 | oldMouseX = e.pageX; 794 | toResize = $(this).closest("div.jstree-grid-column"); 795 | // the max rightmost position we will allow is the right-most of the wrapper minus a buffer (10) 796 | return false; 797 | }) 798 | .on("dblclick", ".jstree-grid-resizable-separator", function (e) { 799 | var clickedSep = $(this), col = clickedSep.closest("div.jstree-grid-column"), 800 | oldPrevColWidth = parseFloat(col.css("width")), newWidth = 0, diff, 801 | colNum = col.prevAll(".jstree-grid-column").length, 802 | oldPrevHeaderInner = col.width(), newPrevColWidth; 803 | 804 | 805 | //find largest width 806 | col.find(".jstree-grid-cell").each(function() { 807 | var item = $(this), width; 808 | item.css("position", "absolute"); 809 | item.css("width", "auto"); 810 | width = item.outerWidth(); 811 | item.css("position", "relative"); 812 | 813 | if (width>newWidth) { 814 | newWidth = width; 815 | } 816 | }); 817 | 818 | diff = newWidth-oldPrevColWidth; 819 | 820 | // make sure that diff cannot be beyond the left limits 821 | diff = diff < 0 ? Math.max(diff,-oldPrevHeaderInner) : diff; 822 | newPrevColWidth = (oldPrevColWidth+diff)+"px"; 823 | 824 | col.width(newPrevColWidth); 825 | col.css("min-width",newPrevColWidth); 826 | col.css("max-width",newPrevColWidth); 827 | 828 | $(this).closest(".jstree-grid-wrapper").find(".jstree").trigger("resize_column.jstree-grid",[colNum,newPrevColWidth]); 829 | }) 830 | .on("click", ".jstree-grid-separator", function (e) { 831 | // don't sort after resize 832 | e.stopPropagation(); 833 | }); 834 | } 835 | this.gridWrapper.on("click", ".jstree-grid-header-cell", function (e) { 836 | if (!_this.sort) { 837 | return; 838 | } 839 | 840 | // get column 841 | var name = $(this).attr(COL_DATA_ATTR); 842 | 843 | // sort order 844 | var symbol; 845 | if (gs.sortOrder === name && gs.sortAsc === true) { 846 | gs.sortAsc = false; 847 | symbol = "↓"; 848 | } else { 849 | gs.sortOrder = name; 850 | gs.sortAsc = true; 851 | symbol = "↑"; 852 | } 853 | 854 | // add sort arrow 855 | $(this.closest('.jstree-grid-wrapper')).find(".jstree-grid-sort-icon").remove(); 856 | $("").addClass("jstree-grid-sort-icon").appendTo($(this)).html(symbol); 857 | 858 | // sort by column 859 | var rootNode = _this.get_node('#'); 860 | _this.sort(rootNode, true); 861 | _this.redraw_node(rootNode, true); 862 | }); 863 | 864 | }; 865 | 866 | this._domManipulation = null; // We'll store the column nodes in this object and an id for the grid-node that started the manipulation { id: "id of the node that started the manipulation", columns: { Key-Value-Pair col-No: Column }} 867 | 868 | function _guid() { 869 | function s4() { 870 | return Math.floor((1 + Math.random()) * 0x10000) 871 | .toString(16) 872 | .substring(1); 873 | } 874 | return s4() + s4() + '-' + s4() + '-' + s4() + '-' + 875 | s4() + '-' + s4() + s4() + s4(); 876 | } 877 | /* 878 | * Trys to detach the tree columns on massive dom manipulations 879 | */ 880 | this._detachColumns = function (id) { 881 | // if the columns are not detached, then detach them 882 | if (this._domManipulation == null) { 883 | var cols = this._gridSettings.columns || [], treecol = this._gridSettings.treecol, mw = this.midWrapper; 884 | this._domManipulation = { id: id, oldActiveElement: document.activeElement, columns: {} }; 885 | for (var i = 0, len = cols.length; i < len; i++) { 886 | //if (treecol === i) { 887 | // continue; 888 | //} 889 | this._domManipulation.columns[i] = mw.children(".jstree-grid-column-" + i)[0]; 890 | this._domManipulation.columns[i].parentNode.removeChild(this._domManipulation.columns[i]); 891 | } 892 | } 893 | return this._domManipulation; 894 | } 895 | 896 | this._reattachColumns = function (id) { 897 | if (this._domManipulation == null) { return false; } 898 | if (this._domManipulation.id === id) { 899 | var cols = this._gridSettings.columns || [], treecol = this._gridSettings.treecol, mw = this.midWrapper; 900 | for (var i = 0, len = cols.length; i < len; i++) { 901 | //if (treecol === i) { 902 | // continue; 903 | //} 904 | mw[0].appendChild(this._domManipulation.columns[i]); 905 | } 906 | if (this._domManipulation.oldActiveElement !== document.activeElement) { $(this._domManipulation.oldActiveElement).focus(); } 907 | this._domManipulation = null; 908 | } 909 | return true; 910 | } 911 | 912 | /* 913 | * Override open_node to detach the columns before redrawing child-nodes, and do reattach them afterwarts 914 | */ 915 | this.open_node = function (obj, callback, animation) { 916 | var isArray = $.isArray(obj); 917 | var node = null; 918 | if (!isArray) { 919 | node = this.get_node(obj); 920 | if (node.id === "#") { return; } // wtf??? we ar in the root and do not need a open! 921 | } 922 | var id = isArray ? _guid() : node.id; 923 | this._detachColumns(id); 924 | var ret = parent.open_node.call(this, obj, callback, animation); 925 | this._reattachColumns(id); 926 | return ret; 927 | } 928 | 929 | /* 930 | * Override redraw_node to correctly insert the grid 931 | */ 932 | this.redraw_node = function (obj, deep, is_callback, force_render) { 933 | var id = $.isArray(obj) ? _guid() : this.get_node(obj).id; 934 | // we detach the columns once 935 | this._detachColumns(id); 936 | // first allow the parent to redraw the node 937 | obj = parent.redraw_node.call(this, obj, deep, is_callback, force_render); 938 | // next prepare the grid for a redrawn node - but only if ths node is not hidden (search does that) 939 | if (obj) { 940 | this._prepare_grid(obj); 941 | } 942 | // don't forget to reattach 943 | this._reattachColumns(id); 944 | return obj; 945 | }; 946 | this.refresh = function () { 947 | this._clean_grid(); 948 | return parent.refresh.apply(this,arguments); 949 | }; 950 | /* 951 | * Override set_id to update cell attributes 952 | */ 953 | this.set_id = function (obj, id) { 954 | var old, uniq = this.uniq; 955 | if(obj) { 956 | old = obj.id; 957 | } 958 | var result = parent.set_id.apply(this,arguments); 959 | if(result) { 960 | if (old !== undefined) { 961 | var grid = this.gridWrapper, oldNodes = [old], i; 962 | // get children 963 | if (obj && obj.children_d) { 964 | oldNodes = oldNodes.concat(obj.children_d); 965 | } 966 | // update id in children 967 | findDataCell(uniq,oldNodes,this._gridSettings.gridcols) 968 | .attr(NODE_DATA_ATTR, obj.id) 969 | .removeClass(generateCellId(uniq,old)) 970 | .addClass(generateCellId(uniq,obj.id)) 971 | .each(function(i,node) { 972 | $(node).attr('id', generateCellId(uniq,obj.id)+(i+1)); 973 | }); 974 | } 975 | } 976 | return result; 977 | }; 978 | 979 | this._hideOrShowTree = function(node, hide) { 980 | //Hides or shows a tree 981 | this._detachColumns(node.id); 982 | // show cells in each detachted column 983 | this._hideOrShowNode(node, hide, this._gridSettings.columns || [], this._gridSettings.treecol); 984 | this._reattachColumns(node.id); 985 | } 986 | this._hideOrShowNode = function(node, hide, cols, treecol) { 987 | //Hides or shows a node with recursive calls to all open child-nodes 988 | for (var i = 0, len = cols.length; i < len; i++) { 989 | if (i === treecol) { continue; } 990 | var cells = findDataCell(this.uniq, node.id, i, $(this._domManipulation.columns[i])); 991 | if (hide) { 992 | cells.addClass("jstree-grid-hidden"); 993 | } else { 994 | cells.removeClass("jstree-grid-hidden"); 995 | } 996 | } 997 | if (node.state.opened && node.children) { 998 | for (var i = 0, len = node.children.length; i < len; i++) { 999 | this._hideOrShowNode(this.get_node(node.children[i]), hide, cols, treecol); 1000 | } 1001 | } 1002 | } 1003 | this._hide_grid = function (node) { 1004 | if (!node) { return true; } 1005 | this._detachColumns(node.id); 1006 | var children = node.children ? node.children : [], cols = this._gridSettings.columns || [], treecol = this._gridSettings.treecol; 1007 | // try to remove all children 1008 | for (var i = 0, len = children.length; i < len; i++) { 1009 | var child = this.get_node(children[i]); 1010 | // go through each column, remove all children with the correct ID name 1011 | for (var j = 0, lenj = cols.length; j < lenj; j++) { 1012 | if (j === treecol) { continue; } 1013 | findDataCell(this.uniq, child.id, j, $(this._domManipulation.columns[j])).remove(); 1014 | } 1015 | if (child.state.opened) { this._hide_grid(child);} 1016 | } 1017 | 1018 | 1019 | this._reattachColumns(node.id); 1020 | }; 1021 | this.holdingCells = {}; 1022 | this.getHoldingCells = function (obj, col, hc) { 1023 | if (obj.state.hidden || !obj.state.opened) { return $(); } 1024 | var ret = $(), children = obj.children || [], child, i, uniq = this.uniq; 1025 | // run through each child, render it, and then render its children recursively 1026 | for (i = 0; i < children.length; i++) { 1027 | child = generateCellId(uniq, children[i]) + col; 1028 | if (hc[child]) { 1029 | ret = ret.add(hc[child]).add(this.getHoldingCells(this.get_node(children[i]), col, hc)); 1030 | //delete hc[child]; 1031 | } 1032 | } 1033 | return (ret); 1034 | }; 1035 | 1036 | /** 1037 | * put a grid cell in edit mode (input field to edit the data) 1038 | * @name edit(obj, col) 1039 | * @param {mixed} obj 1040 | * @param {obj} col definition 1041 | * @param {element} cell element, either span or wrapping div 1042 | */ 1043 | this._edit = function (obj, col, element) { 1044 | if(!obj) { return false; } 1045 | if (!obj.data) {obj.data = {};} 1046 | if (element) { 1047 | element = $(element); 1048 | if (element.prop("tagName").toLowerCase() === "div") { 1049 | element = element.children("span:first"); 1050 | } 1051 | } else { 1052 | // need to find the element - later 1053 | return false; 1054 | } 1055 | var rtl = this._data.core.rtl, 1056 | w = this.element.width(), 1057 | t = obj.data[col.value], 1058 | h1 = $("<"+"div />", { css : { "position" : "absolute", "top" : "-200px", "left" : (rtl ? "0px" : "-1000px"), "visibility" : "hidden" } }).appendTo("body"), 1059 | h2 = $("<"+"input />", { 1060 | "value" : t, 1061 | "class" : "jstree-rename-input", 1062 | "css" : { 1063 | "padding" : "0", 1064 | "border" : "1px solid silver", 1065 | "box-sizing" : "border-box", 1066 | "display" : "inline-block", 1067 | "height" : (this._data.core.li_height) + "px", 1068 | "lineHeight" : (this._data.core.li_height) + "px", 1069 | "width" : "150px" // will be set a bit further down 1070 | }, 1071 | "blur" : $.proxy(function () { 1072 | var v = h2.val(); 1073 | // save the value if changed 1074 | if(v === "" || v === t) { 1075 | v = t; 1076 | } else { 1077 | obj.data[col.value] = v; 1078 | this.element.trigger('update_cell.jstree-grid', { node: obj, col: col.value, value: v, old: t }); 1079 | var id = _guid(); 1080 | this._detachColumns(id); 1081 | this._prepare_grid(this.get_node(obj, true)); 1082 | this._reattachColumns(id); 1083 | } 1084 | h2.remove(); 1085 | element.show(); 1086 | }, this), 1087 | "keydown" : function (event) { 1088 | var key = event.which; 1089 | if(key === 27) { 1090 | this.value = t; 1091 | } 1092 | if(key === 27 || key === 13 || key === 37 || key === 38 || key === 39 || key === 40 || key === 32) { 1093 | event.stopImmediatePropagation(); 1094 | } 1095 | if(key === 27 || key === 13) { 1096 | event.preventDefault(); 1097 | this.blur(); 1098 | } 1099 | }, 1100 | "click" : function (e) { e.stopImmediatePropagation(); }, 1101 | "mousedown" : function (e) { e.stopImmediatePropagation(); }, 1102 | "keyup" : function (event) { 1103 | h2.width(Math.min(h1.text("pW" + this.value).width(),w)); 1104 | }, 1105 | "keypress" : function(event) { 1106 | if(event.which === 13) { return false; } 1107 | } 1108 | }), 1109 | fn = { 1110 | fontFamily : element.css('fontFamily') || '', 1111 | fontSize : element.css('fontSize') || '', 1112 | fontWeight : element.css('fontWeight') || '', 1113 | fontStyle : element.css('fontStyle') || '', 1114 | fontStretch : element.css('fontStretch') || '', 1115 | fontVariant : element.css('fontVariant') || '', 1116 | letterSpacing : element.css('letterSpacing') || '', 1117 | wordSpacing : element.css('wordSpacing') || '' 1118 | }; 1119 | element.hide(); 1120 | element.parent().append(h2); 1121 | h2.css(fn).width(Math.min(h1.text("pW" + h2[0].value).width(),w))[0].select(); 1122 | }; 1123 | 1124 | this.grid_hide_column = function (col) { 1125 | this.midWrapper.find(".jstree-grid-column-"+col).hide(); 1126 | }; 1127 | this.grid_show_column = function (col) { 1128 | this.midWrapper.find(".jstree-grid-column-"+col).show(); 1129 | }; 1130 | 1131 | this._prepare_grid = function (obj) { 1132 | var gs = this._gridSettings, c = gs.treeClass, _this = this, t, 1133 | cols = gs.columns || [], width, tr = gs.isThemeroller, uniq = this.uniq, 1134 | treecol = gs.treecol, 1135 | tree = this.element, rootid = this.rootid, 1136 | classAdd = (tr?"themeroller":"regular"), img, objData = this.get_node(obj), 1137 | defaultWidth = gs.columnWidth, conf = gs.defaultConf, cellClickHandler = function (tree,node,val,col,t) { 1138 | return function(e) { 1139 | //node = tree.find("#"+node.attr("id")); 1140 | var event = jQuery.Event("select_cell.jstree-grid"); 1141 | tree.trigger(event, [{value: val,column: col.header,node: node,grid:$(this),sourceName: col.value}]); 1142 | if (!event.isDefaultPrevented()) { 1143 | node.children(".jstree-anchor").trigger("click.jstree",e); 1144 | } 1145 | }; 1146 | }, cellRightClickHandler = function (tree,node,val,col,t) { 1147 | return function (e) { 1148 | if (gs.gridcontextmenu) { 1149 | e.preventDefault(); 1150 | $.vakata.context.show(this,{ 'x' : e.pageX, 'y' : e.pageY }, gs.gridcontextmenu(_this,tree,node,val,col,t,e.target)); 1151 | } 1152 | }; 1153 | }, 1154 | hoverInHandler = function (node, jsTreeInstance) { 1155 | return function() { jsTreeInstance.hover_node(node); }; 1156 | }, 1157 | hoverOutHandler = function (node, jsTreeInstance) { 1158 | return function() { jsTreeInstance.dehover_node(node); }; 1159 | }, 1160 | i, val, cl, wcl, ccl, a, last, valClass, wideValClass, span, paddingleft, title, gridCellName, gridCellParentId, gridCellParent, 1161 | gridCellPrev, gridCellPrevId, gridCellNext, gridCellNextId, gridCellChild, gridCellChildId, 1162 | col, content, tmpWidth, mw = this.midWrapper, column, lid = objData.id, 1163 | highlightSearch, isClicked, 1164 | peers = this.get_node(objData.parent).children, 1165 | // find my position in the list of peers. "peers" is the list of everyone at my level under my parent, in order 1166 | pos = $.inArray(lid,peers), 1167 | hc = this.holdingCells, rendered = false, closed; 1168 | // get our column definition 1169 | t = $(obj); 1170 | 1171 | // find the a children 1172 | a = t.children("[class~='jstree-anchor']"); 1173 | highlightSearch = a.hasClass(SEARCHCLASS); 1174 | isClicked = a.hasClass("jstree-clicked"); 1175 | 1176 | if (a.length === 1) { 1177 | closed = !objData.state.opened; 1178 | gridCellName = generateCellId(uniq,lid); 1179 | gridCellParentId = objData.parent === "#" ? null : objData.parent; 1180 | a.addClass(c); 1181 | //renderAWidth(a,_this); 1182 | renderATitle(a,t,_this); 1183 | last = a; 1184 | 1185 | // calculate position ids once 1186 | gridCellPrevId = pos <= 0 ? objData.parent : findLastClosedNode(this, peers[pos - 1]); 1187 | gridCellNextId = pos >= peers.length - 1 ? "NULL" : peers[pos + 1]; 1188 | gridCellChildId = objData.children && objData.children.length > 0 ? objData.children[0] : "NULL"; 1189 | 1190 | // find which column our tree shuld go in 1191 | var s = this.settings.grid; 1192 | for (var i = 0, len = cols.length;i
    ' : '';} 1218 | } else { content = val; } 1219 | 1220 | // content cannot be blank, or it messes up heights 1221 | if (content === undefined || content === null || BLANKRE.test(content)) { 1222 | content = " "; 1223 | } 1224 | 1225 | // get the valueClass 1226 | valClass = col.valueClass && objData.data !== null && objData.data !== undefined ? objData.data[col.valueClass] || "" : ""; 1227 | if (valClass && col.valueClassPrefix && col.valueClassPrefix !== "") { 1228 | valClass = col.valueClassPrefix + valClass; 1229 | } 1230 | // get the wideValueClass 1231 | wideValClass = col.wideValueClass && objData.data !== null && objData.data !== undefined ? objData.data[col.wideValueClass] || "" : ""; 1232 | if (wideValClass && col.wideValueClassPrefix && col.wideValueClassPrefix !== "") { 1233 | wideValClass = col.wideValueClassPrefix + wideValClass; 1234 | } 1235 | // get the title 1236 | title = col.title && objData.data !== null && objData.data !== undefined ? objData.data[col.title] || "" : ""; 1237 | // strip out HTML 1238 | title = title.replace(htmlstripre, ''); 1239 | 1240 | // get the width 1241 | paddingleft = 7; 1242 | width = col.width || defaultWidth; 1243 | if (width !== 'auto') { 1244 | width = tmpWidth || (width - paddingleft); 1245 | } 1246 | 1247 | last = findDataCell(uniq, lid, i, column); 1248 | if (!last || last.length < 1) { 1249 | last = $("
    "); 1250 | $("").appendTo(last); 1251 | last.attr("id",gridCellName+i); 1252 | last.addClass(gridCellName); 1253 | last.attr(NODE_DATA_ATTR,lid); 1254 | if (highlightSearch) { 1255 | last.addClass(SEARCHCLASS); 1256 | } else { 1257 | last.removeClass(SEARCHCLASS); 1258 | } 1259 | if (isClicked) { 1260 | last.addClass("jstree-clicked"); 1261 | } else { 1262 | last.removeClass("jstree-clicked"); 1263 | } 1264 | if (this.settings.core.themes.ellipsis === true && i !== treecol) { 1265 | last.addClass('jstree-grid-ellipsis'); 1266 | } 1267 | 1268 | } 1269 | 1270 | // we need to check the hidden-state and see if we need to hide the node 1271 | if (objData.state.hidden) { 1272 | last.addClass("jstree-grid-hidden"); 1273 | } else { 1274 | last.removeClass("jstree-grid-hidden"); 1275 | } 1276 | 1277 | // ditto for the disabled-state and disabling the node 1278 | if (objData.state.disabled) { 1279 | last.addClass('jstree-disabled'); 1280 | } else { 1281 | last.removeClass('jstree-disabled'); 1282 | } 1283 | // we need to put it in the dataCell - after the parent, but the position matters 1284 | // if we have no parent, then we are one of the root nodes, but still need to look at peers 1285 | 1286 | 1287 | // if we are first, i.e. pos === 0, we go right after the parent; 1288 | // if we are not first, and our previous peer (one before us) is closed, we go right after the previous peer cell 1289 | // if we are not first, and our previous peer is opened, then we have to find its youngest & lowest closed child (incl. leaf) 1290 | // 1291 | // probably be much easier to go *before* our next one 1292 | // but that one might not be drawn yet 1293 | // here is the logic for jstree drawing: 1294 | // it draws peers from first to last or from last to first 1295 | // it draws children before a parent 1296 | // 1297 | // so I can rely on my *parent* not being drawn, but I cannot rely on my previous peer or my next peer being drawn 1298 | 1299 | // so we do the following: 1300 | // 1- We are the first child: install after the parent 1301 | // 2- Our previous peer is already drawn: install after the previous peer 1302 | // 3- Our previous peer is not drawn, we have a child that is drawn: install right before our first child 1303 | // 4- Our previous peer is not drawn, we have no child that is drawn, our next peer is drawn: install right before our next peer 1304 | // 5- Our previous peer is not drawn, we have no child that is drawn, our next peer is not drawn: install right after parent 1305 | gridCellPrev = findDataCell(uniq, gridCellPrevId, i, column); 1306 | gridCellNext = findDataCell(uniq, gridCellNextId, i, column); 1307 | gridCellChild = findDataCell(uniq, gridCellChildId, i, column); 1308 | gridCellParent = findDataCell(uniq, gridCellParentId, i, column); 1309 | 1310 | 1311 | // if our parent is already drawn, then we put this in the right order under our parent 1312 | if (gridCellParentId) { 1313 | if (gridCellParent && gridCellParent.length > 0) { 1314 | if (gridCellPrev && gridCellPrev.length > 0) { 1315 | last.insertAfter(gridCellPrev); 1316 | } else if (gridCellChild && gridCellChild.length > 0) { 1317 | last.insertBefore(gridCellChild); 1318 | } else if (gridCellNext && gridCellNext.length > 0) { 1319 | last.insertBefore(gridCellNext); 1320 | } else { 1321 | last.insertAfter(gridCellParent); 1322 | } 1323 | rendered = true; 1324 | } else { 1325 | rendered = false; 1326 | } 1327 | // always put it in the holding cells, and then sort when the parent comes in, in case parent is (re)drawn later 1328 | hc[gridCellName+i] = last; 1329 | } else { 1330 | if (gridCellPrev && gridCellPrev.length > 0) { 1331 | last.insertAfter(gridCellPrev); 1332 | } else if (gridCellChild && gridCellChild.length > 0) { 1333 | last.insertBefore(gridCellChild); 1334 | } else if (gridCellNext && gridCellNext.length > 0) { 1335 | last.insertBefore(gridCellNext); 1336 | } else { 1337 | last.appendTo(column); 1338 | } 1339 | rendered = true; 1340 | } 1341 | // do we have any children waiting for this cell? walk down through the children/grandchildren/etc tree 1342 | if (rendered) { 1343 | var toRen = this.getHoldingCells(objData,i,hc); 1344 | last.after(toRen); 1345 | } 1346 | // need to make the height of this match the line height of the tree. How? 1347 | span = last.children("span"); 1348 | 1349 | // create a span inside the div, so we can control what happens in the whole div versus inside just the text/background 1350 | span.addClass(cl+" "+valClass).html(content); 1351 | last = last.css(conf).addClass("jstree-grid-cell jstree-grid-cell-regular jstree-grid-cell-root-"+rootid+" jstree-grid-cell-"+classAdd+" "+wcl+ " " + wideValClass + (tr?" ui-state-default":"")).addClass("jstree-grid-col-"+i).addClass("jstree-animated"); 1352 | // add click handler for clicking inside a grid cell 1353 | last.click(cellClickHandler(tree,t,val,col,this)); 1354 | last.on("contextmenu",cellRightClickHandler(tree,t,val,col,this)); 1355 | last.hover(hoverInHandler(t, this), hoverOutHandler(t, this)); 1356 | 1357 | if (title) { 1358 | span.attr("title",title); 1359 | } 1360 | 1361 | tree.trigger("render_cell.jstree-grid", [{value: val, column: col.header, node: t, sourceName: col.value}]); 1362 | } 1363 | last.addClass("jstree-grid-cell-last"+(tr?" ui-state-default":"")); 1364 | // if there is no width given for the last column, do it via automatic 1365 | if (cols[cols.length-1].width === undefined) { 1366 | last.addClass("jstree-grid-width-auto").next(".jstree-grid-separator").remove(); 1367 | } 1368 | } 1369 | this.element.css({'overflow-y':'auto !important'}); 1370 | }; 1371 | // clean up holding cells 1372 | this.holdingCells = {}; 1373 | 1374 | // need to do alternating background colors or borders 1375 | }; 1376 | })); 1377 | 1378 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jstreegrid", 3 | "description": "grid plugin for jstree", 4 | "version": "3.10.2", 5 | "url": "https://github.com/deitch/jstree-grid", 6 | "author": { 7 | "name": "Avi Deitcher", 8 | "url": "https://github.com/deitch" 9 | }, 10 | "main": "jstreegrid.js", 11 | "license": "MIT", 12 | "contributors": [ 13 | { 14 | "name": "jochenberger", 15 | "url": "https://github.com/jochenberger" 16 | }, 17 | { 18 | "name": "Tobias Zanke", 19 | "url": "https://github.com/TZanke" 20 | }, 21 | { 22 | "name": "ChrisRaven", 23 | "url": "https://github.com/ChrisRaven" 24 | }, 25 | { 26 | "name": "Brabus", 27 | "url": "https://github.com/side-by-side" 28 | }, 29 | { 30 | "name": "Adam Jimenez", 31 | "url": "https://github.com/adamjimenez" 32 | }, 33 | { 34 | "name": "QueroBartk", 35 | "url": "https://github.com/QueroBartK" 36 | }, 37 | { 38 | "name": "OgreTransporter", 39 | "url": "https://github.com/OgreTransporter" 40 | }, 41 | { 42 | "name": "LlorX", 43 | "url": "https://github.com/Llorx" 44 | }, 45 | { 46 | "name": "PetzeltA", 47 | "url": "https://github.com/petzelta/" 48 | }, 49 | { 50 | "name": "yarn", 51 | "url": "https://github.com/YarnSeemannsgarn" 52 | } 53 | ], 54 | "dependencies": { 55 | "jstree": ">=3.0.0" 56 | }, 57 | "keywords": [ 58 | "jquery", 59 | "jstree", 60 | "grid", 61 | "javascript" 62 | ], 63 | "repository": { 64 | "type": "git", 65 | "url": "http://github.com/deitch/jstree-grid.git" 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /publish: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ###### 4 | # Simple script to run npm publish 5 | ###### 6 | 7 | # what version number are we in git tag and in package.json 8 | NPMVERSION=$(awk '/\Wversion\W/ {print $2}' package.json | sed 's/[\"\,]//g') 9 | GITTAG=$(git describe --abbrev=0) 10 | 11 | # we *should* be checking if this last one changed, and then publish, but later 12 | npm publish 13 | 14 | -------------------------------------------------------------------------------- /tests/README.md: -------------------------------------------------------------------------------- 1 | # jstreegrid tests 2 | 3 | This directory contains the tests for jstreegrid. In order to avoid a lot of duplication, 4 | the tests have been created in a modular fashion. There is a single master file, 5 | `test-master.html`, which provides an interface to loading and running any tests. 6 | 7 | All tests should exist as html partial files in `components/` named for the test, e.g. `my-unique-test.html`. Any `script` tags in the file will be executed. The html partial will be loaded into a `
    ` 8 | 9 | To add a new test: 10 | 11 | 1. Create the test partial `.html` in `components/`, e.g. `my-unique-test.html` 12 | 2. Add the correct partial html and JS scripts to the `my-unique-test.html` 13 | 3. Add the test file name to the `test-list.json` array 14 | 15 | **About ES6/CJS:** Yes, we definitely could use ES6 modules or CommonJS modules (like node), 16 | but for the purpose of clean tests, we stick with native JS understood by the browser, 17 | i.e. no Babel/Traceur transpiling. 18 | -------------------------------------------------------------------------------- /tests/components/api-hide-show-test.html: -------------------------------------------------------------------------------- 1 | 114 |

    API Hide/Show Test

    115 | 116 |
    117 | 118 | 121 | 124 | -------------------------------------------------------------------------------- /tests/components/api-select-test.html: -------------------------------------------------------------------------------- 1 | 63 |

    API Select Test

    64 | 65 |
    66 | -------------------------------------------------------------------------------- /tests/components/chrome-header-test.html: -------------------------------------------------------------------------------- 1 | 74 |

    Column Resize Chrome Test

    75 |

    76 | There is an issue with Chrome/Chromium wherein if a column size is set to 'auto', calling $(col).css("width") returns 0, rather than the actual width. This tests whether or not it works. 77 |

    78 |

    79 | The bug hits primarily during resizing. To test this, try resizing a column in Firefox and then in Chrome. 80 |

    81 |
    82 |
    83 | -------------------------------------------------------------------------------- /tests/components/click-sort-test.html: -------------------------------------------------------------------------------- 1 | 81 |

    Tree Grid Click Sort Test

    82 |
    83 |

    First Tree

    84 |
    85 |
    Tree grid is loading.
    86 | 87 | 88 |
    Search
    89 |
    Click a cell to see results:
    90 |
    Right-click a cell and edit to see results:
    91 |
    92 | -------------------------------------------------------------------------------- /tests/components/clicked-on-show.html: -------------------------------------------------------------------------------- 1 | 43 |

    Tree Grid Clicked On Show Test

    44 |
    45 |
    Two tests are included here: 46 |
      47 |
    • On first rendering, the first child under "Root" should be selected, as should its grid cells
    • 48 |
    • Select any cell, it will highlight all of the cells. Then close and open its parent, and the cell and its grid cells should be clicked.
    • 49 |
    50 |
    51 |
    52 |
    53 | -------------------------------------------------------------------------------- /tests/components/dnd-test.html: -------------------------------------------------------------------------------- 1 | 158 |

    Tree Grid DND Test

    159 | 160 |
    161 |
    162 |
    163 | 164 |
    165 |

    Tree hierarchy

    166 |
    167 |
    168 | 169 |
    170 |

    Tree hierarchy

    171 |
    172 |
    173 |
    174 | -------------------------------------------------------------------------------- /tests/components/dynamic-root-node-test.html: -------------------------------------------------------------------------------- 1 | 85 | 86 |

    Tree Grid Dynamic Root Test

    87 | 88 |
    89 |
    90 |
    91 | 92 | 93 |
    94 | 98 |
    Tree with preset nodes (works)
    99 |
    100 |
    101 | 102 |
    103 | 104 |
    105 | 109 |
    Tree with no nodes (fails)
    110 |
    111 |
    112 | 113 |
    114 | 115 |
    116 | 120 |
    Tree without grid (works)
    121 |
    122 |
    123 |
    124 | 125 |
    126 |
    127 | -------------------------------------------------------------------------------- /tests/components/fixed-widths-resize-test.html: -------------------------------------------------------------------------------- 1 | 44 |

    Tree Grid 3.2.0 Demo

    45 | 46 |
    47 |

    First Tree

    48 |
    49 |
    Tree grid is loading.
    50 |
    51 | -------------------------------------------------------------------------------- /tests/components/fnvalue-test.html: -------------------------------------------------------------------------------- 1 | 172 |

    tharasp issue

    173 |
    174 |
    175 |
    176 | -------------------------------------------------------------------------------- /tests/components/grid-api-test.html: -------------------------------------------------------------------------------- 1 | 35 |

    API Test

    36 |    37 | 38 |
    39 | -------------------------------------------------------------------------------- /tests/components/large-dataset-test.html: -------------------------------------------------------------------------------- 1 | 36 | 46 |

    Large Data Set Search

    47 |
    48 | Should display how long it takes to perform the search. Note that it does it in a crude fashion using a custom event. For true testing, use a profiler.

    49 | Will only search when you press the "Search" button. 50 |

    51 |
    52 | Time for search: milliseconds 53 |
    54 | 55 | 56 |
    57 |
    58 |
    59 | -------------------------------------------------------------------------------- /tests/components/minwidth-maxwidth-test.html: -------------------------------------------------------------------------------- 1 | 57 | 58 |

    Tree Grid minWidth/maxWidth Test

    59 | 60 |
    61 |

    Width Tree

    62 |

    The columns here all have width=auto, minWidth=100 and maxWidth=200. You can resize them, but when auto-rendering, none should be less than 100 or bigger than 200. 63 | Of particular interest is the minWidth of the second column (which has very few characters) and the maxWidth of the third column (which has many). 64 |

    65 |

    Actuals:

    66 |
    67 |
    68 |
    69 | -------------------------------------------------------------------------------- /tests/components/odd-named-id-test.html: -------------------------------------------------------------------------------- 1 | 44 |

    Tree Grid 3.2.0 Demo

    45 | 46 |
    47 |

    First Tree

    48 |
    49 |
    Tree grid is loading.
    50 |
    51 | -------------------------------------------------------------------------------- /tests/components/rename-dynamic-node-test.html: -------------------------------------------------------------------------------- 1 | 101 |

    Rename Dynamic Node

    102 |

    To test:

    103 |
      104 |
    1. See the tree below
    2. 105 |
    3. Select a node
    4. 106 |
    5. Click "create" to create a new node
    6. 107 |
    7. Select a grid cell (not the first one, i.e. not the original tree)
    8. 108 |
    9. Try to rename it using right-click or the "Rename" button
    10. 109 |
    110 | 111 | 112 |
    113 | 114 | 115 | 116 |
    117 | 118 |
    119 | -------------------------------------------------------------------------------- /tests/components/rename_id_test.html: -------------------------------------------------------------------------------- 1 | 31 |

    Rename Issue

    32 |

    To reproduce the issue:

    33 |
      34 |
    1. See the tree below
    2. 35 |
    3. Click the button
    4. 36 |
    5. See that the node is renamed (and given a new id) of "abc"
    6. 37 |
    7. See that a row of the grid is duplicated
    8. 38 |
    39 | 40 |
    41 | -------------------------------------------------------------------------------- /tests/components/render_cell-event-test.html: -------------------------------------------------------------------------------- 1 | 36 | 37 |

    Tree Grid render_cell Event Test

    38 |
    39 |
    40 |
    41 |

    Events

    42 |
    Details in console
    43 |
      44 |
    45 |
    46 | -------------------------------------------------------------------------------- /tests/components/resize-event-test.html: -------------------------------------------------------------------------------- 1 | 45 | 46 |

    Tree Grid Column Resize Event Test

    47 |
    48 | Column N/A now has size UNKNOWN. 49 |
    50 |
    51 |
    52 | -------------------------------------------------------------------------------- /tests/components/search-test.html: -------------------------------------------------------------------------------- 1 | 91 | 101 |

    Search issue

    102 |
    only one search can be active at any time!
    103 |
    104 | The single search box will search all three trees simultaneously. Each tree is configured slightly differently in terms of search parameters. 105 |
    106 | 107 |
    108 | The following search boxes will search in their respective column. This will use a logical AND 109 |
    110 |
    111 | 112 | 113 | 114 | 115 |
    116 |
    117 | The following search boxes will search in their respective column. Each input will start a new search independent of the others 118 |
    119 |
    120 | 121 | 122 | 123 | 124 |
    125 |
    126 |
    127 | show_only_matches=false 128 |
    129 |
    130 |
    131 |
    132 | show_only_matches=true 133 | show_only_matches_children=false 134 |
    135 |
    136 |
    137 |
    138 | show_only_matches=true 139 | show_only_matches_children=true 140 |
    141 |
    142 | -------------------------------------------------------------------------------- /tests/components/select-child-test.html: -------------------------------------------------------------------------------- 1 | 21 |

    Tree Grid Click Child Test

    22 |
    23 |
      24 |
    • Root node 1 25 | 29 |
    • 30 |
    31 |
    32 | -------------------------------------------------------------------------------- /tests/components/select-prevent-test.html: -------------------------------------------------------------------------------- 1 | 83 | 84 |

    Tree Select prevent Test

    85 |
    Select the elements in any of the three rows. 86 |
      87 |
    • The first row does not do event.preventDefault(), so the whole row will be selected and lose focus
    • 88 |
    • The second and third row do event.preventDefault(), so the whole row will not be selected
    • 89 |
    • The fourth row does event.preventDefault(), but also manually selects the row, so the change should go through and the row will be selected
    • 90 |
    91 |
    92 |
    93 |
    94 | -------------------------------------------------------------------------------- /tests/components/sort-without-resizable-test.html: -------------------------------------------------------------------------------- 1 | 85 |

    Tree Grid Click Sort Test

    86 |
    87 |

    First Tree

    88 |
    89 |
    Tree grid is loading.
    90 |
    91 |
    92 |
    93 |

    Second Tree

    94 |
    95 |
    Tree grid is loading.
    96 |
    97 | -------------------------------------------------------------------------------- /tests/components/widths-test.html: -------------------------------------------------------------------------------- 1 | 271 | 272 |

    Tree Grid Widths Test

    273 | 274 |
    275 |
    276 | -------------------------------------------------------------------------------- /tests/test-list.json: -------------------------------------------------------------------------------- 1 | [ 2 | "api-select-test.html", 3 | "api-hide-show-test.html", 4 | "chrome-header-test.html", 5 | "click-sort-test.html", 6 | "clicked-on-show.html", 7 | "dnd-test.html", 8 | "dynamic-root-node-test.html", 9 | "fixed-widths-resize-test.html", 10 | "fnvalue-test.html", 11 | "grid-api-test.html", 12 | "odd-named-id-test.html", 13 | "rename_id_test.html", 14 | "rename-dynamic-node-test.html", 15 | "resize-event-test.html", 16 | "search-test.html", 17 | "select-child-test.html", 18 | "select-prevent-test.html", 19 | "render_cell-event-test.html", 20 | "sort-without-resizable-test.html", 21 | "large-dataset-test.html", 22 | "widths-test.html", 23 | "minwidth-maxwidth-test.html" 24 | ] 25 | -------------------------------------------------------------------------------- /tests/test-master.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | jstree treegrid Tests 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 23 | 38 | 39 | 40 |

    Test Runner

    41 |
    42 | This is a single file to run tests. You can pick which tests you want to run, and they will load and show towards the bottom of this page. 43 |
    44 |
    45 |
    46 |
    47 |
    48 | 49 | 50 | -------------------------------------------------------------------------------- /treegrid.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | jstree treegrid 3.2.0 plugin demo 4 | 5 | 6 | 7 | 8 | 9 | 12 | 236 | 237 | 238 |

    Tree Grid 3.2.0 Demo

    239 | This page gives a demo for using the excellent jstree, built on the 240 | amazing jQuery library, with a tree grid. The treegrid is implemented 241 | as a standard jstree plugin. Simply include jquery and jstree, and the plugin library jstreegrid.js, 242 | and include it as a plugin. Look at the source to this page to see how it is done. 243 |

    244 | Some interesting feature usage. Check out the HTML and JS config to see the details: 245 |

      246 |
    • The first tree has resizable and draggable columns set to true, as well as contextmenu enabled.
    • 247 |
    • The second tree has a max-height set to 30px, causing the tree itself - but not the headers - to scroll.
    • 248 |
    • The third tree uses an alternate them and styling.
    • 249 |
    • The fourth tree uses a function for the value.
    • 250 |
    • The fifth tree has 3 columns, but puts the tree in a different column.
    • 251 |
    252 |
    253 |

    First Tree

    254 |
    255 |
    Tree grid is loading.
    256 | 257 | 258 |
    Search
    259 |
    Click a cell to see results:
    260 |
    Right-click a cell and edit to see results:
    261 |
    262 |
    263 |

    Second Tree

    264 |
    265 |
    Tree grid is loading.
    266 |
    267 |
    268 |

    Third Tree

    269 |
    270 |
    Tree grid is loading.
    271 |
    272 |
    273 |

    Fourth Tree

    274 |
    275 |
    Tree grid is loading.
    276 |
    277 |
    278 |

    Fifth Tree

    279 |
    280 |
    Tree grid is loading.
    281 |
    282 | 283 | 284 | 285 | --------------------------------------------------------------------------------