├── LICENSE.txt ├── README.md ├── css └── style.css ├── docs ├── CHANGELOG.md └── CONTRIBUTING.md ├── favicon.ico ├── img ├── gif │ └── searching.gif ├── logo_70.png └── logo_sm.png ├── index.html ├── js └── main.js ├── lib └── index.d.ts ├── package.json ├── searchAreaControl ├── css │ └── sac-style.css └── searchAreaControl.js └── test.html /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017, Ioannis Kapantzakis 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![npm version](https://badge.fury.io/js/searchareacontrol.svg)](https://badge.fury.io/js/searchareacontrol) 2 | 3 | # SearchAreaControl 4 | 5 | SearchAreaControl is a complete jQuery plugin that let's you **display**, **search** and **select** multiple items of a tree data structure. 6 | 7 | Github **repo** 8 | 9 | Visit the demo page **here** 10 | 11 | #### Table Of Contents 12 | 13 | 14 | - [Dependencies](#dependencies) 15 | - [Installation](#installation) 16 | - [Initialization](#initialization) 17 | - [Options](#options) 18 | - [Methods](#methods) 19 | - [Events](#events) 20 | - [Contributing](#contributing) 21 | - [Changelog](#changelog) 22 | 23 | 24 | ## Dependencies 25 | 26 | In order to start using SearchAreaControl you have to include **jQuery** first. 27 | 28 | ## Installation 29 | 30 | Include the `.css` and `.js` file and you're ready to use it! 31 | 32 | **CSS** 33 | 34 | 35 | 36 | **jQuery** 37 | 38 | 39 | 40 | ### NPM 41 | You can install SearchAreaControl via npm like this: 42 | 43 | npm install searchareacontrol 44 | 45 | ### Typescript 46 | Typescript declaration file is included in the package and it will be automatically available after `npm install`. 47 | 48 | 49 | ## Initialization 50 | 51 | All you need to do in order to initialize the SearchAreaControl plugin is a simple HTML element (button, span, div or anything): 52 | 53 | **HTML** 54 | 55 |

363 | 364 | 365 | 366 |

Now, let's use setSelectedNodes method to initialize the plugin with a selected node. We actually initialize the plugin first, and then call the appropriate method.

367 | 368 |
369 |
370 |
Select node
371 |

372 |

var btn = $('#myButton');
373 | 
374 | // Initialize plugin
375 | btn.searchAreaControl({ 
376 |     data: myData,
377 |     mainButton: {
378 |         defaultText: 'Cars'
379 |     } 
380 | });
381 | 
382 | // Call "setSelectedNodes" method to select one node by "data-id" attribute
383 | btn.searchAreaControl('setSelectedNodes', false, [55]);
384 |

385 |

386 | 387 |

388 |
389 |
390 | 391 |

There are several methods available to use in order to perform various tasks.

392 | 393 |
394 |
395 |
Examples
396 | 397 | 398 |
399 |
setSelectedNodes()
400 |
401 |

Select one or more nodes.

402 |

403 |
$(document).on('click', '#setSelectedNodes_button', function() {      
404 |     $('#myButton').searchAreaControl('setSelectedNodes',false,[57]);
405 | });
406 |

407 |
408 |
409 | 410 | 411 |
412 |
getSelectedByAttribute()
413 |
414 |

You can use getSelectedByAttribute() method to get the selected nodes by specific attribute. Here, we can use the data-id attribute.

415 |

416 |
$(document).on('click', '#getSelectedByAttribute_button', function() {      
417 |     var selected = $('#myButton').searchAreaControl('getSelectedByAttribute','data-id');
418 | });
419 |

420 |

421 |

422 | 423 |
424 |

425 |
426 |
427 | 428 | 429 |
430 |
setDisabled()
431 |
432 |

You can use setDisabled() to enable or disable the button.

433 |

434 |
$(document).on('click', '#setDisabled_button', function() {          
435 |     $('#myButton').searchAreaControl('setDisabled',true);
436 | });
437 |

438 | 439 | 440 |

441 |
442 |
443 | 444 |
445 |
446 | 447 |

There is a number of options you can provide in order to customize the appearance and behaviour of the plugin.

448 |

The following table demonstate some of them. For a complete list of all the available options view the README.

449 | 450 |
451 |
Options
452 | 453 | 454 | 455 | 456 | 457 | 458 | 459 | 460 | 461 | 462 | 463 | 464 | 465 | 466 | 467 | 468 | 469 | 470 | 471 | 472 | 473 | 474 | 475 | 476 | 477 | 478 | 479 | 480 | 481 | 482 | 483 | 484 | 485 | 486 | 487 | 488 | 489 | 490 | 491 | 492 | 493 | 494 | 495 | 496 | 497 | 498 | 499 |
NameTypeDefault valueDescription
dataobject[]Provides the data that are going to be displayed
multiSelectbooleantrueSet to false if you want to be able to select only one item at time
columnsnumber2Set the number of columns that the data will be rendered
selectionByAttributestring'data-id'Set the attribute that you want to select upon. The plugin will search for this attribute to match with the provided search string to filter all the items of the data array
modallHeader.textstring'Search'The text that is going to be displayed in the modal header
mainButton.defaultTextstring'Items'Set the default button text
500 |
501 | 502 |

We have already seen some of the available methods in action.

503 |

The following table demonstate some of them. For a complete list of all the available methods view the README.

504 | 505 |
506 |
Methods
507 | 508 | 509 | 510 | 511 | 512 | 513 | 514 | 515 | 516 | 517 | 518 | 519 | 520 | 521 | 522 | 523 | 524 | 525 | 526 | 527 | 528 | 529 | 530 | 531 | 532 | 533 | 534 | 535 | 536 | 537 | 538 | 539 | 540 | 541 | 542 | 543 | 544 | 545 | 546 | 547 | 548 |
NameParametersReturnDescription
getData()noarrayGet the control's datasource object (array)
clearSelection()novoidClear selected items
getSelectedNodes()noobjectGet an object with two properties, selectedAll (boolean: All items are selected) and selectedNodes (array: Array of selected objects)
updateDatasource(data)data [object]voidUpdate the datasource
destroy()novoidDestroy the plugin
549 |
550 | 551 | 552 | 553 | 554 | 555 | 562 | 563 | 564 | 565 | 566 | 567 | 568 | 569 | 570 | 571 | 572 | -------------------------------------------------------------------------------- /js/main.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var data = [ 3 | { 4 | "code": null, 5 | "name": "Economy car", 6 | "nodeExpanded": false, 7 | "attributes": { 8 | "data-id": "1" 9 | }, 10 | "children": [ 11 | { 12 | "code": null, 13 | "name": "Microcar", 14 | "nodeSelected": true, 15 | "attributes": { 16 | "data-id": "11" 17 | }, 18 | "children": [] 19 | }, 20 | { 21 | "code": null, 22 | "name": "Hatchbacks", 23 | "attributes": { 24 | "data-id": "12" 25 | }, 26 | "children": [ 27 | { 28 | "code": null, 29 | "name": "Ultracompact car", 30 | "nodeSelected": true, 31 | "attributes": { 32 | "data-id": "121" 33 | }, 34 | "children": null 35 | }, 36 | { 37 | "code": null, 38 | "name": "City car", 39 | "attributes": { 40 | "data-id": "122" 41 | }, 42 | "children": null 43 | }, 44 | { 45 | "code": null, 46 | "name": "Supermini/subcompact car", 47 | "attributes": { 48 | "data-id": "123" 49 | }, 50 | "children": null 51 | } 52 | ] 53 | } 54 | ] 55 | }, 56 | { 57 | "code": null, 58 | "name": "Family car", 59 | "attributes": { 60 | "data-id": "2" 61 | }, 62 | "children": [ 63 | { 64 | "code": null, 65 | "name": "Small family car/compact car", 66 | "nodeSelected": true, 67 | "attributes": { 68 | "data-id": "21" 69 | }, 70 | "children": null 71 | }, 72 | { 73 | "code": null, 74 | "name": " Large family / mid-size", 75 | "attributes": { 76 | "data-id": "22" 77 | }, 78 | "children": null 79 | } 80 | ] 81 | }, 82 | { 83 | "code": null, 84 | "name": "Saloons / sedans", 85 | "attributes": { 86 | "data-id": "3" 87 | }, 88 | "children": [ 89 | { 90 | "code": null, 91 | "name": "Large family / mid-size", 92 | "attributes": { 93 | "data-id": "31" 94 | }, 95 | "children": null 96 | }, 97 | { 98 | "code": null, 99 | "name": "Full size / large", 100 | "attributes": { 101 | "data-id": "32" 102 | }, 103 | "children": null 104 | }, 105 | { 106 | "code": null, 107 | "name": "Crossover SUV", 108 | "attributes": { 109 | "data-id": "33" 110 | }, 111 | "children": null 112 | }, 113 | { 114 | "code": null, 115 | "name": "Minivans / MPVs", 116 | "attributes": { 117 | "data-id": "34" 118 | }, 119 | "children": null 120 | } 121 | ] 122 | }, 123 | { 124 | "code": null, 125 | "name": "Luxury vehicle", 126 | "attributes": { 127 | "data-id": "4" 128 | }, 129 | "children": [ 130 | { 131 | "code": null, 132 | "name": "Compact executive", 133 | "attributes": { 134 | "data-id": "41" 135 | }, 136 | "children": null 137 | }, 138 | { 139 | "code": null, 140 | "name": "Executive / mid-luxury", 141 | "attributes": { 142 | "data-id": "42" 143 | }, 144 | "children": null 145 | }, 146 | { 147 | "code": null, 148 | "name": "Full-size luxury / Grand saloon", 149 | "attributes": { 150 | "data-id": "43" 151 | }, 152 | "children": null 153 | }, 154 | { 155 | "code": null, 156 | "name": "Estate cars / station wagons", 157 | "attributes": { 158 | "data-id": "44" 159 | }, 160 | "children": null 161 | }, 162 | { 163 | "code": null, 164 | "name": "Compact executive", 165 | "attributes": { 166 | "data-id": "45" 167 | }, 168 | "children": null 169 | }, 170 | { 171 | "code": null, 172 | "name": "Executive / mid-luxury", 173 | "attributes": { 174 | "data-id": "46" 175 | }, 176 | "children": null 177 | }, 178 | { 179 | "code": null, 180 | "name": "Full-size luxury / Grand saloon", 181 | "attributes": { 182 | "data-id": "47" 183 | }, 184 | "children": null 185 | }, 186 | { 187 | "code": null, 188 | "name": "Estate cars / station wagons", 189 | "attributes": { 190 | "data-id": "48" 191 | }, 192 | "children": null 193 | }, 194 | { 195 | "code": null, 196 | "name": "Compact executive", 197 | "attributes": { 198 | "data-id": "49" 199 | }, 200 | "children": null 201 | }, 202 | { 203 | "code": null, 204 | "name": "Executive / mid-luxury", 205 | "attributes": { 206 | "data-id": "50" 207 | }, 208 | "children": null 209 | }, 210 | { 211 | "code": null, 212 | "name": "Full-size luxury / Grand saloon", 213 | "attributes": { 214 | "data-id": "51" 215 | }, 216 | "children": null 217 | }, 218 | { 219 | "code": null, 220 | "name": "Estate cars / station wagons", 221 | "attributes": { 222 | "data-id": "52" 223 | }, 224 | "children": null 225 | }, 226 | { 227 | "code": null, 228 | "name": "Compact executive", 229 | "attributes": { 230 | "data-id": "53" 231 | }, 232 | "children": null 233 | }, 234 | { 235 | "code": null, 236 | "name": "Executive / mid-luxury", 237 | "attributes": { 238 | "data-id": "54" 239 | }, 240 | "children": null 241 | }, 242 | { 243 | "code": null, 244 | "name": "Full-size luxury / Grand saloon", 245 | "attributes": { 246 | "data-id": "55" 247 | }, 248 | "children": null 249 | }, 250 | { 251 | "code": null, 252 | "name": "Estate cars / station wagons", 253 | "attributes": { 254 | "data-id": "56" 255 | }, 256 | "children": null 257 | }, 258 | { 259 | "code": null, 260 | "name": "Compact executive", 261 | "attributes": { 262 | "data-id": "57" 263 | }, 264 | "children": null 265 | }, 266 | { 267 | "code": null, 268 | "name": "Executive / mid-luxury", 269 | "attributes": { 270 | "data-id": "58" 271 | }, 272 | "children": null 273 | }, 274 | { 275 | "code": null, 276 | "name": "Full-size luxury / Grand saloon", 277 | "attributes": { 278 | "data-id": "59" 279 | }, 280 | "children": null 281 | }, 282 | { 283 | "code": null, 284 | "name": "Estate cars / station wagons", 285 | "attributes": { 286 | "data-id": "60" 287 | }, 288 | "children": null 289 | } 290 | ] 291 | }, 292 | { 293 | "code": null, 294 | "name": "Sports cars", 295 | "attributes": { 296 | "data-id": "5" 297 | }, 298 | "children": [ 299 | { 300 | "code": null, 301 | "name": "Hot hatch", 302 | "attributes": { 303 | "data-id": "51" 304 | }, 305 | "children": null 306 | }, 307 | { 308 | "code": null, 309 | "name": "Sports saloon / sports sedan", 310 | "attributes": { 311 | "data-id": "52" 312 | }, 313 | "children": null 314 | }, 315 | { 316 | "code": null, 317 | "name": "Sports car", 318 | "attributes": { 319 | "data-id": "53" 320 | }, 321 | "children": null 322 | }, 323 | { 324 | "code": null, 325 | "name": "Grand tourer", 326 | "attributes": { 327 | "data-id": "54" 328 | }, 329 | "children": null 330 | }, 331 | { 332 | "code": null, 333 | "name": "Supercar", 334 | "attributes": { 335 | "data-id": "55" 336 | }, 337 | "children": null 338 | }, 339 | { 340 | "code": null, 341 | "name": "Muscle car", 342 | "attributes": { 343 | "data-id": "56" 344 | }, 345 | "children": null 346 | }, 347 | { 348 | "code": null, 349 | "name": "Pony car", 350 | "attributes": { 351 | "data-id": "57" 352 | }, 353 | "children": null 354 | }, 355 | { 356 | "code": null, 357 | "name": "Convertible", 358 | "attributes": { 359 | "data-id": "58" 360 | }, 361 | "children": null 362 | } 363 | ] 364 | }, 365 | { 366 | "code": null, 367 | "name": "Off-roaders", 368 | "attributes": { 369 | "data-id": "6" 370 | }, 371 | "children": [ 372 | { 373 | "code": null, 374 | "name": "Sport utility vehicle", 375 | "attributes": { 376 | "data-id": "61" 377 | }, 378 | "children": null 379 | } 380 | ] 381 | }, 382 | { 383 | "code": null, 384 | "name": "Commercial vehicle", 385 | "attributes": { 386 | "data-id": "7" 387 | }, 388 | "children": [ 389 | { 390 | "code": null, 391 | "name": "Van", 392 | "attributes": { 393 | "data-id": "71" 394 | }, 395 | "children": null 396 | } 397 | ] 398 | } 399 | ]; 400 | 401 | var data2 = [ 402 | { 403 | "code": null, 404 | "name": "Test category 1", 405 | "attributes": { 406 | "data-id": "1" 407 | }, 408 | "children": [ 409 | { 410 | "code": null, 411 | "name": "Test 11", 412 | "attributes": { 413 | "data-id": "11" 414 | }, 415 | "children": null 416 | }, 417 | { 418 | "code": null, 419 | "name": "Test 12", 420 | "attributes": { 421 | "data-id": "12" 422 | }, 423 | "children": [ 424 | { 425 | "code": null, 426 | "name": "Test121", 427 | "attributes": { 428 | "data-id": "121" 429 | }, 430 | "children": null 431 | }, 432 | { 433 | "code": null, 434 | "name": "Test 122", 435 | "attributes": { 436 | "data-id": "122" 437 | }, 438 | "children": null 439 | }, 440 | { 441 | "code": null, 442 | "name": "Test 123", 443 | "attributes": { 444 | "data-id": "123" 445 | }, 446 | "children": null 447 | } 448 | ] 449 | } 450 | ] 451 | }, 452 | { 453 | "code": null, 454 | "name": "Test category 2", 455 | "attributes": { 456 | "data-id": "2" 457 | }, 458 | "children": [ 459 | { 460 | "code": null, 461 | "name": "Test 21", 462 | "attributes": { 463 | "data-id": "21" 464 | }, 465 | "children": null 466 | }, 467 | { 468 | "code": null, 469 | "name": "Test 22", 470 | "attributes": { 471 | "data-id": "22" 472 | }, 473 | "children": null 474 | } 475 | ] 476 | }, 477 | { 478 | "code": null, 479 | "name": "Test category 3", 480 | "attributes": { 481 | "data-id": "3" 482 | }, 483 | "children": [ 484 | { 485 | "code": null, 486 | "name": "Test 31", 487 | "attributes": { 488 | "data-id": "31" 489 | }, 490 | "children": null 491 | }, 492 | { 493 | "code": null, 494 | "name": "Test 32", 495 | "attributes": { 496 | "data-id": "32" 497 | }, 498 | "children": null 499 | }, 500 | { 501 | "code": null, 502 | "name": "Test 33", 503 | "attributes": { 504 | "data-id": "33" 505 | }, 506 | "children": null 507 | }, 508 | { 509 | "code": null, 510 | "name": "Test 34", 511 | "attributes": { 512 | "data-id": "34" 513 | }, 514 | "children": null 515 | } 516 | ] 517 | } 518 | ]; 519 | 520 | $(document).ready(function() { 521 | 522 | $('#myButton').searchAreaControl({ 523 | data: data, 524 | mainButton: { 525 | defaultText: 'Cars' 526 | } 527 | }); 528 | 529 | var btn2 = $('#myButton2'); 530 | btn2.searchAreaControl({ 531 | data: data, 532 | mainButton: { 533 | defaultText: 'Cars' 534 | } 535 | }); 536 | btn2.searchAreaControl('setSelectedNodes', false, [55]); 537 | 538 | $('#myButton3').searchAreaControl({ 539 | data: data, 540 | mainButton: { 541 | defaultText: 'Cars', 542 | showAllText: true 543 | } 544 | }); 545 | 546 | $('#myButton4').searchAreaControl({ 547 | data: data, 548 | mainButton: { 549 | defaultText: 'Cars' 550 | } 551 | }); 552 | 553 | $('#myButton5').searchAreaControl({ 554 | data: data, 555 | mainButton: { 556 | defaultText: 'Cars' 557 | } 558 | }); 559 | 560 | 561 | // TEST ====================================================== // 562 | 563 | $('#btn1').searchAreaControl({ 564 | data: data, 565 | collapseNodes: true, 566 | //allNodesExpanded: false, 567 | mainButton: { 568 | defaultText: 'Cars', 569 | className: 'btn btn-success' 570 | }, 571 | localeData: { 572 | 'en': { 573 | 'Search': 'Search custom' 574 | } 575 | }, 576 | searchBox: { 577 | searchType: { 578 | startsWith: { 579 | text: 'Starts with', 580 | selected: false 581 | }, 582 | existsIn: { 583 | text: 'Exists in', 584 | selected: false 585 | }, 586 | regExp: { 587 | text: 'Regular expression', 588 | selected: true 589 | } 590 | } 591 | }, 592 | popupButtons: { 593 | selectHighlighted: { 594 | visible: true 595 | } 596 | } 597 | }); 598 | 599 | $('#btn2').searchAreaControl({ 600 | data: data2, 601 | //selectedNodes: ['11','121'], 602 | allNodesSelected: true, 603 | mainButton: { 604 | defaultText: 'Data 2' 605 | } 606 | }); 607 | 608 | $('#btn3').searchAreaControl({ 609 | data: data2, 610 | mainButton: { 611 | defaultText: 'Data 2' 612 | } 613 | }); 614 | }); 615 | 616 | $(document).on('click', '#selectPony', function() { 617 | $('#myButton3').searchAreaControl('setSelectedNodes',false,[57]); 618 | }); 619 | 620 | $(document).on('click', '#getSelectedByAttribute', function() { 621 | var selected = $('#myButton4').searchAreaControl('getSelectedByAttribute','data-id'); 622 | var result = (selected.length > 0) ? '[' + selected.join(',') + ']' : '[]'; 623 | $('#getSelectedByAttribute_result').html(result); 624 | }); 625 | 626 | $(document).on('click', '#setDisabled', function() { 627 | $('#myButton5').searchAreaControl('setDisabled',true); 628 | }); 629 | 630 | $(document).on('click', '#setEnabled', function() { 631 | $('#myButton5').searchAreaControl('setDisabled',false); 632 | }); 633 | 634 | // TEST ================================================================================ // 635 | 636 | $(document).on('click', '#updateDatasource_btn1', function() { 637 | $('#btn1').searchAreaControl('updateDatasource', data2); 638 | }); 639 | 640 | $(document).on('click', '#getPopup', function() { 641 | var popup1 = $('#btn1').searchAreaControl('getPopup'); 642 | console.log(popup1); 643 | //popup1.find('.sac-header-holder').css('border', '1px solid red'); 644 | }); 645 | 646 | $(document).on('click', '#setDisbaledNodes', function() { 647 | $('#btn1').searchAreaControl('setDisabledNodes', [21,22], true); 648 | }); 649 | 650 | $(document).on('click', '#enableAll', function() { 651 | $('#btn1').searchAreaControl('enableAllNodes'); 652 | }); 653 | 654 | $(document).on('click', '#destroy_btn1', function() { 655 | $('#btn1').searchAreaControl('destroy'); 656 | }); 657 | 658 | $(document).on('click', '#getSelectedByAttribute_btn1', function() { 659 | var selectedObj = $('#btn1').searchAreaControl('getSelectedNodes'); 660 | console.log(selectedObj); 661 | var selected = $('#btn1').searchAreaControl('getSelectedByAttribute'); 662 | var result = (selected.length > 0) ? '[' + selected.join(',') + ']' : '[]'; 663 | alert(result); 664 | }); 665 | 666 | $(document).on('click', '#destroy_btn2', function() { 667 | $('#btn2').searchAreaControl('destroy'); 668 | }); 669 | 670 | $(document).on('click', '#setSelectedNodes2', function() { 671 | $('#btn2').searchAreaControl('setSelectedNodes', false, [31,32], 'data-id'); 672 | }); 673 | 674 | $(document).on('click', '#getSelectedByAttribute_btn2', function() { 675 | var selected = $('#btn2').searchAreaControl('getSelectedByAttribute','data-id'); 676 | var result = (selected.length > 0) ? '[' + selected.join(',') + ']' : '[]'; 677 | alert(result); 678 | }); 679 | 680 | $(document).on('searchareacontrol.beforeinit', function(e,data) { 681 | console.log(data); 682 | }); 683 | 684 | $(document).on('searchareacontrol.afterinit', function(e,data) { 685 | console.log(data); 686 | }); 687 | 688 | $(document).on('searchareacontrol.popup.shown', function(e,data) { 689 | console.log(data); 690 | }); 691 | 692 | $(document).on('searchareacontrol.popup.hidden', function(e,data) { 693 | console.log(data); 694 | }); 695 | 696 | $(document).on('searchareacontrol.button.click', function(e,data) { 697 | console.log(data); 698 | }); 699 | 700 | $(document).on('searchareacontrol.selectedNodesChanged', function(e,data) { 701 | console.log(data); 702 | }); 703 | 704 | $(document).on('searchareacontrol.popup.beforeshow', function(e,data) { 705 | console.log('searchareacontrol.popup.beforeshow'); 706 | }); 707 | 708 | $(document).on('searchareacontrol.popup.beforehide', function(e,data) { 709 | console.log('searchareacontrol.popup.beforehide'); 710 | }); 711 | 712 | $(document).on('click', '#setLocale', function() { 713 | $('#btn2').searchAreaControl('setLocale', 'el'); 714 | }); 715 | 716 | $(document).on('click', '#btnModal', function() { 717 | $('#myModal').modal('show'); 718 | }); -------------------------------------------------------------------------------- /lib/index.d.ts: -------------------------------------------------------------------------------- 1 | // Type definitions for SearchAreaControl plugin 2 | // Project: SearchAreaControl 3 | // Definitions by: John Kapantzakis 4 | 5 | /// 6 | 7 | export interface SearchAreaControl { 8 | version: string; 9 | } 10 | 11 | export as namespace SAC; 12 | 13 | export interface IOptions { 14 | modalHeader?: IModalHeader, 15 | data?: IData[], 16 | multiSelect?: boolean, 17 | collapseNodes?: boolean, 18 | allNodesExpanded?: boolean, 19 | columns?: number, 20 | selectionByAttribute?: string, 21 | allNodesSelected?: boolean, 22 | selectedNodes?: string[], 23 | locales?: string, 24 | localeData?: any, 25 | searchBox?: ISearchBox, 26 | popupDimensions?: IPopupDimensionsCollection, 27 | mainButton?: IMainButton, 28 | popupButtons?: IPopupButtonsCollection 29 | } 30 | 31 | export interface IModalHeader { 32 | text?: string, 33 | className?: string, 34 | visible?: boolean 35 | } 36 | 37 | export interface IData { 38 | code?: string, 39 | name: string, 40 | attributes?: IDataAttributes, 41 | nodeExpanded?: boolean, 42 | nodeSelected?: boolean, 43 | children?: IData[] 44 | } 45 | 46 | export interface IDataAttributes { 47 | [key: string]: any 48 | } 49 | 50 | export interface ISearchBox { 51 | enabled?: boolean, 52 | minCharactersSearch?: number, 53 | searchBoxClass?: string, 54 | searchBoxPlaceholder?: string, 55 | showSelectedItemsBox?: boolean, 56 | selectedItemsLabelVisible?: boolean, 57 | selectedItemsLabelText?: string, 58 | hideNotFound?: boolean, 59 | searchType?: ISearchTypeCollection 60 | } 61 | 62 | export interface ISearchTypeCollection { 63 | startsWith?: ISearchType, 64 | existsIn?: ISearchType, 65 | regExp?: ISearchType 66 | } 67 | 68 | export interface ISearchType { 69 | text?: string, 70 | selected?: boolean 71 | } 72 | 73 | export interface IPopupDimensionsCollection { 74 | [key: string]: IPopupDimensions 75 | } 76 | 77 | export interface IPopupDimensions { 78 | width?: string, 79 | left?: string, 80 | marginLeft?: string 81 | } 82 | 83 | export interface IMainButton { 84 | defaultText?: string, 85 | className?: string, 86 | defaultNoneText?: string, 87 | defaultAllText?: string, 88 | showAllText?: boolean, 89 | maxSelectedViewText?: number 90 | } 91 | 92 | export interface IPopupButtonsCollection { 93 | selectAll?: IPopupButton, 94 | diselectAll?: IPopupButton, 95 | invertSelection?: IPopupButton, 96 | close?: IPopupButton, 97 | cancel?: IPopupButton, 98 | selectHighlighted?: IPopupButton 99 | } 100 | 101 | export interface IPopupButton { 102 | text?: string, 103 | className?: string, 104 | visible?: boolean, 105 | callback?: any, 106 | index?: number 107 | } 108 | 109 | export interface ISelectedNodesObject { 110 | selectedAll: boolean, 111 | selectedNodes: ISelectedNodes[] 112 | } 113 | 114 | export interface ISelectedNodes { 115 | text: string, 116 | attributes: INodeAttributes 117 | } 118 | 119 | export interface INodeAttributes { 120 | [key: string]: any 121 | } 122 | 123 | declare global { 124 | interface JQuery { 125 | 126 | /** 127 | * Get the control's datasource object (array) 128 | */ 129 | searchAreaControl(method: 'getData'): IData[]; 130 | 131 | /** 132 | * Provide a collection of attributes (selectionByAttribute) to be selected, or set allSelected to true to select all available items. Provide an optional byAttribute parameter to indicate the attribute to select. If not provided, the plugin will try to search selectionByAttribute option value. 133 | */ 134 | searchAreaControl(method: 'setSelectedNodes', allSelected: boolean, collection: Array, byAttribute?: string): void; 135 | 136 | /** 137 | * Clear selected items. 138 | */ 139 | searchAreaControl(method: 'clearSelection'): void; 140 | 141 | /** 142 | * Get an object of type ISelectedNodesObject 143 | */ 144 | searchAreaControl(method: 'getSelectedNodes'): ISelectedNodesObject; 145 | 146 | /** 147 | * Get an array of specific attribute values of the selected items. If attributeName is not provided, the plugin will try to search selected node by selectionByAttribute option 148 | */ 149 | searchAreaControl(method: 'getSelectedByAttribute', attributeName?: string): Array; 150 | 151 | /** 152 | * Set disabled nodes specifying a collection of node attribute values (by default selectionByAttribute attribute). Set diselectDisabled to true if you want to diselect the nodes to be disabled. Provide an optional byAttribute parameter to indicate the attribute to select. If not provided, the plugin will try to search selectionByAttribute option value. 153 | */ 154 | searchAreaControl(method: 'setDisabledNodes', collection: Array, diselectDisabled: boolean, byAttribute?: string): void; 155 | 156 | /** 157 | * Enable all nodes 158 | */ 159 | searchAreaControl(method: 'enableAllNodes'): void; 160 | 161 | /** 162 | * Disable all nodes 163 | */ 164 | searchAreaControl(method: 'disableAllNodes'): void; 165 | 166 | /** 167 | * Get disabled state of main button 168 | */ 169 | searchAreaControl(method: 'getDisabled'): boolean; 170 | 171 | /** 172 | * Toggle main button disabled state 173 | */ 174 | searchAreaControl(method: 'setDisabled', disable: boolean): void; 175 | 176 | /** 177 | * Get the popup jQuery object 178 | */ 179 | searchAreaControl(method: 'getPopup'): JQuery; 180 | 181 | /** 182 | * Update the datasource 183 | */ 184 | searchAreaControl(method: 'updateDatasource', data: IData[]): void; 185 | 186 | /** 187 | * Set new locale and re-init plugin 188 | */ 189 | searchAreaControl(method: 'setLocale', locale: string): void; 190 | 191 | /** 192 | * Destroy the plugin instance 193 | */ 194 | searchAreaControl(method: 'destroy'): void; 195 | 196 | /** 197 | * Generic method function 198 | */ 199 | searchAreaControl(method?: IOptions | string, arg1?: any, arg2?: any, arg3?: any): void; 200 | } 201 | 202 | interface JQueryStatic { 203 | SearchAreaControl: SearchAreaControl; 204 | } 205 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "searchareacontrol", 3 | "version": "2.8.6", 4 | "description": "A jQuery plugin used to display, search and select multiple items", 5 | "main": "searchAreaControl/searchAreaControl.js", 6 | "types": "lib/index.d.ts", 7 | "scripts": { 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/kapantzak/SearchAreaControl.git" 13 | }, 14 | "keywords": [ 15 | "jquery-plugin", 16 | "ecosystem:jquery", 17 | "multiselect" 18 | ], 19 | "dependencies": { 20 | "@types/jquery": "*" 21 | }, 22 | "author": "John Kapantzakis", 23 | "license": "MIT", 24 | "bugs": { 25 | "url": "https://github.com/kapantzak/SearchAreaControl/issues" 26 | }, 27 | "homepage": "https://github.com/kapantzak/SearchAreaControl#readme" 28 | } 29 | -------------------------------------------------------------------------------- /searchAreaControl/css/sac-style.css: -------------------------------------------------------------------------------- 1 | body.sac-noscroll { 2 | overflow: hidden !important; 3 | } 4 | 5 | .sac-popup-overlay { 6 | position: fixed; 7 | top:0; 8 | left:0; 9 | right:0; 10 | bottom:0; 11 | z-index: 9995; 12 | background-color: rgba(0,0,0,0.8); 13 | } 14 | 15 | .sac-popup { 16 | position: fixed; 17 | top:100px; 18 | padding: 15px; 19 | box-shadow: 0 0 5px 0 rgba(0,0,0,0.5); 20 | transform: scale(0.9); 21 | opacity: 0; 22 | transition: all 150ms ease-in-out; 23 | border-radius: 4px; 24 | z-index: 9999; 25 | background-color: #fafafa; 26 | } 27 | 28 | .sac-popup.sac-popup-visible { 29 | opacity: 1; 30 | transform: scale(1); 31 | } 32 | 33 | .sac-header-holder { 34 | width:100%; 35 | margin-bottom: 15px; 36 | } 37 | 38 | .sac-header-h1 { 39 | margin: 0; 40 | font-size: 1.6em; 41 | margin-bottom: 20px; 42 | } 43 | 44 | /* Input area */ 45 | .sac-input-holder { 46 | width: 100%; 47 | margin-bottom: 15px; 48 | } 49 | 50 | .sac-input-elem { 51 | width: 200px; 52 | height: 32px; 53 | border: 1px solid #d8d8d8; 54 | padding: 5px; 55 | box-sizing: border-box; 56 | } 57 | 58 | .sac-input-elem-last { 59 | border-top-right-radius: 4px; 60 | border-bottom-right-radius: 4px; 61 | } 62 | 63 | .sac-seacrh-combo-type { 64 | height: 32px; 65 | border: 1px solid #d8d8d8; 66 | padding: 5px; 67 | background-color: #f1f1f1; 68 | border-right: none; 69 | border-top-left-radius: 4px; 70 | border-bottom-left-radius: 4px; 71 | box-sizing: border-box; 72 | } 73 | 74 | /* List area */ 75 | .sac-popup .sac-ul { 76 | margin: 0; 77 | padding: 0; 78 | } 79 | 80 | .sac-popup .sac-ul > li { 81 | list-style: none; 82 | padding-left: 5px; 83 | } 84 | 85 | .sac-cols-holder { 86 | width: 100%; 87 | float: left; 88 | overflow-y: auto; 89 | padding: 15px 5px; 90 | max-height: 50vh; 91 | border: 1px solid #d8d8d8; 92 | border-radius: 4px; 93 | box-sizing: border-box; 94 | } 95 | 96 | .sac-popup .sac-column-div > .sac-ul { 97 | width:100%; 98 | float: left; 99 | } 100 | 101 | .sac-popup .sac-column-div > .sac-ul > li { 102 | width: 100%; 103 | float: left; 104 | padding-left: 0; 105 | padding-top: 10px; 106 | } 107 | 108 | .sa_holder:not(.sac-searching) > .sac-ul > li { 109 | display: inline-block; 110 | } 111 | 112 | .sac-popup .sac-column-div > .sac-ul > li > .sac-node-name { 113 | font-weight: bold; 114 | } 115 | 116 | .sac-popup .sac-column-div > .sac-ul { font-size: 18px } 117 | .sac-ul { font-size: 0.85em; line-height: 1.5 } 118 | 119 | .sac-node-name { 120 | padding: 5px 10px; 121 | position:relative; 122 | cursor:pointer; 123 | } 124 | 125 | ul:not(.sac-ul-top) .sac-node-name:hover { text-decoration:underline } 126 | 127 | ul:not(.sac-ul-top) .sac-node-name.sa-selected-item:before, 128 | .sac-node-selected:before { 129 | content: "\e013"; 130 | font-family: 'Glyphicons Halflings'; 131 | position:absolute; 132 | color:#13a562; 133 | } 134 | 135 | .sac-nodes-fixed ul:not(.sac-ul-top) .sac-node-name.sa-selected-item:before, 136 | .sac-nodes-fixed .sac-node-selected:before { 137 | left:-10px; 138 | } 139 | 140 | .sac-nodes-collapse ul:not(.sac-ul-top) .sac-node-name.sa-selected-item:before, 141 | .sac-nodes-collapse .sac-node-selected:before { 142 | right:-10px; 143 | } 144 | 145 | .toggleNodeCollapse { 146 | cursor: pointer; 147 | padding-right: 3px; 148 | } 149 | 150 | .toggleNodeCollapse > .fa-caret-right, 151 | .sac-node-collapsed > .toggleNodeCollapse > .fa-caret-down { 152 | display: none; 153 | } 154 | 155 | .sac-node-collapsed > .toggleNodeCollapse > .fa-caret-right { 156 | display: inline-block; 157 | } 158 | 159 | .sac-node-collapsed > .sac-ul { 160 | display: none 161 | } 162 | 163 | .sac-custom-numSpan { 164 | height: 32px; 165 | display: inline-block; 166 | padding: 5px 10px; 167 | border: 1px solid #d8d8d8; 168 | border-left: none; 169 | border-top-right-radius: 4px; 170 | border-bottom-right-radius: 4px; 171 | box-sizing: border-box; 172 | } 173 | 174 | .sac-custom-numSpan-num { 175 | display: inline-block; 176 | width:50px; 177 | padding-left: 5px; 178 | font-weight:bold; 179 | text-align: center; 180 | } 181 | 182 | .multiSelectMaxItemsMsg { display:none } 183 | .modalSelectedItemsMax .multiSelectMaxItemsMsg { display:block } 184 | 185 | /* Selected node --------------------------------------------------------------- */ 186 | 187 | /* .sac-node-selected:before { 188 | content: "\e013"; 189 | font-family: 'Glyphicons Halflings'; 190 | position:absolute; 191 | left:-10px; 192 | color:#13a562; 193 | } */ 194 | 195 | /* Disabled node --------------------------------------------------------------- */ 196 | 197 | .sac-node-name.sac-node-disabled { 198 | cursor: not-allowed; 199 | opacity: 0.5; 200 | pointer-events: none; 201 | } 202 | 203 | /* Footer ---------------------------------------------------------------------- */ 204 | 205 | .sac-footer { 206 | width:100%; 207 | float: left; 208 | margin-top: 25px; 209 | } 210 | 211 | .sac-footer button { 212 | margin-right: 5px; 213 | } 214 | 215 | /* Searching classes ----------------------------------------------------------- */ 216 | 217 | .sac-found-item > span { background-color: yellowgreen } 218 | .sac-searching.sac-searching-hide-not-found li { display: none } 219 | .sac-searching.sac-searching-hide-not-found .sac-found-category, 220 | .sac-searching.sac-searching-hide-not-found .sac-found-category li { display: block } 221 | 222 | /* Helpers --------------------------------------------------------------------- */ 223 | .sac-pull-left { float: left } 224 | 225 | .sac-noselect { 226 | -webkit-touch-callout: none; /* iOS Safari */ 227 | -webkit-user-select: none; /* Safari */ 228 | -khtml-user-select: none; /* Konqueror HTML */ 229 | -moz-user-select: none; /* Firefox */ 230 | -ms-user-select: none; /* Internet Explorer/Edge */ 231 | user-select: none; /* Non-prefixed version, currently 232 | supported by Chrome and Opera */ 233 | } 234 | -------------------------------------------------------------------------------- /searchAreaControl/searchAreaControl.js: -------------------------------------------------------------------------------- 1 | ; (function ($, window, document, undefined) { 2 | 3 | var pluginName = 'searchAreaControl'; 4 | 5 | function Plugin(element, options) { 6 | this.el = element; 7 | this.$el = $(element); 8 | this.opt = $.extend(true, {}, $.fn[pluginName].defaults, options); 9 | this.locales = $.extend(true, {}, $.fn[pluginName].locales, this.opt.localeData); 10 | this.rootClassName = 'elem-searchAreaControl'; 11 | this.popupID = null; 12 | this.init(); 13 | 14 | // Trigger 'searchareacontrol.afterinit' event 15 | var thisEl = this.$el; 16 | setTimeout(function () { 17 | thisEl.trigger('searchareacontrol.afterinit', [{ element: this.$el }]); 18 | }, 10); 19 | } 20 | 21 | Plugin.prototype = { 22 | 23 | init: function () { 24 | var $that = this; 25 | 26 | this.$el.trigger('searchareacontrol.beforeinit', [{ element: this.$el }]); 27 | 28 | this.$el.html(this._localize(this.opt.mainButton.defaultText)); 29 | this._setData_DataSource(this.opt.data); 30 | 31 | // Add root class 32 | this.$el.addClass(this.rootClassName); 33 | 34 | // Add main button class name, if provided 35 | var mainButtonClassName = this.opt.mainButton.className; 36 | if (mainButtonClassName.length > 0) { 37 | this.$el.addClass(mainButtonClassName); 38 | } 39 | 40 | this.$el.trigger('searchareacontrol.beforebuildpopup', [{ element: this.$el }]); 41 | this._buildPopup(pluginName); 42 | 43 | this.$el.trigger('searchareacontrol.beforeinitsearcharea', [{ element: this.$el }]); 44 | this._initSearchArea(); 45 | 46 | this.$el.on('click', function () { 47 | $that.$el.trigger('searchareacontrol.popup.beforeshow', [{ element: $that.$el }]); 48 | $that._togglePopup(true); 49 | }); 50 | }, 51 | 52 | /** 53 | * Build popup overlay and popup HTML markup 54 | */ 55 | _buildPopup: function (pluginName) { 56 | var thisPopupID = (this.popupID !== null) ? this.popupID : this._getNewPopupID(pluginName); 57 | var dimensions = this._getPopupDimensions(); 58 | var popup = $(''); 59 | $('body').append(popup); 60 | this.$el.attr('data-popupid', thisPopupID); 61 | }, 62 | 63 | /** 64 | * Get popup dimensions 65 | */ 66 | _getPopupDimensions: function () { 67 | var w = $(window).width(); 68 | var dimensions = this.opt.popupDimensions; 69 | var dimensionsObj = dimensions.max; 70 | for (var d in dimensions) { 71 | if (!isNaN(d) && w < parseInt(d)) { 72 | dimensionsObj = dimensions[d]; 73 | } 74 | } 75 | var style = 'width:' + dimensionsObj.width + ';'; 76 | style += 'left:' + dimensionsObj.left + ';'; 77 | style += 'margin-left:' + dimensionsObj.marginLeft + ';'; 78 | return style; 79 | }, 80 | 81 | /** 82 | * Recalculate and update popup dimensions 83 | */ 84 | _updatePopupDimensions: function () { 85 | var popup = $('#' + this.popupID); 86 | if (popup && popup.length > 0) { 87 | var dimensions = this._getPopupDimensions(); 88 | popup.attr('style', dimensions); 89 | } 90 | }, 91 | 92 | /** 93 | * Get unique popup id 94 | */ 95 | _getNewPopupID: function (pluginName) { 96 | var thisPopupID = pluginName + '_Popup_' + (this._uuidv4()); 97 | this.popupID = thisPopupID; 98 | return this.popupID; 99 | }, 100 | 101 | _uuidv4: function () { 102 | return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { 103 | var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8); 104 | return v.toString(16); 105 | }); 106 | }, 107 | 108 | _toggleBodyScrolling: function (scroll) { 109 | var body = $('body'); 110 | if (scroll === true) { 111 | body.removeClass('sac-noscroll'); 112 | } else { 113 | body.addClass('sac-noscroll'); 114 | } 115 | }, 116 | 117 | // Data: DataSource ----------------------------------------------------------------- // 118 | 119 | /** 120 | * Store datasource in memory 121 | */ 122 | _setData_DataSource: function (data) { 123 | $.data(this, pluginName + '_data', data); 124 | }, 125 | 126 | /** 127 | * Get stored datasource 128 | */ 129 | _getData_DataSource: function () { 130 | var data = $.data(this, pluginName + '_data'); 131 | return (data) ? data : null; 132 | }, 133 | 134 | // Data: SelectedNodes -------------------------------------------------------------- // 135 | 136 | /** 137 | * Store selected nodes object in memory 138 | */ 139 | _setData_SelectedNodes: function (data) { 140 | $.data(this, pluginName + '_selectedNodes', data); 141 | }, 142 | 143 | /** 144 | * Get stored selected nodes object 145 | */ 146 | _getData_SelectedNodes: function () { 147 | var data = $.data(this, pluginName + '_selectedNodes'); 148 | return (data) ? data : null; 149 | }, 150 | 151 | /** 152 | * Store selected nodes object on popup show 153 | */ 154 | _setData_SelectedNodesOnPopupShow: function (data) { 155 | $.data(this, pluginName + '_selectedNodesOnPopupShow', data); 156 | }, 157 | 158 | /** 159 | * Get the selected nodes object when the popup was opened 160 | */ 161 | _getData_SelectedNodesOnPopupShow: function () { 162 | var data = $.data(this, pluginName + '_selectedNodesOnPopupShow'); 163 | return (data) ? data : null; 164 | }, 165 | 166 | // SEARCH AREA (start) ============================================================== // 167 | 168 | /** 169 | * Toggle popup visibility state 170 | */ 171 | _togglePopup: function (show) { 172 | var popup = $('#' + this.popupID); 173 | if (popup && popup.length > 0) { 174 | var overlay = popup.closest('.sac-popup-overlay'); 175 | if (show === true) { 176 | this._toggleBodyScrolling(false); 177 | this._updatePopupDimensions(); 178 | overlay.show(); 179 | popup.addClass('sac-popup-visible'); 180 | this._setSearchBoxDimensions(); 181 | 182 | // Update selected nodes on popup show object 183 | var selectedNodes = this._getSelectedNodes(); 184 | this._setData_SelectedNodesOnPopupShow(selectedNodes); 185 | 186 | // Check for highlighted nodes on popup show 187 | this._checkForHighlightedNodes(); 188 | 189 | // Remove Bootstrap's custom event listener 190 | $(document).off('focusin.modal'); 191 | 192 | this.$el.trigger('searchareacontrol.popup.shown', [{ element: this.$el, popup: popup }]); 193 | } else { 194 | overlay.hide(); 195 | popup.removeClass('sac-popup-visible'); 196 | this._toggleBodyScrolling(true); 197 | this.$el.trigger('searchareacontrol.popup.hidden', [{ element: this.$el, popup: popup }]); 198 | } 199 | } 200 | }, 201 | 202 | /** 203 | * Initialize searchArea functionality 204 | */ 205 | _initSearchArea: function () { 206 | if (this.opt.modalHeader.visible === true) { 207 | this._buildHeader(); 208 | } 209 | if (this.opt.searchBox.enabled === true) { 210 | this._buildSearchBox(); 211 | this._addSearchBoxEventListeners(); 212 | } 213 | this._buildContent(); 214 | this._buildPopupButtons(); 215 | this._addEventListeners(); 216 | this._applySelection(); 217 | }, 218 | 219 | /** 220 | * Build modal header 221 | */ 222 | _buildHeader: function () { 223 | var popup = $('#' + this.popupID); 224 | if (popup && popup.length > 0) { 225 | var headerOpt = this.opt.modalHeader; 226 | var headerMarkup = '

' + this._localize(headerOpt.text) + '

'; 227 | var header = $(headerMarkup); 228 | popup.append(header); 229 | } 230 | }, 231 | 232 | /** 233 | * Build searchBox and searchBox holder and append to popup 234 | */ 235 | _buildSearchBox: function () { 236 | var popup = $('#' + this.popupID); 237 | if (popup && popup.length > 0) { 238 | 239 | var searchBoxHolder = document.createElement('div'); 240 | searchBoxHolder.className = 'sac-input-holder'; 241 | 242 | // Search type 243 | var comboOpts = this.opt.searchBox.searchType; 244 | var combo = document.createElement('select'); 245 | combo.className = 'sac-seacrh-combo-type'; 246 | 247 | var opt_startsWith = document.createElement('option'); 248 | opt_startsWith.setAttribute('value', '0'); 249 | opt_startsWith.innerHTML = this._localize(comboOpts.startsWith.text); 250 | 251 | var opt_existsIn = document.createElement('option'); 252 | opt_existsIn.setAttribute('value', '1'); 253 | opt_existsIn.innerHTML = this._localize(comboOpts.existsIn.text); 254 | 255 | var opt_regExp = document.createElement('option'); 256 | opt_regExp.setAttribute('value', '2'); 257 | opt_regExp.innerHTML = this._localize(comboOpts.regExp.text); 258 | 259 | if (comboOpts.existsIn.selected === true) { 260 | opt_existsIn.setAttribute('selected', 'selected'); 261 | } else if (comboOpts.regExp.selected === true) { 262 | opt_regExp.setAttribute('selected', 'selected'); 263 | } else { 264 | opt_startsWith.setAttribute('selected', 'selected'); 265 | } 266 | 267 | combo.appendChild(opt_startsWith); 268 | combo.appendChild(opt_existsIn); 269 | combo.appendChild(opt_regExp); 270 | searchBoxHolder.appendChild(combo); 271 | 272 | // Search box 273 | var searchBox = document.createElement('input'); 274 | searchBox.type = 'text'; 275 | searchBox.name = 'searchInput'; 276 | var hiddenNumClass = (this.opt.searchBox.showSelectedItemsBox === true) ? '' : ' sac-input-elem-last '; 277 | searchBox.className = 'sac-input-elem ' + hiddenNumClass + this.opt.searchBox.searchBoxClass; 278 | searchBox.setAttribute('placeholder', this.opt.searchBox.searchBoxPlaceholder); 279 | searchBoxHolder.appendChild(searchBox); 280 | 281 | // Selected nodes num 282 | if (this.opt.searchBox.showSelectedItemsBox) { 283 | var numSpan = $(''); 284 | var numSpanTxt = $('' + this._localize(this.opt.searchBox.selectedItemsLabelText) + ''); 285 | var numSpanNum = $('0'); 286 | if (this.opt.searchBox.selectedItemsLabelVisible === true) { 287 | numSpan.append(numSpanTxt); 288 | } 289 | numSpan.append(numSpanNum); 290 | $(searchBoxHolder).append(numSpan); 291 | } 292 | popup.append($(searchBoxHolder)); 293 | } 294 | }, 295 | 296 | _setSearchBoxDimensions: function () { 297 | var popup = $('#' + this.popupID); 298 | if (popup && popup.length > 0) { 299 | var inputHolder = popup.find('.sac-input-holder'); 300 | var inputHolder_w = inputHolder.outerWidth(); 301 | var searchTypeCombo = popup.find('.sac-seacrh-combo-type'); 302 | var searchTypeCombo_w = searchTypeCombo.outerWidth(); 303 | var selectedNum = popup.find('.sac-custom-numSpan'); 304 | var selectedNum_w = selectedNum.outerWidth(); 305 | var searchInput = popup.find('.sac-input-elem'); 306 | var searchInput_w = inputHolder_w - (searchTypeCombo_w + selectedNum_w) - 2; 307 | searchInput.css('width', searchInput_w + 'px'); 308 | } 309 | }, 310 | 311 | /** 312 | * Build content 313 | */ 314 | _buildContent: function () { 315 | var $that = this; 316 | var popup = $('#' + this.popupID); 317 | if (popup && popup.length > 0) { 318 | 319 | // Split data array to the number of columns parts 320 | var dataLen = this.opt.data.length; 321 | var optCols = this.opt.columns; 322 | var addition = (dataLen % optCols > 0) ? 1 : 0; 323 | var chunk = parseInt(dataLen / optCols) + addition; 324 | 325 | var colsHolder = document.createElement('div'); 326 | var collapseClass = (this.opt.collapseNodes === true) ? 'sac-nodes-collapse' : 'sac-nodes-fixed'; 327 | colsHolder.className = 'sac-cols-holder ' + collapseClass; 328 | 329 | var i, counter; 330 | for (i = 0, counter = 1; i < dataLen; i += chunk, counter++) { 331 | 332 | var dataPart = this.opt.data.slice(i, i + chunk); 333 | var colDivWidth = 100 / this.opt.columns; 334 | var colDivStyle = 'width:' + colDivWidth + '%'; 335 | 336 | if (dataPart.length > 0) { 337 | // Create column div 338 | var col = document.createElement('div'); 339 | col.setAttribute('id', 'sa_col_' + counter); 340 | col.setAttribute('style', colDivStyle); 341 | col.className = 'sac-column-div sac-pull-left'; 342 | 343 | // Create the top level ul 344 | var ul = document.createElement('ul'); 345 | ul.className = 'sac-ul sac-ul-top'; 346 | 347 | // Append children 348 | $.each(dataPart, function (i, node) { 349 | var child = $that._getListItem({ 350 | opt: this.opt, 351 | node: node 352 | }); 353 | // Append children to top level ul 354 | ul.appendChild(child); 355 | }); 356 | 357 | col.appendChild(ul); 358 | colsHolder.appendChild(col); 359 | } 360 | 361 | } 362 | 363 | popup.append($(colsHolder)); 364 | } 365 | }, 366 | 367 | /** 368 | * Get list item 369 | * @param {object} obj Node object 370 | */ 371 | _getListItem: function (obj) { 372 | var $that = this; 373 | if (obj) { 374 | var opt = obj.opt; 375 | var node = obj.node; 376 | 377 | var code = node.code; 378 | var name = node.name; 379 | var attributes = node.attributes; 380 | var nodeExpanded = node.nodeExpanded; 381 | var nodeSelected = node.nodeSelected; 382 | var children = node.children; 383 | 384 | var li = document.createElement('li'); 385 | var arrow = $(''); 386 | var liSpan = document.createElement('span'); 387 | 388 | if ($that.opt.collapseNodes === true && ($that.opt.allNodesExpanded === false || nodeExpanded === false)) { 389 | li.className = 'sac-node-collapsed'; 390 | } 391 | 392 | // Set li's name 393 | if (name) { 394 | var liName = (code) ? code + '.' + name : name; 395 | liSpan.innerHTML = liName; 396 | } 397 | // Set li's attributes 398 | var itemSelected = false; 399 | if (attributes) { 400 | for (var key in attributes) { 401 | if ( 402 | key == $that.opt.selectionByAttribute && 403 | ( 404 | $that.opt.allNodesSelected === true || 405 | ($that.opt.selectedNodes || []).indexOf(attributes[key]) !== -1 || 406 | (($that.opt.selectedNodes || {}).selectedItems || []).indexOf(attributes[key]) !== -1 407 | ) 408 | ) { 409 | itemSelected = true; 410 | } 411 | liSpan.setAttribute(key, attributes[key]); 412 | } 413 | } 414 | var selClass = (itemSelected === true || nodeSelected === true) ? 'sac-node-selected' : ''; 415 | liSpan.className = 'sac-node-name sac-noselect ' + selClass; 416 | 417 | if (children && Array.isArray(children) && children.length > 0 && $that.opt.collapseNodes === true) { 418 | li.appendChild(arrow[0]); 419 | } 420 | li.appendChild(liSpan); 421 | 422 | // Get children 423 | if (children) { 424 | // Create ul 425 | var c_ul = document.createElement('ul'); 426 | c_ul.className = 'sac-ul'; 427 | 428 | // Append children 429 | $.each(children, function (i, childObj) { 430 | var childItem = $that._getListItem({ 431 | opt: opt, 432 | node: childObj 433 | }); 434 | c_ul.appendChild(childItem); 435 | li.appendChild(c_ul); 436 | }); 437 | } 438 | return li; 439 | } 440 | }, 441 | 442 | /** 443 | * Build popup buttons 444 | */ 445 | _buildPopupButtons: function () { 446 | var $that = this; 447 | var popup = $('#' + this.popupID); 448 | if (popup && popup.length > 0) { 449 | var footer = $(''); 450 | popup.append(footer); 451 | 452 | var buttons = this.opt.popupButtons; 453 | var $footer = popup.find('.sac-footer'); 454 | 455 | this._appendDefaultButtons($footer); 456 | } 457 | }, 458 | 459 | /** 460 | * Append default buttons in footer 461 | * @param {object} $footer jQuery footer object 462 | */ 463 | _appendDefaultButtons: function ($footer) { 464 | var $that = this; 465 | var buttons = this.opt.popupButtons; 466 | 467 | var btnArr = []; 468 | for (var btn in buttons) { 469 | if (buttons[btn].visible === true) { 470 | var thisBtn = buttons[btn]; 471 | var btnObj = { 472 | text: thisBtn.text, 473 | className: thisBtn.className, 474 | visible: thisBtn.visible, 475 | callback: thisBtn.callback, 476 | index: thisBtn.index, 477 | key: btn 478 | }; 479 | btnArr.push(btnObj); 480 | } 481 | } 482 | 483 | var indexArr = btnArr.map(function (b) { return b.index; }); 484 | indexArr.sort(); 485 | 486 | for (var i in indexArr) { 487 | var btnKey = btnArr.filter(function (b) { return b.index === indexArr[i]; })[0].key; 488 | var btnObj = buttons[btnKey]; 489 | $footer.append($that._getDefaultButton(btnKey, btnObj)); 490 | } 491 | }, 492 | 493 | /** 494 | * Add event listeners on nodes 495 | */ 496 | _addEventListeners: function () { 497 | var $that = this; 498 | var popup = $('#' + this.popupID); 499 | if (popup && popup.length > 0) { 500 | 501 | // Node 502 | popup.find('.sac-ul') 503 | .not('.sac-ul-top') 504 | .children('li') 505 | .find('.sac-node-name') 506 | .on('click', function () { 507 | var thisNode = $(this); 508 | if (!thisNode.hasClass('sac-node-disabled')) { 509 | if ($that.opt.multiSelect === false) { 510 | popup.find('.sac-node-name').not(this).removeClass('sac-node-selected'); 511 | } 512 | thisNode.toggleClass('sac-node-selected'); 513 | $that._applySelection(); 514 | } 515 | }); 516 | 517 | // Node parent 518 | if (this.opt.multiSelect === true) { 519 | popup.find('.sac-ul.sac-ul-top') 520 | .children('li') 521 | .children('.sac-node-name') 522 | .on('click', function () { 523 | var that = $(this); 524 | var par = that.closest('li'); 525 | var parChildren = par.find('.sac-node-name').not(that).not('.sac-node-disabled'); 526 | var parSelectedChildren = parChildren.filter('.sac-node-selected'); 527 | var parChildrenNum = parChildren.length; 528 | var parSelectedChildrenNum = parSelectedChildren.length; 529 | if (parChildrenNum === parSelectedChildrenNum) { 530 | parChildren.removeClass('sac-node-selected'); 531 | } else { 532 | parChildren.addClass('sac-node-selected'); 533 | } 534 | $that._applySelection(); 535 | }); 536 | } 537 | 538 | // Node collapse 539 | if (this.opt.collapseNodes === true) { 540 | popup.find('.toggleNodeCollapse').on('click', function () { 541 | $(this).closest('li').toggleClass('sac-node-collapsed'); 542 | }); 543 | } 544 | } 545 | }, 546 | 547 | /** 548 | * Add event listeners on search input 549 | */ 550 | _addSearchBoxEventListeners: function () { 551 | var $that = this; 552 | var popup = $('#' + this.popupID); 553 | if (popup && popup.length > 0) { 554 | 555 | var input = popup.find('.sac-input-elem'); 556 | 557 | // Search type combo 558 | popup.find('.sac-seacrh-combo-type').on('change', function () { 559 | $that._searchNodes(input.val()); 560 | }); 561 | 562 | // Search box input 563 | input.on('input', function () { 564 | var inp = $(this); 565 | var val = inp.val(); 566 | $that._searchNodes(val); 567 | }); 568 | } 569 | }, 570 | 571 | /** 572 | * Perform search for given value 573 | * @param {string} val Value to search for 574 | */ 575 | _searchNodes: function (val) { 576 | var $that = this; 577 | var popup = $('#' + this.popupID); 578 | if (popup && popup.length > 0) { 579 | var searchType = popup.find('.sac-seacrh-combo-type').val(); 580 | 581 | // Reset classes 582 | popup.find('li').removeClass('sac-found-category sac-found-item'); 583 | popup.find('.sac-node-name').removeClass('sac-found-item'); 584 | 585 | var valLen = val.length; 586 | if (valLen > 0) { 587 | popup.addClass('sac-searching'); 588 | if ($that.opt.searchBox.hideNotFound === true) { 589 | popup.addClass('sac-searching-hide-not-found'); 590 | } 591 | 592 | var valWithoutAccents = $that._removeAccents(val); 593 | var regExp = new RegExp(valWithoutAccents); 594 | if (searchType == '0') { 595 | var regVal = '^' + valWithoutAccents; 596 | regExp = new RegExp(regVal, 'i'); 597 | } else if (searchType == '1') { 598 | regExp = new RegExp(valWithoutAccents, 'i'); 599 | } 600 | popup.find('.sac-node-name').each(function () { 601 | var elem = $(this); 602 | var textWithoutAccents = $that._removeAccents(elem.text()); 603 | var elemFound = regExp.test(textWithoutAccents); 604 | if (elemFound) { 605 | var fItem = elem.closest('li'); 606 | fItem.addClass('sac-found-item'); 607 | $that._foundParentNodes(elem); 608 | } 609 | }); 610 | } else { 611 | popup.removeClass('sac-searching sac-searching-hide-not-found'); 612 | } 613 | $that._checkForHighlightedNodes(); 614 | } 615 | }, 616 | 617 | _removeAccents: function (str) { 618 | if (str && str.length > 0) { 619 | var accents = 'ΆάΈέΉήΊίΌόΎύΏώΪϊΫϋΐΰÀÁÂÃÄÅàáâãäåÒÓÔÕÕÖØòóôõöøÈÉÊËèéêëðÇçÐÌÍÎÏìíîïÙÚÛÜùúûüÑñŠšŸÿýŽž'; 620 | var accentsOut = "ΑαΕεΗηΙιΟοΥυΩωΙιΥυιυAAAAAAaaaaaaOOOOOOOooooooEEEEeeeeoCcDIIIIiiiiUUUUuuuuNnSsYyyZz"; 621 | str = str.split(''); 622 | var strLen = str.length; 623 | var i, x; 624 | for (i = 0; i < strLen; i++) { 625 | if ((x = accents.indexOf(str[i])) != -1) { 626 | str[i] = accentsOut[x]; 627 | } 628 | } 629 | return str.join(''); 630 | } 631 | return str; 632 | }, 633 | 634 | /** 635 | * Check for hightlighted nodes and toogle button's visibility 636 | */ 637 | _checkForHighlightedNodes: function () { 638 | var nodes = this._getHighlightedNodes(); 639 | this._toggleSelectHighlightedButtonVisibility(nodes !== null && nodes.length > 0); 640 | }, 641 | 642 | /** 643 | * Toogle select highlighted button visibility 644 | */ 645 | _toggleSelectHighlightedButtonVisibility: function (show) { 646 | var $that = this; 647 | var popup = $('#' + this.popupID); 648 | if (popup && popup.length > 0) { 649 | var btn = popup.find('.sac-btn-popup-selectHighlighted'); 650 | if (show === true) { 651 | btn.show(); 652 | } else { 653 | btn.hide(); 654 | } 655 | } 656 | }, 657 | 658 | /** 659 | * Find parent nodes and indicate as found items 660 | * @param {object} elem jQuery node object 661 | */ 662 | _foundParentNodes: function (elem) { 663 | var $that = this; 664 | if (elem) { 665 | var parUl = elem.closest('ul'); 666 | if (parUl.hasClass('sac-ul-top')) { 667 | elem.closest('li').addClass('sac-found-category'); 668 | } 669 | var parNode = parUl.closest('li'); 670 | parNode.addClass('sac-found-category'); 671 | var upperParent = parNode.closest('ul').closest('li'); 672 | if (upperParent && upperParent.length > 0) { 673 | $that._foundParentNodes(parNode); 674 | } 675 | } 676 | }, 677 | 678 | /** 679 | * Get button from options 680 | * @param {string} key Button name 681 | * @param {object} btn Options button object 682 | */ 683 | _getDefaultButton: function (key, btn) { 684 | var $that = this; 685 | var hiddenOnSingleSelection = ['selectAll', 'diselectAll', 'invertSelection']; 686 | var button = null; 687 | 688 | if (this.opt.multiSelect === true || (this.opt.multiSelect === false && hiddenOnSingleSelection.indexOf(key) === -1)) { 689 | var btnClass = 'sac-btn-popup-' + key; 690 | button = $(''); 691 | button.on('click', function () { 692 | switch (key) { 693 | case 'selectAll': 694 | $that._selectAll(); 695 | $that._applySelection(); 696 | break; 697 | case 'diselectAll': 698 | $that._diselectAll(); 699 | $that._applySelection(); 700 | break; 701 | case 'invertSelection': 702 | $that._invertSelection() 703 | $that._applySelection(); 704 | break; 705 | case 'close': 706 | $that._closePopup(); 707 | break; 708 | case 'cancel': 709 | $that._resetSelection(); 710 | $that._closePopup(); 711 | break; 712 | case 'selectHighlighted': 713 | $that._selectHighlighted(); 714 | $that._applySelection(); 715 | break; 716 | } 717 | if (btn.callback && typeof btn.callback === 'function') { 718 | btn.callback(); 719 | } 720 | $that.$el.trigger('searchareacontrol.button.click', [{ element: $that.$el, buttonKey: key }]); 721 | }); 722 | } 723 | return button; 724 | }, 725 | 726 | /** 727 | * Get an array of selected nodes 728 | */ 729 | _getSelectedNodes: function () { 730 | var selectedNodes = []; 731 | var popup = $('#' + this.popupID); 732 | var allNodes = this._getAllNodes(); 733 | if (popup && popup.length > 0 && allNodes !== null) { 734 | allNodes.filter('.sac-node-selected').each(function () { 735 | var node = $(this); 736 | var attributes = {}; 737 | Array.prototype.slice.call(node[0].attributes).forEach(function (item) { 738 | attributes[item.name] = item.value; 739 | }); 740 | selectedNodes.push({ 741 | text: node.text(), 742 | attributes: attributes 743 | }); 744 | }); 745 | } 746 | var selectedAll = (allNodes !== null) ? allNodes.length === selectedNodes.length : false; 747 | return { 748 | selectedAll: selectedAll, 749 | selectedNodes: selectedNodes 750 | } 751 | }, 752 | 753 | /** 754 | * Get selected nodes and store data 755 | */ 756 | _applySelection: function () { 757 | var selectedNodes = this._getSelectedNodes(); 758 | this._setData_SelectedNodes(selectedNodes); 759 | this._updateMainButton(); 760 | if (this.opt.searchBox.showSelectedItemsBox === true) { 761 | this._updateSelectedItemsNum(); 762 | } 763 | this.$el.trigger('searchareacontrol.selectedNodesChanged', [{ element: this.$el, selectedNodes: selectedNodes }]); 764 | }, 765 | 766 | /** 767 | * Update selected items number (if visible) 768 | */ 769 | _updateSelectedItemsNum: function () { 770 | var popup = $('#' + this.popupID); 771 | if (popup && popup.length > 0) { 772 | var numSpan = popup.find('.sac-custom-numSpan-num'); 773 | var selectedNodes = this._getData_SelectedNodes(); 774 | var num = 0; 775 | if (selectedNodes && selectedNodes.hasOwnProperty('selectedNodes')) { 776 | num = (Array.isArray(selectedNodes.selectedNodes)) ? selectedNodes.selectedNodes.length : 0; 777 | } 778 | numSpan.html(num); 779 | } 780 | }, 781 | 782 | /** 783 | * Update main button text 784 | */ 785 | _updateMainButton: function () { 786 | var selNodesData = this._getData_SelectedNodes(); 787 | var selectedNodes = (selNodesData.hasOwnProperty('selectedNodes')) ? selNodesData.selectedNodes : []; 788 | var mainBtnText = this._localize(this.opt.mainButton.defaultText); 789 | if (selectedNodes && Array.isArray(selectedNodes) && selectedNodes.length > 0) { 790 | if (selectedNodes.length <= this.opt.mainButton.maxSelectedViewText) { 791 | mainBtnText = selectedNodes.map(function (node) { 792 | return node.text; 793 | }).join(', '); 794 | } else { 795 | mainBtnText += ' (' + selectedNodes.length + ')'; 796 | } 797 | if (this.opt.mainButton.showAllText === true && selNodesData.selectedAll === true) { 798 | mainBtnText = this._localize(this.opt.mainButton.defaultAllText) + ' ' + mainBtnText; 799 | } 800 | } else { 801 | mainBtnText += ' (' + this._localize(this.opt.mainButton.defaultNoneText) + ')'; 802 | } 803 | this.$el.html(mainBtnText); 804 | }, 805 | 806 | /** 807 | * Select all nodes 808 | */ 809 | _selectAll: function () { 810 | var nodes = this._getAllNodes(); 811 | var popup = $('#' + this.popupID); 812 | if (popup && popup.length > 0 && nodes !== null) { 813 | if (this.opt.multiSelect === true) { 814 | nodes.not('.sac-node-disabled').addClass('sac-node-selected'); 815 | } 816 | } 817 | }, 818 | 819 | /** 820 | * Select highlighted nodes 821 | */ 822 | _selectHighlighted: function () { 823 | var nodes = this._getHighlightedNodes(); 824 | var popup = $('#' + this.popupID); 825 | if (popup && popup.length > 0 && nodes !== null) { 826 | if (this.opt.multiSelect === true) { 827 | nodes.not('.sac-node-disabled').addClass('sac-node-selected'); 828 | } 829 | } 830 | }, 831 | 832 | /** 833 | * Diselect all nodes 834 | */ 835 | _diselectAll: function () { 836 | var nodes = this._getAllNodes(); 837 | var popup = $('#' + this.popupID); 838 | if (popup && popup.length > 0 && nodes !== null) { 839 | nodes.removeClass('sac-node-selected'); 840 | } 841 | }, 842 | 843 | /** 844 | * Invert selection 845 | */ 846 | _invertSelection: function () { 847 | var nodes = this._getAllNodes(); 848 | var popup = $('#' + this.popupID); 849 | if (popup && popup.length > 0) { 850 | if (this.opt.multiSelect === true && nodes !== null) { 851 | nodes.not('.sac-node-disabled').toggleClass('sac-node-selected'); 852 | } else { 853 | console.warn('Unable to perform invertion due to multiSelect option set to false'); 854 | } 855 | } 856 | }, 857 | 858 | /** 859 | * Enable all nodes 860 | */ 861 | _enableAll: function () { 862 | var nodes = this._getAllNodes(); 863 | var popup = $('#' + this.popupID); 864 | if (popup && popup.length > 0 && nodes !== null) { 865 | nodes.removeClass('sac-node-disabled'); 866 | } 867 | }, 868 | 869 | /** 870 | * Disable all nodes 871 | */ 872 | _disableAll: function () { 873 | var nodes = this._getAllNodes(); 874 | var popup = $('#' + this.popupID); 875 | if (popup && popup.length > 0 && nodes !== null) { 876 | nodes.addClass('sac-node-disabled'); 877 | } 878 | }, 879 | 880 | /** 881 | * Get all nodes 882 | */ 883 | _getAllNodes: function () { 884 | var nodes = null; 885 | var popup = $('#' + this.popupID); 886 | if (popup && popup.length > 0) { 887 | nodes = popup.find('.sac-ul').not('.sac-ul-top').children('li').find('.sac-node-name'); 888 | } 889 | return nodes; 890 | }, 891 | 892 | /** 893 | * Get highlighted nodes 894 | */ 895 | _getHighlightedNodes: function () { 896 | var nodes = null; 897 | var popup = $('#' + this.popupID); 898 | if (popup && popup.length > 0) { 899 | nodes = popup.find('.sac-ul').not('.sac-ul-top').children('li').filter('.sac-found-item').find('.sac-node-name'); 900 | } 901 | return nodes; 902 | }, 903 | 904 | /** 905 | * Close popup 906 | */ 907 | _closePopup: function () { 908 | this.$el.trigger('searchareacontrol.popup.beforehide', [{ element: this.$el }]); 909 | this._togglePopup(false); 910 | }, 911 | 912 | /** 913 | * Reset selected nodes (Set selection of nodes that were selected when the popup opened) 914 | */ 915 | _resetSelection: function () { 916 | var selectedNodesOnPopupShow = this._getData_SelectedNodesOnPopupShow(); 917 | this._diselectAll(); 918 | var allSelected = selectedNodesOnPopupShow.selectedAll; 919 | var nodes = selectedNodesOnPopupShow.selectedNodes; 920 | if (allSelected === true && this.opt.multiSelect === true) { 921 | this._selectAll(); 922 | } else if (nodes.length > 0) { 923 | var popup = $('#' + this.popupID); 924 | for (var n in nodes) { 925 | var thisNode = nodes[n]; 926 | popup.find('.sac-node-name').filter('[' + this.opt.selectionByAttribute + '="' + thisNode.attributes[this.opt.selectionByAttribute] + '"]').addClass('sac-node-selected'); 927 | } 928 | } 929 | this._applySelection(); 930 | }, 931 | 932 | /** 933 | * Localize key 934 | */ 935 | _localize: function (key) { 936 | var locale = this.opt.locales; 937 | return (this.locales.hasOwnProperty(locale)) ? ((this.locales[locale].hasOwnProperty(key)) ? this.locales[locale][key] : key) : key; 938 | }, 939 | 940 | // SEARCH AREA (end) ================================================================ // 941 | 942 | // PUBLIC METHODS (start) =========================================================== // 943 | 944 | /** 945 | * Get datasource 946 | */ 947 | getData: function () { 948 | return this._getData_DataSource(); 949 | }, 950 | 951 | /** 952 | * Set selected nodes 953 | * @param {boolean} allSelected 954 | * @param {array} collection 955 | * @param {string} byAttribute Optional selection by attribute (If not provided, 'selectionByAttribute' option will be selected) 956 | */ 957 | setSelectedNodes: function (allSelected, collection, byAttribute) { 958 | var popup = $('#' + this.popupID); 959 | var selectByAttribute = (byAttribute) ? byAttribute : this.opt.selectionByAttribute; 960 | var allNodes = this._getAllNodes(); 961 | if (popup && popup.length > 0 && allNodes !== null) { 962 | if (allSelected === true) { 963 | allNodes.addClass('sac-node-selected'); 964 | } else if (collection && Array.isArray(collection) && collection.length > 0) { 965 | // Reset selections 966 | this._diselectAll(); 967 | for (var item in collection) { 968 | allNodes 969 | .filter('[' + selectByAttribute + '="' + collection[item] + '"]') 970 | .addClass('sac-node-selected'); 971 | } 972 | } 973 | this._applySelection(); 974 | } 975 | }, 976 | 977 | /** 978 | * Set disabled nodes 979 | * @param {array} collection 980 | * @param {boolean} diselectDisabled Diselect nodes to be disabled (if they are selected) 981 | * @param {string} byAttribute Optional selection by attribute (If not provided, 'selectionByAttribute' option will be selected) 982 | */ 983 | setDisabledNodes: function (collection, diselectDisabled, byAttribute) { 984 | var popup = $('#' + this.popupID); 985 | var selectByAttribute = (byAttribute) ? byAttribute : this.opt.selectionByAttribute; 986 | var allNodes = this._getAllNodes(); 987 | if (popup && popup.length > 0 && allNodes !== null) { 988 | if (collection && Array.isArray(collection) && collection.length > 0) { 989 | // Enable all 990 | this._enableAll(); 991 | for (var item in collection) { 992 | var nodesToDisable = allNodes.filter('[' + selectByAttribute + '="' + collection[item] + '"]'); 993 | if (diselectDisabled === true) { 994 | nodesToDisable.removeClass('sac-node-selected'); 995 | } 996 | nodesToDisable.addClass('sac-node-disabled'); 997 | } 998 | } 999 | this._applySelection(); 1000 | } 1001 | }, 1002 | 1003 | /** 1004 | * Enable all nodes 1005 | */ 1006 | enableAllNodes: function () { 1007 | this._enableAll(); 1008 | }, 1009 | 1010 | /** 1011 | * Disable all nodes 1012 | */ 1013 | disableAllNodes: function () { 1014 | this._disableAll(); 1015 | }, 1016 | 1017 | /** 1018 | * Clear selected nodes 1019 | */ 1020 | clearSelection: function () { 1021 | this._diselectAll(); 1022 | this._applySelection(); 1023 | }, 1024 | 1025 | /** 1026 | * Get stored selected nodes 1027 | */ 1028 | getSelectedNodes: function () { 1029 | return this._getData_SelectedNodes(); 1030 | }, 1031 | 1032 | /** 1033 | * Get an array of specific attribute values of the selected nodes 1034 | */ 1035 | getSelectedByAttribute: function (attributeName) { 1036 | var $that = this; 1037 | return this._getData_SelectedNodes().selectedNodes.map(function (node) { 1038 | var attrSelector = (attributeName) ? attributeName : $that.opt.selectionByAttribute; 1039 | return node.attributes[attrSelector]; 1040 | }); 1041 | }, 1042 | 1043 | /** 1044 | * Get disabled state of main button 1045 | */ 1046 | getDisabled: function () { 1047 | return this.$el.is(':disabled'); 1048 | }, 1049 | 1050 | /** 1051 | * Toggle main button disabled state 1052 | */ 1053 | setDisabled: function (disable) { 1054 | this.$el.prop('disabled', disable); 1055 | }, 1056 | 1057 | /** 1058 | * Return popup element if exists 1059 | */ 1060 | getPopup: function () { 1061 | var popup = $('#' + this.popupID); 1062 | return (popup && popup.length > 0) ? popup : null; 1063 | }, 1064 | 1065 | /** 1066 | * Update datasource (options and memory) 1067 | */ 1068 | updateDatasource: function (data) { 1069 | this.opt.data = data; 1070 | this._setData_DataSource(data); 1071 | var popup = $('#' + this.popupID); 1072 | if (popup && popup.length > 0) { 1073 | popup.empty(); 1074 | this.$el.trigger('searchareacontrol.beforeinitsearcharea', [{ element: this.$el }]); 1075 | this._initSearchArea(); 1076 | } 1077 | }, 1078 | 1079 | /** 1080 | * Set new locale and re-init plugin 1081 | */ 1082 | setLocale: function (locale) { 1083 | if (locale && typeof locale === 'string' && locale.length > 0) { 1084 | this.destroy(); 1085 | this.opt.locales = locale; 1086 | this.init(); 1087 | } 1088 | }, 1089 | 1090 | /** 1091 | * Destroy the plugin 1092 | */ 1093 | destroy: function () { 1094 | // Remove button text 1095 | this.$el.empty(); 1096 | // Remove stored data 1097 | this.$el.removeData().removeClass(this.rootClassName); 1098 | // Remove popup 1099 | var popup = $('#' + this.popupID); 1100 | if (popup && popup.length > 0) { 1101 | popup.closest('.sac-popup-overlay').remove(); 1102 | } 1103 | } 1104 | 1105 | // PUBLIC METHODS (end) ============================================================= // 1106 | 1107 | } 1108 | 1109 | $.fn[pluginName] = function (options) { 1110 | var args = arguments; 1111 | if (options === undefined || typeof options === 'object') { 1112 | // New plugin instance with chainability 1113 | return this.each(function () { 1114 | if (!$.data(this, 'plugin_' + pluginName)) { 1115 | $.data(this, 'plugin_' + pluginName, new Plugin(this, options)); 1116 | } 1117 | }); 1118 | } else if (typeof options === 'string' && options[0] !== '_' && options !== 'init') { 1119 | if ((Array.prototype.slice.call(args, 1).length == 0 && $.inArray(options, $.fn[pluginName].methods) != -1) || $.inArray(options, $.fn[pluginName].getters) != -1) { 1120 | // Return method result (no chainability) 1121 | var instance = $.data(this[0], 'plugin_' + pluginName); 1122 | return instance[options].apply(instance, Array.prototype.slice.call(args, 1)); 1123 | } else { 1124 | // Return element (chainability) 1125 | return this.each(function () { 1126 | var instance = $.data(this, 'plugin_' + pluginName); 1127 | if (instance instanceof Plugin && typeof instance[options] === 'function') { 1128 | instance[options].apply(instance, Array.prototype.slice.call(args, 1)); 1129 | } 1130 | }); 1131 | } 1132 | } 1133 | } 1134 | 1135 | /** 1136 | * Set array of accepted public methods 1137 | */ 1138 | $.fn[pluginName].methods = [ 1139 | 'getData', 1140 | 'setSelectedNodes', 1141 | 'clearSelection', 1142 | 'setDisabled', 1143 | 'updateDatasource', 1144 | 'setDisabledNodes', 1145 | 'enableAllNodes', 1146 | 'disableAllNodes', 1147 | 'setLocale', 1148 | 'destroy' 1149 | ]; 1150 | 1151 | /** 1152 | * Public methods (getters) 1153 | */ 1154 | $.fn[pluginName].getters = [ 1155 | 'getSelectedNodes', 1156 | 'getDisabled', 1157 | 'getSelectedByAttribute', 1158 | 'getPopup' 1159 | ]; 1160 | 1161 | $.fn[pluginName].locales = { 1162 | en: { 1163 | 'Search': 'Search', 1164 | 'Selected items': 'Selected items', 1165 | 'Starts with': 'Starts with', 1166 | 'Exists in': 'Exists in', 1167 | 'Regular expression': 'Regular expression', 1168 | 'Items': 'Items', 1169 | 'None': 'None', 1170 | 'All': 'All', 1171 | 'Select all': 'Select all', 1172 | 'Diselect all': 'Diselect all', 1173 | 'Invert selection': 'Invert selection', 1174 | 'Close': 'Close', 1175 | 'Cancel': 'Cancel', 1176 | 'Select highlighted': 'Select highlighted' 1177 | }, 1178 | el: { 1179 | 'Search': 'Αναζήτηση', 1180 | 'Selected items': 'Επιλεγμένα αντικείμενα', 1181 | 'Starts with': 'Ξεκινά με', 1182 | 'Exists in': 'Περιέχει', 1183 | 'Regular expression': 'Κανονική έκφραση', 1184 | 'Items': 'Αντικείμενα', 1185 | 'None': 'Κανένα', 1186 | 'All': 'Όλα', 1187 | 'Select all': 'Επιλογή όλων', 1188 | 'Diselect all': 'Αποεπιλογή όλων', 1189 | 'Invert selection': 'Αντιστροφή επιλογής', 1190 | 'Close': 'Κλείσιμο', 1191 | 'Cancel': 'Άκυρο', 1192 | 'Select highlighted': 'Επιλογή επισημασμένων' 1193 | }, 1194 | ptbr: { 1195 | 'Search': 'Busca', 1196 | 'Selected items': 'Itens Selecionados', 1197 | 'Starts with': 'Começa com', 1198 | 'Exists in': 'Existe em', 1199 | 'Regular expression': 'Expressão regular', 1200 | 'Items': 'Itens', 1201 | 'None': 'Nenhum', 1202 | 'All': 'Tudo', 1203 | 'Select all': 'Selecionar tudo', 1204 | 'Diselect all': 'Deselecionar Tudo', 1205 | 'Invert selection': 'Inverter Seleção', 1206 | 'Close': 'Fechar', 1207 | 'Cancel': 'Cancelar', 1208 | 'Select highlighted': 'Selecionar destacado' 1209 | }, 1210 | ru: { 1211 | 'Search': 'Поиск', 1212 | 'Selected items': 'Выбранные пункты', 1213 | 'Starts with': 'Начинается с', 1214 | 'Exists in': 'Содержится в', 1215 | 'Regular expression': 'Регулярное выражение', 1216 | 'Items': 'Пункты', 1217 | 'None': 'Не выбрано', 1218 | 'All': 'Все', 1219 | 'Select all': 'Выбрать все', 1220 | 'Diselect all': 'Снять выбор со всех', 1221 | 'Invert selection': 'Инвертировать выбор', 1222 | 'Close': 'Закрыть', 1223 | 'Cancel': 'Отмена', 1224 | 'Select highlighted': 'Выбрать выделенные' 1225 | } 1226 | }; 1227 | 1228 | $.fn[pluginName].defaults = { 1229 | modalHeader: { 1230 | text: 'Search', 1231 | className: '', 1232 | visible: true 1233 | }, 1234 | data: [], 1235 | multiSelect: true, 1236 | collapseNodes: false, 1237 | allNodesExpanded: true, 1238 | columns: 2, 1239 | selectionByAttribute: 'data-id', 1240 | allNodesSelected: false, 1241 | selectedNodes: [], 1242 | locales: 'en', 1243 | localeData: null, 1244 | searchBox: { 1245 | enabled: true, 1246 | minCharactersSearch: 2, 1247 | searchBoxClass: '', 1248 | searchBoxPlaceholder: '', 1249 | showSelectedItemsBox: true, 1250 | selectedItemsLabelVisible: true, 1251 | selectedItemsLabelText: 'Selected items', 1252 | hideNotFound: true, 1253 | searchType: { 1254 | startsWith: { 1255 | text: 'Starts with', 1256 | selected: false 1257 | }, 1258 | existsIn: { 1259 | text: 'Exists in', 1260 | selected: true 1261 | }, 1262 | regExp: { 1263 | text: 'Regular expression', 1264 | selected: false 1265 | } 1266 | } 1267 | }, 1268 | popupDimensions: { 1269 | '768': { 1270 | width: '95%', 1271 | left: '2.5%', 1272 | marginLeft: '0' 1273 | }, 1274 | 'max': { 1275 | width: '700px', 1276 | left: '50%', 1277 | marginLeft: '-350px' 1278 | } 1279 | }, 1280 | mainButton: { 1281 | defaultText: 'Items', 1282 | className: '', 1283 | defaultNoneText: 'None', 1284 | defaultAllText: 'All', 1285 | showAllText: true, 1286 | maxSelectedViewText: 1 1287 | }, 1288 | popupButtons: { 1289 | selectAll: { 1290 | text: 'Select all', 1291 | className: 'btn btn-success', 1292 | visible: true, 1293 | callback: null, 1294 | index: 0 1295 | }, 1296 | diselectAll: { 1297 | text: 'Diselect all', 1298 | className: 'btn btn-default', 1299 | visible: true, 1300 | callback: null, 1301 | index: 1 1302 | }, 1303 | invertSelection: { 1304 | text: 'Invert selection', 1305 | className: 'btn btn-default', 1306 | visible: true, 1307 | callback: null, 1308 | index: 2 1309 | }, 1310 | close: { 1311 | text: 'Close', 1312 | className: 'btn btn-default', 1313 | visible: true, 1314 | callback: null, 1315 | index: 3 1316 | }, 1317 | cancel: { 1318 | text: 'Cancel', 1319 | className: 'btn btn-default', 1320 | visible: false, 1321 | callback: null, 1322 | index: 4 1323 | }, 1324 | selectHighlighted: { 1325 | text: 'Select highlighted', 1326 | className: 'btn btn-default', 1327 | visible: false, 1328 | callback: null, 1329 | index: 5 1330 | } 1331 | } 1332 | } 1333 | 1334 | })(jQuery, window, document); 1335 | -------------------------------------------------------------------------------- /test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | SearchAreaControl - jQuery plugin 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 |
22 |
23 | 26 |

Test page

27 | 28 |

29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 |

37 | 38 |

39 | 40 | 41 | 42 | 43 | 44 |

45 | 46 |

47 | 48 |

49 | 50 | 51 | 67 | 68 |
69 |
70 | 71 | 78 | 79 |
80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | --------------------------------------------------------------------------------