kahuna/public/js/components/gr-chips/gr-chips.js (157 lines of code) (raw):

import angular from 'angular'; import './gr-chips.css'; import template from './gr-chips.html'; import {grTextChip} from './gr-text-chip'; import {grFilterChip} from './gr-filter-chip'; import {grStaticFilterChip} from './gr-static-filter-chip'; import {grFilterChooserChip} from './gr-filter-chooser-chip'; export const grChips = angular.module('gr.chips', [ grTextChip.name, grFilterChip.name, grStaticFilterChip.name, grFilterChooserChip.name ]); grChips.controller('grChipsCtrl', ['$scope', function($scope) { const $grChipsCtrl = this; $grChipsCtrl.items = []; $grChipsCtrl.focusedItem = null; $grChipsCtrl.caretStartOffset = null; $grChipsCtrl.caretEndOffset = null; $grChipsCtrl.configureNgModel = function(ngModelCtrl, onChangeExpr, autoCompleteExpr, validKeysExpr, autofocus, placeholder) { $grChipsCtrl.placeholder = placeholder; $grChipsCtrl.onChange = () => onChangeExpr($scope, {$chips: $grChipsCtrl.items}); $grChipsCtrl.getSuggestions = ($chip) => autoCompleteExpr($scope, {$chip}); const validKeys = validKeysExpr($scope); $grChipsCtrl.isValidKey = (key) => { if (Array.isArray(validKeys)) { return validKeys.indexOf(key) !== -1; } else { // No restriction on keys, all valid return true; } }; ngModelCtrl.$render = function() { $grChipsCtrl.items = ngModelCtrl.$viewValue; if (autofocus) { $grChipsCtrl.focusEndOfFirstChip(); } }; }; // Broadcast changes to the items list. // Note that individual chip directives also need to // broadcast updates to chip fields themselves. $scope.$watchCollection(() => $grChipsCtrl.items, (items, previousItems) => { // Ignore initialisation call if (items !== previousItems) { $grChipsCtrl.onChange(); } }); $grChipsCtrl.isEmpty = function() { return $grChipsCtrl.items.length === 1 && $grChipsCtrl.items[0].value === ''; }; $grChipsCtrl.removeChip = function(item) { const index = $grChipsCtrl.items.indexOf(item); if (index !== -1) { removeChipAt(index); } }; $grChipsCtrl.removeLastChip = function() { //do not remove text chip if ($grChipsCtrl.items.length > 1) { removeChipAt($grChipsCtrl.items.length - 1); } }; $grChipsCtrl.setFocusedChip = function(item, caretStart = 0, caretEnd = 0) { const index = $grChipsCtrl.items.indexOf(item); if (index !== -1) { $grChipsCtrl.focusedItem = item; $grChipsCtrl.caretStartOffset = caretStart; $grChipsCtrl.caretEndOffset = caretEnd; return true; } }; $grChipsCtrl.unsetFocusedChip = function(item) { if ($grChipsCtrl.focusedItem === item) { $grChipsCtrl.focusedItem = null; $grChipsCtrl.caretStartOffset = null; $grChipsCtrl.caretEndOffset = null; return true; } }; $grChipsCtrl.focusEndOfChipBefore = function(item) { const index = $grChipsCtrl.items.indexOf(item) - 1; if (index >= 0) { const previousItem = $grChipsCtrl.items[index]; const itemLength = previousItem.value.length; return $grChipsCtrl.setFocusedChip(previousItem, itemLength, itemLength); } }; $grChipsCtrl.focusStartOfChipAfter = function(item) { const index = $grChipsCtrl.items.indexOf(item) + 1; if (index <= $grChipsCtrl.items.length) { return $grChipsCtrl.setFocusedChip($grChipsCtrl.items[index], 0, 0); } }; $grChipsCtrl.focusStartOfFirstChip = function() { const firstItem = $grChipsCtrl.items[0]; if (firstItem) { return $grChipsCtrl.setFocusedChip(firstItem, 0, 0); } }; $grChipsCtrl.focusEndOfFirstChip = function() { const firstItem = $grChipsCtrl.items[0]; if (firstItem) { const itemLength = firstItem.value.length; return $grChipsCtrl.setFocusedChip(firstItem, itemLength, itemLength); } }; $grChipsCtrl.focusEndOfLastChip = function() { const lastItem = $grChipsCtrl.items.slice(-1)[0]; if (lastItem) { const itemLength = lastItem.value.length; return $grChipsCtrl.setFocusedChip(lastItem, itemLength, itemLength); } }; $grChipsCtrl.insertChips = function(newItems) { //always insert chips to the end of items list //NB text chip is styled to appear last but is first in the items list const end = $grChipsCtrl.items.length; $grChipsCtrl.items.splice(end, 0, ...newItems); }; $grChipsCtrl.replaceChip = function(existingItem, newItems) { const index = $grChipsCtrl.items.indexOf(existingItem); $grChipsCtrl.items.splice(index, 1, ...newItems); if ($grChipsCtrl.focusedItem === existingItem) { $grChipsCtrl.setFocusedChip(newItems[0]); } }; function removeChipAt(index) { // If chip to remove is focused, move focus right before or after const removedItem = $grChipsCtrl.items[index]; if (removedItem === $grChipsCtrl.focusedItem) { $grChipsCtrl.focusEndOfChipBefore(removedItem) || $grChipsCtrl.focusStartOfChipAfter(removedItem); } $grChipsCtrl.items.splice(index, 1); } }]); grChips.directive('grChips', ['$parse', function($parse) { return { restrict: 'E', require: 'grChips', template: template, controller: 'grChipsCtrl', controllerAs: '$grChipsCtrl', compile: function compile(element, attrs) { const autoCompleteExpr = $parse(attrs.grAutocomplete); const onChangeExpr = $parse(attrs.grOnChange); const validKeysExpr = $parse(attrs.grValidKeys); return function link(scope, element, attrs, $grChipsCtrl) { const ngModelCtrl = element.controller('ngModel'); const autofocus = 'autofocus' in attrs; const placeholder = attrs.placeholder; $grChipsCtrl.configureNgModel( ngModelCtrl, onChangeExpr, autoCompleteExpr, validKeysExpr, autofocus, placeholder ); }; } }; }]); /* TODO * cannot focus static filter, breaks navigation - auto-completion: key descriptions */