Last active
December 3, 2021 11:57
-
-
Save dmorosinotto/3fbc78f1b2562bf4bf3226f829466158 to your computer and use it in GitHub Desktop.
AngularJS multi-combo directive working with mouse & keyboard + simple responsive style based on font-size - TRY LIVE SAMPLE: https://stackblitz.com/edit/web-platform-jjfqgs?file=index.html
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
angular.module("multiCombo",[]) | |
.directive("multiCombo", function () { | |
return { | |
restrict: 'E', | |
scope: { | |
value: '=', //VALORE MESSO IN BINDING CON LA SELECT USATO PER INIZIALIZZARE/LEGGERE VALORE TORNATO TIPO: T[valProp] | Array<T[valProp]> SE MULTIPLE | |
onSelect: '&', //EVENTO NOTIFICA VALORE CAMBIATO onSelect="handle(value)" E' AGGANCIATO AL ng-change DEL <select> | |
list: '=', //ARRAY DEGLI ITEM DA VISUALIZZARE: T[] | |
//ATTRIBUTI OPZIONALI | |
multiple: '@', //BOOLENAO PER INDICARE SE SI VUOLE SELEZIONE MULTIPLA O SINGOLA | |
size: '@', //NUMERO DI ELEMENTI DI MOSTRARE NELLA DROPDOWN LIST DEFAULT 10 | |
//CONFIGURAZIONI OPTIONALI | |
valProp: '@', //NOME DELLA PROPRIETA DA USARE COME VALORE DELL'<option> (SE NON PASSATO USA INTERA ITEM: T) | |
txtProp: '@', //NOME DELLA PROPRIETA DA USARE COME TESTO DELL'<option> (SE NON PASSATO USA valueProp) | |
fmtValue: '<', //EVENTUALE FUNZIONE DI FORMATTAZIONE DEL value -> SE NON PASSO (v: T|T[]|null)=>string FORMATTA CON String() + join(',') SE MULTIPLE | |
empty: '@', //EVENTUALE STRINGA DA VISUALIZZARE SE NON HO SELEZIONATO NULLA value=null + DEFAULT FALLBACK '--' | |
}, | |
template: function (tElem, tAttr) { | |
var hasMultiple = ('multiple' in tAttr) && String(tAttr['multiple']).toLowerCase() != 'false'; | |
return `<div class="multiCombo" ng-mouseleave="close()"> | |
<button type="button" ng-click="toggleOpen()" title="{{open ? 'Close list' : (empty || 'Choose from list...')}}">{{ (fmtValue && fmtValue(value)) || empty || '--'}}<i class="{{open ? 'icon-chevron-up': 'icon-chevron-down'}}"></i></button> | |
<datalist ng-show="open"> | |
<input type="search" ng-model="search" ng-model-options="{debounce: 250}" placeholder="Filter list..." tabindex="0" ng-keydown="onTabEnter($event)"> | |
<i class="icon-delete red" title="Clear selection" ng-click="close(true)"></i> | |
</input> | |
<select ${ hasMultiple ? 'multiple' : ''} size="${tAttr['size'] || 10}" ng-model="value" ng-change="onSelect({value: value});" ng-click="${!hasMultiple} && close()" tabindex="-1" ng-keydown="onTabEnter($event,true)"> | |
<option ng-hide="true" value="">{{empty||'--'}}</option> | |
<option ng-repeat="item in listFiltered" ng-value="valProp ? item[valProp] : item">{{ (txtProp || valProp) ? item[(txtProp||valProp)] : item }}</option> | |
</select> | |
</datalist> | |
</div>` | |
}, | |
controller: function ($scope, $element) { | |
var btn = $element.find("button")[0]; | |
var inp = $element.find("input" )[0]; | |
var sel = $element.find("select")[0]; | |
var isMultiple = $scope.multiple!=null && String($scope.multiple).toLowerCase() != 'false'; | |
//INIZIALIZZAZIONE FUNZIONE DI FORMATTAZIONE STANDARD value => string SE NON PASSATA! GESTISCE SIA CASO SINGOLO CHE ARRAY DI SELEZIONATI | |
if (typeof $scope.fmtValue != 'function') | |
$scope.fmtValue = function (v/*: null|string|string[] */) { | |
if (Array.isArray(v)) return v.length ? v.join(', ') : ''; | |
else return !!v ? String(v) : ''; | |
} | |
$scope.search = ""; | |
$scope.listFiltered = []; | |
$scope.filterBy = function (list, search) { | |
if (!list) return []; | |
if (!search) { | |
return list; | |
} else { | |
var text = search.toLowerCase().trim(); | |
var prop = $scope.txtProp || $scope.valProp || ''; | |
return list.filter(item => (!prop ? JSON.stringify(item) : String(item[prop])).toLowerCase().includes(text)); | |
} | |
} | |
var watcher = $scope.$watchGroup(['list', 'search'], ([list, search]) => { | |
//listFiltered VIENE COMPUTATO IN AUTOMATICO QUANDO CAMBIA search E/O list | |
$scope.listFiltered = $scope.filterBy(list, search); | |
}); | |
$scope.$on('$destroy', function () { | |
//cleanup watcher | |
if (watcher) watcher(); | |
if (btn) btn = null; | |
if (inp) inp = null; | |
if (sel) sel = null; | |
}); | |
$scope.toggleOpen = function () { | |
var opened = $scope.open = !$scope.open; | |
if (opened) { | |
$scope.search = ''; | |
try { setTimeout(()=>inp.focus(), 50); } catch { }; | |
} | |
} | |
$scope.close = function(clear) { | |
if (clear) $scope.value = null; | |
$scope.open = false; | |
try { setTimeout(() => btn.focus(), 50); } catch { }; | |
} | |
$scope.onTabEnter = function (e, close) { | |
switch (e.key) { | |
case 'Tab': | |
case 'Enter': | |
e.preventDefault(); e.stopPropagation(); | |
if (close) $scope.close(); | |
else try { | |
if (!($scope.value && $scope.value.length)) { //SE NON HO SELEZIONATO NULLA | |
var first = $scope.filterBy($scope.list, $scope.search)[0]; | |
if (first) { | |
var val = $scope.valProp ? first[$scope.valProp] : first; | |
$scope.value = isMultiple ? [val] : val; //PRESELEZIONO PRIMO ELEMENTO | |
} | |
} | |
setTimeout(() => sel.focus(), 50); | |
} catch { }; | |
} | |
} | |
} | |
} | |
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
multi-combo { | |
display: block; | |
} | |
.multiCombo { | |
position: relative; | |
} | |
.multiCombo button { | |
display: inline-block; | |
width: 100%; | |
line-height: 1.5em; | |
padding: 0 0.5em; | |
overflow: hidden; | |
white-space: nowrap; | |
text-overflow: ellipsis; | |
font-size: 1em; | |
border: 1px solid gray; | |
border-radius: 3px; | |
} | |
.multiCombo button i { | |
position: absolute; | |
bottom: 0; | |
right: 0; | |
border: 0; | |
padding: 0.3em; | |
} | |
.multiCombo datalist { | |
position: absolute; | |
display: block; | |
/*top: 1.4em;*/ | |
left: 0; | |
right: 0; | |
z-index: 1; | |
height: 14em; | |
border: 1px solid gray; | |
} | |
.multiCombo datalist input[type=search] { | |
position: absolute; | |
height: 1.4em; | |
width: 100%; | |
top: 0; | |
left: 0; | |
right: 0; | |
font-size: 1em; | |
padding: 0 0.5em; | |
} | |
.multiCombo datalist i { | |
position: absolute; | |
top: 0; | |
right: 0; | |
border: 0; | |
padding: 0.2em; | |
cursor: pointer; | |
} | |
.multiCombo datalist select { | |
position: absolute; | |
padding: 0 0.5em; | |
width: 100%; | |
top: 1.4em; | |
left: 0; | |
right: 0; | |
font-size: 1em; | |
} | |
i[class*=" icon-"], [class^=icon-] { | |
font-family: icomoon; | |
speak: none; | |
font-style: normal; | |
font-weight: 400; | |
font-variant: normal; | |
text-transform: none; | |
line-height: 1; | |
-webkit-font-smoothing: antialiased; | |
-moz-osx-font-smoothing: grayscale; | |
} | |
i.red { | |
color: red; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!-- TRY LIVE SAMPLE: https://stackblitz.com/edit/web-platform-jjfqgs?file=index.html --> | |
<html ng-app="sample"> | |
<head> | |
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.8.2/angular.min.js"></script> | |
<script src="multiCombo.directive.js"></script> | |
<link rel="stylesheet" href="multiCombo.style.css" /> | |
</head> | |
<body> | |
<h1>SAMPLE USECASE OF <code>multi-combo</code></h1> | |
<div ng-controller="SampleController"> | |
<form ng-init="flt={}"> | |
<label>Select collection:</label> | |
<multi-combo | |
list="cols" | |
fmt-value="showTitle" | |
txt-prop="title" | |
value="flt.coll" | |
on-select="Log('SET COLLECTION', value)" | |
></multi-combo> | |
<multi-combo | |
style="text-align: lefts" | |
list="cols" | |
val-prop="identifier" | |
value="flt.coll.identifier" | |
on-select="Log('SET ID',value)" | |
></multi-combo> | |
<label>Select tags:</label> | |
<multi-combo | |
style="font-size: 20px" | |
list="tags" | |
val-prop="title" | |
multiple | |
value="flt.tags" | |
on-select="Log('SET TAGS', value)" | |
></multi-combo> | |
<pre>{{flt | json }}</pre> | |
</form> | |
</div> | |
<script> | |
angular | |
.module('sample', ['multiCombo']) | |
.controller('SampleController', function ($scope) { | |
//PRELOAD TAGS AND COLS | |
function FAKE(n, name) { | |
//GENERATE FAKE Array<{identifier: number, title: string}> | |
return new Array(5) | |
.fill(name + ' ') | |
.map((n, i) => ({ identifier: i + Math.random(), title: n + i })); | |
} | |
$scope.tags = FAKE(20, 'Tag'); | |
$scope.cols = FAKE(10, 'Collection'); | |
$scope.Log = function (msg, value) { | |
console.warn(msg, value, typeof value); | |
}; | |
$scope.showTitle = function (item) { | |
console.dir(item); | |
return item && item.title; | |
}; | |
}); | |
</script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment