From b0f48ee3ad63a99834a3b4bf18a48cf6714f3c0a Mon Sep 17 00:00:00 2001 From: Anthony Lapenna Date: Mon, 7 Oct 2019 16:24:48 +1300 Subject: [PATCH] feat(app): fix XSS vulnerabilities (#3230) --- app/__module.js | 1 + .../isteven-angular-multiselect/.npmignore | 8 + .../isteven-angular-multiselect/LICENSE.txt | 21 + .../isteven-angular-multiselect/README.md | 50 + .../isteven-multi-select.css | 299 +++++ .../isteven-multi-select.js | 1127 +++++++++++++++++ .../isteven-angular-multiselect/package.json | 22 + app/portainer/services/modalService.js | 24 +- app/vendors.js | 2 - package.json | 1 - yarn.lock | 5 - 11 files changed, 1541 insertions(+), 19 deletions(-) create mode 100644 app/libraries/isteven-angular-multiselect/.npmignore create mode 100644 app/libraries/isteven-angular-multiselect/LICENSE.txt create mode 100644 app/libraries/isteven-angular-multiselect/README.md create mode 100644 app/libraries/isteven-angular-multiselect/isteven-multi-select.css create mode 100644 app/libraries/isteven-angular-multiselect/isteven-multi-select.js create mode 100644 app/libraries/isteven-angular-multiselect/package.json diff --git a/app/__module.js b/app/__module.js index 881453c9a..9eaa6248d 100644 --- a/app/__module.js +++ b/app/__module.js @@ -1,4 +1,5 @@ import '../assets/css/app.css'; +import './libraries/isteven-angular-multiselect/isteven-multi-select.css'; import angular from 'angular'; import './agent/_module'; diff --git a/app/libraries/isteven-angular-multiselect/.npmignore b/app/libraries/isteven-angular-multiselect/.npmignore new file mode 100644 index 000000000..84a23b94d --- /dev/null +++ b/app/libraries/isteven-angular-multiselect/.npmignore @@ -0,0 +1,8 @@ +.git +.gitignore +bower.json +CHANGELOG.md +package.json +README.md +screenshot.png +/doc diff --git a/app/libraries/isteven-angular-multiselect/LICENSE.txt b/app/libraries/isteven-angular-multiselect/LICENSE.txt new file mode 100644 index 000000000..6e524fa92 --- /dev/null +++ b/app/libraries/isteven-angular-multiselect/LICENSE.txt @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2014-2015 Ignatius Steven (https://github.com/isteven) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/app/libraries/isteven-angular-multiselect/README.md b/app/libraries/isteven-angular-multiselect/README.md new file mode 100644 index 000000000..9c6255bcf --- /dev/null +++ b/app/libraries/isteven-angular-multiselect/README.md @@ -0,0 +1,50 @@ +# AngularJS MultiSelect +Pure AngularJS directive which creates a dropdown button with multiple or single selections. +Doesn't require jQuery and works well with other Javascript libraries. + +![Screenshot](https://raw.githubusercontent.com/isteven/angular-multi-select/master/screenshot.png) + +### Demo & How To +Go to http://isteven.github.io/angular-multi-select + +### Current Version +4.0.0 + +### Change Log +See CHANGELOG.md. +For those who's upgrading from version 2.x.x, do note that this version is not backward-compatible. Please read the manual +thoroughly and update your code accordingly. + +### Bug Reporting +Please follow these steps: + +1. **READ THE MANUAL AGAIN**. You might have missed something. This includes the MINIMUM ANGULARJS VERSION and the SUPPORTED BROWSERS. +2. The next step is to search in Github's issue section first. There might already be an answer for similar issue. Do check both open and closed issues. +3. If there's no previous issue found, then please create a new issue in https://github.com/isteven/angular-multi-select/issues. +4. Please **replicate the problem in JSFiddle or Plunker** (or any other online JS collaboration tool), and include the URL in the issue you are creating. +5. When you're done, please close the issue you've created. + +### Licence +Released under the MIT license: + +The MIT License (MIT) + +Copyright (c) 2014-2015 Ignatius Steven (https://github.com/isteven) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/app/libraries/isteven-angular-multiselect/isteven-multi-select.css b/app/libraries/isteven-angular-multiselect/isteven-multi-select.css new file mode 100644 index 000000000..44dfc95f5 --- /dev/null +++ b/app/libraries/isteven-angular-multiselect/isteven-multi-select.css @@ -0,0 +1,299 @@ +/* + * Don't modify things marked with ! - unless you know what you're doing + */ + +/* ! vertical layout */ +.multiSelect .vertical { + float: none; +} + +/* ! horizontal layout */ +.multiSelect .horizontal:not(.multiSelectGroup) { + float: left; +} + +/* ! create a "row" */ +.multiSelect .line { + padding: 2px 0px 4px 0px; + max-height: 30px; + overflow: hidden; + box-sizing: content-box; +} + +/* ! create a "column" */ +.multiSelect .acol { + display: inline-block; + min-width: 12px; +} + +/* ! */ +.multiSelect .inlineBlock { + display: inline-block; +} + +/* the multiselect button */ +.multiSelect > button { + display: inline-block; + position: relative; + text-align: center; + cursor: pointer; + border: 1px solid #c6c6c6; + padding: 1px 8px 1px 8px; + font-size: 14px; + min-height : 38px !important; + border-radius: 4px; + color: #555; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + -o-user-select: none; + user-select: none; + white-space:normal; + background-color: #fff; + background-image: linear-gradient(#fff, #f7f7f7); +} + +/* button: hover */ +.multiSelect > button:hover { + background-image: linear-gradient(#fff, #e9e9e9); +} + +/* button: disabled */ +.multiSelect > button:disabled { + background-image: linear-gradient(#fff, #fff); + border: 1px solid #ddd; + color: #999; +} + +/* button: clicked */ +.multiSelect .buttonClicked { + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.15) inset, 0 1px 2px rgba(0, 0, 0, 0.05); +} + +/* labels on the button */ +.multiSelect .buttonLabel { + display: inline-block; + padding: 5px 0px 5px 0px; +} + +/* downward pointing arrow */ +.multiSelect .caret { + display: inline-block; + width: 0; + height: 0; + margin: 0px 0px 1px 12px !important; + vertical-align: middle; + border-top: 4px solid #333; + border-right: 4px solid transparent; + border-left: 4px solid transparent; + border-bottom: 0 dotted; +} + +/* the main checkboxes and helper layer */ +.multiSelect .checkboxLayer { + background-color: #fff; + position: absolute; + z-index: 999; + border: 1px solid rgba(0, 0, 0, 0.15); + border-radius: 4px; + -webkit-box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175); + box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175); + min-width:278px; + display: none !important; +} + +/* container of helper elements */ +.multiSelect .helperContainer { + border-bottom: 1px solid #ddd; + padding: 8px 8px 0px 8px; +} + +/* helper buttons (select all, none, reset); */ +.multiSelect .helperButton { + display: inline; + text-align: center; + cursor: pointer; + border: 1px solid #ccc; + height: 26px; + font-size: 13px; + border-radius: 2px; + color: #666; + background-color: #f1f1f1; + line-height: 1.6; + margin: 0px 0px 8px 0px; +} + +.multiSelect .helperButton.reset{ + float: right; +} + +.multiSelect .helperButton:not( .reset ) { + margin-right: 4px; +} + +/* clear button */ +.multiSelect .clearButton { + position: absolute; + display: inline; + text-align: center; + cursor: pointer; + border: 1px solid #ccc; + height: 22px; + width: 22px; + font-size: 13px; + border-radius: 2px; + color: #666; + background-color: #f1f1f1; + line-height: 1.4; + right : 2px; + top: 4px; +} + +/* filter */ +.multiSelect .inputFilter { + border-radius: 2px; + border: 1px solid #ccc; + height: 26px; + font-size: 14px; + width:100%; + padding-left:7px; + -webkit-box-sizing: border-box; /* Safari/Chrome, other WebKit */ + -moz-box-sizing: border-box; /* Firefox, other Gecko */ + box-sizing: border-box; /* Opera/IE 8+ */ + color: #888; + margin: 0px 0px 8px 0px; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075); +} + +/* helper elements on hover & focus */ +.multiSelect .clearButton:hover, +.multiSelect .helperButton:hover { + border: 1px solid #ccc; + color: #999; + background-color: #f4f4f4; +} +.multiSelect .helperButton:disabled { + color: #ccc; + border: 1px solid #ddd; +} + +.multiSelect .clearButton:focus, +.multiSelect .helperButton:focus, +.multiSelect .inputFilter:focus { + border: 1px solid #66AFE9 !important; + outline: 0; + -webkit-box-shadow: inset 0 0 1px rgba(0,0,0,.065), 0 0 5px rgba(102, 175, 233, .6) !important; + box-shadow: inset 0 0 1px rgba(0,0,0,.065), 0 0 5px rgba(102, 175, 233, .6) !important; +} + +/* container of multi select items */ +.multiSelect .checkBoxContainer { + display: block; + padding: 8px; + overflow: hidden; +} + +/* ! to show / hide the checkbox layer above */ +.multiSelect .show { + display: block !important; +} + +/* item labels */ +.multiSelect .multiSelectItem { + display: block; + padding: 3px; + color: #444; + white-space: nowrap; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + -o-user-select: none; + user-select: none; + border: 1px solid transparent; + position: relative; + min-width:278px; + min-height: 32px; +} + +/* Styling on selected items */ +.multiSelect .multiSelectItem:not(.multiSelectGroup).selected +{ + background-image: linear-gradient( #e9e9e9, #f1f1f1 ); + color: #555; + cursor: pointer; + border-top: 1px solid #e4e4e4; + border-left: 1px solid #e4e4e4; + border-right: 1px solid #d9d9d9; +} + +.multiSelect .multiSelectItem .acol label { + display: inline-block; + padding-right: 30px; + margin: 0px; + font-weight: normal; + line-height: normal; +} + +/* item labels focus on mouse hover */ +.multiSelect .multiSelectItem:hover, +.multiSelect .multiSelectGroup:hover { + background-image: linear-gradient( #c1c1c1, #999 ) !important; + color: #fff !important; + cursor: pointer; + border: 1px solid #ccc !important; +} + +/* item labels focus using keyboard */ +.multiSelect .multiSelectFocus { + background-image: linear-gradient( #c1c1c1, #999 ) !important; + color: #fff !important; + cursor: pointer; + border: 1px solid #ccc !important; +} + +/* change mouse pointer into the pointing finger */ +.multiSelect .multiSelectItem span:hover, +.multiSelect .multiSelectGroup span:hover +{ + cursor: pointer; +} + +/* ! group labels */ +.multiSelect .multiSelectGroup { + display: block; + clear: both; +} + +/* right-align the tick mark (✔) */ +.multiSelect .tickMark { + display:inline-block; + position: absolute; + right: 10px; + top: 7px; + font-size: 10px; +} + +/* hide the original HTML checkbox away */ +.multiSelect .checkbox { + color: #ddd !important; + position: absolute; + left: -9999px; + cursor: pointer; +} + +/* checkboxes currently disabled */ +.multiSelect .disabled, +.multiSelect .disabled:hover, +.multiSelect .disabled label input:hover ~ span { + color: #c4c4c4 !important; + cursor: not-allowed !important; +} + +/* If you use images in button / checkbox label, you might want to change the image style here. */ +.multiSelect img { + vertical-align: middle; + margin-bottom:0px; + max-height: 22px; + max-width:22px; +} diff --git a/app/libraries/isteven-angular-multiselect/isteven-multi-select.js b/app/libraries/isteven-angular-multiselect/isteven-multi-select.js new file mode 100644 index 000000000..02b136aa3 --- /dev/null +++ b/app/libraries/isteven-angular-multiselect/isteven-multi-select.js @@ -0,0 +1,1127 @@ +/* + * Angular JS Multi Select + * Creates a dropdown-like button with checkboxes. + * + * Project started on: Tue, 14 Jan 2014 - 5:18:02 PM + * Current version: 4.0.0 + * + * Released under the MIT License + * -------------------------------------------------------------------------------- + * The MIT License (MIT) + * + * Copyright (c) 2014 Ignatius Steven (https://github.com/isteven) + * + * 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. + * -------------------------------------------------------------------------------- + */ + +'use strict' + +angular.module( 'isteven-multi-select', ['ng'] ).directive( 'istevenMultiSelect' , [ '$sce', '$timeout', '$sanitize', function ( $sce, $timeout, $sanitize) { + return { + restrict: + 'AE', + + scope: + { + // models + inputModel : '=', + outputModel : '=', + + // settings based on attribute + isDisabled : '=', + + // callbacks + onClear : '&', + onClose : '&', + onSearchChange : '&', + onItemClick : '&', + onOpen : '&', + onReset : '&', + onSelectAll : '&', + onSelectNone : '&', + + // i18n + translation : '=' + }, + + /* + * The rest are attributes. They don't need to be parsed / binded, so we can safely access them by value. + * - buttonLabel, directiveId, helperElements, itemLabel, maxLabels, orientation, selectionMode, minSearchLength, + * tickProperty, disableProperty, groupProperty, searchProperty, maxHeight, outputProperties + */ + + templateUrl: + 'isteven-multi-select.htm', + + link: function ( $scope, element, attrs ) { + + $scope.backUp = []; + $scope.varButtonLabel = ''; + $scope.spacingProperty = ''; + $scope.indexProperty = ''; + $scope.orientationH = false; + $scope.orientationV = true; + $scope.filteredModel = []; + $scope.inputLabel = { labelFilter: '' }; + $scope.tabIndex = 0; + $scope.lang = {}; + $scope.helperStatus = { + all : true, + none : true, + reset : true, + filter : true + }; + + var + prevTabIndex = 0, + helperItems = [], + helperItemsLength = 0, + checkBoxLayer = '', + // scrolled = false, + // selectedItems = [], + formElements = [], + vMinSearchLength = 0, + clickedItem = null + + // v3.0.0 + // clear button clicked + $scope.clearClicked = function( e ) { + $scope.inputLabel.labelFilter = ''; + $scope.updateFilter(); + $scope.select( 'clear', e ); + } + + // A little hack so that AngularJS ng-repeat can loop using start and end index like a normal loop + // http://stackoverflow.com/questions/16824853/way-to-ng-repeat-defined-number-of-times-instead-of-repeating-over-array + $scope.numberToArray = function( num ) { + return new Array( num ); + } + + // Call this function when user type on the filter field + $scope.searchChanged = function() { + if ( $scope.inputLabel.labelFilter.length < vMinSearchLength && $scope.inputLabel.labelFilter.length > 0 ) { + return false; + } + $scope.updateFilter(); + } + + $scope.updateFilter = function() + { + // we check by looping from end of input-model + $scope.filteredModel = []; + var i = 0; + + if ( typeof $scope.inputModel === 'undefined' ) { + return false; + } + + for( i = $scope.inputModel.length - 1; i >= 0; i-- ) { + + // if it's group end, we push it to filteredModel[]; + if ( typeof $scope.inputModel[ i ][ attrs.groupProperty ] !== 'undefined' && $scope.inputModel[ i ][ attrs.groupProperty ] === false ) { + $scope.filteredModel.push( $scope.inputModel[ i ] ); + } + + // if it's data + var gotData = false; + if ( typeof $scope.inputModel[ i ][ attrs.groupProperty ] === 'undefined' ) { + + // If we set the search-key attribute, we use this loop. + if ( typeof attrs.searchProperty !== 'undefined' && attrs.searchProperty !== '' ) { + + for (const key in $scope.inputModel[ i ] ) { + if ( + typeof $scope.inputModel[ i ][ key ] !== 'boolean' + && String( $scope.inputModel[ i ][ key ] ).toUpperCase().indexOf( $scope.inputLabel.labelFilter.toUpperCase() ) >= 0 + && attrs.searchProperty.indexOf( key ) > -1 + ) { + gotData = true; + break; + } + } + } + // if there's no search-key attribute, we use this one. Much better on performance. + else { + for ( const key in $scope.inputModel[ i ] ) { + if ( + typeof $scope.inputModel[ i ][ key ] !== 'boolean' + && String( $scope.inputModel[ i ][ key ] ).toUpperCase().indexOf( $scope.inputLabel.labelFilter.toUpperCase() ) >= 0 + ) { + gotData = true; + break; + } + } + } + + if ( gotData === true ) { + // push + $scope.filteredModel.push( $scope.inputModel[ i ] ); + } + } + + // if it's group start + if ( typeof $scope.inputModel[ i ][ attrs.groupProperty ] !== 'undefined' && $scope.inputModel[ i ][ attrs.groupProperty ] === true ) { + + if ( typeof $scope.filteredModel[ $scope.filteredModel.length - 1 ][ attrs.groupProperty ] !== 'undefined' + && $scope.filteredModel[ $scope.filteredModel.length - 1 ][ attrs.groupProperty ] === false ) { + $scope.filteredModel.pop(); + } + else { + $scope.filteredModel.push( $scope.inputModel[ i ] ); + } + } + } + + $scope.filteredModel.reverse(); + + $timeout( function() { + + $scope.getFormElements(); + + // Callback: on filter change + if ( $scope.inputLabel.labelFilter.length > vMinSearchLength ) { + + var filterObj = []; + + angular.forEach( $scope.filteredModel, function( value ) { + if ( typeof value !== 'undefined' ) { + if ( typeof value[ attrs.groupProperty ] === 'undefined' ) { + var tempObj = angular.copy( value ); + var index = filterObj.push( tempObj ); + delete filterObj[ index - 1 ][ $scope.indexProperty ]; + delete filterObj[ index - 1 ][ $scope.spacingProperty ]; + } + } + }); + + $scope.onSearchChange({ + data: + { + keyword: $scope.inputLabel.labelFilter, + result: filterObj + } + }); + } + },0); + }; + + // List all the input elements. We need this for our keyboard navigation. + // This function will be called everytime the filter is updated. + // Depending on the size of filtered mode, might not good for performance, but oh well.. + $scope.getFormElements = function() { + formElements = []; + + var + selectButtons = [], + inputField = [], + checkboxes = [], + clearButton = []; + + // If available, then get select all, select none, and reset buttons + if ( $scope.helperStatus.all || $scope.helperStatus.none || $scope.helperStatus.reset ) { + selectButtons = element.children().children().next().children().children()[ 0 ].getElementsByTagName( 'button' ); + // If available, then get the search box and the clear button + if ( $scope.helperStatus.filter ) { + // Get helper - search and clear button. + inputField = element.children().children().next().children().children().next()[ 0 ].getElementsByTagName( 'input' ); + clearButton = element.children().children().next().children().children().next()[ 0 ].getElementsByTagName( 'button' ); + } + } + else { + if ( $scope.helperStatus.filter ) { + // Get helper - search and clear button. + inputField = element.children().children().next().children().children()[ 0 ].getElementsByTagName( 'input' ); + clearButton = element.children().children().next().children().children()[ 0 ].getElementsByTagName( 'button' ); + } + } + + // Get checkboxes + if ( !$scope.helperStatus.all && !$scope.helperStatus.none && !$scope.helperStatus.reset && !$scope.helperStatus.filter ) { + checkboxes = element.children().children().next()[ 0 ].getElementsByTagName( 'input' ); + } + else { + checkboxes = element.children().children().next().children().next()[ 0 ].getElementsByTagName( 'input' ); + } + + // Push them into global array formElements[] + for ( let i = 0; i < selectButtons.length ; i++ ) { formElements.push( selectButtons[ i ] ); } + for ( let i = 0; i < inputField.length ; i++ ) { formElements.push( inputField[ i ] ); } + for ( let i = 0; i < clearButton.length ; i++ ) { formElements.push( clearButton[ i ] ); } + for ( let i = 0; i < checkboxes.length ; i++ ) { formElements.push( checkboxes[ i ] ); } + } + + // check if an item has attrs.groupProperty (be it true or false) + $scope.isGroupMarker = function( item , type ) { + if ( typeof item[ attrs.groupProperty ] !== 'undefined' && item[ attrs.groupProperty ] === type ) return true; + return false; + } + + $scope.removeGroupEndMarker = function( item ) { + if ( typeof item[ attrs.groupProperty ] !== 'undefined' && item[ attrs.groupProperty ] === false ) return false; + return true; + } + + // call this function when an item is clicked + $scope.syncItems = function( item, e, ng_repeat_index ) { + + e.preventDefault(); + e.stopPropagation(); + + // if the directive is globaly disabled, do nothing + if ( typeof attrs.disableProperty !== 'undefined' && item[ attrs.disableProperty ] === true ) { + return false; + } + + // if item is disabled, do nothing + if ( typeof attrs.isDisabled !== 'undefined' && $scope.isDisabled === true ) { + return false; + } + + // if end group marker is clicked, do nothing + if ( typeof item[ attrs.groupProperty ] !== 'undefined' && item[ attrs.groupProperty ] === false ) { + return false; + } + + var index = $scope.filteredModel.indexOf( item ); + + // if the start of group marker is clicked ( only for multiple selection! ) + // how it works: + // - if, in a group, there are items which are not selected, then they all will be selected + // - if, in a group, all items are selected, then they all will be de-selected + if ( typeof item[ attrs.groupProperty ] !== 'undefined' && item[ attrs.groupProperty ] === true ) { + + // this is only for multiple selection, so if selection mode is single, do nothing + if ( typeof attrs.selectionMode !== 'undefined' && attrs.selectionMode.toUpperCase() === 'SINGLE' ) { + return false; + } + + var i, j; + var startIndex = 0; + var endIndex = $scope.filteredModel.length - 1; + var tempArr = []; + + // nest level is to mark the depth of the group. + // when you get into a group (start group marker), nestLevel++ + // when you exit a group (end group marker), nextLevel-- + var nestLevel = 0; + + // we loop throughout the filtered model (not whole model) + for( i = index ; i < $scope.filteredModel.length ; i++) { + + // this break will be executed when we're done processing each group + if ( nestLevel === 0 && i > index ) + { + break; + } + + if ( typeof $scope.filteredModel[ i ][ attrs.groupProperty ] !== 'undefined' && $scope.filteredModel[ i ][ attrs.groupProperty ] === true ) { + + // To cater multi level grouping + if ( tempArr.length === 0 ) { + startIndex = i + 1; + } + nestLevel = nestLevel + 1; + } + + // if group end + else if ( typeof $scope.filteredModel[ i ][ attrs.groupProperty ] !== 'undefined' && $scope.filteredModel[ i ][ attrs.groupProperty ] === false ) { + + nestLevel = nestLevel - 1; + + // cek if all are ticked or not + if ( tempArr.length > 0 && nestLevel === 0 ) { + + var allTicked = true; + + endIndex = i; + + for ( j = 0; j < tempArr.length ; j++ ) { + if ( typeof tempArr[ j ][ $scope.tickProperty ] !== 'undefined' && tempArr[ j ][ $scope.tickProperty ] === false ) { + allTicked = false; + break; + } + } + + if ( allTicked === true ) { + for ( j = startIndex; j <= endIndex ; j++ ) { + if ( typeof $scope.filteredModel[ j ][ attrs.groupProperty ] === 'undefined' ) { + if ( typeof attrs.disableProperty === 'undefined' ) { + $scope.filteredModel[ j ][ $scope.tickProperty ] = false; + // we refresh input model as well + inputModelIndex = $scope.filteredModel[ j ][ $scope.indexProperty ]; + $scope.inputModel[ inputModelIndex ][ $scope.tickProperty ] = false; + } + else if ( $scope.filteredModel[ j ][ attrs.disableProperty ] !== true ) { + $scope.filteredModel[ j ][ $scope.tickProperty ] = false; + // we refresh input model as well + inputModelIndex = $scope.filteredModel[ j ][ $scope.indexProperty ]; + $scope.inputModel[ inputModelIndex ][ $scope.tickProperty ] = false; + } + } + } + } + + else { + for ( j = startIndex; j <= endIndex ; j++ ) { + if ( typeof $scope.filteredModel[ j ][ attrs.groupProperty ] === 'undefined' ) { + if ( typeof attrs.disableProperty === 'undefined' ) { + $scope.filteredModel[ j ][ $scope.tickProperty ] = true; + // we refresh input model as well + inputModelIndex = $scope.filteredModel[ j ][ $scope.indexProperty ]; + $scope.inputModel[ inputModelIndex ][ $scope.tickProperty ] = true; + + } + else if ( $scope.filteredModel[ j ][ attrs.disableProperty ] !== true ) { + $scope.filteredModel[ j ][ $scope.tickProperty ] = true; + // we refresh input model as well + inputModelIndex = $scope.filteredModel[ j ][ $scope.indexProperty ]; + $scope.inputModel[ inputModelIndex ][ $scope.tickProperty ] = true; + } + } + } + } + } + } + + // if data + else { + tempArr.push( $scope.filteredModel[ i ] ); + } + } + } + + // if an item (not group marker) is clicked + else { + + // If it's single selection mode + if ( typeof attrs.selectionMode !== 'undefined' && attrs.selectionMode.toUpperCase() === 'SINGLE' ) { + + // first, set everything to false + for( i=0 ; i < $scope.filteredModel.length ; i++) { + $scope.filteredModel[ i ][ $scope.tickProperty ] = false; + } + for( i=0 ; i < $scope.inputModel.length ; i++) { + $scope.inputModel[ i ][ $scope.tickProperty ] = false; + } + + // then set the clicked item to true + $scope.filteredModel[ index ][ $scope.tickProperty ] = true; + } + + // Multiple + else { + $scope.filteredModel[ index ][ $scope.tickProperty ] = !$scope.filteredModel[ index ][ $scope.tickProperty ]; + } + + // we refresh input model as well + var inputModelIndex = $scope.filteredModel[ index ][ $scope.indexProperty ]; + $scope.inputModel[ inputModelIndex ][ $scope.tickProperty ] = $scope.filteredModel[ index ][ $scope.tickProperty ]; + } + + // we execute the callback function here + clickedItem = angular.copy( item ); + if ( clickedItem !== null ) { + $timeout( function() { + delete clickedItem[ $scope.indexProperty ]; + delete clickedItem[ $scope.spacingProperty ]; + $scope.onItemClick( { data: clickedItem } ); + clickedItem = null; + }, 0 ); + } + + $scope.refreshOutputModel(); + $scope.refreshButton(); + + // We update the index here + prevTabIndex = $scope.tabIndex; + $scope.tabIndex = ng_repeat_index + helperItemsLength; + + // Set focus on the hidden checkbox + e.target.focus(); + + // set & remove CSS style + $scope.removeFocusStyle( prevTabIndex ); + $scope.setFocusStyle( $scope.tabIndex ); + + if ( typeof attrs.selectionMode !== 'undefined' && attrs.selectionMode.toUpperCase() === 'SINGLE' ) { + // on single selection mode, we then hide the checkbox layer + $scope.toggleCheckboxes( e ); + } + } + + // update $scope.outputModel + $scope.refreshOutputModel = function() { + + $scope.outputModel = []; + var + outputProps = [], + tempObj = {}; + + // v4.0.0 + if ( typeof attrs.outputProperties !== 'undefined' ) { + outputProps = attrs.outputProperties.split(' '); + angular.forEach( $scope.inputModel, function( value ) { + if ( + typeof value !== 'undefined' + && typeof value[ attrs.groupProperty ] === 'undefined' + && value[ $scope.tickProperty ] === true + ) { + tempObj = {}; + angular.forEach( value, function( value1, key1 ) { + if ( outputProps.indexOf( key1 ) > -1 ) { + tempObj[ key1 ] = value1; + } + }); + var index = $scope.outputModel.push( tempObj ); + delete $scope.outputModel[ index - 1 ][ $scope.indexProperty ]; + delete $scope.outputModel[ index - 1 ][ $scope.spacingProperty ]; + } + }); + } + else { + angular.forEach( $scope.inputModel, function( value ) { + if ( + typeof value !== 'undefined' + && typeof value[ attrs.groupProperty ] === 'undefined' + && value[ $scope.tickProperty ] === true + ) { + var temp = angular.copy( value ); + var index = $scope.outputModel.push( temp ); + delete $scope.outputModel[ index - 1 ][ $scope.indexProperty ]; + delete $scope.outputModel[ index - 1 ][ $scope.spacingProperty ]; + } + }); + } + } + + // refresh button label + $scope.refreshButton = function() { + + $scope.varButtonLabel = ''; + var ctr = 0; + + // refresh button label... + if ( $scope.outputModel.length === 0 ) { + // https://github.com/isteven/angular-multi-select/pull/19 + $scope.varButtonLabel = $scope.lang.nothingSelected; + } + else { + var tempMaxLabels = $scope.outputModel.length; + if ( typeof attrs.maxLabels !== 'undefined' && attrs.maxLabels !== '' ) { + tempMaxLabels = attrs.maxLabels; + } + + // if max amount of labels displayed.. + if ( $scope.outputModel.length > tempMaxLabels ) { + $scope.more = true; + } + else { + $scope.more = false; + } + + angular.forEach( $scope.inputModel, function( value ) { + if ( typeof value !== 'undefined' && value[ attrs.tickProperty ] === true ) { + if ( ctr < tempMaxLabels ) { + $scope.varButtonLabel += ( $scope.varButtonLabel.length > 0 ? ',
' : '
') + $scope.writeLabel( value, 'buttonLabel' ); + } + ctr++; + } + }); + + if ( $scope.more === true ) { + // https://github.com/isteven/angular-multi-select/pull/16 + if (tempMaxLabels > 0) { + $scope.varButtonLabel += ', ... '; + } + $scope.varButtonLabel += '(' + $scope.outputModel.length + ')'; + } + } + // $scope.varButtonLabel = $sce.trustAsHtml( $scope.varButtonLabel + '' ); + $scope.varButtonLabel = $sanitize($scope.varButtonLabel + ''); + } + + // Check if a checkbox is disabled or enabled. It will check the granular control (disableProperty) and global control (isDisabled) + // Take note that the granular control has higher priority. + $scope.itemIsDisabled = function( item ) { + + if ( typeof attrs.disableProperty !== 'undefined' && item[ attrs.disableProperty ] === true ) { + return true; + } + else { + if ( $scope.isDisabled === true ) { + return true; + } + else { + return false; + } + } + + } + + // A simple function to parse the item label settings. Used on the buttons and checkbox labels. + $scope.writeLabel = function( item, type ) { + // type is either 'itemLabel' or 'buttonLabel' + var temp = attrs[ type ].split( ' ' ); + var label = ''; + + angular.forEach( temp, function( value ) { + item[ value ] && ( label += ' ' + value.split( '.' ).reduce( function( prev, current ) { + return prev[ current ]; + }, item )); + }); + + if ( type.toUpperCase() === 'BUTTONLABEL' ) { + return label; + } + // return $sce.trustAsHtml( label ); + return $sanitize(label); + } + + // UI operations to show/hide checkboxes based on click event.. + $scope.toggleCheckboxes = function( ) { + + // We grab the button + var clickedEl = element.children()[0]; + + // Just to make sure.. had a bug where key events were recorded twice + angular.element( document ).off( 'click', $scope.externalClickListener ); + angular.element( document ).off( 'keydown', $scope.keyboardListener ); + + // The idea below was taken from another multi-select directive - https://github.com/amitava82/angular-multiselect + // His version is awesome if you need a more simple multi-select approach. + + // close + if ( angular.element( checkBoxLayer ).hasClass( 'show' )) { + + angular.element( checkBoxLayer ).removeClass( 'show' ); + angular.element( clickedEl ).removeClass( 'buttonClicked' ); + angular.element( document ).off( 'click', $scope.externalClickListener ); + angular.element( document ).off( 'keydown', $scope.keyboardListener ); + + // clear the focused element; + $scope.removeFocusStyle( $scope.tabIndex ); + if ( typeof formElements[ $scope.tabIndex ] !== 'undefined' ) { + formElements[ $scope.tabIndex ].blur(); + } + + // close callback + $timeout( function() { + $scope.onClose(); + }, 0 ); + + // set focus on button again + element.children().children()[ 0 ].focus(); + } + // open + else + { + // clear filter + $scope.inputLabel.labelFilter = ''; + $scope.updateFilter(); + + helperItems = []; + helperItemsLength = 0; + + angular.element( checkBoxLayer ).addClass( 'show' ); + angular.element( clickedEl ).addClass( 'buttonClicked' ); + + // Attach change event listener on the input filter. + // We need this because ng-change is apparently not an event listener. + angular.element( document ).on( 'click', $scope.externalClickListener ); + angular.element( document ).on( 'keydown', $scope.keyboardListener ); + + // to get the initial tab index, depending on how many helper elements we have. + // priority is to always focus it on the input filter + $scope.getFormElements(); + $scope.tabIndex = 0; + + var helperContainer = angular.element( element[ 0 ].querySelector( '.helperContainer' ) )[0]; + + if ( typeof helperContainer !== 'undefined' ) { + for ( var i = 0; i < helperContainer.getElementsByTagName( 'BUTTON' ).length ; i++ ) { + helperItems[ i ] = helperContainer.getElementsByTagName( 'BUTTON' )[ i ]; + } + helperItemsLength = helperItems.length + helperContainer.getElementsByTagName( 'INPUT' ).length; + } + + // focus on the filter element on open. + if ( element[ 0 ].querySelector( '.inputFilter' ) ) { + element[ 0 ].querySelector( '.inputFilter' ).focus(); + $scope.tabIndex = $scope.tabIndex + helperItemsLength - 2; + // blur button in vain + angular.element( element ).children()[ 0 ].blur(); + } + // if there's no filter then just focus on the first checkbox item + else { + if ( !$scope.isDisabled ) { + $scope.tabIndex = $scope.tabIndex + helperItemsLength; + if ( $scope.inputModel.length > 0 ) { + formElements[ $scope.tabIndex ].focus(); + $scope.setFocusStyle( $scope.tabIndex ); + // blur button in vain + angular.element( element ).children()[ 0 ].blur(); + } + } + } + + // open callback + $scope.onOpen(); + } + } + + // handle clicks outside the button / multi select layer + $scope.externalClickListener = function( e ) { + + var targetsArr = element.find( e.target.tagName ); + for (var i = 0; i < targetsArr.length; i++) { + if ( e.target == targetsArr[i] ) { + return; + } + } + + angular.element( checkBoxLayer.previousSibling ).removeClass( 'buttonClicked' ); + angular.element( checkBoxLayer ).removeClass( 'show' ); + angular.element( document ).off( 'click', $scope.externalClickListener ); + angular.element( document ).off( 'keydown', $scope.keyboardListener ); + + // close callback + $timeout( function() { + $scope.onClose(); + }, 0 ); + + // set focus on button again + element.children().children()[ 0 ].focus(); + } + + // select All / select None / reset buttons + $scope.select = function( type, e ) { + + var helperIndex = helperItems.indexOf( e.target ); + $scope.tabIndex = helperIndex; + + switch( type.toUpperCase() ) { + case 'ALL': + angular.forEach( $scope.filteredModel, function( value ) { + if ( typeof value !== 'undefined' && value[ attrs.disableProperty ] !== true ) { + if ( typeof value[ attrs.groupProperty ] === 'undefined' ) { + value[ $scope.tickProperty ] = true; + } + } + }); + $scope.refreshOutputModel(); + $scope.refreshButton(); + $scope.onSelectAll(); + break; + case 'NONE': + angular.forEach( $scope.filteredModel, function( value ) { + if ( typeof value !== 'undefined' && value[ attrs.disableProperty ] !== true ) { + if ( typeof value[ attrs.groupProperty ] === 'undefined' ) { + value[ $scope.tickProperty ] = false; + } + } + }); + $scope.refreshOutputModel(); + $scope.refreshButton(); + $scope.onSelectNone(); + break; + case 'RESET': + angular.forEach( $scope.filteredModel, function( value ) { + if ( typeof value[ attrs.groupProperty ] === 'undefined' && typeof value !== 'undefined' && value[ attrs.disableProperty ] !== true ) { + var temp = value[ $scope.indexProperty ]; + value[ $scope.tickProperty ] = $scope.backUp[ temp ][ $scope.tickProperty ]; + } + }); + $scope.refreshOutputModel(); + $scope.refreshButton(); + $scope.onReset(); + break; + case 'CLEAR': + $scope.tabIndex = $scope.tabIndex + 1; + $scope.onClear(); + break; + case 'FILTER': + $scope.tabIndex = helperItems.length - 1; + break; + default: + } + } + + // just to create a random variable name + function genRandomString( length ) { + var possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'; + var temp = ''; + for( var i=0; i < length; i++ ) { + temp += possible.charAt( Math.floor( Math.random() * possible.length )); + } + return temp; + } + + // count leading spaces + $scope.prepareGrouping = function() { + var spacing = 0; + angular.forEach( $scope.filteredModel, function( value ) { + value[ $scope.spacingProperty ] = spacing; + if ( value[ attrs.groupProperty ] === true ) { + spacing+=2; + } + else if ( value[ attrs.groupProperty ] === false ) { + spacing-=2; + } + }); + } + + // prepare original index + $scope.prepareIndex = function() { + var ctr = 0; + angular.forEach( $scope.filteredModel, function( value ) { + value[ $scope.indexProperty ] = ctr; + ctr++; + }); + } + + // navigate using up and down arrow + $scope.keyboardListener = function( e ) { + + var key = e.keyCode ? e.keyCode : e.which; + var isNavigationKey = false; + + // ESC key (close) + if ( key === 27 ) { + e.preventDefault(); + e.stopPropagation(); + $scope.toggleCheckboxes( e ); + } + + + // next element ( tab, down & right key ) + else if ( key === 40 || key === 39 || ( !e.shiftKey && key == 9 ) ) { + + isNavigationKey = true; + prevTabIndex = $scope.tabIndex; + $scope.tabIndex++; + if ( $scope.tabIndex > formElements.length - 1 ) { + $scope.tabIndex = 0; + prevTabIndex = formElements.length - 1; + } + while ( formElements[ $scope.tabIndex ].disabled === true ) { + $scope.tabIndex++; + if ( $scope.tabIndex > formElements.length - 1 ) { + $scope.tabIndex = 0; + } + if ( $scope.tabIndex === prevTabIndex ) { + break; + } + } + } + + // prev element ( shift+tab, up & left key ) + else if ( key === 38 || key === 37 || ( e.shiftKey && key == 9 ) ) { + isNavigationKey = true; + prevTabIndex = $scope.tabIndex; + $scope.tabIndex--; + if ( $scope.tabIndex < 0 ) { + $scope.tabIndex = formElements.length - 1; + prevTabIndex = 0; + } + while ( formElements[ $scope.tabIndex ].disabled === true ) { + $scope.tabIndex--; + if ( $scope.tabIndex === prevTabIndex ) { + break; + } + if ( $scope.tabIndex < 0 ) { + $scope.tabIndex = formElements.length - 1; + } + } + } + + if ( isNavigationKey === true ) { + + e.preventDefault(); + + // set focus on the checkbox + formElements[ $scope.tabIndex ].focus(); + var actEl = document.activeElement; + + if ( actEl.type.toUpperCase() === 'CHECKBOX' ) { + $scope.setFocusStyle( $scope.tabIndex ); + $scope.removeFocusStyle( prevTabIndex ); + } + else { + $scope.removeFocusStyle( prevTabIndex ); + $scope.removeFocusStyle( helperItemsLength ); + $scope.removeFocusStyle( formElements.length - 1 ); + } + } + + isNavigationKey = false; + } + + // set (add) CSS style on selected row + $scope.setFocusStyle = function( tabIndex ) { + angular.element( formElements[ tabIndex ] ).parent().parent().parent().addClass( 'multiSelectFocus' ); + } + + // remove CSS style on selected row + $scope.removeFocusStyle = function( tabIndex ) { + angular.element( formElements[ tabIndex ] ).parent().parent().parent().removeClass( 'multiSelectFocus' ); + } + + /********************* + ********************* + * + * 1) Initializations + * + ********************* + *********************/ + + // attrs to $scope - attrs-$scope - attrs - $scope + // Copy some properties that will be used on the template. They need to be in the $scope. + $scope.groupProperty = attrs.groupProperty; + $scope.tickProperty = attrs.tickProperty; + $scope.directiveId = attrs.directiveId; + + // Unfortunately I need to add these grouping properties into the input model + var tempStr = genRandomString( 5 ); + $scope.indexProperty = 'idx_' + tempStr; + $scope.spacingProperty = 'spc_' + tempStr; + + // set orientation css + if ( typeof attrs.orientation !== 'undefined' ) { + + if ( attrs.orientation.toUpperCase() === 'HORIZONTAL' ) { + $scope.orientationH = true; + $scope.orientationV = false; + } + else + { + $scope.orientationH = false; + $scope.orientationV = true; + } + } + + // get elements required for DOM operation + checkBoxLayer = element.children().children().next()[0]; + + // set max-height property if provided + if ( typeof attrs.maxHeight !== 'undefined' ) { + var layer = element.children().children().children()[0]; + angular.element( layer ).attr( "style", "height:" + attrs.maxHeight + "; overflow-y:scroll;" ); + } + + // some flags for easier checking + for ( var property in $scope.helperStatus ) { + if ( $scope.helperStatus.hasOwnProperty( property )) { + if ( + typeof attrs.helperElements !== 'undefined' + && attrs.helperElements.toUpperCase().indexOf( property.toUpperCase() ) === -1 + ) { + $scope.helperStatus[ property ] = false; + } + } + } + if ( typeof attrs.selectionMode !== 'undefined' && attrs.selectionMode.toUpperCase() === 'SINGLE' ) { + $scope.helperStatus[ 'all' ] = false; + $scope.helperStatus[ 'none' ] = false; + } + + // helper button icons.. I guess you can use html tag here if you want to. + $scope.icon = {}; + $scope.icon.selectAll = '✓'; // a tick icon + $scope.icon.selectNone = '×'; // x icon + $scope.icon.reset = '↶'; // undo icon + // this one is for the selected items + $scope.icon.tickMark = '✓'; // a tick icon + + // configurable button labels + // if ( typeof attrs.translation !== 'undefined' ) { + // $scope.lang.selectAll = $sce.trustAsHtml( $scope.icon.selectAll + '  ' + $scope.translation.selectAll ); + // $scope.lang.selectNone = $sce.trustAsHtml( $scope.icon.selectNone + '  ' + $scope.translation.selectNone ); + // $scope.lang.reset = $sce.trustAsHtml( $scope.icon.reset + '  ' + $scope.translation.reset ); + // $scope.lang.search = $scope.translation.search; + // $scope.lang.nothingSelected = $sce.trustAsHtml( $scope.translation.nothingSelected ); + // } + // else { + // $scope.lang.selectAll = $sce.trustAsHtml( $scope.icon.selectAll + '  Select All' ); + // $scope.lang.selectNone = $sce.trustAsHtml( $scope.icon.selectNone + '  Select None' ); + // $scope.lang.reset = $sce.trustAsHtml( $scope.icon.reset + '  Reset' ); + // $scope.lang.search = 'Search...'; + // $scope.lang.nothingSelected = 'None Selected'; + // } + // $scope.icon.tickMark = $sce.trustAsHtml( $scope.icon.tickMark ); + if ( typeof attrs.translation !== 'undefined' ) { + $scope.lang.selectAll = $sanitize( $scope.icon.selectAll + '  ' + $scope.translation.selectAll ); + $scope.lang.selectNone = $sanitize( $scope.icon.selectNone + '  ' + $scope.translation.selectNone ); + $scope.lang.reset = $sanitize( $scope.icon.reset + '  ' + $scope.translation.reset ); + $scope.lang.search = $scope.translation.search; + $scope.lang.nothingSelected = $sanitize( $scope.translation.nothingSelected ); + } + else { + $scope.lang.selectAll = $sanitize( $scope.icon.selectAll + '  Select All' ); + $scope.lang.selectNone = $sanitize( $scope.icon.selectNone + '  Select None' ); + $scope.lang.reset = $sanitize( $scope.icon.reset + '  Reset' ); + $scope.lang.search = 'Search...'; + $scope.lang.nothingSelected = 'None Selected'; + } + $scope.icon.tickMark = $sanitize( $scope.icon.tickMark ); + + // min length of keyword to trigger the filter function + if ( typeof attrs.MinSearchLength !== 'undefined' && parseInt( attrs.MinSearchLength ) > 0 ) { + vMinSearchLength = Math.floor( parseInt( attrs.MinSearchLength ) ); + } + + /******************************************************* + ******************************************************* + * + * 2) Logic starts here, initiated by watch 1 & watch 2 + * + ******************************************************* + *******************************************************/ + + // watch1, for changes in input model property + // updates multi-select when user select/deselect a single checkbox programatically + // https://github.com/isteven/angular-multi-select/issues/8 + $scope.$watch( 'inputModel' , function( newVal ) { + if ( newVal ) { + $scope.refreshOutputModel(); + $scope.refreshButton(); + } + }, true ); + + // watch2 for changes in input model as a whole + // this on updates the multi-select when a user load a whole new input-model. We also update the $scope.backUp variable + $scope.$watch( 'inputModel' , function( newVal ) { + if ( newVal ) { + $scope.backUp = angular.copy( $scope.inputModel ); + $scope.updateFilter(); + $scope.prepareGrouping(); + $scope.prepareIndex(); + $scope.refreshOutputModel(); + $scope.refreshButton(); + } + }); + + // watch for changes in directive state (disabled or enabled) + $scope.$watch( 'isDisabled' , function( newVal ) { + $scope.isDisabled = newVal; + }); + + // this is for touch enabled devices. We don't want to hide checkboxes on scroll. + var onTouchStart = function() { + $scope.$apply( function() { + $scope.scrolled = false; + }); + }; + angular.element( document ).bind( 'touchstart', onTouchStart); + var onTouchMove = function() { + $scope.$apply( function() { + $scope.scrolled = true; + }); + }; + angular.element( document ).bind( 'touchmove', onTouchMove); + + // unbind document events to prevent memory leaks + $scope.$on( '$destroy', function () { + angular.element( document ).unbind( 'touchstart', onTouchStart); + angular.element( document ).unbind( 'touchmove', onTouchMove); + }); + } + } +}]).run( [ '$templateCache' , function( $templateCache ) { + var template = + '' + + // main button + '' + + // overlay layer + '
' + + // container of the helper elements + '
' + + // container of the first 3 buttons, select all, none and reset + '
' + + // select all + ''+ + // select none + ''+ + // reset + '' + + '
' + + // the search box + '
'+ + // textfield + ''+ + // clear button + ' '+ + '
'+ + '
'+ + // selection items + '
'+ + '
'+ + // this is the spacing for grouped items + '
'+ + '
'+ + '
'+ + ''+ + '
'+ + // the tick/check mark + ''+ + '
'+ + '
'+ + '
'+ + '
'; + $templateCache.put( 'isteven-multi-select.htm' , template ); +}]); diff --git a/app/libraries/isteven-angular-multiselect/package.json b/app/libraries/isteven-angular-multiselect/package.json new file mode 100644 index 000000000..9aa2e3960 --- /dev/null +++ b/app/libraries/isteven-angular-multiselect/package.json @@ -0,0 +1,22 @@ +{ + "name": "isteven-angular-multiselect", + "version": "v4.0.0", + "description": "A multi select dropdown directive for AngularJS", + "main": [ + "isteven-multi-select.js", + "isteven-multi-select.css" + ], + "repository": { + "type": "git", + "url": "https://github.com/isteven/angular-multi-select.git" + }, + "keywords": [ + "angular" + ], + "author": "Ignatius Steven (https://github.com/isteven)", + "license": "MIT", + "bugs": { + "url": "https://github.com/isteven/angular-multi-select/issues" + }, + "homepage": "https://github.com/isteven/angular-multi-select" +} diff --git a/app/portainer/services/modalService.js b/app/portainer/services/modalService.js index dceffadca..4b896859d 100644 --- a/app/portainer/services/modalService.js +++ b/app/portainer/services/modalService.js @@ -1,7 +1,7 @@ import bootbox from 'bootbox'; angular.module('portainer.app') -.factory('ModalService', [function ModalServiceFactory() { +.factory('ModalService', [ '$sanitize', function ModalServiceFactory($sanitize) { 'use strict'; var service = {}; @@ -17,17 +17,18 @@ angular.module('portainer.app') var confirmButtons = function(options) { var buttons = { confirm: { - label: options.buttons.confirm.label, - className: options.buttons.confirm.className + label: $sanitize(options.buttons.confirm.label), + className: $sanitize(options.buttons.confirm.className) }, cancel: { - label: options.buttons.cancel && options.buttons.cancel.label ? options.buttons.cancel.label : 'Cancel' + label: options.buttons.cancel && options.buttons.cancel.label ? $sanitize(options.buttons.cancel.label) : 'Cancel' } }; return buttons; }; service.enlargeImage = function(image) { + image = $sanitize(image); bootbox.dialog({ message: '', className: 'image-zoom-modal', @@ -45,7 +46,7 @@ angular.module('portainer.app') applyBoxCSS(box); }; - service.prompt = function(options){ + function prompt(options){ var box = bootbox.prompt({ title: options.title, inputType: options.inputType, @@ -54,9 +55,9 @@ angular.module('portainer.app') callback: options.callback }); applyBoxCSS(box); - }; + } - service.customPrompt = function(options, optionToggled) { + function customPrompt(options, optionToggled) { var box = bootbox.prompt({ title: options.title, inputType: options.inputType, @@ -67,7 +68,7 @@ angular.module('portainer.app') applyBoxCSS(box); box.find('.bootbox-body').prepend('

' + options.message + '

'); box.find('.bootbox-input-checkbox').prop('checked', optionToggled); - }; + } service.confirmAccessControlUpdate = function(callback) { service.confirm({ @@ -98,6 +99,7 @@ angular.module('portainer.app') }; service.confirmDeletion = function(message, callback) { + message = $sanitize(message); service.confirm({ title: 'Are you sure ?', message: message, @@ -112,7 +114,7 @@ angular.module('portainer.app') }; service.confirmContainerDeletion = function(title, callback) { - service.prompt({ + prompt({ title: title, inputType: 'checkbox', inputOptions: [ @@ -132,7 +134,7 @@ angular.module('portainer.app') }; service.confirmContainerRecreation = function(callback) { - service.customPrompt({ + customPrompt({ title: 'Are you sure?', message: 'You\'re about to re-create this container, any non-persisted data will be lost. This container will be removed and another one will be created using the same configuration.', inputType: 'checkbox', @@ -181,7 +183,7 @@ angular.module('portainer.app') }; service.confirmServiceForceUpdate = function(message, callback) { - service.customPrompt({ + customPrompt({ title: 'Are you sure ?', message: message, inputType: 'checkbox', diff --git a/app/vendors.js b/app/vendors.js index c42341184..b1ff41f4e 100644 --- a/app/vendors.js +++ b/app/vendors.js @@ -1,6 +1,5 @@ import 'ui-select/dist/select.css'; import 'bootstrap/dist/css/bootstrap.css'; -import 'isteven-angular-multiselect/isteven-multi-select.css'; import '@fortawesome/fontawesome-free-webfonts/css/fa-brands.css'; import '@fortawesome/fontawesome-free-webfonts/css/fa-solid.css'; import '@fortawesome/fontawesome-free-webfonts/css/fontawesome.css'; @@ -19,7 +18,6 @@ window.angular = angular; import 'moment'; import '@uirouter/angularjs'; import 'ui-select'; -import 'isteven-angular-multiselect/isteven-multi-select.js'; import 'angular-cookies'; import 'angular-sanitize'; import 'ng-file-upload'; diff --git a/package.json b/package.json index 74a2bd14a..c2ee71be8 100644 --- a/package.json +++ b/package.json @@ -71,7 +71,6 @@ "chart.js": "~2.6.0", "codemirror": "~5.30.0", "filesize": "~3.3.0", - "isteven-angular-multiselect": "~4.0.0", "jquery": "3.4.0", "js-yaml": "~3.13.1", "lodash-es": "^4.17.15", diff --git a/yarn.lock b/yarn.lock index 681a1389e..8ef836d1b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6492,11 +6492,6 @@ istanbul@~0.1.40: which "1.0.x" wordwrap "0.0.x" -isteven-angular-multiselect@~4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/isteven-angular-multiselect/-/isteven-angular-multiselect-4.0.0.tgz#70276da5ff3bc4d9a0887dc585ee26a1a26a8ed6" - integrity sha1-cCdtpf87xNmgiH3Fhe4moaJqjtY= - isurl@^1.0.0-alpha5: version "1.0.0" resolved "https://registry.yarnpkg.com/isurl/-/isurl-1.0.0.tgz#b27f4f49f3cdaa3ea44a0a5b7f3462e6edc39d67"