diff --git a/.github/stale.yml b/.github/stale.yml new file mode 100644 index 0000000..8664a9d --- /dev/null +++ b/.github/stale.yml @@ -0,0 +1,42 @@ +# Configuration for probot-stale - https://github.com/probot/stale + +# Number of days of inactivity before an Issue or Pull Request becomes stale +daysUntilStale: 60 + +# Number of days of inactivity before an Issue or Pull Request with the stale label is closed. +# Set to false to disable. If disabled, issues still need to be closed manually, but will remain marked as stale. +daysUntilClose: 7 + +# Issues or Pull Requests with these labels will never be considered stale. Set to `[]` to disable +exemptLabels: + - pinned + - "[Status] Maybe Later" + +# Set to true to ignore issues in a project (defaults to false) +exemptProjects: false + +# Set to true to ignore issues in a milestone (defaults to false) +exemptMilestones: false + +# Set to true to ignore issues with an assignee (defaults to false) +exemptAssignees: false + +# Label to use when marking as stale +staleLabel: wontfix + +# Comment to post when marking as stale. Set to `false` to disable +markComment: > + This issue has been automatically marked as stale because it has not had + recent activity. It will be closed if no further activity occurs. Thank you + for your contributions. + +# Comment to post when removing the stale label. +# unmarkComment: > +# Your comment here. + +# Comment to post when closing a stale Issue or Pull Request. +# closeComment: > +# Your comment here. + +# Limit the number of actions per hour, from 1-30. Default is 30 +limitPerRun: 30 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..793b3b4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +lib/** +oneoff/** +/node_modules \ No newline at end of file diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..fdf7ed5 --- /dev/null +++ b/.npmignore @@ -0,0 +1 @@ +lib/** diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..41a213c --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2015 Avi Deitcher + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 5ab2c9d..a566aa7 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,16 @@ -jsTreeGrid -========== +# jsTreeGrid +[![CDNJS](https://img.shields.io/cdnjs/v/jstreegrid.svg)](https://cdnjs.com/libraries/jstreegrid) -Overview --------- +UNSUPPORTED: You are welcome to find issues, and submit Pull Requests to fix. Beyond that, there really isn't time to support it. + +## Overview Plugin for the jstree www.jstree.com tree component that provides a grid extension to the tree. Allows any number of columns, and can use any property of the node to display data in the grid For significant changes to v3 compared to v1, see the end of this document. -**Note:** treegrid may have issues when using theme autooading as in: +**Note:** treegrid may have issues when using theme autoloading as in: ````JavaScript core: { @@ -19,16 +20,26 @@ core: { In any case, theme autoloading is strongly discouraged in jstree v3. -Usage ------ -1. Include jquery (>= 1.4.2) and jstree in your page, as usual; use jstree v3 or later. -2. Include jstreegrid.js v3 or later -3. Include grid as a plugin -4. Include relevant parameters. +## License and Support +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. + +If you need commercial support, general assistance or have any questions, please contact [info@atomicinc.com](mailto:info@atomicinc.com). + +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). + + + +## Usage + +1. Include jquery (>= 1.4.2) and jstree in your page, as usual +2. Inclue jstree **>= 3.3.0** +3. Include jstreegrid.js v3 or later +4. Include grid as a plugin +5. Include relevant parameters. ````HTML - + ```` @@ -47,8 +58,14 @@ $("div#id").jstree({ }); ```` -Structure ---------- +As of 3.0.0-beta5, jstree-grid supports AMD, thanks https://github.com/jochenberger + +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. + +As of 3.10.2, jstree-grid supports CommonJS, thanks to https://github.com/YarnSeemannsgarn + +### Structure + The grid is built by adding divs `
` to each `
  • ` entry for a row in the tree. Inside the `
    ` is a `` with the data. Thus, an entry is likely to look like @@ -58,34 +75,48 @@ Thus, an entry is likely to look like We use the div to control the entire height and width, and the span to get access to the actual data itself. -Options -------- -The options are as follows: - -* width: default width for a column for which no width is given. If no width is given, the default is 25px -* columns: an array of columns to create, on order. Each entry is an object with the following parameters: - width: width of the column in pixels. - header: string to use as a header for the column. - cellClass: a CSS class to add to each cell in this column (except for the header) - added to the - wideCellClass: a CSS class to add to each cell in this column (except for the header) - added to the
    - headerClass: a CSS class to add to the header cell in this column - added to the
    - 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. - valueClass: the attribute on the node to use as a class on this cell - added to the - valueClassPrefix: a prefix to add to the valueClass to use as a class on this cell - wideValueClass: the attribute on the node to use as a class on this cell - added to the
    - wideValueClassPrefix: a prefix to add to the wideValueClass to use as a class on this cell -* resizable: true/false if the columns should be resizable. Defaults to false. - -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. +### Options + +#### The options are as follows: + +* `width`: width for the entire jstree-grid. If no width is given, automatically fills the entire viewport (`width: 100%;`) +* `height`: height for the entire jstree-grid. If no height is given, height will reflect the amount of content. +* `fixedHeader`: true/false. If true, then when the tree is scrolled the column headers will remain visible. Defaults to true. +* `columnWidth`: default width for a column for which no width is given. If no width is given, the default is `auto`. +* `columns`: an array of columns to create, on order. Each entry is an object with the following parameters: + * `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. + * `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. + * `minWidth`: Minimum width of the column when width is set to `auto`. Does **not** limit manual column resizing. + * `maxWidth`: Maximum width of the column when width is set to `auto`. Does **not** limit manual column resizing. + * `header`: string to use as a header for the column. + * `headerClass`: a CSS class to add to the header cell in this column + * `headerTitle`: a title to add to the header cell in this column, shown as a tooltip + * `columnClass`: a CSS class to add to the header cell and the column cell + * `cellClass`: a CSS class to add to each cell in this column (except for the header) - added to the `` + * `wideCellClass`: a CSS class to add to each cell in this column (except for the header) - added to the `
    ` + * `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. + * `valueClass`: the attribute on the node to use as a class on this cell - added to the `` + * `valueClassPrefix`: a prefix to add to the valueClass to use as a class on this cell + * `wideValueClass`: the attribute on the node to use as a class on this cell - added to the `
    ` + * `wideValueClassPrefix`: a prefix to add to the wideValueClass to use as a class on this cell + * `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)) +* `resizable`: true/false if the columns should be resizable. Defaults to false. +* `draggable`: true/false if the columns should be draggable (requires jQuery UI with sortable plugin). Defaults to false. +* `stateful`: true/false. If true, then whenever a column width is resized, it will store it in html5 localStorage, if available. Defaults to false. +* `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. +* `contextmenu`: true/false whether or not a context menu for editing the cells should be shown on right-click. Defaults to false. +* `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. +* `caseInsensitive`: true/false whether or not the column sort should be case insensitive. Default value is false (the default sort is case sensitive) + +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. Suppose you have a class "important" which backgrounds in red, and a class "clickable" which changes the cursor to a pointer. -You want the entire width of the cell to be red, but just the word "OK" to be clickable. +You want the entire width of the cell to be red, but just the word "OK" to be clickable. You would ensure that "clickable" is applied to the span, but important to the div. - Value is one of: -* the name of the property of the node data whose content will be used; you can choose which once for the entire grid. -* a function, which will be passed the node's data, and is expected to return the value to use. +* the name of the property of the node data whose content will be used; you can choose which once for the entire grid. +* 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` Thus, if you have a node whose data is given by: @@ -111,7 +142,7 @@ Or, in a function: grid: { columns: [ {width: 50, header: "Nodes"}, - {width: 30, header: "Price", value: function(node){return(node.price);}} + {width: 30, header: "Price", value: function(node){return(node.data.price);}} ] } ```` @@ -122,7 +153,7 @@ Using a function allows you to calculate things, or make conditions: grid: { columns: [ {width: 50, header: "Nodes"}, - {width: 30, header: "Price", value: function(node){return("$"+node.price*2);}} + {width: 30, header: "Price", value: function(node){return("$"+(node.data.price*2));}} ] } ```` @@ -211,23 +242,22 @@ grid: { } ```` -Finally, you can change a node contents on the fly using "change_node.jstree". You change the attribute of the node, then trigger the event, +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, for example: ````JavaScript -var node = $("#jstree").jstree(true).get_node("my_node"); +var tree = $("#jstree").jstree(true), node = tree.get_node("my_node"); node.data.value = 25; -node.trigger("change_node.jstree"); +tree.trigger("change_node.jstree",node); ```` -HTML ----- -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 +### HTML + +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 base tree node cell, which follows jstree rules. -Heights -------- -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. +### Heights +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. ````HTML ').appendTo("head"); } - + this.gridWrapper = $("
    ").addClass("jstree-grid-wrapper").insertAfter(container); + this.midWrapper = $("
    ").addClass("jstree-grid-midwrapper").appendTo(this.gridWrapper); + // set the wrapper width + if (s.width) { + this.gridWrapper.width(s.width); + } + if (s.height) { + this.gridWrapper.height(s.height); + } + // create the data columns + 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); + } + this.midWrapper.children("div:eq("+treecol+")").append(container); + container.addClass("jstree-grid-cell"); + + //move header with scroll + if (gs.fixedHeader) { + this.gridWrapper.scroll(function() { + $(this).find('.jstree-grid-header').css('top', $(this).scrollTop()); + }); + } + + // copy original sort function + var defaultSort = $.proxy(this.settings.sort, this); + + // override sort function + this.settings.sort = function (a, b) { + var bigger, colrefs = this.colrefs; + + if (gs.sortOrder==='text') { + var caseInsensitiveSort = this.get_text(a).toLowerCase().localeCompare(this.get_text(b).toLowerCase()); + bigger = gs.caseInsensitive ? (caseInsensitiveSort === 1) : (defaultSort(a, b) === 1); + } else { + // gs.sortOrder just refers to the unique random name for this column + // we need to get the correct value + var nodeA = this.get_node(a), nodeB = this.get_node(b), + value = colrefs[gs.sortOrder].value, + valueA = typeof(value) === 'function' ? value(nodeA) : nodeA.data[value], + valueB = typeof(value) === 'function' ? value(nodeB) : nodeB.data[value]; + if(typeof(valueA) && typeof(valueB) !== 'undefined') { + bigger = gs.caseInsensitive ? valueA.toLowerCase() > valueB.toLowerCase(): valueA > valueB ; + } + } + + if (!gs.sortAsc) + bigger = !bigger; + + return bigger ? 1 : -1; + }; + + // sortable columns when jQuery UI is available + if (gs.draggable) { + if (!$.ui || !$.ui.sortable) { + console.warn('[jstree-grid] draggable option requires jQuery UI'); + } else { + var from, to; + + $(this.midWrapper).sortable({ + axis: "x", + handle: ".jstree-grid-header", + cancel: ".jstree-grid-separator", + start: function (event, ui) { + from = ui.item.index(); + }, + stop: function (event, ui) { + to = ui.item.index(); + gs.columns.splice(to, 0, gs.columns.splice(from, 1)[0]); + } + }); + } + } + +//public function. validate searchObject keys, set columnSearch flag, calls jstree search and reset columnSearch flag + this.searchColumn = function (searchObj) { + var validatedSearchObj = {}; + + if(typeof searchObj == 'object') { + for(var columnIndex in searchObj) { + if(searchObj.hasOwnProperty(columnIndex)) { + // keys should be the index of a column. This means the following: + // only integers and smaller than the number of columns and bigger or equal to 0 + // (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) + if (columnIndex % 1 === 0 && columnIndex < cols.length && columnIndex >= 0) { + validatedSearchObj[columnIndex] = searchObj[columnIndex]; + } + } + } + } + columnSearch = validatedSearchObj; + + if(Object.keys(validatedSearchObj).length !== 0){ + //the search string doesn't matter. we'll use the search string in the columnSearch object! + this.search('someValue'); + } else { // nothing to search so reset jstree's search by passing an empty string + this.search(''); + } + columnSearch = false; + } + + + // set default search for each column with no user defined search function (used when doing a columnSearch) + for (var i = 0, len = cols.length; idiv.jstree-grid-cell-root-'+this.rootid+' {line-height: '+anchorHeight+'px; height: '+anchorHeight+'px;}').appendTo("head"); + + // add container classes to the wrapper - EXCEPT those that are added by jstree, i.e. "jstree" and "jstree-*" + q = cls.split(/\s+/).map(function(i){ + var match = i.match(/^jstree(-|$)/); + return (match ? "" : i); + }); + this.gridWrapper.addClass(q.join(" ")); + + },this)) .on("move_node.jstree",$.proxy(function(e,data){ var node = data.new_instance.element; - renderAWidth(node,this); + //renderAWidth(node,this); // check all the children, because we could drag a tree over node.find("li > a").each($.proxy(function(i,elm){ - renderAWidth($(elm),this); + //renderAWidth($(elm),this); },this)); - + + },this)) + .on("hover_node.jstree",$.proxy(function(node,selected,event){ + var id = selected.node.id; + if (this._hover_node !== null && this._hover_node !== undefined) { + findDataCell(this.uniq,this._hover_node,this._gridSettings.gridcols).removeClass("jstree-hovered"); + } + this._hover_node = id; + findDataCell(this.uniq,id,this._gridSettings.gridcols).addClass("jstree-hovered"); + },this)) + .on("dehover_node.jstree",$.proxy(function(node,selected,event){ + var id = selected.node.id; + this._hover_node = null; + findDataCell(this.uniq,id,this._gridSettings.gridcols).removeClass("jstree-hovered"); },this)) .on("select_node.jstree",$.proxy(function(node,selected,event){ + var id = selected.node.id; + findDataCell(this.uniq,id,this._gridSettings.gridcols).addClass("jstree-clicked"); this.get_node(selected.node.id,true).children("div.jstree-grid-cell").addClass("jstree-clicked"); },this)) .on("deselect_node.jstree",$.proxy(function(node,selected,event){ - this.get_node(selected.node.id,true).children("div.jstree-grid-cell").removeClass("jstree-clicked"); - },this)); + var id = selected.node.id; + findDataCell(this.uniq,id,this._gridSettings.gridcols).removeClass("jstree-clicked"); + },this)) + .on("deselect_all.jstree",$.proxy(function(node,selected,event){ + // get all of the ids that were unselected + var ids = selected.node || [], i; + findDataCell(this.uniq,ids,this._gridSettings.gridcols).removeClass("jstree-clicked"); + },this)) + .on("search.jstree", $.proxy(function (e, data) { + // search sometimes filters, so we need to hide all of the appropriate grid cells as well, and show only the matches + var grid = this.gridWrapper, that = this, nodesToShow, startTime = new Date().getTime(), + ids = getIds(data.nodes.filter(".jstree-node")), endTime; + this.holdingCells = {}; + if (data.nodes.length) { + var id = _guid(); + // save the cells we will hide + var cells = grid.find('div.jstree-grid-cell-regular'); + this._detachColumns(id); + if(this._data.search.som) { + // create the list of nodes we want to look at + if(this._data.search.smc) { + nodesToShow = data.nodes.add(data.nodes.find('.jstree-node')); + } + nodesToShow = (nodesToShow || data.nodes).add(data.nodes.parentsUntil(".jstree")); + + // hide all of the grid cells + cells.hide(); + // show only those that match + nodesToShow.filter(".jstree-node").each(function (i,node) { + var id = node.id; + if (id) { + that._prepare_grid(node); + for (var i = 0, len = that._gridSettings.gridcols.length; i < len; i++) { + if (i === that._gridSettings.treecol) { continue; } + findDataCell(that.uniq, id, that._gridSettings.gridcols[i], $(that._domManipulation.columns[i])).show(); + } + } + }); + } + + for (var i = 0, len = this._gridSettings.gridcols.length; i < len; i++) { + if (i === this._gridSettings.treecol) { continue; } + findDataCell(that.uniq, ids, this._gridSettings.gridcols[i], $(this._domManipulation.columns[i])).addClass(SEARCHCLASS); + } + this._reattachColumns(id); + endTime = new Date().getTime(); + this.element.trigger("search-complete.jstree-grid", [{time:endTime-startTime}]); + } + return true; + }, this)) + .on("clear_search.jstree", $.proxy(function (e, data) { + // search has been cleared, so we need to show all rows + var grid = this.gridWrapper, ids = getIds(data.nodes.filter(".jstree-node")); + grid.find('div.jstree-grid-cell').show(); + findDataCell(this.uniq,ids,this._gridSettings.gridcols).removeClass(SEARCHCLASS); + return true; + }, this)) + .on("copy_node.jstree", function (e, data) { + var newtree = data.new_instance, oldtree = data.old_instance, obj = newtree.get_node(data.node,true); + copyData(oldtree, data.original, newtree, data.node, true); + newtree._detachColumns(obj.id); + newtree._prepare_grid(obj); + newtree._reattachColumns(obj.id); + return true; + }) + .on("show_ellipsis.jstree", $.proxy(function (e, data) { + this.gridWrapper.find(".jstree-grid-cell").add(".jstree-grid-header", this.gridWrapper).addClass("jstree-grid-ellipsis"); + return true; + }, this)) + .on("hide_ellipsis.jstree", $.proxy(function (e, data) { + this.gridWrapper.find(".jstree-grid-cell").add(".jstree-grid-header", this.gridWrapper).removeClass("jstree-grid-ellipsis"); + return true; + }, this)) + .on("enable_node.jstree", $.proxy(function (e, data) { + var id = data.node.id; + findDataCell(this.uniq,id,this._gridSettings.gridcols).removeClass("jstree-disabled"); + this.get_node(data.node.id,true).children("div.jstree-grid-cell").removeClass("jstree-disabled"); + }, this)) + .on("disable_node.jstree", $.proxy(function (e, data) { + var id = data.node.id; + findDataCell(this.uniq,id,this._gridSettings.gridcols).addClass("jstree-disabled"); + this.get_node(data.node.id,true).children("div.jstree-grid-cell").addClass("jstree-disabled"); + }, this)) + ; if (this._gridSettings.isThemeroller) { this.element .on("select_node.jstree",$.proxy(function(e,data){ - data.rslt.obj.children("a").nextAll("div").addClass("ui-state-active"); + data.rslt.obj.children("[class~='jstree-anchor']").nextAll("div").addClass("ui-state-active"); },this)) .on("deselect_node.jstree deselect_all.jstree",$.proxy(function(e,data){ - data.rslt.obj.children("a").nextAll("div").removeClass("ui-state-active"); + data.rslt.obj.children("[class~='jstree-anchor']").nextAll("div").removeClass("ui-state-active"); },this)) .on("hover_node.jstree",$.proxy(function(e,data){ - data.rslt.obj.children("a").nextAll("div").addClass("ui-state-hover"); + data.rslt.obj.children("[class~='jstree-anchor']").nextAll("div").addClass("ui-state-hover"); },this)) .on("dehover_node.jstree",$.proxy(function(e,data){ - data.rslt.obj.children("a").nextAll("div").removeClass("ui-state-hover"); + data.rslt.obj.children("[class~='jstree-anchor']").nextAll("div").removeClass("ui-state-hover"); + },this)); + } + + if (this._gridSettings.stateful) { + this.element + .on("resize_column.jstree-grid",$.proxy(function(e,col,width){ + localStorage['jstree-root-'+this.rootid+'-column-'+col] = width; },this)); } }; + + // tear down the tree entirely this.teardown = function() { - var gridparent = this.parent, container = this.element; + var gw = this.gridWrapper, container = this.element, gridparent = gw.parent(); container.detach(); - $("div.jstree-grid-wrapper",gridparent).remove(); + gw.remove(); gridparent.append(container); parent.teardown.call(this); }; + // clean the grid in case of redraw or refresh entire tree + this._clean_grid = function (target,id) { + var grid = this.gridWrapper; + if (target) { + findDataCell(this.uniq,id,this._gridSettings.gridcols).remove(); + } else { + // get all of the `div` children in all of the `td` in dataRow except for :first (that is the tree itself) and remove + grid.find("div.jstree-grid-cell-regular").remove(); + } + }; + // prepare the headers this._prepare_headers = function() { - var header, i, gs = this._gridSettings,cols = gs.columns || [], width, defaultWidth = gs.columnWidth, resizable = gs.resizable || false, - cl, val, margin, last, tr = gs.isThemeroller, classAdd = (tr?"themeroller":"regular"), - container = this.element, gridparent = container.parent(), hasHeaders = 0, - conf = gs.defaultConf, isClickedSep = false, oldMouseX = 0, newMouseX = 0, currentTree = null, colNum = 0, toResize = null, clickedSep = null, borPadWidth = 0; + var header, i, col, _this = this, gs = this._gridSettings,cols = gs.columns || [], width, defaultWidth = gs.columnWidth, resizable = gs.resizable || false, + cl, ccl, val, name, last, tr = gs.isThemeroller, classAdd = (tr?"themeroller":"regular"), puller, + hasHeaders = false, gridparent = this.gridparent, rootid = this.rootid, + conf = gs.defaultConf, coluuid, + borPadWidth = 0, totalWidth = 0; + // save the original parent so we can reparent on destroy this.parent = gridparent; - - - // set up the wrapper, if not already done - header = this.header || $("
    ").addClass((tr?"ui-widget-header ":"")+"jstree-grid-header jstree-grid-header-"+classAdd); - + + // save the references to columns by unique ID + this.colrefs = {}; + + // create the headers - for (i=0;i"); + //col.appendTo(colgroup); cl = cols[i].headerClass || ""; + ccl = cols[i].columnClass || ""; val = cols[i].header || ""; + headerTitle = cols[i].headerTitle || ""; + do { + coluuid = String(Math.floor(Math.random()*10000)); + } while(this.colrefs[coluuid] !== undefined); + // create a unique name for this column + name = cols[i].value ? coluuid : "text"; + this.colrefs[name] = cols[i]; + if (val) {hasHeaders = true;} - width = cols[i].width || defaultWidth; + if(gs.stateful && localStorage['jstree-root-'+rootid+'-column-'+i]) + width = localStorage['jstree-root-'+rootid+'-column-'+i]; + else + width = cols[i].width || defaultWidth; + + var minWidth = cols[i].minWidth || width; + var maxWidth = cols[i].maxWidth || width; + + // we only deal with borders if width is not auto and not percentages borPadWidth = tr ? 1+6 : 2+8; // account for the borders and padding - width -= borPadWidth; - margin = i === 0 ? 3 : 0; - last = $("
    ").css(conf).css({"margin-left": margin,"width":width, "padding": "1 3 2 5"}).addClass((tr?"ui-widget-header ":"")+"jstree-grid-header jstree-grid-header-cell jstree-grid-header-"+classAdd+" "+cl).text(val).appendTo(header) - .after("
     
    "); - } - if (last) { + if (width !== 'auto' && typeof(width) !== "string") { + width -= borPadWidth; + } + col = this.midWrapper.children("div.jstree-grid-column-"+i); + 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); last.addClass((tr?"ui-widget-header ":"")+"jstree-grid-header jstree-grid-header-"+classAdd); + if (this.settings.core.themes.ellipsis === true){ + last.addClass('jstree-grid-ellipsis'); + } + + // add title and strip out HTML + var title = gs.headerAsTitle && !headerTitle ? val : (headerTitle ? headerTitle : ""); + title = title.replace(htmlstripre, ''); + if (title) { + last.attr("title",title); + } + + last.prependTo(col); + last.attr(COL_DATA_ATTR, name); + totalWidth += last.outerWidth(); + puller = $("
     
    ").appendTo(last); + col.width(width); + col.css("min-width", minWidth); + col.css("max-width", maxWidth); + } + + last.addClass((tr?"ui-widget-header ":"")+"jstree-grid-header jstree-grid-header-last jstree-grid-header-"+classAdd); + // if there is no width given for the last column, do it via automatic + if (cols[cols.length-1].width === undefined) { + totalWidth -= width; + col.css({width:"auto"}); + last.addClass("jstree-grid-width-auto").next(".jstree-grid-separator").remove(); } - // add a clearer - $("
    ").css("clear","both").appendTo(header); - // did we have any real columns? if (hasHeaders) { - $("
    ").addClass("jstree-grid-wrapper").appendTo(gridparent).append(header).append(container); // save the offset of the div from the body - gs.divOffset = header.parent().offset().left; + //gs.divOffset = header.parent().offset().left; gs.header = header; + } else { + $("div.jstree-grid-header").hide(); } - if (!bound && resizable) { - bound = true; - $(document).on("selectstart", ".jstree-grid-separator", function () { return false; }); - $(document).on("mousedown", ".jstree-grid-separator", function (e) { - clickedSep = $(this); - isClickedSep = true; - currentTree = clickedSep.parents(".jstree-grid-wrapper").children(".jstree"); - oldMouseX = e.clientX; - colNum = clickedSep.prevAll(".jstree-grid-header").length-1; - toResize = clickedSep.prev().add(currentTree.find(".jstree-grid-col-"+colNum)); - return false; - }); - $(document) - .mouseup(function () { - var i, ref, cols, widths, headers, w; + if (!this.bound && resizable) { + this.bound = true; + $(document).mouseup(function () { + var ref, cols, width, headers, currentTree, colNum; + if (isClickedSep) { + colNum = toResize.prevAll(".jstree-grid-column").length; + currentTree = toResize.closest(".jstree-grid-wrapper").find(".jstree"); + ref = $.jstree.reference(currentTree); + cols = ref.settings.grid.columns; + headers = toResize.parent().children("div.jstree-grid-column"); + 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; } + width = ref._gridSettings.columns[colNum].width = parseFloat(toResize.css("width")); + isClickedSep = false; + toResize = null; + + currentTree.trigger("resize_column.jstree-grid", [colNum,width]); + } + }).mousemove(function (e) { if (isClickedSep) { - ref = $.jstree.reference(currentTree); - cols = ref.settings.grid.columns; - headers = clickedSep.parent().children(".jstree-grid-header"); - widths = []; - if (isNaN(colNum) || colNum < 0) { ref._gridSettings.treeWidthDiff = currentTree.find("ins:eq(0)").width() + currentTree.find("a:eq(0)").width() - ref._gridSettings.columns[0].width; } - isClickedSep = false; - for (i=0;i 0 || oldPrevHeaderInner > 0) && newPrevColWidth > MINCOLWIDTH) { + toResize.width(newPrevColWidth+"px"); + toResize.css("min-width",newPrevColWidth+"px"); + toResize.css("max-width",newPrevColWidth+"px"); + oldMouseX = newMouseX; + } } - - currentTree.trigger("resize_column.jstree-grid", widths); } - }) - .mousemove(function (e) { - if (isClickedSep) { - newMouseX = e.clientX; - var diff = newMouseX - oldMouseX; - toResize.each(function () { this.style.width = parseFloat(this.style.width) + diff + "px"; }); - oldMouseX = newMouseX; + }); + this.gridWrapper.on("selectstart", ".jstree-grid-resizable-separator", function () { + return false; + }).on("mousedown", ".jstree-grid-resizable-separator", function (e) { + isClickedSep = true; + oldMouseX = e.pageX; + toResize = $(this).closest("div.jstree-grid-column"); + // the max rightmost position we will allow is the right-most of the wrapper minus a buffer (10) + return false; + }) + .on("dblclick", ".jstree-grid-resizable-separator", function (e) { + var clickedSep = $(this), col = clickedSep.closest("div.jstree-grid-column"), + oldPrevColWidth = parseFloat(col.css("width")), newWidth = 0, diff, + colNum = col.prevAll(".jstree-grid-column").length, + oldPrevHeaderInner = col.width(), newPrevColWidth; + + + //find largest width + col.find(".jstree-grid-cell").each(function() { + var item = $(this), width; + item.css("position", "absolute"); + item.css("width", "auto"); + width = item.outerWidth(); + item.css("position", "relative"); + + if (width>newWidth) { + newWidth = width; } }); + + diff = newWidth-oldPrevColWidth; + + // make sure that diff cannot be beyond the left limits + diff = diff < 0 ? Math.max(diff,-oldPrevHeaderInner) : diff; + newPrevColWidth = (oldPrevColWidth+diff)+"px"; + + col.width(newPrevColWidth); + col.css("min-width",newPrevColWidth); + col.css("max-width",newPrevColWidth); + + $(this).closest(".jstree-grid-wrapper").find(".jstree").trigger("resize_column.jstree-grid",[colNum,newPrevColWidth]); + }) + .on("click", ".jstree-grid-separator", function (e) { + // don't sort after resize + e.stopPropagation(); + }); } - }; + this.gridWrapper.on("click", ".jstree-grid-header-cell", function (e) { + if (!_this.sort) { + return; + } + + // get column + var name = $(this).attr(COL_DATA_ATTR); + + // sort order + var symbol; + if (gs.sortOrder === name && gs.sortAsc === true) { + gs.sortAsc = false; + symbol = "↓"; + } else { + gs.sortOrder = name; + gs.sortAsc = true; + symbol = "↑"; + } + + // add sort arrow + $(this.closest('.jstree-grid-wrapper')).find(".jstree-grid-sort-icon").remove(); + $("").addClass("jstree-grid-sort-icon").appendTo($(this)).html(symbol); + + // sort by column + var rootNode = _this.get_node('#'); + _this.sort(rootNode, true); + _this.redraw_node(rootNode, true); + }); + + }; + + 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 }} + + function _guid() { + function s4() { + return Math.floor((1 + Math.random()) * 0x10000) + .toString(16) + .substring(1); + } + return s4() + s4() + '-' + s4() + '-' + s4() + '-' + + s4() + '-' + s4() + s4() + s4(); + } + /* + * Trys to detach the tree columns on massive dom manipulations + */ + this._detachColumns = function (id) { + // if the columns are not detached, then detach them + if (this._domManipulation == null) { + var cols = this._gridSettings.columns || [], treecol = this._gridSettings.treecol, mw = this.midWrapper; + this._domManipulation = { id: id, oldActiveElement: document.activeElement, columns: {} }; + for (var i = 0, len = cols.length; i < len; i++) { + //if (treecol === i) { + // continue; + //} + this._domManipulation.columns[i] = mw.children(".jstree-grid-column-" + i)[0]; + this._domManipulation.columns[i].parentNode.removeChild(this._domManipulation.columns[i]); + } + } + return this._domManipulation; + } + + this._reattachColumns = function (id) { + if (this._domManipulation == null) { return false; } + if (this._domManipulation.id === id) { + var cols = this._gridSettings.columns || [], treecol = this._gridSettings.treecol, mw = this.midWrapper; + for (var i = 0, len = cols.length; i < len; i++) { + //if (treecol === i) { + // continue; + //} + mw[0].appendChild(this._domManipulation.columns[i]); + } + if (this._domManipulation.oldActiveElement !== document.activeElement) { $(this._domManipulation.oldActiveElement).focus(); } + this._domManipulation = null; + } + return true; + } + + /* + * Override open_node to detach the columns before redrawing child-nodes, and do reattach them afterwarts + */ + this.open_node = function (obj, callback, animation) { + var isArray = $.isArray(obj); + var node = null; + if (!isArray) { + node = this.get_node(obj); + if (node.id === "#") { return; } // wtf??? we ar in the root and do not need a open! + } + var id = isArray ? _guid() : node.id; + this._detachColumns(id); + var ret = parent.open_node.call(this, obj, callback, animation); + this._reattachColumns(id); + return ret; + } + /* * Override redraw_node to correctly insert the grid */ - this.redraw_node = function(obj, deep, is_callback) { + this.redraw_node = function (obj, deep, is_callback, force_render) { + var id = $.isArray(obj) ? _guid() : this.get_node(obj).id; + // we detach the columns once + this._detachColumns(id); // first allow the parent to redraw the node - obj = parent.redraw_node.call(this, obj, deep, is_callback); - // next prepare the grid + obj = parent.redraw_node.call(this, obj, deep, is_callback, force_render); + // next prepare the grid for a redrawn node - but only if ths node is not hidden (search does that) + if (obj) { + this._prepare_grid(obj); + } + // don't forget to reattach + this._reattachColumns(id); + return obj; + }; + this.refresh = function () { + this._clean_grid(); + return parent.refresh.apply(this,arguments); + }; + /* + * Override set_id to update cell attributes + */ + this.set_id = function (obj, id) { + var old, uniq = this.uniq; if(obj) { - this._prepare_grid(obj); + old = obj.id; } - return obj; + var result = parent.set_id.apply(this,arguments); + if(result) { + if (old !== undefined) { + var grid = this.gridWrapper, oldNodes = [old], i; + // get children + if (obj && obj.children_d) { + oldNodes = oldNodes.concat(obj.children_d); + } + // update id in children + findDataCell(uniq,oldNodes,this._gridSettings.gridcols) + .attr(NODE_DATA_ATTR, obj.id) + .removeClass(generateCellId(uniq,old)) + .addClass(generateCellId(uniq,obj.id)) + .each(function(i,node) { + $(node).attr('id', generateCellId(uniq,obj.id)+(i+1)); + }); + } + } + return result; + }; + + this._hideOrShowTree = function(node, hide) { + //Hides or shows a tree + this._detachColumns(node.id); + // show cells in each detachted column + this._hideOrShowNode(node, hide, this._gridSettings.columns || [], this._gridSettings.treecol); + this._reattachColumns(node.id); + } + this._hideOrShowNode = function(node, hide, cols, treecol) { + //Hides or shows a node with recursive calls to all open child-nodes + for (var i = 0, len = cols.length; i < len; i++) { + if (i === treecol) { continue; } + var cells = findDataCell(this.uniq, node.id, i, $(this._domManipulation.columns[i])); + if (hide) { + cells.addClass("jstree-grid-hidden"); + } else { + cells.removeClass("jstree-grid-hidden"); + } + } + if (node.state.opened && node.children) { + for (var i = 0, len = node.children.length; i < len; i++) { + this._hideOrShowNode(this.get_node(node.children[i]), hide, cols, treecol); + } + } + } + this._hide_grid = function (node) { + if (!node) { return true; } + this._detachColumns(node.id); + var children = node.children ? node.children : [], cols = this._gridSettings.columns || [], treecol = this._gridSettings.treecol; + // try to remove all children + for (var i = 0, len = children.length; i < len; i++) { + var child = this.get_node(children[i]); + // go through each column, remove all children with the correct ID name + for (var j = 0, lenj = cols.length; j < lenj; j++) { + if (j === treecol) { continue; } + findDataCell(this.uniq, child.id, j, $(this._domManipulation.columns[j])).remove(); + } + if (child.state.opened) { this._hide_grid(child);} + } + + + this._reattachColumns(node.id); }; + this.holdingCells = {}; + this.getHoldingCells = function (obj, col, hc) { + if (obj.state.hidden || !obj.state.opened) { return $(); } + var ret = $(), children = obj.children || [], child, i, uniq = this.uniq; + // run through each child, render it, and then render its children recursively + for (i = 0; i < children.length; i++) { + child = generateCellId(uniq, children[i]) + col; + if (hc[child]) { + ret = ret.add(hc[child]).add(this.getHoldingCells(this.get_node(children[i]), col, hc)); + //delete hc[child]; + } + } + return (ret); + }; + + /** + * put a grid cell in edit mode (input field to edit the data) + * @name edit(obj, col) + * @param {mixed} obj + * @param {obj} col definition + * @param {element} cell element, either span or wrapping div + */ + this._edit = function (obj, col, element) { + if(!obj) { return false; } + if (!obj.data) {obj.data = {};} + if (element) { + element = $(element); + if (element.prop("tagName").toLowerCase() === "div") { + element = element.children("span:first"); + } + } else { + // need to find the element - later + return false; + } + var rtl = this._data.core.rtl, + w = this.element.width(), + t = obj.data[col.value], + h1 = $("<"+"div />", { css : { "position" : "absolute", "top" : "-200px", "left" : (rtl ? "0px" : "-1000px"), "visibility" : "hidden" } }).appendTo("body"), + h2 = $("<"+"input />", { + "value" : t, + "class" : "jstree-rename-input", + "css" : { + "padding" : "0", + "border" : "1px solid silver", + "box-sizing" : "border-box", + "display" : "inline-block", + "height" : (this._data.core.li_height) + "px", + "lineHeight" : (this._data.core.li_height) + "px", + "width" : "150px" // will be set a bit further down + }, + "blur" : $.proxy(function () { + var v = h2.val(); + // save the value if changed + if(v === "" || v === t) { + v = t; + } else { + obj.data[col.value] = v; + this.element.trigger('update_cell.jstree-grid', { node: obj, col: col.value, value: v, old: t }); + var id = _guid(); + this._detachColumns(id); + this._prepare_grid(this.get_node(obj, true)); + this._reattachColumns(id); + } + h2.remove(); + element.show(); + }, this), + "keydown" : function (event) { + var key = event.which; + if(key === 27) { + this.value = t; + } + if(key === 27 || key === 13 || key === 37 || key === 38 || key === 39 || key === 40 || key === 32) { + event.stopImmediatePropagation(); + } + if(key === 27 || key === 13) { + event.preventDefault(); + this.blur(); + } + }, + "click" : function (e) { e.stopImmediatePropagation(); }, + "mousedown" : function (e) { e.stopImmediatePropagation(); }, + "keyup" : function (event) { + h2.width(Math.min(h1.text("pW" + this.value).width(),w)); + }, + "keypress" : function(event) { + if(event.which === 13) { return false; } + } + }), + fn = { + fontFamily : element.css('fontFamily') || '', + fontSize : element.css('fontSize') || '', + fontWeight : element.css('fontWeight') || '', + fontStyle : element.css('fontStyle') || '', + fontStretch : element.css('fontStretch') || '', + fontVariant : element.css('fontVariant') || '', + letterSpacing : element.css('letterSpacing') || '', + wordSpacing : element.css('wordSpacing') || '' + }; + element.hide(); + element.parent().append(h2); + h2.css(fn).width(Math.min(h1.text("pW" + h2[0].value).width(),w))[0].select(); + }; + + this.grid_hide_column = function (col) { + this.midWrapper.find(".jstree-grid-column-"+col).hide(); + }; + this.grid_show_column = function (col) { + this.midWrapper.find(".jstree-grid-column-"+col).show(); + }; + this._prepare_grid = function (obj) { - var gs = this._gridSettings, c = gs.treeClass, _this = this, t, cols = gs.columns || [], width, tr = gs.isThemeroller, + var gs = this._gridSettings, c = gs.treeClass, _this = this, t, + cols = gs.columns || [], width, tr = gs.isThemeroller, uniq = this.uniq, + treecol = gs.treecol, + tree = this.element, rootid = this.rootid, classAdd = (tr?"themeroller":"regular"), img, objData = this.get_node(obj), - defaultWidth = gs.columnWidth, conf = gs.defaultConf, cellClickHandler = function (val,col,s) { - return function() { - $(this).trigger("select_cell.jstree-grid", [{value: val,column: col.header,node: $(this).closest("li"),sourceName: col.value,sourceType: s}]); + defaultWidth = gs.columnWidth, conf = gs.defaultConf, cellClickHandler = function (tree,node,val,col,t) { + return function(e) { + //node = tree.find("#"+node.attr("id")); + var event = jQuery.Event("select_cell.jstree-grid"); + tree.trigger(event, [{value: val,column: col.header,node: node,grid:$(this),sourceName: col.value}]); + if (!event.isDefaultPrevented()) { + node.children(".jstree-anchor").trigger("click.jstree",e); + } + }; + }, cellRightClickHandler = function (tree,node,val,col,t) { + return function (e) { + if (gs.gridcontextmenu) { + e.preventDefault(); + $.vakata.context.show(this,{ 'x' : e.pageX, 'y' : e.pageY }, gs.gridcontextmenu(_this,tree,node,val,col,t,e.target)); + } }; - }; + }, + hoverInHandler = function (node, jsTreeInstance) { + return function() { jsTreeInstance.hover_node(node); }; + }, + hoverOutHandler = function (node, jsTreeInstance) { + return function() { jsTreeInstance.dehover_node(node); }; + }, + i, val, cl, wcl, ccl, a, last, valClass, wideValClass, span, paddingleft, title, gridCellName, gridCellParentId, gridCellParent, + gridCellPrev, gridCellPrevId, gridCellNext, gridCellNextId, gridCellChild, gridCellChildId, + col, content, tmpWidth, mw = this.midWrapper, column, lid = objData.id, + highlightSearch, isClicked, + peers = this.get_node(objData.parent).children, + // find my position in the list of peers. "peers" is the list of everyone at my level under my parent, in order + pos = $.inArray(lid,peers), + hc = this.holdingCells, rendered = false, closed; // get our column definition t = $(obj); - var i, val, cl, wcl, a, last, valClass, wideValClass, span, paddingleft, title, isAlreadyGrid, col, content, s, tmpWidth; - + // find the a children - a = t.children("a"); - isAlreadyGrid = a.hasClass(c); - + a = t.children("[class~='jstree-anchor']"); + highlightSearch = a.hasClass(SEARCHCLASS); + isClicked = a.hasClass("jstree-clicked"); + if (a.length === 1) { - a.prev().css("float","left"); + closed = !objData.state.opened; + gridCellName = generateCellId(uniq,lid); + gridCellParentId = objData.parent === "#" ? null : objData.parent; a.addClass(c); - renderAWidth(a,_this); + //renderAWidth(a,_this); renderATitle(a,t,_this); - last = a; - for (i=1;i= peers.length - 1 ? "NULL" : peers[pos + 1]; + gridCellChildId = objData.children && objData.children.length > 0 ? objData.children[0] : "NULL"; + + // find which column our tree shuld go in + var s = this.settings.grid; + for (var i = 0, len = cols.length;i
    ' : '';} } else { content = val; } + // content cannot be blank, or it messes up heights + if (content === undefined || content === null || BLANKRE.test(content)) { + content = " "; + } + // get the valueClass - valClass = col.valueClass && t.attr(col.valueClass) ? t.attr(col.valueClass) : ""; + valClass = col.valueClass && objData.data !== null && objData.data !== undefined ? objData.data[col.valueClass] || "" : ""; if (valClass && col.valueClassPrefix && col.valueClassPrefix !== "") { valClass = col.valueClassPrefix + valClass; } // get the wideValueClass - wideValClass = col.wideValueClass && t.attr(col.wideValueClass) ? t.attr(col.wideValueClass) : ""; + wideValClass = col.wideValueClass && objData.data !== null && objData.data !== undefined ? objData.data[col.wideValueClass] || "" : ""; if (wideValClass && col.wideValueClassPrefix && col.wideValueClassPrefix !== "") { wideValClass = col.wideValueClassPrefix + wideValClass; } // get the title - title = col.title && t.attr(col.title) ? t.attr(col.title) : ""; + title = col.title && objData.data !== null && objData.data !== undefined ? objData.data[col.title] || "" : ""; // strip out HTML title = title.replace(htmlstripre, ''); - + // get the width paddingleft = 7; width = col.width || defaultWidth; - //tmpWidth = $.support.boxModel ? $(".jstree-grid-col-"+i+":first",t).width() : $(".jstree-grid-col-"+i+":first",t).outerWidth(); - width = tmpWidth || (width - paddingleft); - - last = isAlreadyGrid ? a.nextAll("div:eq("+(i-1)+")") : $("
    ").insertAfter(last); - span = isAlreadyGrid ? last.children("span") : $("").appendTo(last); + if (width !== 'auto') { + width = tmpWidth || (width - paddingleft); + } + + last = findDataCell(uniq, lid, i, column); + if (!last || last.length < 1) { + last = $("
    "); + $("").appendTo(last); + last.attr("id",gridCellName+i); + last.addClass(gridCellName); + last.attr(NODE_DATA_ATTR,lid); + if (highlightSearch) { + last.addClass(SEARCHCLASS); + } else { + last.removeClass(SEARCHCLASS); + } + if (isClicked) { + last.addClass("jstree-clicked"); + } else { + last.removeClass("jstree-clicked"); + } + if (this.settings.core.themes.ellipsis === true && i !== treecol) { + last.addClass('jstree-grid-ellipsis'); + } + + } + + // we need to check the hidden-state and see if we need to hide the node + if (objData.state.hidden) { + last.addClass("jstree-grid-hidden"); + } else { + last.removeClass("jstree-grid-hidden"); + } + + // ditto for the disabled-state and disabling the node + if (objData.state.disabled) { + last.addClass('jstree-disabled'); + } else { + last.removeClass('jstree-disabled'); + } + // we need to put it in the dataCell - after the parent, but the position matters + // if we have no parent, then we are one of the root nodes, but still need to look at peers + + + // if we are first, i.e. pos === 0, we go right after the parent; + // if we are not first, and our previous peer (one before us) is closed, we go right after the previous peer cell + // if we are not first, and our previous peer is opened, then we have to find its youngest & lowest closed child (incl. leaf) + // + // probably be much easier to go *before* our next one + // but that one might not be drawn yet + // here is the logic for jstree drawing: + // it draws peers from first to last or from last to first + // it draws children before a parent + // + // so I can rely on my *parent* not being drawn, but I cannot rely on my previous peer or my next peer being drawn + + // so we do the following: + // 1- We are the first child: install after the parent + // 2- Our previous peer is already drawn: install after the previous peer + // 3- Our previous peer is not drawn, we have a child that is drawn: install right before our first child + // 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 + // 5- Our previous peer is not drawn, we have no child that is drawn, our next peer is not drawn: install right after parent + gridCellPrev = findDataCell(uniq, gridCellPrevId, i, column); + gridCellNext = findDataCell(uniq, gridCellNextId, i, column); + gridCellChild = findDataCell(uniq, gridCellChildId, i, column); + gridCellParent = findDataCell(uniq, gridCellParentId, i, column); + + + // if our parent is already drawn, then we put this in the right order under our parent + if (gridCellParentId) { + if (gridCellParent && gridCellParent.length > 0) { + if (gridCellPrev && gridCellPrev.length > 0) { + last.insertAfter(gridCellPrev); + } else if (gridCellChild && gridCellChild.length > 0) { + last.insertBefore(gridCellChild); + } else if (gridCellNext && gridCellNext.length > 0) { + last.insertBefore(gridCellNext); + } else { + last.insertAfter(gridCellParent); + } + rendered = true; + } else { + rendered = false; + } + // always put it in the holding cells, and then sort when the parent comes in, in case parent is (re)drawn later + hc[gridCellName+i] = last; + } else { + if (gridCellPrev && gridCellPrev.length > 0) { + last.insertAfter(gridCellPrev); + } else if (gridCellChild && gridCellChild.length > 0) { + last.insertBefore(gridCellChild); + } else if (gridCellNext && gridCellNext.length > 0) { + last.insertBefore(gridCellNext); + } else { + last.appendTo(column); + } + rendered = true; + } + // do we have any children waiting for this cell? walk down through the children/grandchildren/etc tree + if (rendered) { + var toRen = this.getHoldingCells(objData,i,hc); + last.after(toRen); + } + // need to make the height of this match the line height of the tree. How? + span = last.children("span"); // create a span inside the div, so we can control what happens in the whole div versus inside just the text/background - span.addClass(cl+" "+valClass).css({"margin-right":"0px","display":"inline-block","*display":"inline","*+display":"inline"}).html(content) + span.addClass(cl+" "+valClass).html(content); + 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"); // add click handler for clicking inside a grid cell - .click(cellClickHandler(val,col,s)); - last = last.css(conf).css({width: width,"padding-left":paddingleft+"px"}).addClass("jstree-grid-cell jstree-grid-cell-"+classAdd+" "+wcl+ " " + wideValClass + (tr?" ui-state-default":"")).addClass("jstree-grid-col-"+i); - + last.click(cellClickHandler(tree,t,val,col,this)); + last.on("contextmenu",cellRightClickHandler(tree,t,val,col,this)); + last.hover(hoverInHandler(t, this), hoverOutHandler(t, this)); + if (title) { span.attr("title",title); } - } + tree.trigger("render_cell.jstree-grid", [{value: val, column: col.header, node: t, sourceName: col.value}]); + } last.addClass("jstree-grid-cell-last"+(tr?" ui-state-default":"")); - $("
    ").css("clear","both").insertAfter(last); + // if there is no width given for the last column, do it via automatic + if (cols[cols.length-1].width === undefined) { + last.addClass("jstree-grid-width-auto").next(".jstree-grid-separator").remove(); + } } - this.element.css({'overflow-y':'auto !important'}); + this.element.css({'overflow-y':'auto !important'}); }; + // clean up holding cells + this.holdingCells = {}; // need to do alternating background colors or borders }; -}(jQuery)); +})); + diff --git a/package.json b/package.json new file mode 100644 index 0000000..9bf9776 --- /dev/null +++ b/package.json @@ -0,0 +1,67 @@ +{ + "name": "jstreegrid", + "description": "grid plugin for jstree", + "version": "3.10.2", + "url": "https://github.com/deitch/jstree-grid", + "author": { + "name": "Avi Deitcher", + "url": "https://github.com/deitch" + }, + "main": "jstreegrid.js", + "license": "MIT", + "contributors": [ + { + "name": "jochenberger", + "url": "https://github.com/jochenberger" + }, + { + "name": "Tobias Zanke", + "url": "https://github.com/TZanke" + }, + { + "name": "ChrisRaven", + "url": "https://github.com/ChrisRaven" + }, + { + "name": "Brabus", + "url": "https://github.com/side-by-side" + }, + { + "name": "Adam Jimenez", + "url": "https://github.com/adamjimenez" + }, + { + "name": "QueroBartk", + "url": "https://github.com/QueroBartK" + }, + { + "name": "OgreTransporter", + "url": "https://github.com/OgreTransporter" + }, + { + "name": "LlorX", + "url": "https://github.com/Llorx" + }, + { + "name": "PetzeltA", + "url": "https://github.com/petzelta/" + }, + { + "name": "yarn", + "url": "https://github.com/YarnSeemannsgarn" + } + ], + "dependencies": { + "jstree": ">=3.0.0" + }, + "keywords": [ + "jquery", + "jstree", + "grid", + "javascript" + ], + "repository": { + "type": "git", + "url": "http://github.com/deitch/jstree-grid.git" + } +} diff --git a/publish b/publish new file mode 100755 index 0000000..8e645cf --- /dev/null +++ b/publish @@ -0,0 +1,13 @@ +#!/bin/bash + +###### +# Simple script to run npm publish +###### + +# what version number are we in git tag and in package.json +NPMVERSION=$(awk '/\Wversion\W/ {print $2}' package.json | sed 's/[\"\,]//g') +GITTAG=$(git describe --abbrev=0) + +# we *should* be checking if this last one changed, and then publish, but later +npm publish + diff --git a/tests/README.md b/tests/README.md new file mode 100644 index 0000000..8217727 --- /dev/null +++ b/tests/README.md @@ -0,0 +1,17 @@ +# jstreegrid tests + +This directory contains the tests for jstreegrid. In order to avoid a lot of duplication, +the tests have been created in a modular fashion. There is a single master file, +`test-master.html`, which provides an interface to loading and running any tests. + +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 `
    ` + +To add a new test: + +1. Create the test partial `.html` in `components/`, e.g. `my-unique-test.html` +2. Add the correct partial html and JS scripts to the `my-unique-test.html` +3. Add the test file name to the `test-list.json` array + +**About ES6/CJS:** Yes, we definitely could use ES6 modules or CommonJS modules (like node), +but for the purpose of clean tests, we stick with native JS understood by the browser, +i.e. no Babel/Traceur transpiling. diff --git a/tests/components/api-hide-show-test.html b/tests/components/api-hide-show-test.html new file mode 100644 index 0000000..a767d6c --- /dev/null +++ b/tests/components/api-hide-show-test.html @@ -0,0 +1,123 @@ + +

    API Hide/Show Test

    + +
    + + + diff --git a/tests/components/api-select-test.html b/tests/components/api-select-test.html new file mode 100644 index 0000000..c7e869a --- /dev/null +++ b/tests/components/api-select-test.html @@ -0,0 +1,65 @@ + +

    API Select Test

    + +
    diff --git a/tests/components/chrome-header-test.html b/tests/components/chrome-header-test.html new file mode 100644 index 0000000..1e94936 --- /dev/null +++ b/tests/components/chrome-header-test.html @@ -0,0 +1,82 @@ + +

    Column Resize Chrome Test

    +

    + 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. +

    +

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

    +
    +
    diff --git a/tests/components/click-sort-test.html b/tests/components/click-sort-test.html new file mode 100644 index 0000000..c033c3d --- /dev/null +++ b/tests/components/click-sort-test.html @@ -0,0 +1,91 @@ + +

    Tree Grid Click Sort Test

    +
    +

    First Tree

    +
    +
    Tree grid is loading.
    + + +
    Search
    +
    Click a cell to see results:
    +
    Right-click a cell and edit to see results:
    +
    diff --git a/tests/components/clicked-on-show.html b/tests/components/clicked-on-show.html new file mode 100644 index 0000000..6d126f4 --- /dev/null +++ b/tests/components/clicked-on-show.html @@ -0,0 +1,52 @@ + +

    Tree Grid Clicked On Show Test

    +
    +
    Two tests are included here: +
      +
    • On first rendering, the first child under "Root" should be selected, as should its grid cells
    • +
    • 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.
    • +
    +
    +
    +
    diff --git a/tests/components/dnd-test.html b/tests/components/dnd-test.html new file mode 100644 index 0000000..72b029a --- /dev/null +++ b/tests/components/dnd-test.html @@ -0,0 +1,173 @@ + +

    Tree Grid DND Test

    + +
    +
    +
    + +
    +

    Tree hierarchy

    +
    +
    + +
    +

    Tree hierarchy

    +
    +
    +
    diff --git a/tests/components/dynamic-root-node-test.html b/tests/components/dynamic-root-node-test.html new file mode 100644 index 0000000..af0e63b --- /dev/null +++ b/tests/components/dynamic-root-node-test.html @@ -0,0 +1,126 @@ + + +

    Tree Grid Dynamic Root Test

    + +
    +
    +
    + + +
    + +
    Tree with preset nodes (works)
    +
    +
    + +
    + +
    + +
    Tree with no nodes (fails)
    +
    +
    + +
    + +
    + +
    Tree without grid (works)
    +
    +
    +
    + +
    +
    diff --git a/tests/components/fixed-widths-resize-test.html b/tests/components/fixed-widths-resize-test.html new file mode 100644 index 0000000..bb9d64a --- /dev/null +++ b/tests/components/fixed-widths-resize-test.html @@ -0,0 +1,50 @@ + +

    Tree Grid 3.2.0 Demo

    + +
    +

    First Tree

    +
    +
    Tree grid is loading.
    +
    diff --git a/tests/components/fnvalue-test.html b/tests/components/fnvalue-test.html new file mode 100644 index 0000000..844ec03 --- /dev/null +++ b/tests/components/fnvalue-test.html @@ -0,0 +1,175 @@ + +

    tharasp issue

    +
    +
    +
    diff --git a/tests/components/grid-api-test.html b/tests/components/grid-api-test.html new file mode 100644 index 0000000..93418d1 --- /dev/null +++ b/tests/components/grid-api-test.html @@ -0,0 +1,38 @@ + +

    API Test

    +   + +
    diff --git a/tests/components/large-dataset-test.html b/tests/components/large-dataset-test.html new file mode 100644 index 0000000..843b4f7 --- /dev/null +++ b/tests/components/large-dataset-test.html @@ -0,0 +1,58 @@ + + +

    Large Data Set Search

    +
    + 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.

    + Will only search when you press the "Search" button. +

    +
    + Time for search: milliseconds +
    + + +
    +
    +
    diff --git a/tests/components/minwidth-maxwidth-test.html b/tests/components/minwidth-maxwidth-test.html new file mode 100644 index 0000000..8e3af4c --- /dev/null +++ b/tests/components/minwidth-maxwidth-test.html @@ -0,0 +1,68 @@ + + +

    Tree Grid minWidth/maxWidth Test

    + +
    +

    Width Tree

    +

    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. + 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). +

    +

    Actuals:

    +
    +
    +
    diff --git a/tests/components/odd-named-id-test.html b/tests/components/odd-named-id-test.html new file mode 100644 index 0000000..0c277d6 --- /dev/null +++ b/tests/components/odd-named-id-test.html @@ -0,0 +1,50 @@ + +

    Tree Grid 3.2.0 Demo

    + +
    +

    First Tree

    +
    +
    Tree grid is loading.
    +
    diff --git a/tests/components/rename-dynamic-node-test.html b/tests/components/rename-dynamic-node-test.html new file mode 100644 index 0000000..e4921c7 --- /dev/null +++ b/tests/components/rename-dynamic-node-test.html @@ -0,0 +1,118 @@ + +

    Rename Dynamic Node

    +

    To test:

    +
      +
    1. See the tree below
    2. +
    3. Select a node
    4. +
    5. Click "create" to create a new node
    6. +
    7. Select a grid cell (not the first one, i.e. not the original tree)
    8. +
    9. Try to rename it using right-click or the "Rename" button
    10. +
    + + +
    + + + +
    + +
    diff --git a/tests/components/rename_id_test.html b/tests/components/rename_id_test.html new file mode 100644 index 0000000..78fb12c --- /dev/null +++ b/tests/components/rename_id_test.html @@ -0,0 +1,40 @@ + +

    Rename Issue

    +

    To reproduce the issue:

    +
      +
    1. See the tree below
    2. +
    3. Click the button
    4. +
    5. See that the node is renamed (and given a new id) of "abc"
    6. +
    7. See that a row of the grid is duplicated
    8. +
    + +
    diff --git a/tests/components/render_cell-event-test.html b/tests/components/render_cell-event-test.html new file mode 100644 index 0000000..8fa0940 --- /dev/null +++ b/tests/components/render_cell-event-test.html @@ -0,0 +1,45 @@ + + +

    Tree Grid render_cell Event Test

    +
    +
    +
    +

    Events

    +
    Details in console
    +
      +
    +
    diff --git a/tests/components/resize-event-test.html b/tests/components/resize-event-test.html new file mode 100644 index 0000000..2599790 --- /dev/null +++ b/tests/components/resize-event-test.html @@ -0,0 +1,51 @@ + + +

    Tree Grid Column Resize Event Test

    +
    + Column N/A now has size UNKNOWN. +
    +
    +
    diff --git a/tests/components/search-test.html b/tests/components/search-test.html new file mode 100644 index 0000000..2201165 --- /dev/null +++ b/tests/components/search-test.html @@ -0,0 +1,141 @@ + + +

    Search issue

    +
    only one search can be active at any time!
    +
    + The single search box will search all three trees simultaneously. Each tree is configured slightly differently in terms of search parameters. +
    + +
    + The following search boxes will search in their respective column. This will use a logical AND +
    +
    + + + + +
    +
    + The following search boxes will search in their respective column. Each input will start a new search independent of the others +
    +
    + + + + +
    +
    +
    + show_only_matches=false +
    +
    +
    +
    + show_only_matches=true + show_only_matches_children=false +
    +
    +
    +
    + show_only_matches=true + show_only_matches_children=true +
    +
    diff --git a/tests/components/select-child-test.html b/tests/components/select-child-test.html new file mode 100644 index 0000000..82e1d5e --- /dev/null +++ b/tests/components/select-child-test.html @@ -0,0 +1,31 @@ + +

    Tree Grid Click Child Test

    +
    + +
    diff --git a/tests/components/select-prevent-test.html b/tests/components/select-prevent-test.html new file mode 100644 index 0000000..13701e1 --- /dev/null +++ b/tests/components/select-prevent-test.html @@ -0,0 +1,93 @@ + + +

    Tree Select prevent Test

    +
    Select the elements in any of the three rows. +
      +
    • The first row does not do event.preventDefault(), so the whole row will be selected and lose focus
    • +
    • The second and third row do event.preventDefault(), so the whole row will not be selected
    • +
    • The fourth row does event.preventDefault(), but also manually selects the row, so the change should go through and the row will be selected
    • +
    +
    +
    +
    diff --git a/tests/components/sort-without-resizable-test.html b/tests/components/sort-without-resizable-test.html new file mode 100644 index 0000000..95aa571 --- /dev/null +++ b/tests/components/sort-without-resizable-test.html @@ -0,0 +1,96 @@ + +

    Tree Grid Click Sort Test

    +
    +

    First Tree

    +
    +
    Tree grid is loading.
    +
    +
    +
    +

    Second Tree

    +
    +
    Tree grid is loading.
    +
    diff --git a/tests/components/widths-test.html b/tests/components/widths-test.html new file mode 100644 index 0000000..ac10d7a --- /dev/null +++ b/tests/components/widths-test.html @@ -0,0 +1,275 @@ + + +

    Tree Grid Widths Test

    + +
    +
    diff --git a/tests/test-list.json b/tests/test-list.json new file mode 100644 index 0000000..32814b7 --- /dev/null +++ b/tests/test-list.json @@ -0,0 +1,24 @@ +[ + "api-select-test.html", + "api-hide-show-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", + "odd-named-id-test.html", + "rename_id_test.html", + "rename-dynamic-node-test.html", + "resize-event-test.html", + "search-test.html", + "select-child-test.html", + "select-prevent-test.html", + "render_cell-event-test.html", + "sort-without-resizable-test.html", + "large-dataset-test.html", + "widths-test.html", + "minwidth-maxwidth-test.html" +] diff --git a/tests/test-master.html b/tests/test-master.html new file mode 100644 index 0000000..9c9483f --- /dev/null +++ b/tests/test-master.html @@ -0,0 +1,49 @@ + + + jstree treegrid Tests + + + + + + + + + + + + + + + + + + + + +

    Test Runner

    +
    + 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. +
    +
    +
    +
    +
    + + diff --git a/treegrid.html b/treegrid.html index 3f0bd1a..632d221 100644 --- a/treegrid.html +++ b/treegrid.html @@ -1,9 +1,10 @@ - jstree treegrid 3.0.0 plugin demo - - - + jstree treegrid 3.2.0 plugin demo + + + +