diff --git a/Gruntfile.js b/Gruntfile.js index b269b7a..10a704a 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -51,7 +51,7 @@ module.exports = function(grunt) { }, files: { - 'dist/jquery.tree-multiselect.min.css': 'src/style.scss' + 'dist/jquery.tree-multiselect.min.css': 'sass/style.scss' } }, @@ -61,7 +61,7 @@ module.exports = function(grunt) { }, files: { - 'dist/jquery.tree-multiselect.css': 'src/style.scss' + 'dist/jquery.tree-multiselect.css': 'sass/style.scss' } } }, diff --git a/README.md b/README.md index c2bf2cc..0d111d9 100644 --- a/README.md +++ b/README.md @@ -79,6 +79,7 @@ Name | Default | Description `unselectAllText` | `Unselect All` | Only used if `enableSelectAll` is active `freeze` | `false` | Disables selection/deselection of options; aka display-only `hideSidePanel` | `false` | Hide the right panel showing all the selected items +`maxSelections` | `0` | A number that sets the maximum number of options that can be selected. Any positive integer is valid; anything else (such as `0` or `-1`) means no limit `onChange` | `null` | Callback for when select is changed. Called with (allSelectedItems, addedItems, removedItems), each of which is an array of objects with the properties `text`, `value`, `initialIndex`, and `section` `onlyBatchSelection` | `false` | Only sections can be checked, not individual items `sortable` | `false` | Selected options can be sorted by dragging (requires jQuery UI) diff --git a/conf/karma.js b/conf/karma.js index a18950f..e31e7a1 100644 --- a/conf/karma.js +++ b/conf/karma.js @@ -62,7 +62,7 @@ module.exports = function(config) { ], paths: [ - 'src', + 'src/tree-multiselect', 'node_modules' ] }, diff --git a/dist/jquery.tree-multiselect.css b/dist/jquery.tree-multiselect.css index b8ecb8d..52be481 100644 --- a/dist/jquery.tree-multiselect.css +++ b/dist/jquery.tree-multiselect.css @@ -1,4 +1,4 @@ -/* jQuery Tree Multiselect v2.5.0 | (c) Patrick Tsai | MIT Licensed */ +/* jQuery Tree Multiselect v2.5.1 | (c) Patrick Tsai | MIT Licensed */ div.tree-multiselect { border: 2px solid #D8D8D8; border-radius: 5px; diff --git a/dist/jquery.tree-multiselect.js b/dist/jquery.tree-multiselect.js index 91dbe3d..0b97702 100644 --- a/dist/jquery.tree-multiselect.js +++ b/dist/jquery.tree-multiselect.js @@ -1,35 +1,39 @@ -/* jQuery Tree Multiselect v2.5.0 | (c) Patrick Tsai | MIT Licensed */ +/* jQuery Tree Multiselect v2.5.1 | (c) Patrick Tsai | MIT Licensed */ (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o 0) { + var currentLength = this.keysToAdd.length - this.keysToRemove.length + this.selectedKeys.length; + if (currentLength > this.params.maxSelections) { + var _keysToRemove; + + var lengthToCut = currentLength - this.params.maxSelections; + var keysToCut = []; + if (lengthToCut > this.selectedKeys.length) { + keysToCut.push.apply(keysToCut, _toConsumableArray(this.selectedKeys)); + lengthToCut -= this.selectedKeys.length; + keysToCut.push.apply(keysToCut, _toConsumableArray(this.keysToAdd.splice(0, lengthToCut))); + } else { + keysToCut.push.apply(keysToCut, _toConsumableArray(this.selectedKeys.slice(0, lengthToCut))); + } + (_keysToRemove = this.keysToRemove).push.apply(_keysToRemove, keysToCut); + } + } + // remove items first for (var ii = 0; ii < this.keysToRemove.length; ++ii) { // remove the selected divs @@ -758,6 +793,8 @@ Tree.prototype.render = function (noCallbacks) { valHash[value] = kk; } // TODO is there a better way to sort the values other than by HTML? + // NOTE: the following does not work since jQuery duplicates option values with the same value + // this.$originalSelect.val(vals); var options = this.$originalSelect.find('option').toArray(); options.sort(function (a, b) { var aValue = valHash[a.value] || 0; @@ -769,8 +806,6 @@ Tree.prototype.render = function (noCallbacks) { this.$originalSelect.find('option').each(function (idx, el) { this.selected = !!originalValsHash[Util.getKey(el)]; }); - // NOTE: the following does not work since jQuery duplicates option values with the same value - //this.$originalSelect.val(vals).change(); this.$originalSelect.change(); if (!noCallbacks && this.params.onChange) { @@ -792,7 +827,7 @@ Tree.prototype.render = function (noCallbacks) { module.exports = Tree; -},{"./ast":1,"./search":3,"./ui-builder":6,"./utility":9}],6:[function(require,module,exports){ +},{"./ast":2,"./search":6,"./ui-builder":8,"./utility":11}],8:[function(require,module,exports){ 'use strict'; function UiBuilder($el, hideSidePanel) { @@ -825,7 +860,7 @@ UiBuilder.prototype.remove = function () { module.exports = UiBuilder; -},{}],7:[function(require,module,exports){ +},{}],9:[function(require,module,exports){ "use strict"; // keeps if pred is true @@ -954,7 +989,7 @@ exports.intersectMany = function (arrays) { return finalOutput; }; -},{}],8:[function(require,module,exports){ +},{}],10:[function(require,module,exports){ 'use strict'; exports.createNode = function (tag, props) { @@ -1060,7 +1095,7 @@ exports.createSection = function (astSection, createCheckboxes, disableCheckboxe return sectionNode; }; -},{}],9:[function(require,module,exports){ +},{}],11:[function(require,module,exports){ 'use strict'; exports.array = require('./array'); @@ -1078,4 +1113,13 @@ exports.getKey = function (el) { return parseInt(el.getAttribute('data-key')); }; -},{"./array":7,"./dom":8}]},{},[4]); +exports.isInteger = function (value) { + var x; + if (isNaN(value)) { + return false; + } + x = parseFloat(value); + return (x | 0) === x; +}; + +},{"./array":9,"./dom":10}]},{},[1]); diff --git a/dist/jquery.tree-multiselect.min.css b/dist/jquery.tree-multiselect.min.css index 612e4d3..8232de5 100644 --- a/dist/jquery.tree-multiselect.min.css +++ b/dist/jquery.tree-multiselect.min.css @@ -1,2 +1,2 @@ -/* jQuery Tree Multiselect v2.5.0 | (c) Patrick Tsai | MIT Licensed */ +/* jQuery Tree Multiselect v2.5.1 | (c) Patrick Tsai | MIT Licensed */ div.tree-multiselect{border:2px solid #D8D8D8;border-radius:5px;display:table;height:inherit;width:100%}div.tree-multiselect>div.selected,div.tree-multiselect>div.selections{display:inline-block;box-sizing:border-box;overflow:auto;padding:1%;vertical-align:top;width:50%}div.tree-multiselect>div.selections{border-right:solid 2px #D8D8D8}div.tree-multiselect>div.selections div.item{margin-left:16px}div.tree-multiselect>div.selections div.item label{cursor:pointer;display:inline}div.tree-multiselect>div.selections div.item label.disabled{color:#D8D8D8}div.tree-multiselect>div.selections.no-border{border-right:none}div.tree-multiselect>div.selected>div.item{background:#EAEAEA;border-radius:2px;padding:2px 5px;overflow:auto}div.tree-multiselect>div.selected.ui-sortable>div.item:hover{cursor:move}div.tree-multiselect div.section>div.section,div.tree-multiselect div.section>div.item{padding-left:20px}div.tree-multiselect div.section.collapsed>div.title span.collapse-section:after{content:"+"}div.tree-multiselect div.section.collapsed:not([searchhit])>.item,div.tree-multiselect div.section.collapsed:not([searchhit])>.section{display:none}div.tree-multiselect div.title,div.tree-multiselect div.item{margin-bottom:2px}div.tree-multiselect div.title{background:#777;color:white;padding:2px}div.tree-multiselect div.title>*{display:inline-block}div.tree-multiselect div.title>span.collapse-section{margin:0 3px;width:8px}div.tree-multiselect div.title>span.collapse-section:after{content:"-"}div.tree-multiselect div.title:hover{cursor:pointer}div.tree-multiselect input[type=checkbox]{display:inline;margin-right:5px}div.tree-multiselect input[type=checkbox]:not([disabled]):hover{cursor:pointer}div.tree-multiselect span.remove-selected,div.tree-multiselect span.description{background:#777;border-radius:2px;color:white;margin-right:5px;padding:0 3px}div.tree-multiselect span.remove-selected:hover{cursor:pointer}div.tree-multiselect span.description:hover{cursor:help}div.tree-multiselect div.temp-description-popup{background:#EAEAEA;border:2px solid #676767;border-radius:3px;padding:5px}div.tree-multiselect span.section-name{float:right;font-style:italic}div.tree-multiselect .auxiliary{display:table;width:100%}div.tree-multiselect .auxiliary input.search{border:2px solid #D8D8D8;display:table-cell;margin:0;padding:5px;width:100%}div.tree-multiselect .auxiliary .select-all-container{display:table-cell;text-align:right}div.tree-multiselect .auxiliary .select-all-container span.select-all,div.tree-multiselect .auxiliary .select-all-container span.unselect-all{margin-right:5px;padding-right:5px}div.tree-multiselect .auxiliary .select-all-container span.select-all:hover,div.tree-multiselect .auxiliary .select-all-container span.unselect-all:hover{cursor:pointer}div.tree-multiselect .auxiliary .select-all-container span.select-all{border-right:2px solid #D8D8D8} diff --git a/dist/jquery.tree-multiselect.min.js b/dist/jquery.tree-multiselect.min.js index 48b47f7..5c69ed6 100644 --- a/dist/jquery.tree-multiselect.min.js +++ b/dist/jquery.tree-multiselect.min.js @@ -1,2 +1,2 @@ -/* jQuery Tree Multiselect v2.5.0 | (c) Patrick Tsai | MIT Licensed */ -!function e(t,i,s){function n(a,o){if(!i[a]){if(!t[a]){var c="function"==typeof require&&require;if(!o&&c)return c(a,!0);if(r)return r(a,!0);var l=new Error("Cannot find module '"+a+"'");throw l.code="MODULE_NOT_FOUND",l}var d=i[a]={exports:{}};t[a][0].call(d.exports,function(e){var i=t[a][1][e];return n(i||e)},d,d.exports,e,t,i,s)}return i[a].exports}for(var r="function"==typeof require&&require,a=0;a0?c.split(n.params.sectionDelimiter):[],u=0;u span.description",function(){var e=jQuery(this).parent(),t=e.attr("data-description"),i=document.createElement("div");i.className="temp-description-popup",i.innerHTML=t,i.style.position="absolute",e.append(i)}),this.$selectionContainer.on("mouseleave","div.item > span.description",function(){jQuery(this).parent().find("div.temp-description-popup").remove()})},n.prototype.handleSectionCheckboxMarkings=function(){var e=this;this.$selectionContainer.on("click","input.section[type=checkbox]",function(){var t=jQuery(this).closest("div.section").find("div.item").map(function(t,i){var s=c.getKey(i);if(!e.astItems[s].disabled)return s}).get();if(this.checked){var i;(i=e.keysToAdd).push.apply(i,s(t)),c.array.uniq(e.keysToAdd)}else{var n;(n=e.keysToRemove).push.apply(n,s(t)),c.array.uniq(e.keysToRemove)}e.render()})},n.prototype.redrawSectionCheckboxes=function(e){var t=3,i=this;if((e=e||this.$selectionContainer).find("> div.section").each(function(){var e=i.redrawSectionCheckboxes(jQuery(this));t&=e}),t)for(var s=e.find("> div.item > input[type=checkbox]"),n=0;n div.title > input[type=checkbox]");return r.length&&(r=r[0],1&t?(r.checked=!0,r.indeterminate=!1):2&t?(r.checked=!1,r.indeterminate=!1):(r.checked=!1,r.indeterminate=!0)),t},n.prototype.addCollapsibility=function(){var e=this.$selectionContainer.find("div.title"),t=c.dom.createNode("span",{class:"collapse-section"});e.prepend(t);var i=this.$selectionContainer.find("div.section");this.params.startCollapsed&&i.addClass("collapsed"),this.$selectionContainer.on("click","div.title",function(e){"INPUT"!==e.target.nodeName&&(jQuery(this).parent().toggleClass("collapsed"),e.stopPropagation())})},n.prototype.createSearchBar=function(e){var t=new a(this.astItems,this.astSections,this.params.searchParams),i=c.dom.createNode("input",{class:"search",placeholder:"Search..."});e.appendChild(i),this.$selectionContainer.on("input","input.search",function(){var e=this.value;t.search(e)})},n.prototype.createSelectAllButtons=function(e){var t=c.dom.createNode("span",{class:"select-all",text:this.params.selectAllText}),i=c.dom.createNode("span",{class:"unselect-all",text:this.params.unselectAllText}),n=c.dom.createNode("div",{class:"select-all-container"});n.appendChild(t),n.appendChild(i),e.appendChild(n);var r=this;this.$selectionContainer.on("click","span.select-all",function(){r.keysToAdd=Object.keys(r.astItems),r.render()}),this.$selectionContainer.on("click","span.unselect-all",function(){var e;(e=r.keysToRemove).push.apply(e,s(r.selectedKeys)),r.render()})},n.prototype.armRemoveSelectedOnClick=function(){var e=this;this.$selectedContainer.on("click","span.remove-selected",function(){var t=this.parentNode,i=c.getKey(t);e.keysToRemove.push(i),e.render()})},n.prototype.updateSelectedAndOnChange=function(){var e=this;if(this.$selectionContainer.on("click","input.option[type=checkbox]",function(){var t=this,i=t.parentNode,s=c.getKey(i);c.assert(s||0===s),t.checked?e.keysToAdd.push(s):e.keysToRemove.push(s),e.render()}),this.params.sortable&&!this.params.freeze){var t=null,i=null;this.$selectedContainer.sortable({start:function(e,i){t=i.item.index()},stop:function(s,n){i=n.item.index(),t!==i&&(c.array.moveEl(e.selectedKeys,t,i),e.render())}})}},n.prototype.render=function(e){var t,i=this;c.array.uniq(this.keysToAdd),c.array.uniq(this.keysToRemove),c.array.subtract(this.keysToAdd,this.selectedKeys),c.array.intersect(this.keysToRemove,this.selectedKeys);for(var n=0;n'),s=jQuery('
');t&&s.addClass("no-border"),i.append(s);var n=jQuery('
');t||i.append(n),this.$el=e,this.$treeContainer=i,this.$selectionContainer=s,this.$selectedContainer=n}s.prototype.attach=function(){this.$el.after(this.$treeContainer)},s.prototype.remove=function(){this.$treeContainer.remove()},t.exports=s},{}],7:[function(e,t,i){"use strict";function s(e,t){for(var i=0,s=0;s0&&t[0]<=i[0];++t[0]){for(var n=!1,r=1;ri[r]){n=!0;break}}if(n)break;for(var a=!0,o=1;o0?c.split(n.params.sectionDelimiter):[],u=0;u span.description",function(){var e=jQuery(this).parent(),t=e.attr("data-description"),i=document.createElement("div");i.className="temp-description-popup",i.innerHTML=t,i.style.position="absolute",e.append(i)}),this.$selectionContainer.on("mouseleave","div.item > span.description",function(){jQuery(this).parent().find("div.temp-description-popup").remove()})},n.prototype.handleSectionCheckboxMarkings=function(){var e=this;this.$selectionContainer.on("click","input.section[type=checkbox]",function(){var t=jQuery(this).closest("div.section").find("div.item").map(function(t,i){var s=c.getKey(i);if(!e.astItems[s].disabled)return s}).get();if(this.checked){var i;(i=e.keysToAdd).push.apply(i,s(t)),c.array.uniq(e.keysToAdd)}else{var n;(n=e.keysToRemove).push.apply(n,s(t)),c.array.uniq(e.keysToRemove)}e.render()})},n.prototype.redrawSectionCheckboxes=function(e){var t=3,i=this;if((e=e||this.$selectionContainer).find("> div.section").each(function(){var e=i.redrawSectionCheckboxes(jQuery(this));t&=e}),t)for(var s=e.find("> div.item > input[type=checkbox]"),n=0;n div.title > input[type=checkbox]");return r.length&&(r=r[0],1&t?(r.checked=!0,r.indeterminate=!1):2&t?(r.checked=!1,r.indeterminate=!1):(r.checked=!1,r.indeterminate=!0)),t},n.prototype.addCollapsibility=function(){var e=this.$selectionContainer.find("div.title"),t=c.dom.createNode("span",{class:"collapse-section"});e.prepend(t);var i=this.$selectionContainer.find("div.section");this.params.startCollapsed&&i.addClass("collapsed"),this.$selectionContainer.on("click","div.title",function(e){"INPUT"!==e.target.nodeName&&(jQuery(this).parent().toggleClass("collapsed"),e.stopPropagation())})},n.prototype.createSearchBar=function(e){var t=new a(this.astItems,this.astSections,this.params.searchParams),i=c.dom.createNode("input",{class:"search",placeholder:"Search..."});e.appendChild(i),this.$selectionContainer.on("input","input.search",function(){var e=this.value;t.search(e)})},n.prototype.createSelectAllButtons=function(e){var t=c.dom.createNode("span",{class:"select-all",text:this.params.selectAllText}),i=c.dom.createNode("span",{class:"unselect-all",text:this.params.unselectAllText}),n=c.dom.createNode("div",{class:"select-all-container"});n.appendChild(t),n.appendChild(i),e.appendChild(n);var r=this;this.$selectionContainer.on("click","span.select-all",function(){r.keysToAdd=Object.keys(r.astItems),r.render()}),this.$selectionContainer.on("click","span.unselect-all",function(){var e;(e=r.keysToRemove).push.apply(e,s(r.selectedKeys)),r.render()})},n.prototype.armRemoveSelectedOnClick=function(){var e=this;this.$selectedContainer.on("click","span.remove-selected",function(){var t=this.parentNode,i=c.getKey(t);e.keysToRemove.push(i),e.render()})},n.prototype.updateSelectedAndOnChange=function(){var e=this;if(this.$selectionContainer.on("click","input.option[type=checkbox]",function(){var t=this,i=t.parentNode,s=c.getKey(i);c.assert(s||0===s),t.checked?e.keysToAdd.push(s):e.keysToRemove.push(s),e.render()}),this.params.sortable&&!this.params.freeze){var t=null,i=null;this.$selectedContainer.sortable({start:function(e,i){t=i.item.index()},stop:function(s,n){i=n.item.index(),t!==i&&(c.array.moveEl(e.selectedKeys,t,i),e.render())}})}},n.prototype.render=function(e){var t,i=this;if(c.array.uniq(this.keysToAdd),c.array.uniq(this.keysToRemove),c.array.subtract(this.keysToAdd,this.selectedKeys),c.array.intersect(this.keysToRemove,this.selectedKeys),c.isInteger(this.params.maxSelections)&&this.params.maxSelections>0){var n=this.keysToAdd.length-this.keysToRemove.length+this.selectedKeys.length;if(n>this.params.maxSelections){var r,a=n-this.params.maxSelections,o=[];a>this.selectedKeys.length?(o.push.apply(o,s(this.selectedKeys)),a-=this.selectedKeys.length,o.push.apply(o,s(this.keysToAdd.splice(0,a)))):o.push.apply(o,s(this.selectedKeys.slice(0,a))),(r=this.keysToRemove).push.apply(r,o)}}for(var l=0;l'),s=jQuery('
');t&&s.addClass("no-border"),i.append(s);var n=jQuery('
');t||i.append(n),this.$el=e,this.$treeContainer=i,this.$selectionContainer=s,this.$selectedContainer=n}s.prototype.attach=function(){this.$el.after(this.$treeContainer)},s.prototype.remove=function(){this.$treeContainer.remove()},t.exports=s},{}],9:[function(e,t,i){"use strict";function s(e,t){for(var i=0,s=0;s0&&t[0]<=i[0];++t[0]){for(var n=!1,r=1;ri[r]){n=!0;break}}if(n)break;for(var a=!0,o=1;o { 'use strict'; - $.fn.treeMultiselect = require('./main'); + $.fn.treeMultiselect = require('./tree-multiselect/main'); })(jQuery); diff --git a/src/tree-multiselect/ast/index.js b/src/tree-multiselect/ast/index.js new file mode 100644 index 0000000..eadbd4c --- /dev/null +++ b/src/tree-multiselect/ast/index.js @@ -0,0 +1,17 @@ +const Item = require('./item'); +const Section = require('./section'); + +exports.createLookup = function(arr) { + return { + arr: arr, + children: {} + }; +}; + +exports.createSection = function(obj) { + return new Section(obj); +}; + +exports.createItem = function(obj) { + return new Item(obj); +}; diff --git a/src/tree-multiselect/ast/item.js b/src/tree-multiselect/ast/item.js new file mode 100644 index 0000000..1b2be96 --- /dev/null +++ b/src/tree-multiselect/ast/item.js @@ -0,0 +1,34 @@ +const Util = require('../utility'); + +function Item(obj) { + obj = obj || {}; + + this.treeId = obj.treeId; + this.id = obj.id; + this.value = obj.value; + this.text = obj.text; + this.description = obj.description; + this.initialIndex = obj.initialIndex ? parseInt(obj.initialIndex) : null; + this.section = obj.section; + this.disabled = obj.disabled; + this.selected = obj.selected; + + this.node = null; +} + +Item.prototype.isSection = function() { + return false; +}; + +Item.prototype.isItem = function() { + return true; +}; + +Item.prototype.render = function(createCheckboxes, disableCheckboxes) { + if (!this.node) { + this.node = Util.dom.createSelection(this, createCheckboxes, disableCheckboxes); + } + return this.node; +}; + +module.exports = Item; diff --git a/src/tree-multiselect/ast/section.js b/src/tree-multiselect/ast/section.js new file mode 100644 index 0000000..e21b655 --- /dev/null +++ b/src/tree-multiselect/ast/section.js @@ -0,0 +1,29 @@ +const Util = require('../utility'); + +function Section(obj) { + obj = obj || {}; + + this.treeId = obj.treeId; + this.id = obj.id; + this.name = obj.name; + this.items = []; + + this.node = null; +} + +Section.prototype.isSection = function() { + return true; +}; + +Section.prototype.isItem = function() { + return false; +}; + +Section.prototype.render = function(createCheckboxes, disableCheckboxes) { + if (!this.node) { + this.node = Util.dom.createSection(this, createCheckboxes, disableCheckboxes); + } + return this.node; +}; + +module.exports = Section; diff --git a/src/main.js b/src/tree-multiselect/main.js similarity index 98% rename from src/main.js rename to src/tree-multiselect/main.js index ad525d7..1858279 100644 --- a/src/main.js +++ b/src/tree-multiselect/main.js @@ -35,6 +35,7 @@ function mergeDefaultOptions(options) { unselectAllText: 'Unselect All', freeze: false, hideSidePanel: false, + maxSelections: 0, onChange: null, onlyBatchSelection: false, searchable: false, diff --git a/src/search.js b/src/tree-multiselect/search.js similarity index 100% rename from src/search.js rename to src/tree-multiselect/search.js diff --git a/src/tree.js b/src/tree-multiselect/tree.js similarity index 94% rename from src/tree.js rename to src/tree-multiselect/tree.js index e0f7509..d9cd6c2 100644 --- a/src/tree.js +++ b/src/tree-multiselect/tree.js @@ -373,6 +373,23 @@ Tree.prototype.render = function(noCallbacks) { Util.array.subtract(this.keysToAdd, this.selectedKeys); Util.array.intersect(this.keysToRemove, this.selectedKeys); + // check for max number of selections + if (Util.isInteger(this.params.maxSelections) && this.params.maxSelections > 0) { + const currentLength = this.keysToAdd.length - this.keysToRemove.length + this.selectedKeys.length; + if (currentLength > this.params.maxSelections) { + let lengthToCut = currentLength - this.params.maxSelections; + let keysToCut = []; + if (lengthToCut > this.selectedKeys.length) { + keysToCut.push(...this.selectedKeys); + lengthToCut -= this.selectedKeys.length; + keysToCut.push(...(this.keysToAdd.splice(0, lengthToCut))); + } else { + keysToCut.push(...this.selectedKeys.slice(0, lengthToCut)); + } + this.keysToRemove.push(...keysToCut); + } + } + // remove items first for (let ii = 0; ii < this.keysToRemove.length; ++ii) { // remove the selected divs @@ -424,6 +441,8 @@ Tree.prototype.render = function(noCallbacks) { valHash[value] = kk; } // TODO is there a better way to sort the values other than by HTML? + // NOTE: the following does not work since jQuery duplicates option values with the same value + // this.$originalSelect.val(vals); let options = this.$originalSelect.find('option').toArray(); options.sort(function(a, b) { let aValue = valHash[a.value] || 0; @@ -435,8 +454,6 @@ Tree.prototype.render = function(noCallbacks) { this.$originalSelect.find('option').each(function(idx, el) { this.selected = !!originalValsHash[Util.getKey(el)]; }); - // NOTE: the following does not work since jQuery duplicates option values with the same value - //this.$originalSelect.val(vals).change(); this.$originalSelect.change(); if (!noCallbacks && this.params.onChange) { diff --git a/src/ui-builder.js b/src/tree-multiselect/ui-builder.js similarity index 100% rename from src/ui-builder.js rename to src/tree-multiselect/ui-builder.js diff --git a/src/utility/array.js b/src/tree-multiselect/utility/array.js similarity index 100% rename from src/utility/array.js rename to src/tree-multiselect/utility/array.js diff --git a/src/utility/dom.js b/src/tree-multiselect/utility/dom.js similarity index 100% rename from src/utility/dom.js rename to src/tree-multiselect/utility/dom.js diff --git a/src/utility/index.js b/src/tree-multiselect/utility/index.js similarity index 67% rename from src/utility/index.js rename to src/tree-multiselect/utility/index.js index 1955dbd..8105466 100644 --- a/src/utility/index.js +++ b/src/tree-multiselect/utility/index.js @@ -12,3 +12,12 @@ exports.getKey = function(el) { exports.assert(el); return parseInt(el.getAttribute('data-key')); }; + +exports.isInteger = function(value) { + var x; + if (isNaN(value)) { + return false; + } + x = parseFloat(value); + return (x | 0) === x; +}; diff --git a/test/integration/options.test.js b/test/integration/options.test.js index af27686..898fa9e 100644 --- a/test/integration/options.test.js +++ b/test/integration/options.test.js @@ -427,4 +427,41 @@ describe('Options', () => { $selected = Common.selected({value: 'one'}); assert.equal($selected.length, 1); }); + + it('can set a maximum number of selections', () => { + $("select").append(""); + $("select").append(""); + $("select").append(""); + $("select").append(""); + $("select").treeMultiselect({maxSelections: 2}); + + assert.equal(Common.selected().length, 2); + assert.deepEqual($("select").val(), ['three', 'four']) + + var $checkbox = Common.selectionCheckbox(); + $checkbox.first().click(); + + assert.equal(Common.selected().length, 2); + assert.deepEqual($("select").val(), ['four', 'one']) + }) + + it('maximum number of selections doesn\'t work with negative numbers', () => { + $("select").append(""); + $("select").append(""); + $("select").append(""); + $("select").append(""); + $("select").treeMultiselect({maxSelections: -1}); + + assert.equal(Common.selected().length, 4); + }) + + it('maximum number of selections doesn\'t work with non-numerical truthy values', () => { + $("select").append(""); + $("select").append(""); + $("select").append(""); + $("select").append(""); + $("select").treeMultiselect({maxSelections: true}); + + assert.equal(Common.selected().length, 4); + }) }); diff --git a/test/test.html b/test/test.html index da2cb5a..1c86617 100644 --- a/test/test.html +++ b/test/test.html @@ -76,6 +76,7 @@ var tree3 = $("#test-select-3").treeMultiselect({ allowBatchSelection: false, enableSelectAll: true, + maxSelections: 4, searchable: true, sortable: true, startCollapsed: true diff --git a/test/unit/utility.test.js b/test/unit/utility.test.js index 56ec285..606be8a 100644 --- a/test/unit/utility.test.js +++ b/test/unit/utility.test.js @@ -174,18 +174,6 @@ describe('Utility', () => { assert.isFalse(option.isSection()); }); - it('can tell that AST item is not a section', () => { - var option = Ast.createItem({ - id: 0, - value: 'val', - text: 'text', - description: 'description' - }); - - assert(option.isItem()); - assert.isFalse(option.isSection()); - }); - it('creates selection node with all properties', () => { var section = Ast.createSection('name');