Repository: patosai/tree-multiselect.js
Branch: master
Commit: 393723ef511a
Files: 40
Total size: 155.1 KB
Directory structure:
gitextract_btu2m2g5/
├── .babelrc
├── .circleci/
│ └── config.yml
├── .eslintrc.yml
├── .gitignore
├── .npmignore
├── Gruntfile.js
├── LICENSE
├── README.md
├── bower.json
├── conf/
│ └── karma.js
├── dist/
│ ├── jquery.tree-multiselect.css
│ └── jquery.tree-multiselect.js
├── package.json
├── release.sh
├── sass/
│ └── style.scss
├── src/
│ ├── tree-multiselect/
│ │ ├── ast/
│ │ │ ├── common.js
│ │ │ ├── index.js
│ │ │ ├── item.js
│ │ │ └── section.js
│ │ ├── main.js
│ │ ├── search.js
│ │ ├── tree.js
│ │ ├── ui-builder.js
│ │ └── utility/
│ │ ├── array.js
│ │ ├── dom.js
│ │ └── index.js
│ └── tree-multiselect.js
└── test/
├── integration/
│ ├── common.js
│ ├── initial-load.test.js
│ ├── interactivity.test.js
│ ├── options.test.js
│ ├── reloading.test.js
│ ├── removing.test.js
│ ├── search.test.js
│ ├── section-checkboxes.test.js
│ ├── section-selections.test.js
│ └── single-selections.test.js
├── test-performance.html
├── test.html
└── unit/
└── utility.test.js
================================================
FILE CONTENTS
================================================
================================================
FILE: .babelrc
================================================
{
"presets": ["@babel/preset-env"]
}
================================================
FILE: .circleci/config.yml
================================================
version: "2.1"
orbs:
browser-tools: circleci/browser-tools@1.4.0
jobs:
build:
docker:
- image: cimg/node:18.7.0-browsers
working_directory: ~/tree-multiselect
steps:
- browser-tools/install-chrome
- checkout
- restore_cache:
keys:
- v1-dependencies-{{ checksum "package.json" }}
# fallback to using the latest cache if no exact match is found
- v1-dependencies-
- run: npm install
- save_cache:
paths:
- node_modules
key: v1-dependencies-{{ checksum "package.json" }}
- run: npm test
================================================
FILE: .eslintrc.yml
================================================
env:
browser: true
es6: true
extends: standard
globals:
jQuery: true
module: true
exports: true
require: true
parserOptions:
sourceType: module
rules:
indent:
- error
- 2
linebreak-style:
- error
- unix
quotes:
- error
- single
semi:
- error
- always
prefer-const: off
no-var: off
object-curly-spacing: off
================================================
FILE: .gitignore
================================================
bower_components/
node_modules/
coverage/
================================================
FILE: .npmignore
================================================
*
!dist/
!src/
!package.json
!LICENSE
================================================
FILE: Gruntfile.js
================================================
const sass = require('node-sass');
module.exports = function(grunt) {
grunt.initConfig({
pkg: grunt.file.readJSON('package.json'),
eslint: {
target: ['src/**/*.js']
},
browserify: {
dist: {
options: {
transform: ['babelify']
},
files: {
'dist/jquery.tree-multiselect.js': ['src/tree-multiselect.js']
}
}
},
// Karma runner
karma: {
options: {
configFile: 'conf/karma.js',
},
local: {},
watch: {
autoWatch: true,
singleRun: false
}
},
// SASS compiler
sass: {
options: {
implementation: sass,
sourceMap: false
},
min: {
options: {
outputStyle: 'nested'
},
files: {
'dist/jquery.tree-multiselect.css': 'sass/style.scss'
}
},
build: {
options: {
outputStyle: 'compressed'
},
files: {
'dist/jquery.tree-multiselect.min.css': 'sass/style.scss'
}
}
},
// Uglify JS
uglify: {
dist: {
options: {
preserveComments: false,
},
files: {
'dist/jquery.tree-multiselect.min.js': ['dist/jquery.tree-multiselect.js']
}
}
},
// Put headers on distributed files
usebanner: {
dist: {
options: {
position: 'top',
banner: "/* jQuery Tree Multiselect v<%= pkg.version %> | (c) Patrick Tsai | MIT Licensed */",
linebreak: true
},
files: {
src: ['dist/*.js', 'dist/*.css']
}
}
}
});
grunt.loadNpmTasks('grunt-banner');
grunt.loadNpmTasks('grunt-browserify');
grunt.loadNpmTasks('grunt-eslint');
grunt.loadNpmTasks('grunt-karma');
grunt.loadNpmTasks('grunt-sass');
grunt.loadNpmTasks('grunt-contrib-uglify');
grunt.registerTask('lint', ['eslint']);
grunt.registerTask('build', ['browserify']);
grunt.registerTask('test', ['lint', 'karma:local']);
grunt.registerTask('test-watch', ['karma:watch']);
grunt.registerTask('release', ['test', 'build', 'uglify', 'sass:build', 'sass:min', 'usebanner']);
grunt.registerTask('watch', ['test-watch']);
grunt.registerTask('default', 'test');
};
================================================
FILE: LICENSE
================================================
The MIT License (MIT)
Copyright (c) 2015 Patrick Tsai
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.
================================================
FILE: README.md
================================================
## jQuery Tree Multiselect
[](https://circleci.com/gh/patosai/tree-multiselect.js)
[](https://codecov.io/gh/patosai/tree-multiselect.js)
[](https://david-dm.org/patosai/tree-multiselect.js#info=devDependencies)
**This plugin allows you to add a sweet treeview frontend to a `<select>` node.**
The underlying `<select>` node can be used as it was before. This means you can still use `$("select").val()` or `selectElement.value` to get the value, as if there was no plugin. If you want to add options dynamically, please continue reading, there are some more steps you need to take.
* Make sure you've got `<meta charset="UTF-8">` in your `<head>` or some of the symbols may look strange.
* Requires jQuery v1.8+

### Demo
<a target="_blank" href="https://patosai.com/tree-multiselect">My website has a simple demo running.</a>
### How To Use
1. Set the `multiple="multiple"` attribute on your `<select>`
2. Add attributes to `<option>` nodes
3. Execute `$.treeMultiselect(params)` with whatever params you want
### Setting up your `<select>`
* Make sure your `<select>` has the `multiple` attribute set.
The `<option>` children can have the following attributes.
#### Option Attributes
Option Attribute name | Description
----------------------------- | ---------------------------------
`selected` | Have the option pre-selected. This is actually part of the HTML spec. For specified ordering of these, use `data-index`
`readonly` | User cannot modify the value of the option. Option can be selected (ex. `<option selected readonly ...`)
`data-section` | The section the option will be in; can be nested
`data-description` | A description of the attribute; will be shown on the multiselect
`data-index` | For pre-selected options, display options in this order, lowest index first. Repeated items with the same index will be shown before items with a higher index. Otherwise items will be displayed in the order of the original `<select>`
All of the above are optional.
Your `data-section` can have multiple section names, separated by the `sectionDelimiter` option. If you don't have a `data-section` on an option, the option will be on the top level (no section).
Ex. `data-section="top/middle/inner"` will show up as
- `top`
- `middle`
- `inner`
- your option
### API
#### `$.treeMultiselect(params)`
Renders a tree for the given jQuery `<select>` nodes. `params` is optional.
```javascript
$("select").treeMultiselect();
```
```javascript
let params = {searchable: true};
$("select").treeMultiselect(params);
```
```javascript
function treeOnChange(allSelectedItems, addedItems, removedItems) {
console.log("something changed!");
}
$("select").treeMultiselect({
allowBatchSelection: false,
onChange: treeOnChange,
startCollapsed: true
});
```
##### Params
Name | Default | Description
----------------------- | -------------- | ---------------
`allowBatchSelection` | `true` | Sections have checkboxes which when checked, check everything within them
`collapsible` | `true` | Adds collapsibility to sections
`enableSelectAll` | `false` | Enables selection of all or no options
`selectAllText` | `Select All` | Only used if `enableSelectAll` is active
`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)
`searchable` | `false` | Allows searching of options
`searchParams` | `['value', 'text', 'description', 'section']` | Set items to be searched. Array must contain `'value'`, `'text'`, or `'description'`, and/or `'section'`
`sectionDelimiter` | `/` | Separator between sections in the select option `data-section` attribute
`showSectionOnSelected` | `true` | Show section name on the selected items
`startCollapsed` | `false` | Activated only if `collapsible` is true; sections are collapsed initially
#### Examples
#### `.remove()`
Removes the tree from the DOM. Leaves the original `<select>` intact.
```javascript
let trees = $("select").treeMultiselect({searchable: true});
let firstTree = trees[0];
firstTree.remove();
```
#### `.reload()`
Reinitializes the tree. You can add `<option>` children to the original `<select>` and call `.reload()` to render the new options. User-changed selections will be saved.
```javascript
let trees = $("select").treeMultiselect();
let firstTree = trees[0];
// add an option
$("select#id").append("<option value='newValue' data-section='New Section' selected='selected' data-description='New value'>New Value</option>");
firstTree.reload();
```
### Installation
Load `jquery.tree-multiselect.min.js` on to your web page. The css file is optional (but recommended).
You can also use bower - `bower install tree-multiselect`
### How to build
You need to have grunt-cli installed so you can run the `grunt` command.
- Run tests: `grunt` or `grunt test`
- Build dist JavaScript file: `grunt build`
- Build Sass: `grunt sass`
- Build everything: `grunt release`
### FAQ
`Help! The first element is selected when I create the tree. How do I make the first element not selected?`
You didn't set the `multiple` attribute on your `<select>`. This is a property of single-option select nodes - the first option is selected.
### License
MIT licensed.
================================================
FILE: bower.json
================================================
{
"name": "tree-multiselect",
"description": "jQuery multiple select with nested options",
"main": [
"src/tree-multiselect.js",
"src/style.scss"
],
"authors": [
"Patrick Tsai"
],
"license": "MIT",
"keywords": [
"tree",
"multiselect",
"select",
"jquery",
"options",
"checkbox"
],
"homepage": "https://github.com/patosai/tree-multiselect",
"ignore": [
"*",
"!dist/",
"!src/",
"!package.json",
"!LICENSE"
],
"repository": {
"type": "git",
"url": "git+https://github.com/patosai/tree-multiselect.git"
}
}
================================================
FILE: conf/karma.js
================================================
module.exports = function(config) {
config.set({
basePath: '../',
frameworks: ['browserify', 'mocha', 'chai'],
plugins: [
'karma-browserify',
'karma-chai',
'karma-chrome-launcher',
'karma-coverage',
'karma-mocha',
'karma-mocha-reporter',
],
files: [
'test/vendor/jquery-1.11.3.min.js',
'test/vendor/jquery-ui.min.js',
'src/tree-multiselect.js',
'test/**/*.test.js'
],
exclude: [],
preprocessors: {
// browserify handles istanbul coverage
'src/tree-multiselect.js': ['browserify'],
'test/**/*.test.js': ['browserify']
},
reporters: ['mocha', 'coverage'],
port: 9876,
colors: true,
// config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
logLevel: config.LOG_INFO,
autoWatch: false,
browsers: ['ChromeHeadless'],
singleRun: true,
concurrency: Infinity,
browserify: {
debug: true,
transform: [
['babelify'],
['browserify-istanbul', {
instrumenterConfig: {
embedSource: true
}
}]
],
paths: [
'src/tree-multiselect',
'node_modules'
]
},
client: {
mocha: {
fullTrace: true
}
},
mochaReporter: {
showDiff: true
},
coverageReporter: {
dir: 'coverage/',
reporters: [
{type: 'text-summary'},
{type: 'lcovonly'},
{type: 'html'}
]
}
});
}
================================================
FILE: dist/jquery.tree-multiselect.css
================================================
/* jQuery Tree Multiselect v2.6.3 | (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 *[searchhit=false] {
display: none; }
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; }
================================================
FILE: dist/jquery.tree-multiselect.js
================================================
/* jQuery Tree Multiselect v2.6.3 | (c) Patrick Tsai | MIT Licensed */
(function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i<t.length;i++)o(t[i]);return o}return r})()({1:[function(require,module,exports){
"use strict";
(function ($) {
'use strict';
$.fn.treeMultiselect = require('./tree-multiselect/main');
})(jQuery);
},{"./tree-multiselect/main":6}],2:[function(require,module,exports){
"use strict";
var SEARCH_HIT_ATTR = 'searchhit';
var SEARCH_HIT_ATTR_VAL_TRUE = 'true';
var SEARCH_HIT_ATTR_VAL_FALSE = 'false';
exports.addSearchHitMarker = function (node, isSearchHit) {
if (node) {
isSearchHit = isSearchHit ? SEARCH_HIT_ATTR_VAL_TRUE : SEARCH_HIT_ATTR_VAL_FALSE;
node.setAttribute(SEARCH_HIT_ATTR, isSearchHit);
}
};
exports.removeSearchHitMarker = function (node, isSearchHit) {
if (node) {
node.removeAttribute(SEARCH_HIT_ATTR);
}
};
exports.isNotSearchHit = function (node) {
return node && node.getAttribute(SEARCH_HIT_ATTR) === SEARCH_HIT_ATTR_VAL_FALSE;
};
},{}],3:[function(require,module,exports){
"use strict";
var Item = require('./item');
var 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);
};
},{"./item":4,"./section":5}],4:[function(require,module,exports){
"use strict";
var AstCommon = require('./common');
var 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.addSearchHitMarker = function (isSearchHit) {
AstCommon.addSearchHitMarker(this.node, isSearchHit);
};
Item.prototype.removeSearchHitMarker = function (isSearchHit) {
AstCommon.removeSearchHitMarker(this.node, isSearchHit);
};
Item.prototype.isNotSearchHit = function () {
return AstCommon.isNotSearchHit(this.node);
};
Item.prototype.render = function (createCheckboxes, disableCheckboxes) {
if (!this.node) {
this.node = Util.dom.createSelection(this, createCheckboxes, disableCheckboxes);
}
return this.node;
};
module.exports = Item;
},{"../utility":12,"./common":2}],5:[function(require,module,exports){
"use strict";
var AstCommon = require('./common');
var 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.addSearchHitMarker = function (isSearchHit) {
AstCommon.addSearchHitMarker(this.node, isSearchHit);
};
Section.prototype.removeSearchHitMarker = function (isSearchHit) {
AstCommon.removeSearchHitMarker(this.node, isSearchHit);
};
Section.prototype.isNotSearchHit = function () {
return AstCommon.isNotSearchHit(this.node);
};
Section.prototype.render = function (createCheckboxes, disableCheckboxes) {
if (!this.node) {
this.node = Util.dom.createSection(this, createCheckboxes, disableCheckboxes);
}
return this.node;
};
module.exports = Section;
},{"../utility":12,"./common":2}],6:[function(require,module,exports){
"use strict";
var Tree = require('./tree');
var uniqueId = 0;
function treeMultiselect(opts) {
var _this = this;
var options = mergeDefaultOptions(opts);
return this.map(function () {
var $originalSelect = _this;
$originalSelect.attr('multiple', '').css('display', 'none');
var tree = new Tree(uniqueId, $originalSelect, options);
tree.initialize();
++uniqueId;
return {
reload: function reload() {
tree.reload();
},
remove: function remove() {
tree.remove();
}
};
});
}
;
function mergeDefaultOptions(options) {
var defaults = {
allowBatchSelection: true,
collapsible: true,
enableSelectAll: false,
selectAllText: 'Select All',
unselectAllText: 'Unselect All',
freeze: false,
hideSidePanel: false,
maxSelections: 0,
onChange: null,
onlyBatchSelection: false,
searchable: false,
searchParams: ['value', 'text', 'description', 'section'],
sectionDelimiter: '/',
showSectionOnSelected: true,
sortable: false,
startCollapsed: false
};
return jQuery.extend({}, defaults, options);
}
module.exports = treeMultiselect;
},{"./tree":8}],7:[function(require,module,exports){
"use strict";
var Util = require('./utility');
var MAX_SAMPLE_SIZE = 3;
function Search(searchHitAttr, astItems, astSections, searchParams) {
this.searchHitAttr = searchHitAttr;
this.index = {}; // key: at most three-letter combinations, value: array of data-key
// key: data-key, value: DOM node
this.astItems = astItems;
this.astItemKeys = Object.keys(astItems);
this.astSections = astSections;
this.astSectionKeys = Object.keys(astSections);
this.setSearchParams(searchParams);
this.buildIndex();
}
Search.prototype.setSearchParams = function (searchParams) {
Util.assert(Array.isArray(searchParams));
var allowedParams = {
value: true,
text: true,
description: true,
section: true
};
this.searchParams = [];
for (var ii = 0; ii < searchParams.length; ++ii) {
if (allowedParams[searchParams[ii]]) {
this.searchParams.push(searchParams[ii]);
}
}
};
Search.prototype.buildIndex = function () {
var _this = this;
var _loop = function _loop(astItemKey) {
var astItem = _this.astItems[astItemKey];
var searchItems = [];
_this.searchParams.forEach(function (searchParam) {
searchItems.push(astItem[searchParam]);
});
Util.array.removeFalseyExceptZero(searchItems);
var searchWords = searchItems.map(function (item) {
return item.toLowerCase();
});
searchWords.forEach(function (searchWord) {
var words = searchWord.split(' ');
words.forEach(function (word) {
_this._addToIndex(word, astItem.id);
});
});
};
// trigrams
for (var astItemKey in this.astItems) {
_loop(astItemKey);
}
};
Search.prototype._addToIndex = function (key, id) {
for (var sampleSize = 1; sampleSize <= MAX_SAMPLE_SIZE; ++sampleSize) {
for (var startOffset = 0; startOffset < key.length - sampleSize + 1; ++startOffset) {
var minikey = key.substring(startOffset, startOffset + sampleSize);
if (!this.index[minikey]) {
this.index[minikey] = [];
} // don't duplicate
// this takes advantage of the fact that the minikeys with same id's are added sequentially
var length = this.index[minikey].length;
if (length === 0 || this.index[minikey][length - 1] !== id) {
this.index[minikey].push(id);
}
}
}
};
Search.prototype.search = function (value) {
var _this2 = this;
value = value.trim();
if (!value) {
this.astItemKeys.forEach(function (id) {
_this2.astItems[id].removeSearchHitMarker();
});
this.astSectionKeys.forEach(function (id) {
_this2.astSections[id].removeSearchHitMarker();
});
return;
}
value = value.toLowerCase();
var searchWords = value.split(' ').filter(function (word) {
return word;
});
var searchChunks = [];
searchWords.forEach(function (searchWord) {
var chunks = splitWord(searchWord);
chunks.forEach(function (chunk) {
searchChunks.push(_this2.index[chunk] || []);
});
});
var matchedNodeIds = Util.array.intersectMany(searchChunks); // now we have id's that match search query
this.handleNodeVisibilities(matchedNodeIds);
};
Search.prototype.handleNodeVisibilities = function (shownNodeIds) {
var _this3 = this;
var shownNodeIdsHash = {};
var sectionsToNotHideHash = {};
shownNodeIds.forEach(function (id) {
shownNodeIdsHash[id] = true;
var node = _this3.astItems[id].node; // now search for parent sections
node = node.parentNode;
while (!node.className.match(/tree-multiselect/)) {
if (node.className.match(/section/)) {
var key = Util.getKey(node);
Util.assert(key || key === 0);
if (sectionsToNotHideHash[key]) {
break;
} else {
sectionsToNotHideHash[key] = true;
}
}
node = node.parentNode;
}
}); // hide selections
this.astItemKeys.forEach(function (id) {
var isSearchHit = !!shownNodeIdsHash[id];
_this3.astItems[id].addSearchHitMarker(isSearchHit);
});
this.astSectionKeys.forEach(function (id) {
var isSearchHit = !!sectionsToNotHideHash[id];
_this3.astSections[id].addSearchHitMarker(isSearchHit);
});
}; // split word into three letter (or less) pieces
function splitWord(word) {
Util.assert(word);
if (word.length < MAX_SAMPLE_SIZE) {
return [word];
}
var chunks = [];
for (var ii = 0; ii < word.length - MAX_SAMPLE_SIZE + 1; ++ii) {
chunks.push(word.substring(ii, ii + MAX_SAMPLE_SIZE));
}
return chunks;
}
module.exports = Search;
},{"./utility":12}],8:[function(require,module,exports){
"use strict";
function _toConsumableArray(arr) { return _arrayWithoutHoles(arr) || _iterableToArray(arr) || _unsupportedIterableToArray(arr) || _nonIterableSpread(); }
function _nonIterableSpread() { throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); }
function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); }
function _iterableToArray(iter) { if (typeof Symbol !== "undefined" && iter[Symbol.iterator] != null || iter["@@iterator"] != null) return Array.from(iter); }
function _arrayWithoutHoles(arr) { if (Array.isArray(arr)) return _arrayLikeToArray(arr); }
function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) { arr2[i] = arr[i]; } return arr2; }
var Ast = require('./ast');
var Search = require('./search');
var UiBuilder = require('./ui-builder');
var Util = require('./utility');
var SEARCH_HIT_ATTR = 'searchhit';
function Tree(id, $originalSelect, params) {
this.id = id;
this.$originalSelect = $originalSelect;
this.params = params;
this.resetState();
}
Tree.prototype.initialize = function () {
this.generateSelections(this.$selectionContainer[0]);
this.popupDescriptionHover();
if (this.params.allowBatchSelection) {
this.handleSectionCheckboxMarkings();
}
if (this.params.collapsible) {
this.addCollapsibility();
}
if (this.params.searchable || this.params.enableSelectAll) {
var auxiliaryBox = Util.dom.createNode('div', {
"class": 'auxiliary'
});
this.$selectionContainer.prepend(auxiliaryBox, this.$selectionContainer.firstChild);
if (this.params.searchable) {
this.createSearchBar(auxiliaryBox);
}
if (this.params.enableSelectAll) {
this.createSelectAllButtons(auxiliaryBox);
}
}
this.armRemoveSelectedOnClick();
this.updateSelectedAndOnChange();
this.render(true);
this.uiBuilder.attach();
};
Tree.prototype.remove = function () {
this.uiBuilder.remove();
this.resetState();
};
Tree.prototype.reload = function () {
var _this = this;
var selectedOptions = {};
this.selectedKeys.forEach(function (key) {
var value = _this.astItems[key].value;
selectedOptions[value] = true;
});
this.remove();
this.$originalSelect.children('option').each(function (idx, element) {
var value = element.value;
if (selectedOptions[value]) {
element.setAttribute('selected', 'selected');
} else {
element.removeAttribute('selected');
}
});
this.initialize();
this.render(true);
};
Tree.prototype.resetState = function () {
this.uiBuilder = new UiBuilder(this.$originalSelect, this.params.hideSidePanel);
this.$treeContainer = this.uiBuilder.$treeContainer;
this.$selectionContainer = this.uiBuilder.$selectionContainer;
this.$selectedContainer = this.uiBuilder.$selectedContainer; // data-key is key, provides DOM node
this.astItems = {};
this.astSections = {};
this.selectedNodes = {};
this.selectedKeys = [];
this.keysToAdd = [];
this.keysToRemove = [];
};
Tree.prototype.generateSelections = function (parentNode) {
var options = this.$originalSelect.children('option');
var ast = this.createAst(options);
this.generateHtml(ast, parentNode);
};
Tree.prototype.createAst = function (options) {
var _this$keysToAdd;
var data = [];
var lookup = Ast.createLookup(data);
var self = this;
var itemId = 0;
var sectionId = 0;
var initialIndexItems = [];
var keysToAddAtEnd = [];
options.each(function () {
var option = this;
option.setAttribute('data-key', itemId);
var item = Ast.createItem({
treeId: self.id,
id: itemId,
value: option.value,
text: option.text,
description: option.getAttribute('data-description'),
initialIndex: option.getAttribute('data-index'),
section: option.getAttribute('data-section'),
disabled: option.hasAttribute('readonly'),
selected: option.hasAttribute('selected')
});
if (item.initialIndex && item.selected) {
initialIndexItems[item.initialIndex] = initialIndexItems[item.initialIndex] || [];
initialIndexItems[item.initialIndex].push(itemId);
} else if (item.selected) {
keysToAddAtEnd.push(itemId);
}
self.astItems[itemId] = item;
++itemId;
var lookupPosition = lookup;
var section = item.section;
var sectionParts = section && section.length > 0 ? section.split(self.params.sectionDelimiter) : [];
for (var ii = 0; ii < sectionParts.length; ++ii) {
var sectionPart = sectionParts[ii];
if (lookupPosition.children[sectionPart]) {
lookupPosition = lookupPosition.children[sectionPart];
} else {
var newSection = Ast.createSection({
treeId: self.id,
id: sectionId,
name: sectionPart
});
++sectionId;
lookupPosition.arr.push(newSection);
var newLookupNode = Ast.createLookup(newSection.items);
lookupPosition.children[sectionPart] = newLookupNode;
lookupPosition = newLookupNode;
}
}
lookupPosition.arr.push(item);
});
this.keysToAdd = Util.array.flatten(initialIndexItems);
Util.array.removeFalseyExceptZero(this.keysToAdd);
(_this$keysToAdd = this.keysToAdd).push.apply(_this$keysToAdd, keysToAddAtEnd);
Util.array.uniq(this.keysToAdd);
return data;
};
Tree.prototype.generateHtml = function (astArr, parentNode) {
for (var ii = 0; ii < astArr.length; ++ii) {
var astObj = astArr[ii];
if (astObj.isSection()) {
this.astSections[astObj.id] = astObj;
var createCheckboxes = this.params.allowBatchSelection;
var disableCheckboxes = this.params.freeze;
var node = astObj.render(createCheckboxes, disableCheckboxes);
parentNode.appendChild(node);
this.generateHtml(astObj.items, node);
} else if (astObj.isItem()) {
this.astItems[astObj.id] = astObj;
var _createCheckboxes = !this.params.onlyBatchSelection;
var _disableCheckboxes = this.params.freeze;
var _node = astObj.render(_createCheckboxes, _disableCheckboxes);
parentNode.appendChild(_node);
}
}
};
Tree.prototype.popupDescriptionHover = function () {
this.$selectionContainer.on('mouseenter', 'div.item > span.description', function () {
var $item = jQuery(this).parent();
var description = $item.attr('data-description');
var descriptionDiv = document.createElement('div');
descriptionDiv.className = 'temp-description-popup';
descriptionDiv.innerHTML = description;
descriptionDiv.style.position = 'absolute';
$item.append(descriptionDiv);
});
this.$selectionContainer.on('mouseleave', 'div.item > span.description', function () {
var $item = jQuery(this).parent();
$item.find('div.temp-description-popup').remove();
});
};
Tree.prototype.handleSectionCheckboxMarkings = function () {
var self = this;
this.$selectionContainer.on('click', 'input.section[type=checkbox]', function () {
var $section = jQuery(this).closest('div.section');
var $items = $section.find('div.item');
var keys = $items.map(function (idx, el) {
var key = Util.getKey(el);
var astItem = self.astItems[key];
if (!astItem.disabled && !astItem.isNotSearchHit()) {
return key;
}
return null;
}).get();
if (this.checked) {
var _self$keysToAdd;
// TODO why does this always take this branch
(_self$keysToAdd = self.keysToAdd).push.apply(_self$keysToAdd, _toConsumableArray(keys));
Util.array.uniq(self.keysToAdd);
} else {
var _self$keysToRemove;
(_self$keysToRemove = self.keysToRemove).push.apply(_self$keysToRemove, _toConsumableArray(keys));
Util.array.uniq(self.keysToRemove);
}
self.render();
});
};
Tree.prototype.redrawSectionCheckboxes = function ($section) {
$section = $section || this.$selectionContainer; // returns array; bit 1 is all children are true, bit 0 is all children are false
var returnVal = 3;
var self = this;
var $childSections = $section.find('> div.section');
$childSections.each(function () {
var result = self.redrawSectionCheckboxes(jQuery(this));
returnVal &= result;
});
if (returnVal) {
var $childCheckboxes = $section.find('> div.item > input[type=checkbox]');
for (var ii = 0; ii < $childCheckboxes.length; ++ii) {
if ($childCheckboxes[ii].checked) {
returnVal &= ~2;
} else {
returnVal &= ~1;
}
if (returnVal === 0) {
break;
}
}
}
var sectionCheckbox = $section.find('> div.title > input[type=checkbox]');
if (sectionCheckbox.length) {
sectionCheckbox = sectionCheckbox[0];
if (returnVal & 1) {
sectionCheckbox.checked = true;
sectionCheckbox.indeterminate = false;
} else if (returnVal & 2) {
sectionCheckbox.checked = false;
sectionCheckbox.indeterminate = false;
} else {
sectionCheckbox.checked = false;
sectionCheckbox.indeterminate = true;
}
}
return returnVal;
};
Tree.prototype.addCollapsibility = function () {
var titleSelector = 'div.title';
var $titleDivs = this.$selectionContainer.find(titleSelector);
var collapseSpan = Util.dom.createNode('span', {
"class": 'collapse-section'
});
$titleDivs.prepend(collapseSpan);
var sectionSelector = 'div.section';
var $sectionDivs = this.$selectionContainer.find(sectionSelector);
if (this.params.startCollapsed) {
$sectionDivs.addClass('collapsed');
}
this.$selectionContainer.on('click', titleSelector, function (event) {
if (event.target.nodeName === 'INPUT') {
return;
}
var $section = jQuery(this).parent();
$section.toggleClass('collapsed');
event.stopPropagation();
});
};
Tree.prototype.createSearchBar = function (parentNode) {
var searchObj = new Search(SEARCH_HIT_ATTR, this.astItems, this.astSections, this.params.searchParams);
var searchNode = Util.dom.createNode('input', {
"class": 'search',
placeholder: 'Search...'
});
parentNode.appendChild(searchNode);
this.$selectionContainer.on('input', 'input.search', function () {
var searchText = this.value;
searchObj.search(searchText);
});
};
Tree.prototype.createSelectAllButtons = function (parentNode) {
var selectAllNode = Util.dom.createNode('span', {
"class": 'select-all',
text: this.params.selectAllText
});
var unselectAllNode = Util.dom.createNode('span', {
"class": 'unselect-all',
text: this.params.unselectAllText
});
var selectAllContainer = Util.dom.createNode('div', {
"class": 'select-all-container'
});
selectAllContainer.appendChild(selectAllNode);
selectAllContainer.appendChild(unselectAllNode);
parentNode.appendChild(selectAllContainer);
var self = this;
this.$selectionContainer.on('click', 'span.select-all', function () {
var _self$keysToAdd2;
(_self$keysToAdd2 = self.keysToAdd).push.apply(_self$keysToAdd2, _toConsumableArray(self.unfilteredNodeIds()));
self.render();
});
this.$selectionContainer.on('click', 'span.unselect-all', function () {
var _self$keysToRemove2;
(_self$keysToRemove2 = self.keysToRemove).push.apply(_self$keysToRemove2, _toConsumableArray(self.unfilteredNodeIds()));
self.render();
});
};
Tree.prototype.unfilteredNodeIds = function () {
var self = this;
return Object.keys(self.astItems).filter(function (key) {
return !self.astItems[key].node.hasAttribute(SEARCH_HIT_ATTR) || self.astItems[key].node.getAttribute(SEARCH_HIT_ATTR) === 'true';
});
};
Tree.prototype.armRemoveSelectedOnClick = function () {
var self = this;
this.$selectedContainer.on('click', 'span.remove-selected', function () {
var parentNode = this.parentNode;
var key = Util.getKey(parentNode);
self.keysToRemove.push(key);
self.render();
});
};
Tree.prototype.updateSelectedAndOnChange = function () {
var self = this;
this.$selectionContainer.on('click', 'input.option[type=checkbox]', function () {
var checkbox = this;
var selection = checkbox.parentNode;
var key = Util.getKey(selection);
Util.assert(key || key === 0);
if (checkbox.checked) {
self.keysToAdd.push(key);
} else {
self.keysToRemove.push(key);
}
self.render();
});
if (this.params.sortable && !this.params.freeze) {
var startIndex = null;
var endIndex = null;
this.$selectedContainer.sortable({
start: function start(event, ui) {
startIndex = ui.item.index();
},
stop: function stop(event, ui) {
endIndex = ui.item.index();
if (startIndex === endIndex) {
return;
}
Util.array.moveEl(self.selectedKeys, startIndex, endIndex);
self.render();
}
});
}
};
Tree.prototype.render = function (noCallbacks) {
var _this$selectedKeys,
_this2 = this;
// fix arrays first
Util.array.uniq(this.keysToAdd);
Util.array.uniq(this.keysToRemove);
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) {
var currentLength = this.keysToAdd.length - this.keysToRemove.length + this.selectedKeys.length;
if (currentLength > this.params.maxSelections) {
var _this$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)));
}
(_this$keysToRemove = this.keysToRemove).push.apply(_this$keysToRemove, keysToCut);
}
} // remove items first
for (var ii = 0; ii < this.keysToRemove.length; ++ii) {
// remove the selected divs
var node = this.selectedNodes[this.keysToRemove[ii]];
if (node) {
// slightly more verbose than node.remove(), but more browser support
node.parentNode.removeChild(node);
this.selectedNodes[this.keysToRemove[ii]] = null;
} // uncheck these checkboxes
var selectionNode = this.astItems[this.keysToRemove[ii]].node;
selectionNode.getElementsByTagName('INPUT')[0].checked = false;
}
Util.array.subtract(this.selectedKeys, this.keysToRemove); // now add items
for (var jj = 0; jj < this.keysToAdd.length; ++jj) {
// create selected divs
var key = this.keysToAdd[jj];
var astItem = this.astItems[key];
this.selectedKeys.push(key);
var selectedNode = Util.dom.createSelected(astItem, this.params.freeze, this.params.showSectionOnSelected);
this.selectedNodes[astItem.id] = selectedNode;
this.$selectedContainer.append(selectedNode); // check the checkboxes
var inputNode = astItem.node.getElementsByTagName('INPUT')[0];
if (inputNode) {
inputNode.checked = true;
}
}
(_this$selectedKeys = this.selectedKeys).push.apply(_this$selectedKeys, _toConsumableArray(this.keysToAdd));
Util.array.uniq(this.selectedKeys); // redraw section checkboxes
this.redrawSectionCheckboxes(); // now fix original select
var originalValsHash = {}; // valHash hashes a value to an index
var valHash = {};
for (var kk = 0; kk < this.selectedKeys.length; ++kk) {
var value = this.astItems[this.selectedKeys[kk]].value;
originalValsHash[this.selectedKeys[kk]] = true;
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;
var bValue = valHash[b.value] || 0;
return aValue - bValue;
});
this.$originalSelect.html(options);
this.$originalSelect.find('option').each(function (idx, el) {
this.selected = !!originalValsHash[Util.getKey(el)];
});
this.$originalSelect.change();
if (!noCallbacks && this.params.onChange) {
var optionsSelected = this.selectedKeys.map(function (key) {
return _this2.astItems[key];
});
var optionsAdded = this.keysToAdd.map(function (key) {
return _this2.astItems[key];
});
var optionsRemoved = this.keysToRemove.map(function (key) {
return _this2.astItems[key];
});
this.params.onChange(optionsSelected, optionsAdded, optionsRemoved);
}
this.keysToRemove = [];
this.keysToAdd = [];
};
module.exports = Tree;
},{"./ast":3,"./search":7,"./ui-builder":9,"./utility":12}],9:[function(require,module,exports){
"use strict";
function UiBuilder($el, hideSidePanel) {
var $tree = jQuery('<div class="tree-multiselect"></div>');
var $selections = jQuery('<div class="selections"></div>');
if (hideSidePanel) {
$selections.addClass('no-border');
}
$tree.append($selections);
var $selected = jQuery('<div class="selected"></div>');
if (!hideSidePanel) {
$tree.append($selected);
}
this.$el = $el;
this.$treeContainer = $tree;
this.$selectionContainer = $selections;
this.$selectedContainer = $selected;
}
UiBuilder.prototype.attach = function () {
this.$el.after(this.$treeContainer);
};
UiBuilder.prototype.remove = function () {
this.$treeContainer.remove();
};
module.exports = UiBuilder;
},{}],10:[function(require,module,exports){
"use strict";
// keeps if pred is true
function filterInPlace(arr, pred) {
var idx = 0;
for (var ii = 0; ii < arr.length; ++ii) {
if (pred(arr[ii])) {
arr[idx] = arr[ii];
++idx;
}
}
arr.length = idx;
}
exports.flatten = function (arr, r) {
if (!Array.isArray(arr)) {
return arr;
}
r = r || [];
for (var ii = 0; ii < arr.length; ++ii) {
if (Array.isArray(arr[ii])) {
r.concat(exports.flatten(arr[ii], r));
} else {
r.push(arr[ii]);
}
}
return r;
};
exports.uniq = function (arr) {
var hash = {};
var pred = function pred(val) {
var returnVal = !hash[val];
hash[val] = true;
return returnVal;
};
filterInPlace(arr, pred);
};
exports.removeFalseyExceptZero = function (arr) {
var pred = function pred(val) {
return val || val === 0;
};
filterInPlace(arr, pred);
};
exports.moveEl = function (arr, oldPos, newPos) {
var el = arr[oldPos];
arr.splice(oldPos, 1);
arr.splice(newPos, 0, el);
};
exports.subtract = function (arr, arrExcluded) {
var hash = {};
for (var ii = 0; ii < arrExcluded.length; ++ii) {
hash[arrExcluded[ii]] = true;
}
var pred = function pred(val) {
return !hash[val];
};
filterInPlace(arr, pred);
};
exports.intersect = function (arr, arrExcluded) {
var hash = {};
for (var ii = 0; ii < arrExcluded.length; ++ii) {
hash[arrExcluded[ii]] = true;
}
var pred = function pred(val) {
return hash[val];
};
filterInPlace(arr, pred);
}; // takes in array of arrays
// arrays are presorted
exports.intersectMany = function (arrays) {
var indexLocations = [];
var maxIndexLocations = [];
arrays.forEach(function (array) {
indexLocations.push(0);
maxIndexLocations.push(array.length - 1);
});
var finalOutput = [];
for (; indexLocations.length > 0 && indexLocations[0] <= maxIndexLocations[0]; ++indexLocations[0]) {
// advance indices to be at least equal to first array element
var terminate = false;
for (var ii = 1; ii < arrays.length; ++ii) {
while (arrays[ii][indexLocations[ii]] < arrays[0][indexLocations[0]] && indexLocations[ii] <= maxIndexLocations[ii]) {
++indexLocations[ii];
}
if (indexLocations[ii] > maxIndexLocations[ii]) {
terminate = true;
break;
}
}
if (terminate) {
break;
} // check element equality
var shouldAdd = true;
for (var jj = 1; jj < arrays.length; ++jj) {
if (arrays[0][indexLocations[0]] !== arrays[jj][indexLocations[jj]]) {
shouldAdd = false;
break;
}
}
if (shouldAdd) {
finalOutput.push(arrays[0][indexLocations[0]]);
}
}
return finalOutput;
};
},{}],11:[function(require,module,exports){
"use strict";
exports.createNode = function (tag, props) {
var node = document.createElement(tag);
if (props) {
for (var key in props) {
if (Object.prototype.hasOwnProperty.call(props, key) && key !== 'text') {
node.setAttribute(key, props[key]);
}
}
if (props.text) {
node.textContent = props.text;
}
}
return node;
};
exports.createSelection = function (astItem, createCheckboxes, disableCheckboxes) {
var props = {
"class": 'item',
'data-key': astItem.id,
'data-value': astItem.value
};
var hasDescription = !!astItem.description;
if (hasDescription) {
props['data-description'] = astItem.description;
}
if (astItem.initialIndex) {
props['data-index'] = astItem.initialIndex;
}
var selectionNode = exports.createNode('div', props);
if (hasDescription) {
var popup = exports.createNode('span', {
"class": 'description',
text: '?'
});
selectionNode.appendChild(popup);
}
if (!createCheckboxes) {
selectionNode.innerText = astItem.text || astItem.value;
} else {
var optionLabelCheckboxId = "treemultiselect-".concat(astItem.treeId, "-").concat(astItem.id);
var inputCheckboxProps = {
"class": 'option',
type: 'checkbox',
id: optionLabelCheckboxId
};
if (disableCheckboxes || astItem.disabled) {
inputCheckboxProps.disabled = true;
}
var inputCheckbox = exports.createNode('input', inputCheckboxProps); // prepend child
selectionNode.insertBefore(inputCheckbox, selectionNode.firstChild);
var labelProps = {
"class": astItem.disabled ? 'disabled' : '',
"for": optionLabelCheckboxId,
text: astItem.text || astItem.value
};
var label = exports.createNode('label', labelProps);
selectionNode.appendChild(label);
}
return selectionNode;
};
exports.createSelected = function (astItem, disableRemoval, showSectionOnSelected) {
var node = exports.createNode('div', {
"class": 'item',
'data-key': astItem.id,
'data-value': astItem.value,
text: astItem.text
});
if (!disableRemoval && !astItem.disabled) {
var removalSpan = exports.createNode('span', {
"class": 'remove-selected',
text: '×'
});
node.insertBefore(removalSpan, node.firstChild);
}
if (showSectionOnSelected) {
var sectionSpan = exports.createNode('span', {
"class": 'section-name',
text: astItem.section
});
node.appendChild(sectionSpan);
}
return node;
};
exports.createSection = function (astSection, createCheckboxes, disableCheckboxes) {
var sectionNode = exports.createNode('div', {
"class": 'section',
'data-key': astSection.id
});
var titleNode = exports.createNode('div', {
"class": 'title',
text: astSection.name
});
if (createCheckboxes) {
var checkboxProps = {
"class": 'section',
type: 'checkbox'
};
if (disableCheckboxes) {
checkboxProps.disabled = true;
}
var checkboxNode = exports.createNode('input', checkboxProps);
titleNode.insertBefore(checkboxNode, titleNode.firstChild);
}
sectionNode.appendChild(titleNode);
return sectionNode;
};
},{}],12:[function(require,module,exports){
"use strict";
exports.array = require('./array');
exports.assert = function (bool, message) {
if (!bool) {
throw new Error(message || 'Assertion failed');
}
};
exports.dom = require('./dom');
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;
};
},{"./array":10,"./dom":11}]},{},[1]);
================================================
FILE: package.json
================================================
{
"name": "tree-multiselect",
"version": "2.6.3",
"description": "jQuery multiple select with nested options",
"browser": "dist/jquery.tree-multiselect.min.js",
"keywords": [
"tree",
"multiselect",
"select",
"jquery",
"options",
"checkbox"
],
"scripts": {
"release": "grunt release",
"test": "grunt test"
},
"repository": {
"type": "git",
"url": "git+https://github.com/patosai/tree-multiselect.git"
},
"author": "Patrick Tsai",
"license": "MIT",
"bugs": {
"url": "https://github.com/patosai/tree-multiselect/issues"
},
"homepage": "https://github.com/patosai/tree-multiselect#readme",
"devDependencies": {
"@babel/preset-env": "^7.18.10",
"babel-core": "^6.26.3",
"babelify": "^10.0.0",
"browserify": "^17.0.0",
"browserify-istanbul": "^3.0.1",
"chai": "^4.3.6",
"codecov": "^3.8.3",
"eslint": "^8.22.0",
"eslint-config-standard": "^17.0.0",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^6.0.0",
"grunt": "^1.5.3",
"grunt-banner": "^0.6.0",
"grunt-browserify": "^6.0.0",
"grunt-contrib-uglify": "^5.2.2",
"grunt-coveralls": "^2.0.0",
"grunt-eslint": "^24.0.0",
"grunt-karma": "^4.0.2",
"grunt-sass": "^3.1.0",
"karma": "^6.4.0",
"karma-browserify": "^8.1.0",
"karma-chai": "^0.1.0",
"karma-chrome-launcher": "^3.1.1",
"karma-coverage": "^2.2.0",
"karma-mocha": "^2.0.1",
"karma-mocha-reporter": "^2.2.5",
"mocha": "^10.0.0",
"node-sass": "^7.0.1"
}
}
================================================
FILE: release.sh
================================================
#!/usr/bin/env bash
set -x
VERSION=$(node -e "console.log(require('./package.json').version);")
grunt release
git add .
git commit -m "Release version $VERSION"
git tag v$VERSION
git push && git push --tags
echo "Updated git and git tags to $VERSION"
npm publish
echo "Updated npm package"
================================================
FILE: sass/style.scss
================================================
$color-border-light: #D8D8D8;
$color-border-dark: #676767;
$color-bg-dark: #777;
$color-bg-light: #EAEAEA;
div.tree-multiselect {
border: 2px solid $color-border-light;
border-radius: 5px;
display: table;
height: inherit;
width: 100%;
> div.selected,
> div.selections {
display: inline-block;
box-sizing: border-box;
overflow: auto;
padding: 1%;
vertical-align: top;
width: 50%;
}
> div.selections {
border-right: solid 2px $color-border-light;
div.item {
margin-left: 16px;
label {
cursor: pointer;
display: inline;
&.disabled {
color: $color-border-light;
}
}
}
*[searchhit=false] {
display: none;
}
&.no-border {
border-right: none;
}
}
> div.selected {
> div.item {
background: $color-bg-light;
border-radius: 2px;
padding: 2px 5px;
overflow: auto;
}
&.ui-sortable > div.item:hover {
cursor: move;
}
}
div.section {
> div.section,
> div.item {
padding-left: 20px;
}
&.collapsed {
> div.title span.collapse-section {
&:after {
content: "+";
}
}
&:not([searchhit]) {
> .item,
> .section {
display: none;
}
}
}
}
div.title,
div.item {
margin-bottom: 2px;
}
div.title {
background: $color-bg-dark;
color: white;
padding: 2px;
> * {
display: inline-block;
}
> span.collapse-section {
margin: 0 3px;
width: 8px;
&:after {
content: "-";
}
}
&:hover {
cursor: pointer;
}
}
input[type=checkbox] {
display: inline;
margin-right: 5px;
&:not([disabled]):hover {
cursor: pointer;
}
}
span.remove-selected,
span.description {
background: $color-bg-dark;
border-radius: 2px;
color: white;
margin-right: 5px;
padding: 0 3px;
}
span.remove-selected {
&:hover {
cursor: pointer;
}
}
span.description {
&:hover {
cursor: help;
}
}
div.temp-description-popup {
background: $color-bg-light;
border: 2px solid $color-border-dark;
border-radius: 3px;
padding: 5px;
}
span.section-name {
float: right;
font-style: italic;
}
.auxiliary {
display: table;
width: 100%;
input.search {
border: 2px solid $color-border-light;
display: table-cell;
margin: 0;
padding: 5px;
width: 100%;
}
.select-all-container {
display: table-cell;
text-align: right;
span.select-all,
span.unselect-all {
margin-right: 5px;
padding-right: 5px;
&:hover {
cursor: pointer;
}
}
span.select-all {
border-right: 2px solid $color-border-light;
}
}
}
}
================================================
FILE: src/tree-multiselect/ast/common.js
================================================
const SEARCH_HIT_ATTR = 'searchhit';
const SEARCH_HIT_ATTR_VAL_TRUE = 'true';
const SEARCH_HIT_ATTR_VAL_FALSE = 'false';
exports.addSearchHitMarker = function (node, isSearchHit) {
if (node) {
isSearchHit = isSearchHit ? SEARCH_HIT_ATTR_VAL_TRUE : SEARCH_HIT_ATTR_VAL_FALSE;
node.setAttribute(SEARCH_HIT_ATTR, isSearchHit);
}
};
exports.removeSearchHitMarker = function (node, isSearchHit) {
if (node) {
node.removeAttribute(SEARCH_HIT_ATTR);
}
};
exports.isNotSearchHit = function (node) {
return node && node.getAttribute(SEARCH_HIT_ATTR) === SEARCH_HIT_ATTR_VAL_FALSE;
};
================================================
FILE: src/tree-multiselect/ast/index.js
================================================
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);
};
================================================
FILE: src/tree-multiselect/ast/item.js
================================================
const AstCommon = require('./common');
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.addSearchHitMarker = function (isSearchHit) {
AstCommon.addSearchHitMarker(this.node, isSearchHit);
};
Item.prototype.removeSearchHitMarker = function (isSearchHit) {
AstCommon.removeSearchHitMarker(this.node, isSearchHit);
};
Item.prototype.isNotSearchHit = function () {
return AstCommon.isNotSearchHit(this.node);
};
Item.prototype.render = function (createCheckboxes, disableCheckboxes) {
if (!this.node) {
this.node = Util.dom.createSelection(this, createCheckboxes, disableCheckboxes);
}
return this.node;
};
module.exports = Item;
================================================
FILE: src/tree-multiselect/ast/section.js
================================================
const AstCommon = require('./common');
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.addSearchHitMarker = function (isSearchHit) {
AstCommon.addSearchHitMarker(this.node, isSearchHit);
};
Section.prototype.removeSearchHitMarker = function (isSearchHit) {
AstCommon.removeSearchHitMarker(this.node, isSearchHit);
};
Section.prototype.isNotSearchHit = function () {
return AstCommon.isNotSearchHit(this.node);
};
Section.prototype.render = function (createCheckboxes, disableCheckboxes) {
if (!this.node) {
this.node = Util.dom.createSection(this, createCheckboxes, disableCheckboxes);
}
return this.node;
};
module.exports = Section;
================================================
FILE: src/tree-multiselect/main.js
================================================
let Tree = require('./tree');
let uniqueId = 0;
function treeMultiselect (opts) {
let options = mergeDefaultOptions(opts);
return this.map(() => {
let $originalSelect = this;
$originalSelect.attr('multiple', '').css('display', 'none');
let tree = new Tree(uniqueId, $originalSelect, options);
tree.initialize();
++uniqueId;
return {
reload: function () {
tree.reload();
},
remove: function () {
tree.remove();
}
};
});
};
function mergeDefaultOptions (options) {
let defaults = {
allowBatchSelection: true,
collapsible: true,
enableSelectAll: false,
selectAllText: 'Select All',
unselectAllText: 'Unselect All',
freeze: false,
hideSidePanel: false,
maxSelections: 0,
onChange: null,
onlyBatchSelection: false,
searchable: false,
searchParams: ['value', 'text', 'description', 'section'],
sectionDelimiter: '/',
showSectionOnSelected: true,
sortable: false,
startCollapsed: false
};
return jQuery.extend({}, defaults, options);
}
module.exports = treeMultiselect;
================================================
FILE: src/tree-multiselect/search.js
================================================
let Util = require('./utility');
const MAX_SAMPLE_SIZE = 3;
function Search (searchHitAttr, astItems, astSections, searchParams) {
this.searchHitAttr = searchHitAttr;
this.index = {}; // key: at most three-letter combinations, value: array of data-key
// key: data-key, value: DOM node
this.astItems = astItems;
this.astItemKeys = Object.keys(astItems);
this.astSections = astSections;
this.astSectionKeys = Object.keys(astSections);
this.setSearchParams(searchParams);
this.buildIndex();
}
Search.prototype.setSearchParams = function (searchParams) {
Util.assert(Array.isArray(searchParams));
let allowedParams = {
value: true,
text: true,
description: true,
section: true
};
this.searchParams = [];
for (let ii = 0; ii < searchParams.length; ++ii) {
if (allowedParams[searchParams[ii]]) {
this.searchParams.push(searchParams[ii]);
}
}
};
Search.prototype.buildIndex = function () {
// trigrams
for (const astItemKey in this.astItems) {
const astItem = this.astItems[astItemKey];
let searchItems = [];
this.searchParams.forEach((searchParam) => {
searchItems.push(astItem[searchParam]);
});
Util.array.removeFalseyExceptZero(searchItems);
let searchWords = searchItems.map((item) => {
return item.toLowerCase();
});
searchWords.forEach((searchWord) => {
let words = searchWord.split(' ');
words.forEach((word) => {
this._addToIndex(word, astItem.id);
});
});
}
};
Search.prototype._addToIndex = function (key, id) {
for (let sampleSize = 1; sampleSize <= MAX_SAMPLE_SIZE; ++sampleSize) {
for (let startOffset = 0; startOffset < key.length - sampleSize + 1; ++startOffset) {
let minikey = key.substring(startOffset, startOffset + sampleSize);
if (!this.index[minikey]) {
this.index[minikey] = [];
}
// don't duplicate
// this takes advantage of the fact that the minikeys with same id's are added sequentially
let length = this.index[minikey].length;
if (length === 0 || this.index[minikey][length - 1] !== id) {
this.index[minikey].push(id);
}
}
}
};
Search.prototype.search = function (value) {
value = value.trim();
if (!value) {
this.astItemKeys.forEach((id) => {
this.astItems[id].removeSearchHitMarker();
});
this.astSectionKeys.forEach((id) => {
this.astSections[id].removeSearchHitMarker();
});
return;
}
value = value.toLowerCase();
let searchWords = value.split(' ').filter(word => word);
let searchChunks = [];
searchWords.forEach((searchWord) => {
let chunks = splitWord(searchWord);
chunks.forEach((chunk) => {
searchChunks.push(this.index[chunk] || []);
});
});
let matchedNodeIds = Util.array.intersectMany(searchChunks);
// now we have id's that match search query
this.handleNodeVisibilities(matchedNodeIds);
};
Search.prototype.handleNodeVisibilities = function (shownNodeIds) {
let shownNodeIdsHash = {};
let sectionsToNotHideHash = {};
shownNodeIds.forEach((id) => {
shownNodeIdsHash[id] = true;
let node = this.astItems[id].node;
// now search for parent sections
node = node.parentNode;
while (!node.className.match(/tree-multiselect/)) {
if (node.className.match(/section/)) {
let key = Util.getKey(node);
Util.assert(key || key === 0);
if (sectionsToNotHideHash[key]) {
break;
} else {
sectionsToNotHideHash[key] = true;
}
}
node = node.parentNode;
}
});
// hide selections
this.astItemKeys.forEach((id) => {
let isSearchHit = !!shownNodeIdsHash[id];
this.astItems[id].addSearchHitMarker(isSearchHit);
});
this.astSectionKeys.forEach((id) => {
let isSearchHit = !!sectionsToNotHideHash[id];
this.astSections[id].addSearchHitMarker(isSearchHit);
});
};
// split word into three letter (or less) pieces
function splitWord (word) {
Util.assert(word);
if (word.length < MAX_SAMPLE_SIZE) {
return [word];
}
let chunks = [];
for (let ii = 0; ii < word.length - MAX_SAMPLE_SIZE + 1; ++ii) {
chunks.push(word.substring(ii, ii + MAX_SAMPLE_SIZE));
}
return chunks;
}
module.exports = Search;
================================================
FILE: src/tree-multiselect/tree.js
================================================
let Ast = require('./ast');
let Search = require('./search');
let UiBuilder = require('./ui-builder');
let Util = require('./utility');
const SEARCH_HIT_ATTR = 'searchhit';
function Tree (id, $originalSelect, params) {
this.id = id;
this.$originalSelect = $originalSelect;
this.params = params;
this.resetState();
}
Tree.prototype.initialize = function () {
this.generateSelections(this.$selectionContainer[0]);
this.popupDescriptionHover();
if (this.params.allowBatchSelection) {
this.handleSectionCheckboxMarkings();
}
if (this.params.collapsible) {
this.addCollapsibility();
}
if (this.params.searchable || this.params.enableSelectAll) {
let auxiliaryBox = Util.dom.createNode('div', {class: 'auxiliary'});
this.$selectionContainer.prepend(auxiliaryBox, this.$selectionContainer.firstChild);
if (this.params.searchable) {
this.createSearchBar(auxiliaryBox);
}
if (this.params.enableSelectAll) {
this.createSelectAllButtons(auxiliaryBox);
}
}
this.armRemoveSelectedOnClick();
this.updateSelectedAndOnChange();
this.render(true);
this.uiBuilder.attach();
};
Tree.prototype.remove = function () {
this.uiBuilder.remove();
this.resetState();
};
Tree.prototype.reload = function () {
let selectedOptions = {};
this.selectedKeys.forEach((key) => {
let value = this.astItems[key].value;
selectedOptions[value] = true;
});
this.remove();
this.$originalSelect.children('option').each((idx, element) => {
let value = element.value;
if (selectedOptions[value]) {
element.setAttribute('selected', 'selected');
} else {
element.removeAttribute('selected');
}
});
this.initialize();
this.render(true);
};
Tree.prototype.resetState = function () {
this.uiBuilder = new UiBuilder(this.$originalSelect, this.params.hideSidePanel);
this.$treeContainer = this.uiBuilder.$treeContainer;
this.$selectionContainer = this.uiBuilder.$selectionContainer;
this.$selectedContainer = this.uiBuilder.$selectedContainer;
// data-key is key, provides DOM node
this.astItems = {};
this.astSections = {};
this.selectedNodes = {};
this.selectedKeys = [];
this.keysToAdd = [];
this.keysToRemove = [];
};
Tree.prototype.generateSelections = function (parentNode) {
let options = this.$originalSelect.children('option');
let ast = this.createAst(options);
this.generateHtml(ast, parentNode);
};
Tree.prototype.createAst = function (options) {
let data = [];
let lookup = Ast.createLookup(data);
let self = this;
let itemId = 0;
let sectionId = 0;
let initialIndexItems = [];
let keysToAddAtEnd = [];
options.each(function () {
let option = this;
option.setAttribute('data-key', itemId);
let item = Ast.createItem({
treeId: self.id,
id: itemId,
value: option.value,
text: option.text,
description: option.getAttribute('data-description'),
initialIndex: option.getAttribute('data-index'),
section: option.getAttribute('data-section'),
disabled: option.hasAttribute('readonly'),
selected: option.hasAttribute('selected')
});
if (item.initialIndex && item.selected) {
initialIndexItems[item.initialIndex] = initialIndexItems[item.initialIndex] || [];
initialIndexItems[item.initialIndex].push(itemId);
} else if (item.selected) {
keysToAddAtEnd.push(itemId);
}
self.astItems[itemId] = item;
++itemId;
let lookupPosition = lookup;
let section = item.section;
let sectionParts = (section && section.length > 0) ? section.split(self.params.sectionDelimiter) : [];
for (let ii = 0; ii < sectionParts.length; ++ii) {
let sectionPart = sectionParts[ii];
if (lookupPosition.children[sectionPart]) {
lookupPosition = lookupPosition.children[sectionPart];
} else {
let newSection = Ast.createSection({
treeId: self.id,
id: sectionId,
name: sectionPart
});
++sectionId;
lookupPosition.arr.push(newSection);
let newLookupNode = Ast.createLookup(newSection.items);
lookupPosition.children[sectionPart] = newLookupNode;
lookupPosition = newLookupNode;
}
}
lookupPosition.arr.push(item);
});
this.keysToAdd = Util.array.flatten(initialIndexItems);
Util.array.removeFalseyExceptZero(this.keysToAdd);
this.keysToAdd.push(...keysToAddAtEnd);
Util.array.uniq(this.keysToAdd);
return data;
};
Tree.prototype.generateHtml = function (astArr, parentNode) {
for (let ii = 0; ii < astArr.length; ++ii) {
const astObj = astArr[ii];
if (astObj.isSection()) {
this.astSections[astObj.id] = astObj;
let createCheckboxes = this.params.allowBatchSelection;
let disableCheckboxes = this.params.freeze;
let node = astObj.render(createCheckboxes, disableCheckboxes);
parentNode.appendChild(node);
this.generateHtml(astObj.items, node);
} else if (astObj.isItem()) {
this.astItems[astObj.id] = astObj;
let createCheckboxes = !this.params.onlyBatchSelection;
let disableCheckboxes = this.params.freeze;
let node = astObj.render(createCheckboxes, disableCheckboxes);
parentNode.appendChild(node);
}
}
};
Tree.prototype.popupDescriptionHover = function () {
this.$selectionContainer.on('mouseenter', 'div.item > span.description', function () {
let $item = jQuery(this).parent();
let description = $item.attr('data-description');
let descriptionDiv = document.createElement('div');
descriptionDiv.className = 'temp-description-popup';
descriptionDiv.innerHTML = description;
descriptionDiv.style.position = 'absolute';
$item.append(descriptionDiv);
});
this.$selectionContainer.on('mouseleave', 'div.item > span.description', function () {
let $item = jQuery(this).parent();
$item.find('div.temp-description-popup').remove();
});
};
Tree.prototype.handleSectionCheckboxMarkings = function () {
let self = this;
this.$selectionContainer.on('click', 'input.section[type=checkbox]', function () {
let $section = jQuery(this).closest('div.section');
let $items = $section.find('div.item');
let keys = $items.map((idx, el) => {
let key = Util.getKey(el);
let astItem = self.astItems[key];
if (!astItem.disabled && !astItem.isNotSearchHit()) {
return key;
}
return null;
}).get();
if (this.checked) {
// TODO why does this always take this branch
self.keysToAdd.push(...keys);
Util.array.uniq(self.keysToAdd);
} else {
self.keysToRemove.push(...keys);
Util.array.uniq(self.keysToRemove);
}
self.render();
});
};
Tree.prototype.redrawSectionCheckboxes = function ($section) {
$section = $section || this.$selectionContainer;
// returns array; bit 1 is all children are true, bit 0 is all children are false
let returnVal = 0b11;
let self = this;
let $childSections = $section.find('> div.section');
$childSections.each(function () {
let result = self.redrawSectionCheckboxes(jQuery(this));
returnVal &= result;
});
if (returnVal) {
let $childCheckboxes = $section.find('> div.item > input[type=checkbox]');
for (let ii = 0; ii < $childCheckboxes.length; ++ii) {
if ($childCheckboxes[ii].checked) {
returnVal &= ~0b10;
} else {
returnVal &= ~0b01;
}
if (returnVal === 0) {
break;
}
}
}
let sectionCheckbox = $section.find('> div.title > input[type=checkbox]');
if (sectionCheckbox.length) {
sectionCheckbox = sectionCheckbox[0];
if (returnVal & 0b01) {
sectionCheckbox.checked = true;
sectionCheckbox.indeterminate = false;
} else if (returnVal & 0b10) {
sectionCheckbox.checked = false;
sectionCheckbox.indeterminate = false;
} else {
sectionCheckbox.checked = false;
sectionCheckbox.indeterminate = true;
}
}
return returnVal;
};
Tree.prototype.addCollapsibility = function () {
let titleSelector = 'div.title';
let $titleDivs = this.$selectionContainer.find(titleSelector);
let collapseSpan = Util.dom.createNode('span', {class: 'collapse-section'});
$titleDivs.prepend(collapseSpan);
let sectionSelector = 'div.section';
let $sectionDivs = this.$selectionContainer.find(sectionSelector);
if (this.params.startCollapsed) {
$sectionDivs.addClass('collapsed');
}
this.$selectionContainer.on('click', titleSelector, function (event) {
if (event.target.nodeName === 'INPUT') {
return;
}
let $section = jQuery(this).parent();
$section.toggleClass('collapsed');
event.stopPropagation();
});
};
Tree.prototype.createSearchBar = function (parentNode) {
let searchObj = new Search(SEARCH_HIT_ATTR, this.astItems, this.astSections, this.params.searchParams);
let searchNode = Util.dom.createNode('input', {class: 'search', placeholder: 'Search...'});
parentNode.appendChild(searchNode);
this.$selectionContainer.on('input', 'input.search', function () {
let searchText = this.value;
searchObj.search(searchText);
});
};
Tree.prototype.createSelectAllButtons = function (parentNode) {
let selectAllNode = Util.dom.createNode('span', {class: 'select-all', text: this.params.selectAllText});
let unselectAllNode = Util.dom.createNode('span', {class: 'unselect-all', text: this.params.unselectAllText});
let selectAllContainer = Util.dom.createNode('div', {class: 'select-all-container'});
selectAllContainer.appendChild(selectAllNode);
selectAllContainer.appendChild(unselectAllNode);
parentNode.appendChild(selectAllContainer);
let self = this;
this.$selectionContainer.on('click', 'span.select-all', function () {
self.keysToAdd.push(...self.unfilteredNodeIds());
self.render();
});
this.$selectionContainer.on('click', 'span.unselect-all', function () {
self.keysToRemove.push(...self.unfilteredNodeIds());
self.render();
});
};
Tree.prototype.unfilteredNodeIds = function () {
let self = this;
return Object.keys(self.astItems).filter((key) => {
return !self.astItems[key].node.hasAttribute(SEARCH_HIT_ATTR) ||
self.astItems[key].node.getAttribute(SEARCH_HIT_ATTR) === 'true';
});
};
Tree.prototype.armRemoveSelectedOnClick = function () {
let self = this;
this.$selectedContainer.on('click', 'span.remove-selected', function () {
let parentNode = this.parentNode;
let key = Util.getKey(parentNode);
self.keysToRemove.push(key);
self.render();
});
};
Tree.prototype.updateSelectedAndOnChange = function () {
let self = this;
this.$selectionContainer.on('click', 'input.option[type=checkbox]', function () {
let checkbox = this;
let selection = checkbox.parentNode;
let key = Util.getKey(selection);
Util.assert(key || key === 0);
if (checkbox.checked) {
self.keysToAdd.push(key);
} else {
self.keysToRemove.push(key);
}
self.render();
});
if (this.params.sortable && !this.params.freeze) {
let startIndex = null;
let endIndex = null;
this.$selectedContainer.sortable({
start: function (event, ui) {
startIndex = ui.item.index();
},
stop: function (event, ui) {
endIndex = ui.item.index();
if (startIndex === endIndex) {
return;
}
Util.array.moveEl(self.selectedKeys, startIndex, endIndex);
self.render();
}
});
}
};
Tree.prototype.render = function (noCallbacks) {
// fix arrays first
Util.array.uniq(this.keysToAdd);
Util.array.uniq(this.keysToRemove);
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
let node = this.selectedNodes[this.keysToRemove[ii]];
if (node) {
// slightly more verbose than node.remove(), but more browser support
node.parentNode.removeChild(node);
this.selectedNodes[this.keysToRemove[ii]] = null;
}
// uncheck these checkboxes
let selectionNode = this.astItems[this.keysToRemove[ii]].node;
selectionNode.getElementsByTagName('INPUT')[0].checked = false;
}
Util.array.subtract(this.selectedKeys, this.keysToRemove);
// now add items
for (let jj = 0; jj < this.keysToAdd.length; ++jj) {
// create selected divs
let key = this.keysToAdd[jj];
let astItem = this.astItems[key];
this.selectedKeys.push(key);
let selectedNode = Util.dom.createSelected(astItem, this.params.freeze, this.params.showSectionOnSelected);
this.selectedNodes[astItem.id] = selectedNode;
this.$selectedContainer.append(selectedNode);
// check the checkboxes
let inputNode = astItem.node.getElementsByTagName('INPUT')[0];
if (inputNode) {
inputNode.checked = true;
}
}
this.selectedKeys.push(...this.keysToAdd);
Util.array.uniq(this.selectedKeys);
// redraw section checkboxes
this.redrawSectionCheckboxes();
// now fix original select
let originalValsHash = {};
// valHash hashes a value to an index
let valHash = {};
for (let kk = 0; kk < this.selectedKeys.length; ++kk) {
let value = this.astItems[this.selectedKeys[kk]].value;
originalValsHash[this.selectedKeys[kk]] = true;
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;
let bValue = valHash[b.value] || 0;
return aValue - bValue;
});
this.$originalSelect.html(options);
this.$originalSelect.find('option').each(function (idx, el) {
this.selected = !!originalValsHash[Util.getKey(el)];
});
this.$originalSelect.change();
if (!noCallbacks && this.params.onChange) {
let optionsSelected = this.selectedKeys.map((key) => {
return this.astItems[key];
});
let optionsAdded = this.keysToAdd.map((key) => {
return this.astItems[key];
});
let optionsRemoved = this.keysToRemove.map((key) => {
return this.astItems[key];
});
this.params.onChange(optionsSelected, optionsAdded, optionsRemoved);
}
this.keysToRemove = [];
this.keysToAdd = [];
};
module.exports = Tree;
================================================
FILE: src/tree-multiselect/ui-builder.js
================================================
function UiBuilder ($el, hideSidePanel) {
let $tree = jQuery('<div class="tree-multiselect"></div>');
let $selections = jQuery('<div class="selections"></div>');
if (hideSidePanel) {
$selections.addClass('no-border');
}
$tree.append($selections);
let $selected = jQuery('<div class="selected"></div>');
if (!hideSidePanel) {
$tree.append($selected);
}
this.$el = $el;
this.$treeContainer = $tree;
this.$selectionContainer = $selections;
this.$selectedContainer = $selected;
}
UiBuilder.prototype.attach = function () {
this.$el.after(this.$treeContainer);
};
UiBuilder.prototype.remove = function () {
this.$treeContainer.remove();
};
module.exports = UiBuilder;
================================================
FILE: src/tree-multiselect/utility/array.js
================================================
// keeps if pred is true
function filterInPlace (arr, pred) {
var idx = 0;
for (var ii = 0; ii < arr.length; ++ii) {
if (pred(arr[ii])) {
arr[idx] = arr[ii];
++idx;
}
}
arr.length = idx;
}
exports.flatten = function (arr, r) {
if (!Array.isArray(arr)) {
return arr;
}
r = r || [];
for (var ii = 0; ii < arr.length; ++ii) {
if (Array.isArray(arr[ii])) {
r.concat(exports.flatten(arr[ii], r));
} else {
r.push(arr[ii]);
}
}
return r;
};
exports.uniq = function (arr) {
var hash = {};
var pred = function (val) {
var returnVal = !hash[val];
hash[val] = true;
return returnVal;
};
filterInPlace(arr, pred);
};
exports.removeFalseyExceptZero = function (arr) {
var pred = function (val) {
return val || val === 0;
};
filterInPlace(arr, pred);
};
exports.moveEl = function (arr, oldPos, newPos) {
var el = arr[oldPos];
arr.splice(oldPos, 1);
arr.splice(newPos, 0, el);
};
exports.subtract = function (arr, arrExcluded) {
var hash = {};
for (var ii = 0; ii < arrExcluded.length; ++ii) {
hash[arrExcluded[ii]] = true;
}
var pred = function (val) {
return !hash[val];
};
filterInPlace(arr, pred);
};
exports.intersect = function (arr, arrExcluded) {
var hash = {};
for (var ii = 0; ii < arrExcluded.length; ++ii) {
hash[arrExcluded[ii]] = true;
}
var pred = function (val) {
return hash[val];
};
filterInPlace(arr, pred);
};
// takes in array of arrays
// arrays are presorted
exports.intersectMany = function (arrays) {
var indexLocations = [];
var maxIndexLocations = [];
arrays.forEach((array) => {
indexLocations.push(0);
maxIndexLocations.push(array.length - 1);
});
var finalOutput = [];
for (; indexLocations.length > 0 && indexLocations[0] <= maxIndexLocations[0]; ++indexLocations[0]) {
// advance indices to be at least equal to first array element
var terminate = false;
for (var ii = 1; ii < arrays.length; ++ii) {
while (arrays[ii][indexLocations[ii]] < arrays[0][indexLocations[0]] &&
indexLocations[ii] <= maxIndexLocations[ii]) {
++indexLocations[ii];
}
if (indexLocations[ii] > maxIndexLocations[ii]) {
terminate = true;
break;
}
}
if (terminate) {
break;
}
// check element equality
var shouldAdd = true;
for (var jj = 1; jj < arrays.length; ++jj) {
if (arrays[0][indexLocations[0]] !== arrays[jj][indexLocations[jj]]) {
shouldAdd = false;
break;
}
}
if (shouldAdd) {
finalOutput.push(arrays[0][indexLocations[0]]);
}
}
return finalOutput;
};
================================================
FILE: src/tree-multiselect/utility/dom.js
================================================
exports.createNode = function (tag, props) {
var node = document.createElement(tag);
if (props) {
for (var key in props) {
if (Object.prototype.hasOwnProperty.call(props, key) && key !== 'text') {
node.setAttribute(key, props[key]);
}
}
if (props.text) {
node.textContent = props.text;
}
}
return node;
};
exports.createSelection = function (astItem, createCheckboxes, disableCheckboxes) {
var props = {
class: 'item',
'data-key': astItem.id,
'data-value': astItem.value
};
var hasDescription = !!astItem.description;
if (hasDescription) {
props['data-description'] = astItem.description;
}
if (astItem.initialIndex) {
props['data-index'] = astItem.initialIndex;
}
var selectionNode = exports.createNode('div', props);
if (hasDescription) {
var popup = exports.createNode('span', {class: 'description', text: '?'});
selectionNode.appendChild(popup);
}
if (!createCheckboxes) {
selectionNode.innerText = astItem.text || astItem.value;
} else {
var optionLabelCheckboxId = `treemultiselect-${astItem.treeId}-${astItem.id}`;
var inputCheckboxProps = {
class: 'option',
type: 'checkbox',
id: optionLabelCheckboxId
};
if (disableCheckboxes || astItem.disabled) {
inputCheckboxProps.disabled = true;
}
var inputCheckbox = exports.createNode('input', inputCheckboxProps);
// prepend child
selectionNode.insertBefore(inputCheckbox, selectionNode.firstChild);
var labelProps = {
class: astItem.disabled ? 'disabled' : '',
for: optionLabelCheckboxId,
text: astItem.text || astItem.value
};
var label = exports.createNode('label', labelProps);
selectionNode.appendChild(label);
}
return selectionNode;
};
exports.createSelected = function (astItem, disableRemoval, showSectionOnSelected) {
var node = exports.createNode('div', {
class: 'item',
'data-key': astItem.id,
'data-value': astItem.value,
text: astItem.text
});
if (!disableRemoval && !astItem.disabled) {
var removalSpan = exports.createNode('span', {class: 'remove-selected', text: '×'});
node.insertBefore(removalSpan, node.firstChild);
}
if (showSectionOnSelected) {
var sectionSpan = exports.createNode('span', {class: 'section-name', text: astItem.section});
node.appendChild(sectionSpan);
}
return node;
};
exports.createSection = function (astSection, createCheckboxes, disableCheckboxes) {
var sectionNode = exports.createNode('div', {class: 'section', 'data-key': astSection.id});
var titleNode = exports.createNode('div', {class: 'title', text: astSection.name});
if (createCheckboxes) {
var checkboxProps = {
class: 'section',
type: 'checkbox'
};
if (disableCheckboxes) {
checkboxProps.disabled = true;
}
var checkboxNode = exports.createNode('input', checkboxProps);
titleNode.insertBefore(checkboxNode, titleNode.firstChild);
}
sectionNode.appendChild(titleNode);
return sectionNode;
};
================================================
FILE: src/tree-multiselect/utility/index.js
================================================
exports.array = require('./array');
exports.assert = function (bool, message) {
if (!bool) {
throw new Error(message || 'Assertion failed');
}
};
exports.dom = require('./dom');
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;
};
================================================
FILE: src/tree-multiselect.js
================================================
(($) => {
'use strict';
$.fn.treeMultiselect = require('./tree-multiselect/main');
})(jQuery);
================================================
FILE: test/integration/common.js
================================================
chai.config.includeStack = true;
var $fixture = null;
var selectCount = 1;
function createFixtureSelect() {
var select = document.createElement("select");
select.id = 'select-' + selectCount;
++selectCount;
select.setAttribute("multiple", "multiple");
$fixture.append(select);
return select;
}
beforeEach(() => {
$fixture = $("#fixture");
if (!$fixture.length) {
$fixture = $("<div id='fixture'></div>");
$("body").append($fixture);
}
selectCount = 0;
$fixture.empty();
createFixtureSelect();
});
module.exports = {
assertSelection(el, params) {
var $el = $(el);
assert($el.hasClass('item'));
assert.equal(this.textOf($el), params.text);
assert.equal($el.attr('data-value'), params.value);
},
assertSelected(el, params) {
var $el = $(el);
assert($el.hasClass('item'));
assert.equal(this.textOf($el), params.text);
assert.equal($el.attr('data-value'), params.value);
var $sectionName = $el.children(".section-name");
assert.equal($sectionName.length, 1);
assert.equal(this.textOf($sectionName), params.section);
},
createFixtureSelect: createFixtureSelect,
textOf(el) {
var $el = $(el);
var $label = $el.children("label");
if ($label.length) {
return $label.first().text();
} else {
return $el.clone().children().remove().end().text();
}
},
// DOM element finders
find(container, options) {
var text = null;
var value = null;
if (options) {
text = options.text;
value = options.value;
}
var selector = container + (value ? `[data-value=${value}]` : '');
var $els = $(selector);
if (text) {
$els = $els.filter((idx, el) => {
return this.textOf(el) === text;
});
}
return $els;
},
findCheckbox(container, options) {
var $els = this.find(container, options).children("input[type=checkbox]");
if (options && options.checked) {
$els = $els.filter((idx, el) => {
return el.checked === options.checked;
});
}
return $els;
},
selection(options) {
return this.find('.selections .item', options);
},
selected(options) {
return this.find('.selected .item', options);
},
section(options) {
// need to search title text, then go back up to section
return this.find('.selections .section > .title', options).parent();
},
selectionCheckbox(options) {
return this.findCheckbox(".selections .item", options);
},
sectionCheckbox(options) {
return this.findCheckbox(".selections .section > .title", options);
}
};
================================================
FILE: test/integration/initial-load.test.js
================================================
var Common = require('./common');
describe('Initial Load', () => {
it('creates container', () => {
assert.equal($(".tree-multiselect").length, 0);
assert.equal($(".tree-multiselect div.selections").length, 0);
assert.equal($(".tree-multiselect div.selected").length, 0);
$("select").treeMultiselect();
assert.equal($(".tree-multiselect").length, 1);
assert.equal($(".tree-multiselect div.selections").length, 1);
assert.equal($(".tree-multiselect div.selected").length, 1);
});
it('renders option', () => {
$("select").append("<option value='one' data-section='section'>One</option>");
$("select").treeMultiselect();
var $selections = Common.selection();
assert.equal($selections.length, 1);
var $selection = $selections.first();
Common.assertSelection($selection, {text: 'One', value: 'one'});
var $checkbox = $selection.children("input[type=checkbox]");
var $label = $selection.children("label");
assert.equal($checkbox.length, 1);
assert.equal($label.length, 1);
assert.equal($checkbox.attr('id'), $label.attr('for'));
});
it('renders multiple options', () => {
$("select").append("<option value='one' data-section='section'>One</option>");
$("select").append("<option value='two' data-section='section'>Two</option>");
$("select").treeMultiselect();
var $selections = Common.selection();
assert.equal($selections.length, 2);
Common.assertSelection($selections[0], {text: 'One', value: 'one'});
Common.assertSelection($selections[1], {text: 'Two', value: 'two'});
});
it('renders options with the same value', () => {
$("select").append("<option value='one' data-section='section'>One</option>");
$("select").append("<option value='one' data-section='section'>One duplicate</option>");
$("select").treeMultiselect();
var $selections = Common.selection();
assert.equal($selections.length, 2);
Common.assertSelection($selections[0], {text: 'One', value: 'one'});
Common.assertSelection($selections[1], {text: 'One duplicate', value: 'one'});
});
it('renders options with the same value and text', () => {
$("select").append("<option value='one' data-section='section'>One</option>");
$("select").append("<option value='one' data-section='section'>One</option>");
$("select").treeMultiselect();
var $selections = Common.selection();
assert.equal($selections.length, 2);
Common.assertSelection($selections[0], {text: 'One', value: 'one'});
Common.assertSelection($selections[1], {text: 'One', value: 'one'});
});
it('renders options without data-section', () => {
$("select").append("<option value='one'>One</option>");
$("select").append("<option value='two'>Two</option>");
$("select").treeMultiselect();
var $selections = Common.selection();
assert.equal($selections.length, 2);
Common.assertSelection($selections[0], {text: 'One', value: 'one'});
Common.assertSelection($selections[1], {text: 'Two', value: 'two'});
});
it('respects selected attribute', () => {
$("select").append("<option value='one' data-section='section' selected>One</option>");
$("select").treeMultiselect();
var $selections = Common.selection();
assert.equal($selections.length, 1);
var $selected = Common.selected();
assert.equal($selected.length, 1);
assert.deepEqual($("select").val(), ['one']);
});
it('respects data-index attribute', () => {
$("select").append("<option value='one' data-section='section' data-index='1' selected>One</option>");
$("select").treeMultiselect();
var $selections = Common.selection();
assert.equal($selections.length, 1);
var $selected = Common.selected();
assert.equal($selected.length, 1);
Common.assertSelection($selected, {text: 'One', value: 'one'});
assert.deepEqual($("select").val(), ['one']);
});
it("only respects data-index if selected", () => {
$("select").append("<option value='one' data-section='section' data-index='2'>One</option>");
$("select").append("<option value='two' data-section='section' data-index='2' selected>Two</option>");
$("select").treeMultiselect();
var $selections = Common.selection();
assert.equal($selections.length, 2);
var $selected = Common.selected();
assert.equal($selected.length, 1);
Common.assertSelection($selected, {text: 'Two', value: 'two'});
assert.deepEqual($("select").val(), ['two']);
});
it('renders selected item correctly', () => {
$("select").append("<option value='one' data-section='section/foo/bar' data-index='1' selected>One</option>");
$("select").treeMultiselect();
var $selections = Common.selection();
assert.equal($selections.length, 1);
var $selected = Common.selected();
assert.equal($selected.length, 1);
Common.assertSelection($selected, {text: 'One', value: 'one'});
var $sectionName = $selected.first().children(".section-name");
assert.equal($sectionName.length, 1);
assert.equal(Common.textOf($sectionName[0]), "section/foo/bar");
});
it('ranks data-index lowest to highest', () => {
$("select").append("<option value='two' data-section='section' data-index='2' selected>Two</option>");
$("select").append("<option value='three' data-section='section' data-index='3' selected>Three</option>");
$("select").append("<option value='one' data-section='section' data-index='1' selected>One</option>");
$("select").treeMultiselect();
var $selections = Common.selection();
assert.equal($selections.length, 3);
var $selected = Common.selected();
assert.equal($selected.length, 3);
Common.assertSelection($selected[0], {text: 'One', value: 'one'});
Common.assertSelection($selected[1], {text: 'Two', value: 'two'});
Common.assertSelection($selected[2], {text: 'Three', value: 'three'});
assert.deepEqual($("select").val(), ['one', 'two', 'three']);
});
it('handles non-consecutive data-index', () => {
$("select").append("<option value='two' data-section='section' data-index='593' selected>Two</option>");
$("select").append("<option value='one' data-section='section' data-index='1' selected>One</option>");
$("select").treeMultiselect();
var $selections = Common.selection();
assert.equal($selections.length, 2);
var $selected = Common.selected();
assert.equal($selected.length, 2);
Common.assertSelection($selected[0], {text: 'One', value: 'one'});
Common.assertSelection($selected[1], {text: 'Two', value: 'two'});
assert.deepEqual($("select").val(), ['one', 'two']);
});
it('ranks data-index higher than selected attribute', () => {
$("select").append("<option value='one' data-section='section' selected>One</option>");
$("select").append("<option value='two' data-section='section' selected data-index='300'>Two</option>");
$("select").treeMultiselect();
var $selections = Common.selection();
assert.equal($selections.length, 2);
var $selected = Common.selected();
assert.equal($selected.length, 2);
Common.assertSelection($selected[0], {text: 'Two', value: 'two'});
Common.assertSelection($selected[1], {text: 'One', value: 'one'});
assert.deepEqual($("select").val(), ['two', 'one']);
});
it("data-index doesn't do string comparison", () => {
$("select").append("<option value='one' data-section='section' data-index='2' selected>One</option>");
$("select").append("<option value='two' data-section='section' data-index='10' selected>Two</option>");
$("select").treeMultiselect();
var $selections = Common.selection();
assert.equal($selections.length, 2);
var $selected = Common.selected();
assert.equal($selected.length, 2);
Common.assertSelection($selected[0], {text: 'One', value: 'one'});
Common.assertSelection($selected[1], {text: 'Two', value: 'two'});
assert.deepEqual($("select").val(), ['one', 'two']);
});
it("repeated data-index results in original order", () => {
$("select").append("<option value='two' data-section='section' data-index='2' selected>Two</option>");
$("select").append("<option value='one' data-section='section' data-index='2' selected>One</option>");
$("select").treeMultiselect();
var $selections = Common.selection();
assert.equal($selections.length, 2);
var $selected = Common.selected();
assert.equal($selected.length, 2);
Common.assertSelection($selected[0], {text: 'Two', value: 'two'});
Common.assertSelection($selected[1], {text: 'One', value: 'one'});
assert.deepEqual($("select").val(), ['two', 'one']);
});
it("repeated data-index comes before sequential data-index", () => {
$("select").append("<option value='one' data-section='section' data-index='2' selected>One</option>");
$("select").append("<option value='two' data-section='section' data-index='3' selected>Two</option>");
$("select").append("<option value='three' data-section='section' data-index='2' selected>Three</option>");
$("select").treeMultiselect();
var $selections = Common.selection();
assert.equal($selections.length, 3);
var $selected = Common.selected();
assert.equal($selected.length, 3);
Common.assertSelection($selected[0], {text: 'One', value: 'one'});
Common.assertSelection($selected[1], {text: 'Three', value: 'three'});
Common.assertSelection($selected[2], {text: 'Two', value: 'two'});
assert.deepEqual($("select").val(), ['one', 'three', 'two']);
});
});
================================================
FILE: test/integration/interactivity.test.js
================================================
var Common = require('./common');
describe('Interactivity', () => {
it('data-description pops up when moused over', () => {
$("select").append("<option value='one' data-section='foo' selected='selected' data-description='One'>One</option>");
$("select").treeMultiselect();
var $descriptions = $("div.item > span.description");
assert.equal($descriptions.length, 1);
$descriptions.first().mouseenter();
var $tempPopup = $(".temp-description-popup");
assert.equal($tempPopup.length, 1);
assert.equal(Common.textOf($tempPopup), 'One');
$descriptions.first().mouseleave();
$tempPopup = $(".temp-description-popup");
assert.equal($tempPopup.length, 0);
});
it('data-description is removed when mouse leaves', () => {
$("select").append("<option value='one' data-section='foo' selected='selected' data-description='One'>One</option>");
$("select").treeMultiselect();
var $descriptions = $("div.item > span.description");
assert.equal($descriptions.length, 1);
$descriptions.first().mouseenter();
var $tempPopup = $(".temp-description-popup");
assert.equal($tempPopup.length, 1);
$descriptions.first().mouseleave();
$tempPopup = $(".temp-description-popup");
assert.equal($tempPopup.length, 0);
});
//it('collapses when clicking on titlebar', () => {
//$("select").append("<option value='one' data-section='foo' data-description='One'>One</option>");
//$("select").append("<option value='two' data-section='foo' selected='selected' data-description='Two'>Two</option>");
//$("select").append("<option value='three' data-section='foo' data-description='Three'>Three</option>");
//$("select").treeMultiselect();
//var $title = Common.section({text: 'foo'}).children(".title");
//assert.equal($("div.selections div.item:visible").length, 3);
//$title.click();
//assert.equal($("div.selections div.item:visible").length, 0);
//});
//it('collapse indicator changes', () => {
//$("select").append("<option value='one' data-section='foo' data-description='One'>One</option>");
//$("select").append("<option value='two' data-section='foo' selected='selected' data-description='Two'>Two</option>");
//$("select").append("<option value='three' data-section='foo' data-description='Three'>Three</option>");
//$("select").treeMultiselect();
//var $title = Common.section({text: 'foo'}).children(".title");
//var $collapse = $title.children(".collapse-section");
//assert.equal($collapse.text(), '-');
//$title.click();
//assert.equal($collapse.text(), '+');
//});
it('has correct label id', () => {
$("select").append("<option value='one' data-section='foo' data-description='One'>One</option>");
$("select").treeMultiselect();
var $option = $("input.option");
assert.equal($option.length, 1);
assert.equal($("#" + $option.attr('id')).length, 1);
$("body").append("select#two");
$("#two").append("<option value='two' data-section='foo' data-description='Two'>Two</option>");
$("#two").treeMultiselect();
assert.equal($("#" + $option.attr('id')).length, 1);
})
});
================================================
FILE: test/integration/options.test.js
================================================
var Common = require('./common');
describe('Options', () => {
it('is collapsible', () => {
$("select").append("<option value='one' data-section='test'>One</option>");
$("select").append("<option value='two' data-section='test'>Two</option>");
$("select").append("<option value='three' data-section='test'>Three</option>");
$("select").treeMultiselect();
var $section = Common.section();
assert.equal($section.length, 1);
var $title = $section.children("div.title");
assert.equal($title.length, 1);
Common.section().each(function() {
assert(!$(this).hasClass("collapsed"));
});
$title.click();
Common.section().each(function() {
assert($(this).hasClass("collapsed"));
});
});
it('startCollapsed', () => {
$("select").append("<option value='one' data-section='test'>One</option>");
$("select").append("<option value='two' data-section='test/inner'>Two</option>");
$("select").append("<option value='three' data-section='test/inner2'>Three</option>");
$("select").append("<option value='1' data-section='another'>Beep</option>");
var options = {
startCollapsed: true
};
$("select").treeMultiselect(options);
var $section = Common.section();
assert.equal($section.length, 4);
var $hiddenSections = $section.filter((idx, el) => {
return $(el).hasClass("collapsed");
});
assert.equal($hiddenSections.length, 4);
});
it("startCollapsed doesn't do anything if collapsible is false", () => {
$("select").append("<option value='one' data-section='test'>One</option>");
$("select").append("<option value='two' data-section='test'>Two</option>");
$("select").append("<option value='three' data-section='test'>Three</option>");
$("select").append("<option value='four' data-section='test/inner'>Four</option>");
$("select").append("<option value='five' data-section='test/inner2'>Five</option>");
$("select").append("<option value='Six' data-section='test/inner2'>Six</option>");
var options = {
collapsible: false,
startCollapsed: true
};
$("select").treeMultiselect(options);
var $section = Common.section();
assert.equal($section.length, 3);
var $title = $section.children("div.title");
assert.equal($title.length, 3);
Common.selection().each(function() {
assert.notOk($(this).hasClass("collapsed"));
});
$title.each(() => {
$(this).click();
Common.selection().each(function() {
assert.notOk($(this).hasClass("collapsed"));
});
});
});
it('can set a different section delimiter', () => {
var options = {
sectionDelimiter: '-'
};
$("select").append("<option value='one' data-section='top-inner'>One</option>");
$("select").append("<option value='two' data-section='top-inner'>Two</option>");
$("select").append("<option value='three' data-section='top-inner2'>Three</option>");
$("select").treeMultiselect(options);
var $selections = Common.selection();
assert.equal($selections.length, 3);
var $sections = Common.section();
assert.equal($selections.length, 3);
var $innerSections = $sections.first().children(".section");
assert.equal($innerSections.length, 2);
assert.equal(Common.textOf($innerSections.first().children('div.title')), 'inner');
assert.equal(Common.textOf($innerSections.last().children('div.title')), 'inner2');
});
it('can disable batch selection', () => {
var options = {
allowBatchSelection: false
};
$("select").append("<option value='one' data-section='test'>One</option>");
$("select").append("<option value='two' data-section='test'>Two</option>");
$("select").append("<option value='three' data-section='test'>Three</option>");
$("select").append("<option value='four' data-section='test/inner'>Four</option>");
$("select").append("<option value='five' data-section='test/inner2'>Five</option>");
$("select").append("<option value='Six' data-section='test/inner2'>Six</option>");
$("select").treeMultiselect(options);
assert.equal($("input.section[type=checkbox]").length, 0);
});
it('can disable section display for selected items', () => {
$("select").append("<option value='one' data-section='test' data-description='foobar' selected='selected'>One</option>");
var options = {
showSectionOnSelected: false
};
$("select").treeMultiselect(options);
var $selectedItem = Common.selection();
assert.equal($selectedItem.length, 1);
assert.equal($selectedItem.find("span.section-name").length, 0);
});
it('can freeze selections', () => {
$("select").append("<option value='one' data-section='test'>One</option>");
$("select").append("<option value='two' data-section='test' selected='selected'>Two</option>");
var options = {
freeze: true
};
$("select").treeMultiselect(options);
var $checkboxes = Common.selection().children("input[type=checkbox]");
assert.equal($checkboxes.length, 2);
$checkboxes.each(function() {
var $checkbox = $(this);
assert($checkbox.attr('disabled'));
});
var removeSpans = $("div.selected span.remove-selected");
assert.equal(removeSpans.length, 0);
});
it('section checkboxes are indetermine if some children are selected', () => {
$("select").append("<option value='one' data-section='test'>One</option>");
$("select").append("<option value='two' data-section='test' selected='selected'>Two</option>");
var options = {
freeze: true
};
$("select").treeMultiselect(options);
var $sectionCheckbox = Common.sectionCheckbox({text: 'test'});
assert.equal($sectionCheckbox.length, 1);
var checkbox = $sectionCheckbox[0];
assert(checkbox.indeterminate);
assert(!checkbox.checked);
});
it('applies only to one tree and not another', () => {
$("select").append("<option value='one' data-section='test'>One</option>");
$("select").treeMultiselect();
$("#fixture").append("<select id='frozen'></select>");
$("select#frozen").append("<option value='two' data-section='anothertest' selected='selected'>Two</option>");
var options = {
freeze: true
};
$("select#frozen").treeMultiselect(options);
var $frozenOption = Common.selection({text: 'Two'});
assert.equal($frozenOption.length, 1);
assert($frozenOption.find("input[type=checkbox]").attr('disabled'));
var $unfrozenOption = Common.selection({text: 'One'});
assert.equal($unfrozenOption.length, 1);
var $checkbox = $unfrozenOption.find("input[type=checkbox]");
assert.notOk($checkbox.attr('disabled'));
$checkbox.click();
var $unfrozenSelection = Common.selected({text: 'One'});
assert.equal($unfrozenSelection.length, 1);
assert.deepEqual($("select").val(), ['one']);
assert.deepEqual($("select#frozen").val(), ['two']);
});
it('hides side panel', () => {
$("select").append("<option value='one' data-section='test'>One</option>");
var options = {
hideSidePanel: true
};
$("select").treeMultiselect(options);
assert.equal($("div.selected").length, 0);
});
it('onlyBatchSelection gives checkboxes to only sections', () => {
$("select").append("<option value='one' data-section='test'>One</option>");
var options = {
onlyBatchSelection: true
};
$("select").treeMultiselect(options);
assert.equal($("input.section[type=checkbox]").length, 1);
assert.equal($("input.option[type=checkbox]").length, 0);
});
it('calls onChange with correct arguments when item is added', (done) => {
$("select").append("<option value='one' data-section='test' selected='selected'>One</option>");
$("select").append("<option value='two' data-section='test'>Two</option>");
var options = {
onChange: function(all, added, removed) {
assert.equal(all.length, 2);
assert.equal(added.length, 1);
assert.equal(removed.length, 0);
var expectedSecondSelections = [all[1], added[0]];
for (var i = 0; i < expectedSecondSelections.length; ++i) {
var selection = expectedSecondSelections[i];
assert.equal(selection.text, 'Two');
assert.equal(selection.value, 'two');
assert.isNull(selection.initialIndex);
assert.equal(selection.section, 'test');
}
assert.equal(all[0].text, 'One');
assert.equal(all[0].value, 'one');
assert.isNull(all[0].initialIndex);
assert.equal(all[0].section, 'test');
done();
}
};
$("select").treeMultiselect(options);
var $item = Common.selection({text: 'Two'});
assert.equal($item.length, 1);
$item.find("input[type=checkbox]").click();
});
it('calls onChange with correct arguments when item is removed', (done) => {
$("select").append("<option value='one' data-section='test' selected='selected'>One</option>");
$("select").append("<option value='two' data-section='test'>Two</option>");
var options = {
onChange: function(all, added, removed) {
assert.equal(all.length, 0);
assert.equal(added.length, 0);
assert.equal(removed.length, 1);
assert.equal(removed[0].text, 'One');
assert.equal(removed[0].value, 'one');
assert.isNull(removed[0].initialIndex);
assert.equal(removed[0].section, 'test');
done();
}
};
$("select").treeMultiselect(options);
var $item = Common.selection({text: 'One'});
assert.equal($item.length, 1);
$item.find("input[type=checkbox]").click();
});
it('fixes original select value when sorted', () => {
$("select").append("<option value='one' data-section='test' selected>One</option>");
$("select").append("<option value='two' data-section='test' selected>Two</option>");
$("select").treeMultiselect({ sortable: true });
assert.deepEqual($("select").val(), ['one', 'two']);
var $selected = Common.selected();
assert.equal($selected.length, 2);
var $one = $selected.first();
var $two = $selected.last();
assert($("div.selected").sortable('option', 'start'));
$("div.selected").sortable('option', 'start')(null, {
item: $one
});
$one.insertAfter($two);
assert($("div.selected").sortable('option', 'stop'));
$("div.selected").sortable('option', 'stop')(null, {
item: $one
});
assert.deepEqual($("select").val(), ['two', 'one']);
});
it('puts selected items in right order when sorted', () => {
$("select").append("<option value='one' data-section='test' selected>One</option>");
$("select").append("<option value='two' data-section='test' selected>Two</option>");
$("select").treeMultiselect({ sortable: true });
var $selected = Common.selected();
assert.equal($selected.length, 2);
var $one = $selected.first();
var $two = $selected.last();
Common.assertSelected($one, {text: 'One', value: 'one', section: 'test'})
Common.assertSelected($two, {text: 'Two', value: 'two', section: 'test'})
assert($("div.selected").sortable('option', 'start'));
$("div.selected").sortable('option', 'start')(null, {
item: $one
});
$one.insertAfter($two);
assert($("div.selected").sortable('option', 'stop'));
$("div.selected").sortable('option', 'stop')(null, {
item: $one
});
$selected = Common.selected();
assert.equal($selected.length, 2);
Common.assertSelected($selected.first(), {text: 'Two', value: 'two', section: 'test'})
Common.assertSelected($selected.last(), {text: 'One', value: 'one', section: 'test'})
});
it("doesn't do anything when sorted in same order", () => {
$("select").append("<option value='one' data-section='test' selected>One</option>");
$("select").append("<option value='two' data-section='test' selected>Two</option>");
$("select").treeMultiselect({ sortable: true });
var $selected = Common.selected();
assert.equal($selected.length, 2);
var $one = $selected.first();
var $two = $selected.last();
Common.assertSelected($one, {text: 'One', value: 'one', section: 'test'})
Common.assertSelected($two, {text: 'Two', value: 'two', section: 'test'})
assert($("div.selected").sortable('option', 'start'));
$("div.selected").sortable('option', 'start')(null, {
item: $one
});
assert($("div.selected").sortable('option', 'stop'));
$("div.selected").sortable('option', 'stop')(null, {
item: $one
});
$selected = Common.selected();
assert.equal($selected.length, 2);
Common.assertSelected($selected.first(), {text: 'One', value: 'one', section: 'test'})
Common.assertSelected($selected.last(), {text: 'Two', value: 'two', section: 'test'})
});
it('select all button works', () => {
$("select").append("<option value='one' data-section='test'>One</option>");
$("select").append("<option value='two' data-section='test'>Two</option>");
$("select").treeMultiselect({ enableSelectAll: true });
var $selectAll = $(".select-all");
assert.equal($selectAll.length, 1);
var $selectedItems = Common.selected();
assert.equal($selectedItems.length, 0);
assert.deepEqual($("select").val(), null);
$selectAll.click();
$selectedItems = Common.selected();
assert.equal($selectedItems.length, 2);
assert.deepEqual($("select").val(), ['one', 'two']);
});
it('unselect button works', () => {
$("select").append("<option value='one' data-section='test' selected='selected'>One</option>");
$("select").append("<option value='two' data-section='test' selected='selected'>Two</option>");
$("select").treeMultiselect({ enableSelectAll: true });
var $unselectAll = $(".unselect-all");
assert.equal($unselectAll.length, 1);
var $selectedItems = Common.selected();
assert.equal($selectedItems.length, 2);
assert.deepEqual($("select").val(), ['one', 'two']);
$unselectAll.click();
$selectedItems = Common.selected();
assert.equal($selectedItems.length, 0);
assert.deepEqual($("select").val(), null);
});
it('select all text option', () => {
$("select").append("<option value='one' data-section='test' selected='selected'>One</option>");
var selectAllText = "foobar";
$("select").treeMultiselect({ enableSelectAll: true, selectAllText: selectAllText });
var $selectAll = $(".select-all");
assert.equal($selectAll.text(), selectAllText);
});
it('unselect all text option', () => {
$("select").append("<option value='one' data-section='test' selected='selected'>One</option>");
var unselectAllText = "foobar";
$("select").treeMultiselect({ enableSelectAll: true, unselectAllText: unselectAllText });
var $unselectAll = $(".unselect-all");
assert.equal($unselectAll.text(), unselectAllText);
});
it('can have individual readonly attributes', () => {
$("select").append("<option value='one' data-section='test' selected='selected'>One</option>");
$("select").append("<option value='two' data-section='test' selected='selected' readonly>Two</option>");
$("select").treeMultiselect();
var $firstSelectionCheckbox = Common.selectionCheckbox({value: 'one'});
assert.equal($firstSelectionCheckbox.length, 1);
assert.isFalse($firstSelectionCheckbox.prop('disabled'));
$firstSelectionCheckbox = Common.selectionCheckbox({value: 'two'});
assert.equal($firstSelectionCheckbox.length, 1);
assert.isTrue($firstSelectionCheckbox.prop('disabled'));
});
it('has readonly attributes that still appear in select val', () => {
$("select").append("<option value='one' data-section='test' selected readonly>One</option>");
$("select").treeMultiselect();
assert.deepEqual($("select").val(), ['one'])
});
it('cannot remove readonly elements by selected elements', () => {
$("select").append("<option value='one' data-section='test' selected='selected' readonly>One</option>");
$("select").append("<option value='two' data-section='test' selected='selected'>Two</option>");
$("select").treeMultiselect();
var $selected = Common.selected();
assert.equal($selected.length, 2);
var $selectedCheckboxes = $selected.children("span.remove-selected");
assert.equal($selectedCheckboxes.length, 1);
$selectedCheckboxes.click();
$selected = Common.selected();
assert.equal($selected.length, 1);
$selected = Common.selected({value: 'one'});
assert.equal($selected.length, 1);
});
it('can set a maximum number of selections', () => {
$("select").append("<option value='one' data-section='test' selected='selected'>One</option>");
$("select").append("<option value='two' data-section='test' selected='selected'>Two</option>");
$("select").append("<option value='three' data-section='test' selected='selected'>Three</option>");
$("select").append("<option value='four' data-section='test' selected='selected'>Four</option>");
$("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("<option value='one' data-section='test' selected='selected'>One</option>");
$("select").append("<option value='two' data-section='test' selected='selected'>Two</option>");
$("select").append("<option value='three' data-section='test' selected='selected'>Three</option>");
$("select").append("<option value='four' data-section='test' selected='selected'>Four</option>");
$("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("<option value='one' data-section='test' selected='selected'>One</option>");
$("select").append("<option value='two' data-section='test' selected='selected'>Two</option>");
$("select").append("<option value='three' data-section='test' selected='selected'>Three</option>");
$("select").append("<option value='four' data-section='test' selected='selected'>Four</option>");
$("select").treeMultiselect({maxSelections: true});
assert.equal(Common.selected().length, 4);
})
});
================================================
FILE: test/integration/reloading.test.js
================================================
var Common = require('./common');
describe('Reloading', () => {
it('can reload tree', () => {
$("select").append("<option value='one' data-section='foo' selected='selected' data-description='One'>One</option>");
var trees = $("select").treeMultiselect();
var tree = trees[0];
assert.equal(Common.selection().length, 1);
assert.equal(Common.selection({text: 'One'}).length, 1);
$("select").append("<option value='two' data-section='foo' selected='selected' data-description='Two'>Two</option>");
tree.reload();
assert.equal(Common.selection().length, 2);
assert.equal(Common.selection({text: 'One'}).length, 1);
assert.equal(Common.selection({text: 'Two'}).length, 1);
});
it('reload saves user-changed options', () => {
$("select").append("<option value='one' data-section='foo' selected='selected' data-description='One'>One</option>");
var trees = $("select").treeMultiselect();
var tree = trees[0];
assert.equal(Common.selection().length, 1);
assert.equal(Common.selected().length, 1);
assert.equal($("select").val().length, 1);
$("select").val([]);
$("select").change();
$("select").append("<option value='two' data-section='foo' selected='selected' data-description='Two'>Two</option>");
tree.reload();
assert.equal(Common.selection().length, 2);
assert.equal(Common.selected().length, 1);
assert.deepEqual($("select").val(), ['one']);
});
});
================================================
FILE: test/integration/removing.test.js
================================================
var Common = require('./common');
describe('Removing', () => {
it('can remove tree', () => {
$("select").append("<option value='one' data-section='foo' selected='selected' data-description='One'>One</option>");
var trees = $("select").treeMultiselect();
var tree = trees[0];
assert.equal($(".tree-multiselect").length, 1);
tree.remove();
assert.equal($(".tree-multiselect").length, 0);
});
});
================================================
FILE: test/integration/search.test.js
================================================
var Common = require('./common');
function getVisibleSelections(props) {
return Common.selection(props).filter((_, el) => {
return el.getAttribute('searchhit') === 'true';
})
}
function getHiddenSelections(props) {
return Common.selection(props).filter((_, el) => {
return el.getAttribute('searchhit') === 'false';
})
}
describe('Search', () => {
var $input;
describe('default behavior', () => {
beforeEach(() => {
// value, section
$("select").append("<option value='abcde' data-section='s1'></option>");
// section
$("select").append("<option value='fghij' data-section='s1'>yyyyy</option>");
// text
$("select").append("<option value='KLMNOP' data-section='s2'>klmnop</option>");
// description
$("select").append("<option value='QRS' data-section='ttt/uuu/vvv' data-description='fox'></option>");
// description with spaces
$("select").append("<option value='passion' data-section='s3' data-description='Passion'></option>");
$("select").append("<option value='passionfruit' data-section='fruit' data-description='Passion Fruit'></option>");
$("select").treeMultiselect({searchable: true, enableSelectAll: true});
$input = $("input.search");
assert.equal($input.length, 1);
});
it('matches on value', () => {
['a', 'c', 'abc', 'cde', 'bcd', 'abcde', 'abcd'].forEach((searchTerm) => {
$input.val(searchTerm).trigger('input');
assert.equal(Common.selection({value: 'abcde'}).attr('searchhit'), 'true');
assert.equal(Common.selection({value: 'fghij'}).attr('searchhit'), 'false');
assert.equal(Common.selection({value: 'KLMNOP'}).attr('searchhit'), 'false');
assert.equal(Common.selection({value: 'QRS'}).attr('searchhit'), 'false');
});
['fghij', 'ghi', 'fgh'].forEach((searchTerm) => {
$input.val(searchTerm).trigger('input');
assert.equal(Common.selection({value: 'abcde'}).attr('searchhit'), 'false');
assert.equal(Common.selection({value: 'fghij'}).attr('searchhit'), 'true');
assert.equal(Common.selection({value: 'KLMNOP'}).attr('searchhit'), 'false');
assert.equal(Common.selection({value: 'QRS'}).attr('searchhit'), 'false');
});
['q', 'qr', 'qrs', 'rs'].forEach((searchTerm) => {
$input.val(searchTerm).trigger('input');
assert.equal(Common.selection({value: 'abcde'}).attr('searchhit'), 'false');
assert.equal(Common.selection({value: 'fghij'}).attr('searchhit'), 'false');
assert.equal(Common.selection({value: 'KLMNOP'}).attr('searchhit'), 'false');
assert.equal(Common.selection({value: 'QRS'}).attr('searchhit'), 'true');
});
});
it('matches on section', () => {
$input.val('s1').trigger('input');
assert.equal(Common.selection({value: 'abcde'}).attr('searchhit'), 'true');
assert.equal(Common.selection({value: 'fghij'}).attr('searchhit'), 'true');
assert.equal(Common.selection({value: 'KLMNOP'}).attr('searchhit'), 'false');
assert.equal(Common.selection({value: 'QRS'}).attr('searchhit'), 'false');
});
it('matches on text', () => {
$input.val('yyy').trigger('input');
assert.equal(Common.selection({value: 'abcde'}).attr('searchhit'), 'false');
assert.equal(Common.selection({value: 'fghij'}).attr('searchhit'), 'true');
assert.equal(Common.selection({value: 'KLMNOP'}).attr('searchhit'), 'false');
assert.equal(Common.selection({value: 'QRS'}).attr('searchhit'), 'false');
});
it('matches on description', () => {
$input.val('fox').trigger('input');
assert.equal(Common.selection({value: 'abcde'}).attr('searchhit'), 'false');
assert.equal(Common.selection({value: 'fghij'}).attr('searchhit'), 'false');
assert.equal(Common.selection({value: 'KLMNOP'}).attr('searchhit'), 'false');
assert.equal(Common.selection({value: 'QRS'}).attr('searchhit'), 'true');
});
it('hides sections with no nodes visible', () => {
$input.val('s1').trigger('input');
assert.equal(Common.section({text: 's1'}).attr('searchhit'), 'true');
assert.equal(Common.section({text: 's2'}).attr('searchhit'), 'false');
assert.equal(Common.section({text: 'ttt'}).attr('searchhit'), 'false');
assert.equal(Common.section({text: 'uuu'}).attr('searchhit'), 'false');
assert.equal(Common.section({text: 'vvv'}).attr('searchhit'), 'false');
$input.val('uuu').trigger('input');
assert.equal(Common.section({text: 's1'}).attr('searchhit'), 'false');
assert.equal(Common.section({text: 's2'}).attr('searchhit'), 'false');
assert.equal(Common.section({text: 'ttt'}).attr('searchhit'), 'true');
assert.equal(Common.section({text: 'uuu'}).attr('searchhit'), 'true');
assert.equal(Common.section({text: 'vvv'}).attr('searchhit'), 'true');
});
it('shows all sections when no search term is entered', () => {
$input.val('43t#Q%').trigger('input'); // no nodes should be shown
assert.equal(getVisibleSelections().length, 0);
$input.val('').trigger('input'); // no nodes should be shown
assert.equal(getHiddenSelections().length, 0);
});
it('only adds filtered selections when using section checkbox', () => {
$input.val('abcde').trigger('input');
assert.equal(Common.selected().length, 0);
Common.sectionCheckbox({text: 's1'}).click();
assert.equal(Common.selected().length, 1);
$input.val('fox').trigger('input');
assert.equal(Common.selected().length, 1);
var $tttCheckbox = Common.sectionCheckbox({text: 'ttt'})
$tttCheckbox.click();
assert.equal(Common.selected().length, 2);
$tttCheckbox.click();
assert.equal(Common.selected().length, 1);
});
it('only adds filtered selections when selecting and unselecting all', () => {
$input.val('s1').trigger('input');
var $selectAll = $(".select-all");
var $unselectAll = $(".unselect-all");
assert.equal($selectAll.length, 1);
assert.equal($unselectAll.length, 1);
assert.equal(Common.selected().length, 0);
$selectAll.click();
assert.equal(Common.selected().length, 2);
$input.val('abcde').trigger('input');
$unselectAll.click();
assert.equal(Common.selected().length, 1);
})
it('handles empty search queries', () => {
$input.val(' ').trigger('input');
assert.equal(getVisibleSelections().length, 0);
assert.equal(getHiddenSelections().length, 0);
})
it('handles search queries with spaces in them', () => {
$input.val('passion ').trigger('input');
assert.equal(getVisibleSelections().length, 2);
assert.equal(getHiddenSelections().length, 4);
$input.val('passion f ').trigger('input');
assert.equal(getVisibleSelections().length, 1);
assert.equal(getHiddenSelections().length, 5);
})
});
describe('custom search params', () => {
beforeEach(() => {
$("select").append("<option value='abcde' data-section='s1' data-description='xyz'>ayy</option>");
});
it('section only', () => {
$("select").treeMultiselect({searchable: true, searchParams: ['section']});
$input = $("input.search");
$input.val('s1').trigger('input');
assert.equal(getVisibleSelections().length, 1);
$input.val('abc').trigger('input');
assert.equal(getVisibleSelections().length, 0);
$input.val('ayy').trigger('input');
assert.equal(getVisibleSelections().length, 0);
$input.val('xyz').trigger('input');
assert.equal(getVisibleSelections().length, 0);
});
it('value only', () => {
$("select").treeMultiselect({searchable: true, searchParams: ['value']});
$input = $("input.search");
$input.val('s1').trigger('input');
assert.equal(getVisibleSelections().length, 0);
$input.val('abc').trigger('input');
assert.equal(getVisibleSelections().length, 1);
$input.val('ayy').trigger('input');
assert.equal(getVisibleSelections().length, 0);
$input.val('xyz').trigger('input');
assert.equal(getVisibleSelections().length, 0);
});
it('text and description', () => {
$("select").treeMultiselect({searchable: true, searchParams: ['text', 'description']});
$input = $("input.search");
$input.val('s1').trigger('input');
assert.equal(getVisibleSelections().length, 0);
$input.val('abc').trigger('input');
assert.equal(getVisibleSelections().length, 0);
$input.val('ayy').trigger('input');
assert.equal(getVisibleSelections().length, 1);
$input.val('xyz').trigger('input');
assert.equal(getVisibleSelections().length, 1);
});
});
});
================================================
FILE: test/integration/section-checkboxes.test.js
================================================
var Common = require('./common');
describe('Section Checkboxes', () => {
it('is all checked when all children are selected initially', () => {
$("select").append("<option value='one' data-section='foo' selected>One</option>");
$("select").append("<option value='two' data-section='foo' selected>Two</option>");
$("select").treeMultiselect();
assert.equal($("input[type=checkbox]").length, 3);
assert.equal($("input.option[type=checkbox]").length, 2);
assert.equal($("input.section[type=checkbox]").length, 1);
assert.equal($("input[type=checkbox]:checked").length, 3);
});
it('should all be checked when all children are selected', () => {
$("select").append("<option value='one' data-section='foo' selected>One</option>");
$("select").append("<option value='two' data-section='foo'>Two</option>");
$("select").treeMultiselect();
assert.equal($("input[type=checkbox]").length, 3);
assert.equal($("input.option[type=checkbox]").length, 2);
assert.equal($("input.section[type=checkbox]").length, 1);
assert.equal($("input.section[type=checkbox]:checked").length, 0);
assert.equal($("input.option[type=checkbox]:checked").length, 1);
Common.selection().last().children("input[type=checkbox]").click();
assert.equal($("input.section[type=checkbox]:checked").length, 1);
assert.equal($("input.option[type=checkbox]:checked").length, 2);
});
it('should not check top level parent if only one child section is completely checked', () => {
$("select").append("<option value='one' data-section='foo/bar' selected='selected'>One</option>");
$("select").append("<option value='two' data-section='foo/baz'>Two</option>");
$("select").treeMultiselect();
var $topLevel = Common.section({text: 'foo'});
assert.notOk($topLevel.find("> div.title > input.section[type=checkbox]").is(":checked"));
});
it('should uncheck parent sections when a child is unselected', () => {
$("select").append("<option value='one' data-section='foo' selected>One</option>");
$("select").treeMultiselect();
assert.equal($("input.section[type=checkbox]:checked").length, 1);
Common.selection().first().children("input.option[type=checkbox]").click();
assert.equal($("input.section[type=checkbox]:checked").length, 0);
});
it('checks nested titles', () => {
$("select").append("<option value='one' data-section='top/middle/inner'>One</option>");
$("select").treeMultiselect();
assert.equal(Common.sectionCheckbox().length, 3);
assert.equal(Common.selectionCheckbox().length, 1);
assert.equal($("input[type=checkbox]").length, 4);
assert.equal($("input[type=checkbox]:checked").length, 0);
var $middleSectionCheckbox = Common.sectionCheckbox({text: 'middle'});
$middleSectionCheckbox.click();
assert.equal($("div.title > input[type=checkbox]:checked").length, 3);
assert.equal($("div.item > input[type=checkbox]:checked").length, 1);
assert.equal($("input[type=checkbox]:checked").length, 4);
});
it('only checks relevant titles', () => {
$("select").append("<option value='one' data-section='top/middle/inner'>One</option>");
$("select").append("<option value='two' data-section='top'>Two</option>");
$("select").treeMultiselect();
assert.notOk(Common.sectionCheckbox({text: 'top'}).prop('checked'));
assert.notOk(Common.sectionCheckbox({text: 'middle'}).prop('checked'));
assert.notOk(Common.sectionCheckbox({text: 'inner'}).prop('checked'));
assert.notOk(Common.selectionCheckbox({text: 'One'}).prop('checked'));
Common.sectionCheckbox({text: 'middle'}).click();
assert.notOk(Common.sectionCheckbox({text: 'top'}).prop('checked'));
assert(Common.sectionCheckbox({text: 'middle'}).prop('checked'));
assert(Common.sectionCheckbox({text: 'inner'}).prop('checked'));
assert(Common.selectionCheckbox({text: 'One'}).prop('checked'));
});
it('checkbox is indeterminate when some children are selected', () => {
$("select").append("<option value='one' data-section='top/middle/inner' selected>One</option>");
$("select").append("<option value='two' data-section='top'>Two</option>");
$("select").treeMultiselect();
var $topCheckbox = Common.sectionCheckbox({text: 'top'});
assert.notOk($topCheckbox.prop('checked'));
assert($topCheckbox.prop('indeterminate'));
});
it('checkbox is not indeterminate when all children are selected', () => {
$("select").append("<option value='one' data-section='top' selected>One</option>");
$("select").append("<option value='two' data-section='top' selected>Two</option>");
$("select").treeMultiselect();
var $titleCheckbox = Common.sectionCheckbox({text: 'top'});
assert.notOk($titleCheckbox.prop('indeterminate'));
});
it('checkbox is not indeterminate when no children are selected', () => {
$("select").append("<option value='one' data-section='top'>One</option>");
$("select").append("<option value='two' data-section='top'>Two</option>");
$("select").treeMultiselect();
var $titleCheckbox = Common.sectionCheckbox({text: 'top'});
assert.notOk($titleCheckbox.prop('indeterminate'));
});
it('checkbox doesn\'t select unselected readonly children', () => {
$("select").append("<option value='one' data-section='top' readonly>One</option>");
$("select").append("<option value='two' data-section='top'>Two</option>");
$("select").treeMultiselect();
assert.equal(Common.selected().length, 0);
var $titleCheckbox = Common.sectionCheckbox({text: 'top'});
$titleCheckbox.click();
assert.equal(Common.selected().length, 1);
assert.equal(Common.selected({value: 'two'}).length, 1);
});
it('checkbox doesn\'t unselect selected readonly children', () => {
$("select").append("<option value='one' data-section='top' selected readonly>One</option>");
$("select").append("<option value='two' data-section='top' selected>Two</option>");
$("select").treeMultiselect();
assert.equal(Common.selected().length, 2);
var $titleCheckbox = Common.sectionCheckbox({text: 'top'});
$titleCheckbox.click();
assert.equal(Common.selected().length, 1);
assert.equal(Common.selected({value: 'one'}).length, 1);
});
});
================================================
FILE: test/integration/section-selections.test.js
================================================
var Common = require('./common');
describe('Section Selections', () => {
it('adds all child elements when section checkbox clicked', () => {
$("select").append("<option value='one' data-section='foo'>One</option>");
$("select").append("<option value='two' data-section='foo'>Two</option>");
$("select").treeMultiselect();
assert.deepEqual($("select").val(), null);
var $selected = Common.selected();
assert.equal($selected.length, 0);
var $checkbox = Common.sectionCheckbox();
assert.equal($checkbox.length, 1);
$checkbox.click();
assert.deepEqual($("select").val(), ['one', 'two']);
$selected = Common.selected();
assert.equal($selected.length, 2);
Common.assertSelected($selected[0], {text: 'One', value: 'one', section: 'foo'});
Common.assertSelected($selected[1], {text: 'Two', value: 'two', section: 'foo'});
});
it("doesn't add child elements twice when other child elements are selected", () => {
$("select").append("<option value='one' data-section='foo'>One</option>");
$("select").append("<option value='two' data-section='foo'>Two</option>");
$("select").append("<option value='three' data-section='foo/bar/baz' selected>Three</option>");
$("select").append("<option value='four' data-section='foo/bar/baz' selected>Four</option>");
$("select").treeMultiselect();
assert.deepEqual($("select").val(), ['three', 'four']);
var $selected = Common.selected();
assert.equal($selected.length, 2);
Common.assertSelected($selected[0], {text: 'Three', value: 'three', section: 'foo/bar/baz'});
Common.assertSelected($selected[1], {text: 'Four', value: 'four', section: 'foo/bar/baz'});
var $checkbox = Common.sectionCheckbox();
assert.equal($checkbox.length, 3);
$checkbox.first().click();
assert.deepEqual($("select").val(), ['three', 'four', 'one', 'two']);
$selected = Common.selected();
assert.equal($selected.length, 4);
Common.assertSelected($selected[0], {text: 'Three', value: 'three', section: 'foo/bar/baz'});
Common.assertSelected($selected[1], {text: 'Four', value: 'four', section: 'foo/bar/baz'});
Common.assertSelected($selected[2], {text: 'One', value: 'one', section: 'foo'});
Common.assertSelected($selected[3], {text: 'Two', value: 'two', section: 'foo'});
});
it('removes child elements when section unselected', () => {
$("select").append("<option value='one' data-section='foo' selected>One</option>");
$("select").append("<option value='two' data-section='foo' selected>Two</option>");
$("select").append("<option value='three' data-section='foo/bar/baz' selected>Three</option>");
$("select").append("<option value='four' data-section='foo/bar/baz' selected>Four</option>");
$("select").treeMultiselect();
assert.deepEqual($("select").val(), ['one', 'two', 'three', 'four']);
var $selected = Common.selected();
Common.assertSelected($selected[0], {text: 'One', value: 'one', section: 'foo'});
Common.assertSelected($selected[1], {text: 'Two', value: 'two', section: 'foo'});
Common.assertSelected($selected[2], {text: 'Three', value: 'three', section: 'foo/bar/baz'});
Common.assertSelected($selected[3], {text: 'Four', value: 'four', section: 'foo/bar/baz'});
var $checkbox = Common.sectionCheckbox({text: 'baz'});
assert.equal($checkbox.length, 1);
$checkbox.click();
assert.deepEqual($("select").val(), ['one', 'two']);
$selected = Common.selected();
assert.equal($selected.length, 2);
Common.assertSelected($selected[0], {text: 'One', value: 'one', section: 'foo'});
Common.assertSelected($selected[1], {text: 'Two', value: 'two', section: 'foo'});
});
});
================================================
FILE: test/integration/single-selections.test.js
================================================
var Common = require('./common');
describe('Single Selection', () => {
it('can add an item', () => {
$("select").append("<option value='one' data-section='section'>One</option>");
$("select").treeMultiselect();
assert.deepEqual($("select").val(), null);
var $checkbox = Common.selectionCheckbox({checked: false});
assert.equal($checkbox.length, 1);
$checkbox.click();
var $checkboxChecked = Common.selectionCheckbox({checked: true});
assert.equal($checkboxChecked.length, 1);
assert.deepEqual($("select").val(), ['one']);
});
it('can remove an item', () => {
$("select").append("<option value='one' data-section='section' selected>One</option>");
$("select").treeMultiselect();
assert.deepEqual($("select").val(), ['one']);
var $checkbox = Common.selectionCheckbox({checked: true});
assert.equal($checkbox.length, 1);
$checkbox.click();
var $checkboxChecked = Common.selectionCheckbox({checked: true});
assert.equal($checkboxChecked.length, 0);
assert.deepEqual($("select").val(), null);
});
it('can add an item with the same text', () => {
$("select").append("<option value='one' data-section='section'>One</option>");
$("select").append("<option value='two' data-section='section' selected>One</option>");
$("select").treeMultiselect();
assert.deepEqual($("select").val(), ['two']);
var $checkboxChecked = Common.selectionCheckbox({checked: true});
assert.equal($checkboxChecked.length, 1);
var $checkbox = Common.selectionCheckbox();
assert.equal($checkbox.length, 2);
$checkbox.first().click();
$checkboxChecked = Common.selectionCheckbox({checked: true});
assert.equal($checkboxChecked.length, 2);
assert.deepEqual($("select").val(), ['two', 'one']);
});
it('can add an item with the same value as another', () => {
$("select").append("<option value='one' data-section='section'>One</option>");
$("select").append("<option value='one' data-section='section' selected>One2</option>");
$("select").append("<option value='one' data-section='section'>One3</option>");
$("select").treeMultiselect();
assert.deepEqual($("select").val(), ['one']);
var $checkboxChecked = Common.selectionCheckbox({checked: true});
assert.equal($checkboxChecked.length, 1);
var $checkbox = Common.selectionCheckbox();
assert.equal($checkbox.length, 3);
$checkbox.last().click();
$checkboxChecked = Common.selectionCheckbox({checked: true});
assert.equal($checkboxChecked.length, 2);
assert.deepEqual($("select").val(), ['one', 'one']);
});
it('can remove an item with the same value as another', () => {
$("select").append("<option value='one' data-section='section'>One</option>");
$("select").append("<option value='one' data-section='section' selected>One2</option>");
$("select").append("<option value='one' data-section='section' selected>One3</option>");
$("select").treeMultiselect();
assert.deepEqual($("select").val(), ['one', 'one']);
var $checkboxChecked = Common.selectionCheckbox({checked: true});
assert.equal($checkboxChecked.length, 2);
var $checkbox = Common.selectionCheckbox();
assert.equal($checkbox.length, 3);
$checkbox.last().click();
$checkboxChecked = Common.selectionCheckbox({checked: true});
assert.equal($checkboxChecked.length, 1);
assert.deepEqual($("select").val(), ['one']);
});
it('can remove an item by selected item remove button', () => {
$("select").append("<option value='one' data-section='section' selected>One</option>");
$("select").treeMultiselect();
assert.deepEqual($("select").val(), ['one']);
var $selected = Common.selected();
assert.equal($selected.length, 1);
var $removeSpan = $selected.children(".remove-selected");
$removeSpan.click();
assert.equal(Common.selected().length, 0);
assert.equal($("select").val(), null);
});
it('can remove an item by unchecking selection checkbox', () => {
$("select").append("<option value='one' data-section='section' selected>One</option>");
$("select").treeMultiselect();
assert.deepEqual($("select").val(), ['one']);
var $selections = Common.selection();
var $checkbox = $selections.children("input[type=checkbox]");
$checkbox.click();
assert.deepEqual($("select").val(), null);
});
it('removing an item does not remove any others', () => {
$("select").append("<option value='one' data-section='section' selected>One</option>");
$("select").append("<option value='two' data-section='section' selected>Two</option>");
$("select").append("<option value='three' data-section='section' selected>Three</option>");
$("select").treeMultiselect();
assert.deepEqual($("select").val(), ['one', 'two', 'three']);
var $selections = Common.selected();
assert.equal($selections.length, 3);
var $removeSpan = $selections.first().children(".remove-selected");
$removeSpan.click();
assert.equal(Common.selected().length, 2);
assert.deepEqual($("select").val(), ['two', 'three']);
});
it('fires change event on original select', (done) => {
$("select").append("<option value='one' data-section='section' selected>One</option>");
$("select").treeMultiselect();
$("select").change(function() {
done();
});
Common.selection().children("input[type=checkbox]").click();
});
});
================================================
FILE: test/test-performance.html
================================================
<!DOCTYPE html5>
<html>
<head>
<title>Tree Multiselect test</title>
<meta charset="UTF-8">
<script src="./vendor/jquery-1.11.3.min.js"></script>
<script src="./vendor/jquery-ui.min.js"></script>
<script src="../dist/jquery.tree-multiselect.js"></script>
<style>
* {
font-family: sans-serif;
}
</style>
<link rel="stylesheet" href="../dist/jquery.tree-multiselect.min.css">
</head>
<body>
<select id="test-select" multiple="multiple">
<option value="blueberry" data-section="Smoothies">Blueberry</option>
<option value="strawberry" data-section="Smoothies">Strawberries</option>
<option value="peach" data-section="Smoothies">Peach</option>
<option value="milk tea" data-section="Smoothies/Bubble Tea">Milk Tea</option>
<option value="green apple" data-section="Smoothies/Bubble Tea">Green Apple</option>
<option value="passion fruit" data-section="Smoothies/Bubble Tea" data-description="The greatest flavor" selected="selected">Passion Fruit</option>
</select>
<script type="text/javascript">
var $select = $('#test-select');
for (var ii = 0; ii < 2000; ++ii) {
var $option = $('<option value="fruit' + ii +'" data-section="Smoothies/' + ii + '" data-description="The greatest flavor" selected="selected">Passion Fruit</option>');
$select.append($option);
}
var time = new Date();
console.profile('tree-multiselect');
$select.treeMultiselect({ enableSelectAll: true, sortable: true, searchable: true });
console.profileEnd();
console.log("time elapsed - " + (new Date() - time) + "ms");
</script>
</body>
</html>
================================================
FILE: test/test.html
================================================
<!DOCTYPE html5>
<html>
<head>
<title>Tree Multiselect test</title>
<meta charset="UTF-8">
<style>
* {
font-family: sans-serif;
}
</style>
<link rel="stylesheet" href="../dist/jquery.tree-multiselect.min.css">
<script src="./vendor/jquery-1.11.3.min.js"></script>
<script src="./vendor/jquery-ui.min.js"></script>
<script src="../dist/jquery.tree-multiselect.js"></script>
</head>
<body>
<select id="test-select" multiple="multiple">
<option value="blueberry" data-section="Smoothies">Blueberry</option>
<option value="longan" data-description="Really good :o" selected="selected">Longan</option>
<option value="milk tea" data-section="Smoothies/Bubble Tea">Milk Tea</option>
<option value="green apple" data-section="Smoothies/Bubble Tea">Green Apple</option>
<option value="passion fruit" data-section="Smoothies/Bubble Tea" data-description="The greatest flavor" selected="selected">Passion Fruit</option>
<option value="strawberry" data-section="Smoothies">Strawberries</option>
<option value="peach" data-section="Smoothies">Peach</option>
</select>
<select id="test-select-2" multiple="multiple">
<option value="foo" data-section="section/ayy/lmao">foo</option>
<option value="bar" data-section="section/ayy/lmao" readonly>bar</option>
<option value="disabled1" selected readonly>readonly and selected item</option>
<option value="disabled1" readonly>readonly item</option>
</select>
<select id="test-select-3" multiple="multiple">
<option value="baz1" data-section="hidden stuff">baz1</option>
<option value="baz2" data-section="hidden stuff">baz2</option>
<option value="baz3" data-section="hidden stuff">baz3</option>
<option value="baz4" data-section="hidden stuff">baz4</option>
<option value="quux1" data-section="section">quux1</option>
<option value="quux2" data-section="section">quux2</option>
<option value="quux3" data-section="section">quux3</option>
<option value="quux4" data-section="section">quux4</option>
<option value="abc" data-section="section/ayy">abc</option>
<option value="taeyeon" data-section="section/ayy">태연</option>
<option value="ayyy" >ayyy</option>
<option value="disabled1" readonly>wow it's readonly</option>
<option value="disabled2" selected readonly>also readonly!</option>
</select>
<select id="test-select-4" multiple="multiple">
<option value="baz1" data-section="hidden stuff" selected data-index="1">baz1</option>
<option value="baz2" data-section="hidden stuff" selected data-index="1">baz2</option>
<option value="baz3" data-section="hidden stuff" selected data-index="1">baz3</option>
<option value="baz4" data-section="hidden stuff" selected data-index="1">baz4</option>
<option value="quux1" data-section="section" selected data-index="1">quux1</option>
<option value="quux2" data-section="section" selected data-index="1">quux2</option>
<option value="quux3" data-section="section" selected data-index="1">quux3</option>
<option value="quux4" data-section="section" selected data-index="1">quux4</option>
<option value="abc" data-section="section/ayy" selected data-index="1">abc</option>
<option value="ayyy" >ayyy</option>
<option value="disabled1" readonly>wow it's readonly</option>
<option value="disabled2" selected readonly>also readonly!</option>
</select>
<script type="text/javascript">
var tree1 = $("#test-select").treeMultiselect({ enableSelectAll: true, sortable: true });
var tree2 = $("#test-select-2").treeMultiselect({
searchable: true
});
var tree3 = $("#test-select-3").treeMultiselect({
allowBatchSelection: false,
enableSelectAll: true,
maxSelections: 4,
searchable: true,
sortable: true,
startCollapsed: true
});
var tree4 = $("#test-select-4").treeMultiselect({
allowBatchSelection: true,
enableSelectAll: true,
searchable: true,
sortable: true,
startCollapsed: true
});
</script>
</body>
</html>
================================================
FILE: test/unit/utility.test.js
================================================
var Ast = require('ast');
var Util = require('utility');
describe('Utility', () => {
it('asserts', () => {
var func = () => {
Util.assert(true);
}
assert.doesNotThrow(func);
func = () => {
Util.assert(false);
}
assert.throws(func);
func = () => {
Util.assert(NaN);
}
assert.throws(func);
});
it('getKey', () => {
var el = $("<div data-key='4'></div>")[0];
assert.equal(Util.getKey(el), 4);
el = $("<div></div>")[0];
assert.isNaN(Util.getKey(el));
el = $("<div data-key='foobar'></div>")[0];
assert.isNaN(Util.getKey(el));
});
it('isInteger', () => {
assert.isTrue(Util.isInteger(1));
assert.isTrue(Util.isInteger(-1));
assert.isTrue(Util.isInteger(0));
assert.isTrue(Util.isInteger(" 3 "));
assert.isFalse(Util.isInteger(true));
assert.isFalse(Util.isInteger(null));
assert.isFalse(Util.isInteger([]));
assert.isFalse(Util.isInteger(function () {}));
})
describe('array', () => {
it('subtract', () => {
var arr1 = [1, 2, 5, 7, 0];
var arr2 = [2, 7, 8];
Util.array.subtract(arr1, arr2);
assert.deepEqual(arr1, [1, 5, 0]);
arr1 = [];
arr2 = [];
Util.array.subtract(arr1, arr2);
assert.deepEqual(arr1, []);
arr1 = [];
arr2 = [0];
Util.array.subtract(arr1, arr2);
assert.deepEqual(arr1, []);
arr1 = [6, 8, 1256];
arr2 = [];
Util.array.subtract(arr1, arr2);
assert.deepEqual(arr1, [6, 8, 1256]);
arr1 = ["foo", "bar"];
arr2 = ["baz"];
Util.array.subtract(arr1, arr2);
assert.deepEqual(arr1, ["foo", "bar"]);
});
it('uniq', () => {
var arr = [1, 2, 5, 7, 0];
Util.array.uniq(arr);
assert.deepEqual(arr, [1, 2, 5, 7, 0]);
arr = [];
Util.array.uniq(arr);
assert.deepEqual(arr, []);
arr = ["abc", "abc", "ghi"];
Util.array.uniq(arr);
assert.deepEqual(arr, ["abc", "ghi"]);
arr = [123, 678, 900, 123];
Util.array.uniq(arr);
assert.deepEqual(arr, [123, 678, 900]);
});
it('removeFalseyExceptZero', () => {
var arr = [1, 4, 0, null, NaN, undefined, 3];
Util.array.removeFalseyExceptZero(arr);
assert.deepEqual(arr, [1, 4, 0, 3]);
});
it('moveEl in-place', () => {
var arr = [0, 1, 2, 3, 4, 5];
Util.array.moveEl(arr, 5, 1);
assert.deepEqual(arr, [0, 5, 1, 2, 3, 4]);
arr = [0, 1, 2, 3, 4];
Util.array.moveEl(arr, 4, 0);
assert.deepEqual(arr, [4, 0, 1, 2, 3]);
arr = [0, 1, 2, 3, 4];
Util.array.moveEl(arr, 0, 4);
assert.deepEqual(arr, [1, 2, 3, 4, 0]);
});
it('intersect', () => {
var arr = [0, 1, 2, 3, 4, 5];
var arr2 = [0, 2, 4, 6];
Util.array.intersect(arr, arr2);
assert.deepEqual(arr, [0, 2, 4]);
arr = [1, 17, 536, 24];
arr2 = [536, 0, 0, 0];
Util.array.intersect(arr, arr2);
assert.deepEqual(arr, [536]);
arr = [0, 0, 0, 0, 0, 0];
arr2 = [0, 1];
Util.array.intersect(arr, arr2);
assert.deepEqual(arr, [0, 0, 0, 0, 0, 0]);
});
it('intersectMany', () => {
var arrays = [[1, 3, 5, 7], [2, 3, 6, 7], [3, 9]];
assert.deepEqual(Util.array.intersectMany(arrays), [3]);
arrays = [[1, 3, 5, 7], [2, 3, 6, 7], [9]];
assert.deepEqual(Util.array.intersectMany(arrays), []);
arrays = [[1, 2, 3, 4], [4, 5, 6, 7], [8, 9]];
assert.deepEqual(Util.array.intersectMany(arrays), []);
arrays = [[1, 2, 3, 4], [1, 2, 3, 4], [1, 2, 3, 4]];
assert.deepEqual(Util.array.intersectMany(arrays), [1, 2, 3, 4]);
arrays = [[9, 10, 11, 12], [1, 2, 3, 4], [1, 2, 3, 4]];
assert.deepEqual(Util.array.intersectMany(arrays), []);
});
it('flatten', () => {
assert.deepEqual(Util.array.flatten([[1], [2], [3, [[4], 'foo', 'bar']]]), [1, 2, 3, 4, 'foo', 'bar']);
assert.deepEqual(Util.array.flatten([]), []);
assert.deepEqual(Util.array.flatten(null), null);
assert.deepEqual(Util.array.flatten('foo'), 'foo');
});
});
describe('dom', () => {
it('creates nodes with correct tag', () => {
var node = Util.dom.createNode('div');
assert.equal(node.tagName, 'DIV');
node = Util.dom.createNode('span');
assert.equal(node.tagName, 'SPAN');
});
it('creates nodes with correct properties', () => {
var props = {
foo: 'bar',
baz: 'over 9000',
text: 'foo'
}
var node = Util.dom.createNode('div', props);
assert.equal(node.attributes.length, 2);
assert.equal(node.getAttribute('foo'), props.foo);
assert.equal(node.getAttribute('baz'), props.baz);
assert.equal(node.textContent, props.text);
});
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');
assert(section.isSection());
assert.isFalse(section.isItem());
});
it('creates selection node with value as text', () => {
var option = Ast.createItem({
id: 0,
value: 'val',
text: null
});
var node = Util.dom.createSelection(option, 0, true, true);
assert.equal(node.getAttribute('data-value'), 'val');
});
});
});
gitextract_btu2m2g5/
├── .babelrc
├── .circleci/
│ └── config.yml
├── .eslintrc.yml
├── .gitignore
├── .npmignore
├── Gruntfile.js
├── LICENSE
├── README.md
├── bower.json
├── conf/
│ └── karma.js
├── dist/
│ ├── jquery.tree-multiselect.css
│ └── jquery.tree-multiselect.js
├── package.json
├── release.sh
├── sass/
│ └── style.scss
├── src/
│ ├── tree-multiselect/
│ │ ├── ast/
│ │ │ ├── common.js
│ │ │ ├── index.js
│ │ │ ├── item.js
│ │ │ └── section.js
│ │ ├── main.js
│ │ ├── search.js
│ │ ├── tree.js
│ │ ├── ui-builder.js
│ │ └── utility/
│ │ ├── array.js
│ │ ├── dom.js
│ │ └── index.js
│ └── tree-multiselect.js
└── test/
├── integration/
│ ├── common.js
│ ├── initial-load.test.js
│ ├── interactivity.test.js
│ ├── options.test.js
│ ├── reloading.test.js
│ ├── removing.test.js
│ ├── search.test.js
│ ├── section-checkboxes.test.js
│ ├── section-selections.test.js
│ └── single-selections.test.js
├── test-performance.html
├── test.html
└── unit/
└── utility.test.js
SYMBOL INDEX (43 symbols across 11 files)
FILE: dist/jquery.tree-multiselect.js
function r (line 2) | function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==...
function Item (line 64) | function Item(obj) {
function Section (line 115) | function Section(obj) {
function treeMultiselect (line 161) | function treeMultiselect(opts) {
function mergeDefaultOptions (line 184) | function mergeDefaultOptions(options) {
function Search (line 215) | function Search(searchHitAttr, astItems, astSections, searchParams) {
function splitWord (line 365) | function splitWord(word) {
function _toConsumableArray (line 386) | function _toConsumableArray(arr) { return _arrayWithoutHoles(arr) || _it...
function _nonIterableSpread (line 388) | function _nonIterableSpread() { throw new TypeError("Invalid attempt to ...
function _unsupportedIterableToArray (line 390) | function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (ty...
function _iterableToArray (line 392) | function _iterableToArray(iter) { if (typeof Symbol !== "undefined" && i...
function _arrayWithoutHoles (line 394) | function _arrayWithoutHoles(arr) { if (Array.isArray(arr)) return _array...
function _arrayLikeToArray (line 396) | function _arrayLikeToArray(arr, len) { if (len == null || len > arr.leng...
function Tree (line 408) | function Tree(id, $originalSelect, params) {
function UiBuilder (line 929) | function UiBuilder($el, hideSidePanel) {
function filterInPlace (line 964) | function filterInPlace(arr, pred) {
FILE: src/tree-multiselect/ast/common.js
constant SEARCH_HIT_ATTR (line 1) | const SEARCH_HIT_ATTR = 'searchhit';
constant SEARCH_HIT_ATTR_VAL_TRUE (line 2) | const SEARCH_HIT_ATTR_VAL_TRUE = 'true';
constant SEARCH_HIT_ATTR_VAL_FALSE (line 3) | const SEARCH_HIT_ATTR_VAL_FALSE = 'false';
FILE: src/tree-multiselect/ast/item.js
function Item (line 5) | function Item (obj) {
FILE: src/tree-multiselect/ast/section.js
function Section (line 5) | function Section (obj) {
FILE: src/tree-multiselect/main.js
function treeMultiselect (line 5) | function treeMultiselect (opts) {
function mergeDefaultOptions (line 29) | function mergeDefaultOptions (options) {
FILE: src/tree-multiselect/search.js
constant MAX_SAMPLE_SIZE (line 3) | const MAX_SAMPLE_SIZE = 3;
function Search (line 5) | function Search (searchHitAttr, astItems, astSections, searchParams) {
function splitWord (line 145) | function splitWord (word) {
FILE: src/tree-multiselect/tree.js
constant SEARCH_HIT_ATTR (line 6) | const SEARCH_HIT_ATTR = 'searchhit';
function Tree (line 8) | function Tree (id, $originalSelect, params) {
FILE: src/tree-multiselect/ui-builder.js
function UiBuilder (line 1) | function UiBuilder ($el, hideSidePanel) {
FILE: src/tree-multiselect/utility/array.js
function filterInPlace (line 2) | function filterInPlace (arr, pred) {
FILE: test/integration/common.js
function createFixtureSelect (line 5) | function createFixtureSelect() {
method assertSelection (line 27) | assertSelection(el, params) {
method assertSelected (line 34) | assertSelected(el, params) {
method textOf (line 46) | textOf(el) {
method find (line 57) | find(container, options) {
method findCheckbox (line 78) | findCheckbox(container, options) {
method selection (line 88) | selection(options) {
method selected (line 92) | selected(options) {
method section (line 96) | section(options) {
method selectionCheckbox (line 101) | selectionCheckbox(options) {
method sectionCheckbox (line 105) | sectionCheckbox(options) {
FILE: test/integration/search.test.js
function getVisibleSelections (line 3) | function getVisibleSelections(props) {
function getHiddenSelections (line 9) | function getHiddenSelections(props) {
Condensed preview — 40 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (168K chars).
[
{
"path": ".babelrc",
"chars": 39,
"preview": "{\n \"presets\": [\"@babel/preset-env\"]\n}\n"
},
{
"path": ".circleci/config.yml",
"chars": 620,
"preview": "version: \"2.1\"\norbs:\n browser-tools: circleci/browser-tools@1.4.0\njobs:\n build:\n docker:\n - image: cimg/node:1"
},
{
"path": ".eslintrc.yml",
"chars": 367,
"preview": "env:\n browser: true\n es6: true\nextends: standard\nglobals:\n jQuery: true\n module: true\n exports: true\n require: tru"
},
{
"path": ".gitignore",
"chars": 42,
"preview": "bower_components/\nnode_modules/\ncoverage/\n"
},
{
"path": ".npmignore",
"chars": 38,
"preview": "*\n!dist/\n!src/\n!package.json\n!LICENSE\n"
},
{
"path": "Gruntfile.js",
"chars": 2304,
"preview": "const sass = require('node-sass');\n\nmodule.exports = function(grunt) {\n grunt.initConfig({\n\n pkg: grunt.file.readJSO"
},
{
"path": "LICENSE",
"chars": 1079,
"preview": "The MIT License (MIT)\n\nCopyright (c) 2015 Patrick Tsai\n\nPermission is hereby granted, free of charge, to any person obta"
},
{
"path": "README.md",
"chars": 6535,
"preview": "## jQuery Tree Multiselect\n[](https://circ"
},
{
"path": "bower.json",
"chars": 593,
"preview": "{\n \"name\": \"tree-multiselect\",\n \"description\": \"jQuery multiple select with nested options\",\n \"main\": [\n \"src/tree"
},
{
"path": "conf/karma.js",
"chars": 1547,
"preview": "module.exports = function(config) {\n config.set({\n\n basePath: '../',\n\n frameworks: ['browserify', 'mocha', 'chai'"
},
{
"path": "dist/jquery.tree-multiselect.css",
"chars": 3772,
"preview": "/* jQuery Tree Multiselect v2.6.3 | (c) Patrick Tsai | MIT Licensed */\ndiv.tree-multiselect {\n border: 2px solid #D8D8D"
},
{
"path": "dist/jquery.tree-multiselect.js",
"chars": 34545,
"preview": "/* jQuery Tree Multiselect v2.6.3 | (c) Patrick Tsai | MIT Licensed */\n(function(){function r(e,n,t){function o(i,f){if("
},
{
"path": "package.json",
"chars": 1595,
"preview": "{\n \"name\": \"tree-multiselect\",\n \"version\": \"2.6.3\",\n \"description\": \"jQuery multiple select with nested options\",\n \""
},
{
"path": "release.sh",
"chars": 294,
"preview": "#!/usr/bin/env bash\nset -x\nVERSION=$(node -e \"console.log(require('./package.json').version);\")\ngrunt release\ngit add .\n"
},
{
"path": "sass/style.scss",
"chars": 2907,
"preview": "$color-border-light: #D8D8D8;\n$color-border-dark: #676767;\n\n$color-bg-dark: #777;\n$color-bg-light: #EAEAEA;\n\ndiv.tree-mu"
},
{
"path": "src/tree-multiselect/ast/common.js",
"chars": 600,
"preview": "const SEARCH_HIT_ATTR = 'searchhit';\nconst SEARCH_HIT_ATTR_VAL_TRUE = 'true';\nconst SEARCH_HIT_ATTR_VAL_FALSE = 'false';"
},
{
"path": "src/tree-multiselect/ast/index.js",
"chars": 299,
"preview": "const Item = require('./item');\nconst Section = require('./section');\n\nexports.createLookup = function (arr) {\n return "
},
{
"path": "src/tree-multiselect/ast/item.js",
"chars": 1140,
"preview": "const AstCommon = require('./common');\n\nconst Util = require('../utility');\n\nfunction Item (obj) {\n obj = obj || {};\n\n "
},
{
"path": "src/tree-multiselect/ast/section.js",
"chars": 947,
"preview": "const AstCommon = require('./common');\n\nconst Util = require('../utility');\n\nfunction Section (obj) {\n obj = obj || {};"
},
{
"path": "src/tree-multiselect/main.js",
"chars": 1116,
"preview": "let Tree = require('./tree');\n\nlet uniqueId = 0;\n\nfunction treeMultiselect (opts) {\n let options = mergeDefaultOptions("
},
{
"path": "src/tree-multiselect/search.js",
"chars": 4280,
"preview": "let Util = require('./utility');\n\nconst MAX_SAMPLE_SIZE = 3;\n\nfunction Search (searchHitAttr, astItems, astSections, sea"
},
{
"path": "src/tree-multiselect/tree.js",
"chars": 15281,
"preview": "let Ast = require('./ast');\nlet Search = require('./search');\nlet UiBuilder = require('./ui-builder');\nlet Util = requir"
},
{
"path": "src/tree-multiselect/ui-builder.js",
"chars": 705,
"preview": "function UiBuilder ($el, hideSidePanel) {\n let $tree = jQuery('<div class=\"tree-multiselect\"></div>');\n\n let $selectio"
},
{
"path": "src/tree-multiselect/utility/array.js",
"chars": 2689,
"preview": "// keeps if pred is true\nfunction filterInPlace (arr, pred) {\n var idx = 0;\n for (var ii = 0; ii < arr.length; ++ii) {"
},
{
"path": "src/tree-multiselect/utility/dom.js",
"chars": 3059,
"preview": "exports.createNode = function (tag, props) {\n var node = document.createElement(tag);\n\n if (props) {\n for (var key "
},
{
"path": "src/tree-multiselect/utility/index.js",
"chars": 440,
"preview": "exports.array = require('./array');\n\nexports.assert = function (bool, message) {\n if (!bool) {\n throw new Error(mess"
},
{
"path": "src/tree-multiselect.js",
"chars": 99,
"preview": "(($) => {\n 'use strict';\n $.fn.treeMultiselect = require('./tree-multiselect/main');\n})(jQuery);\n"
},
{
"path": "test/integration/common.js",
"chars": 2593,
"preview": "chai.config.includeStack = true;\n\nvar $fixture = null;\nvar selectCount = 1;\nfunction createFixtureSelect() {\n var selec"
},
{
"path": "test/integration/initial-load.test.js",
"chars": 9577,
"preview": "var Common = require('./common');\n\ndescribe('Initial Load', () => {\n it('creates container', () => {\n assert.equal($"
},
{
"path": "test/integration/interactivity.test.js",
"chars": 3170,
"preview": "var Common = require('./common');\n\ndescribe('Interactivity', () => {\n it('data-description pops up when moused over', ("
},
{
"path": "test/integration/options.test.js",
"chars": 18871,
"preview": "var Common = require('./common');\n\ndescribe('Options', () => {\n it('is collapsible', () => {\n $(\"select\").append(\"<o"
},
{
"path": "test/integration/reloading.test.js",
"chars": 1458,
"preview": "var Common = require('./common');\n\ndescribe('Reloading', () => {\n it('can reload tree', () => {\n $(\"select\").append("
},
{
"path": "test/integration/removing.test.js",
"chars": 425,
"preview": "var Common = require('./common');\n\ndescribe('Removing', () => {\n it('can remove tree', () => {\n $(\"select\").append(\""
},
{
"path": "test/integration/search.test.js",
"chars": 8800,
"preview": "var Common = require('./common');\n\nfunction getVisibleSelections(props) {\n return Common.selection(props).filter((_, el"
},
{
"path": "test/integration/section-checkboxes.test.js",
"chars": 6294,
"preview": "var Common = require('./common');\n\ndescribe('Section Checkboxes', () => {\n it('is all checked when all children are sel"
},
{
"path": "test/integration/section-selections.test.js",
"chars": 3725,
"preview": "var Common = require('./common');\n\ndescribe('Section Selections', () => {\n it('adds all child elements when section che"
},
{
"path": "test/integration/single-selections.test.js",
"chars": 5467,
"preview": "var Common = require('./common');\n\ndescribe('Single Selection', () => {\n it('can add an item', () => {\n $(\"select\")."
},
{
"path": "test/test-performance.html",
"chars": 1695,
"preview": "<!DOCTYPE html5>\n<html>\n <head>\n <title>Tree Multiselect test</title>\n\n <meta charset=\"UTF-8\">\n\n <script src=\""
},
{
"path": "test/test.html",
"chars": 4241,
"preview": "<!DOCTYPE html5>\n<html>\n <head>\n <title>Tree Multiselect test</title>\n\n <meta charset=\"UTF-8\">\n\n <style>\n "
},
{
"path": "test/unit/utility.test.js",
"chars": 5584,
"preview": "var Ast = require('ast');\nvar Util = require('utility');\n\ndescribe('Utility', () => {\n it('asserts', () => {\n var fu"
}
]
About this extraction
This page contains the full source code of the patosai/tree-multiselect.js GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 40 files (155.1 KB), approximately 41.2k tokens, and a symbol index with 43 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.