2013-06-30 7 views
9

Sto provando a navigare attraverso un elenco di record utilizzando solo la tastiera. Quando la pagina viene caricata, il "focus" predefinito deve essere sul primo record, quando l'utente fa clic sulla freccia giù sulla tastiera, il record successivo deve essere focalizzato. Quando l'utente fa clic sulla freccia su, il record precedente deve essere focalizzato. Quando l'utente fa clic sul pulsante Invio, dovrebbe portarli alla pagina dei dettagli di quel record.Navigare nell'interfaccia utente utilizzando solo la tastiera

Here's what I have so far on Plunkr.

Sembra che questo è supportato in AngularJS a 1.1.5 (instabile), che non possiamo usare in produzione. Attualmente sto usando 1.0.7. Spero di fare qualcosa di simile - la chiave dovrebbe essere gestita a livello di documento. Quando l'utente preme un determinato tasto, il codice dovrebbe cercare in una serie di chiavi consentite. Se viene trovata una corrispondenza (ad esempio, il codice di una chiave giù), dovrebbe spostare lo stato attivo (applicare il .highlight css) all'elemento successivo. Quando viene premuto enter, dovrebbe prendere il record quale .highlight css e ottenere l'id del record per un'ulteriore elaborazione.

Grazie!

risposta

14

Ecco l'esempio quello che si potrebbe scegliere di fare: http://plnkr.co/edit/XRGPYCk6auOxmylMe0Uu?p=preview

<body key-trap> 
    <div ng-controller="testCtrl"> 
    <li ng-repeat="record in records"> 
     <div class="record" 
      ng-class="{'record-highlight': record.navIndex == focu sIndex}"> 
     {{ record.name }} 
     </div> 
    </li> 
    </div> 
</body> 

Questo è l'approccio più semplice mi veniva in mente. Lega una direttiva keyTrap a body che rileva l'evento keydown e il messaggio $broadcast agli ambiti figlio. L'ambito del portacampione rileverà il messaggio e semplicemente incrementerà o decrementa il focusIndex o attiva una funzione open se si colpisce enter.

EDIT

http://plnkr.co/edit/rwUDTtkQkaQ0dkIFflcy?p=preview

ora supporta, ordinato/elenco filtrato.

La parte di gestione degli eventi non è stata modificata, ma ora utilizza $index e anche la tecnica di caching dell'elenco filtrato combinata per tenere traccia di quale elemento viene messo a fuoco.

+0

È un buon approccio, ma non funziona quando le voci sono ordinate ('registra nei record | orderBy: '-name''). Hai una soluzione anche per questo? (non solo per questo caso ma anche più generico) – akirk

+2

Grazie per il feedback. È sempre divertente e piacevole essere sfidati con casi d'uso più difficili. Aggiungo il codice aggiuntivo che supporta l'elenco ordinato/filtrato. – Tosh

+0

Grazie! La tua soluzione è stata davvero d'ispirazione. – akirk

1

Avevo un requisito simile per supportare la navigazione dell'interfaccia utente utilizzando i tasti freccia. Quello che ho finalmente si avvicinò con è gestore keydown eventi del DOM incapsulata all'interno di una direttiva AngularJS:

HTML:

<ul ng-controller="MainCtrl"> 
    <li ng-repeat="record in records"> 
     <div focusable tag="record" on-key="onKeyPressed" class="record"> 
      {{ record.name }} 
     </div> 
    </li> 
</ul> 

CSS:

.record { 
    color: #000; 
    background-color: #fff; 
} 
.record:focus { 
    color: #fff; 
    background-color: #000; 
    outline: none; 
} 

JS:

module.directive('focusable', function() { 
    return { 
     restrict: 'A', 
     link: function (scope, element, attrs) { 
      element.attr('tabindex', '-1'); // make it focusable 

      var tag = attrs.tag ? scope.$eval(attrs.tag) : undefined; // get payload if defined 
      var onKeyHandler = attrs.onKey ? scope.$eval(attrs.onKey) : undefined; 

      element.bind('keydown', function (event) { 
       var target = event.target; 
       var key = event.which; 

       if (isArrowKey(key)) { 
        var nextFocused = getNextElement(key); // determine next element that should get focused 
        if (nextFocused) { 
         nextFocused.focus(); 
         event.preventDefault(); 
         event.stopPropagation(); 
        } 
       } 
       else if (onKeyHandler) { 
        var keyHandled = scope.$apply(function() { 
         return onKeyHandler.call(target, key, tag); 
        }); 

        if (keyHandled) { 
         event.preventDefault(); 
         event.stopPropagation(); 
        } 
       } 
      }); 
     } 
    }; 
}); 

function MainCtrl ($scope, $element) { 
    $scope.onKeyPressed = function (key, record) { 
     if (isSelectionKey(key)) { 
      process(record); 
      return true; 
     } 
     return false; 
    }; 

    $element.children[0].focus(); // focus first record 
} 
+0

(isArrowKey (chiave) e getNextElement undefined .... –

+0

@SidBhalke, 'isArrowKey()' determina se è stato premuto un tasto freccia, ad esempio 'chiave> = 37 && chiave <= 40'. La funzione' getNextElement() 'restituisce l'elemento da focalizzare in base alla direzione della chiave e alla logica di navigazione.Possono essere casi di codice hardcoded, o in generale cercare l'elemento più vicino usando il suo' getBoundingClientRect() '. –

+0

http://stackoverflow.com/questions/ 27956752/how-to-select-next-previous-rows-column-on-keydown-event ... puoi vedere questo link, –

2

Questo è la direttiva qui sotto che avevo costruito una volta per un problema simile. Questa direttiva ascolta gli eventi della tastiera e modifica la selezione della riga.

Questo collegamento ha una spiegazione completa su come costruirlo. Change row selection using arrows.

Ecco la direttiva

foodApp.directive('arrowSelector',['$document',function($document){ 
return{ 
    restrict:'A', 
    link:function(scope,elem,attrs,ctrl){ 
     var elemFocus = false;    
     elem.on('mouseenter',function(){ 
      elemFocus = true; 
     }); 
     elem.on('mouseleave',function(){ 
      elemFocus = false; 
     }); 
     $document.bind('keydown',function(e){ 
      if(elemFocus){ 
       if(e.keyCode == 38){ 
        console.log(scope.selectedRow); 
        if(scope.selectedRow == 0){ 
         return; 
        } 
        scope.selectedRow--; 
        scope.$apply(); 
        e.preventDefault(); 
       } 
       if(e.keyCode == 40){ 
        if(scope.selectedRow == scope.foodItems.length - 1){ 
         return; 
        } 
        scope.selectedRow++; 
        scope.$apply(); 
        e.preventDefault(); 
       } 
      } 
     }); 
    } 
}; 

}]);

<table class="table table-bordered" arrow-selector>....</table> 

E il ripetitore

 <tr ng-repeat="item in foodItems" ng-class="{'selected':$index == selectedRow}"> 
4

Tutte le soluzioni proposte fino ad oggi hanno un unico problema comune. Le direttive non sono riutilizzabili, richiedono la conoscenza delle variabili create nell'oggetto $ parent fornito dal controller. Ciò significa che se si volesse utilizzare la stessa direttiva in una vista diversa, sarebbe necessario implementare nuovamente tutto ciò che si è fatto con il controller precedente e assicurarsi di utilizzare gli stessi nomi di variabili per le cose, poiché le direttive hanno fondamentalmente nomi di variabili $ scope con hard coding in loro. Sicuramente non sarebbe possibile utilizzare la stessa direttiva due volte all'interno dello stesso ambito genitore.

Il modo per aggirare questo è utilizzare l'ambito isolato nella direttiva. In questo modo è possibile rendere riutilizzabile la direttiva indipendentemente dall'ambito $ padre mediante la parametrizzazione generica degli elementi richiesti dall'ambito principale.

Nella mia soluzione l'unica cosa che il controller deve fare è fornire una variabileIndice selezionata che la direttiva usa per tracciare quale riga della tabella è attualmente selezionata. Avrei potuto isolare la responsabilità di questa variabile alla direttiva ma facendo in modo che il controllore fornisse la variabile che consente di manipolare la riga attualmente selezionata nella tabella all'esterno della direttiva. Ad esempio, è possibile implementare "sul clic selezionare la riga" nel controller mentre si utilizzano ancora i tasti freccia per la navigazione nella direttiva.

La direttiva:

angular 
    .module('myApp') 
    .directive('cdArrowTable', cdArrowTable); 
    .directive('cdArrowRow', cdArrowRow); 

function cdArrowTable() { 
    return { 
     restrict:'A', 
     scope: { 
      collection: '=cdArrowTable', 
      selectedIndex: '=selectedIndex', 
      onEnter: '&onEnter' 
     }, 
     link: function(scope, element, attrs, ctrl) { 
      // Ensure the selectedIndex doesn't fall outside the collection 
      scope.$watch('collection.length', function(newValue, oldValue) { 
       if (scope.selectedIndex > newValue - 1) { 
        scope.selectedIndex = newValue - 1; 
       } else if (oldValue <= 0) { 
        scope.selectedIndex = 0; 
       } 
      }); 

      element.bind('keydown', function(e) { 
       if (e.keyCode == 38) { // Up Arrow 
        if (scope.selectedIndex == 0) { 
         return; 
        } 
        scope.selectedIndex--; 
        e.preventDefault(); 
       } else if (e.keyCode == 40) { // Down Arrow 
        if (scope.selectedIndex == scope.collection.length - 1) { 
         return; 
        } 
        scope.selectedIndex++; 
        e.preventDefault(); 
       } else if (e.keyCode == 13) { // Enter 
        if (scope.selectedIndex >= 0) { 
         scope.collection[scope.selectedIndex].wasHit = true; 
         scope.onEnter({row: scope.collection[scope.selectedIndex]}); 
        } 
        e.preventDefault(); 
       } 

       scope.$apply(); 
      }); 
     } 
    }; 
} 

function cdArrowRow($timeout) { 
    return { 
     restrict: 'A', 
     scope: { 
      row: '=cdArrowRow', 
      selectedIndex: '=selectedIndex', 
      rowIndex: '=rowIndex', 
      selectedClass: '=selectedClass', 
      enterClass: '=enterClass', 
      enterDuration: '=enterDuration' // milliseconds 
     }, 
     link: function(scope, element, attrs, ctr) { 
      // Apply provided CSS class to row for provided duration 
      scope.$watch('row.wasHit', function(newValue) { 
       if (newValue === true) { 
        element.addClass(scope.enterClass); 
        $timeout(function() { scope.row.wasHit = false;}, scope.enterDuration); 
       } else { 
        element.removeClass(scope.enterClass); 
       } 
      }); 

      // Apply/remove provided CSS class to the row if it is the selected row. 
      scope.$watch('selectedIndex', function(newValue, oldValue) { 
       if (newValue === scope.rowIndex) { 
        element.addClass(scope.selectedClass); 
       } else if (oldValue === scope.rowIndex) { 
        element.removeClass(scope.selectedClass); 
       } 
      }); 

      // Handles applying/removing selected CSS class when the collection data is filtered. 
      scope.$watch('rowIndex', function(newValue, oldValue) { 
       if (newValue === scope.selectedIndex) { 
        element.addClass(scope.selectedClass); 
       } else if (oldValue === scope.selectedIndex) { 
        element.removeClass(scope.selectedClass); 
       } 
      }); 
     } 
    } 
} 

La presente direttiva non solo permette di navigare una tabella utilizzando i tasti freccia, ma consente di associare un metodo di callback per il tasto Invio. In questo modo, quando viene premuto il tasto Invio, la riga attualmente selezionata verrà inclusa come argomento per il metodo di callback registrato con la direttiva (onEnter).

Come un piccolo vantaggio aggiuntivo è anche possibile passare una classe CSS e la durata alla direttiva cdArrowRow in modo che quando la chiave di invio viene colpita su una riga selezionata la classe CSS passata verrà applicata all'elemento riga quindi rimosso dopo la durata passata (in millisecondi). In pratica, ciò ti consente di fare qualcosa come fare in modo che la riga lampeggi di un colore diverso quando viene premuto il tasto Invio.

View Usage:

<table cd-arrow-table="displayedCollection" 
     selected-index="selectedIndex" 
     on-enter="addToDB(row)"> 
    <thead> 
     <tr> 
      <th>First Name</th> 
      <th>Last Name</th> 
     </tr> 
    </thead> 
    <tbody> 
     <tr ng-repeat="row in displayedCollection" 
      cd-arrow-row="row" 
      selected-index="selectedIndex" 
      row-index="$index" 
      selected-class="'mySelcetedClass'" 
      enter-class="'myEnterClass'" 
      enter-duration="150" 
     > 
      <td>{{row.firstName}}</td> 
      <td>{{row.lastName}}</td> 
     </tr> 
    </tbody> 
</table> 

Controller:

angular 
    .module('myApp') 
    .controller('MyController', myController); 

    function myController($scope) { 
     $scope.selectedIndex = 0; 
     $scope.displayedCollection = [ 
      {firstName:"John", lastName: "Smith"}, 
      {firstName:"Jane", lastName: "Doe"} 
     ]; 
     $scope.addToDB; 

     function addToDB(item) { 
      // Do stuff with the row data 
     } 
    } 
1

Si potrebbe creare un servizio di navigazione tabella che segue la riga corrente ed espone metodi di navigazione per modificare il valore e set della riga corrente messa a fuoco a riga.

Quindi tutto ciò che si dovrebbe fare è creare una direttiva di binding delle chiavi in ​​cui è possibile tenere traccia degli eventi key down e attivare i metodi esposti dal servizio di navigazione della tabella, su chiave su o giù.

Ho usato un controller per collegare i metodi di servizio alla direttiva di binding delle chiavi tramite un oggetto di configurazione chiamato 'keyDefinitions'.

È possibile estendere i keyDefinitions per includere il Enter chiave (Codice: 13) e agganciare al valore dell'indice $ selezionato tramite la proprietà servizio 'tableNavigationService.currentRow' o '$ scope.data', poi passarlo come parametro per la tua funzione di invio() personalizzata.

Spero che questo sia utile a qualcuno.

Ho inviato la mia soluzione a questo problema al seguente indirizzo plunker:

Keyboard Navigation Service Demo

HTML:

<div key-watch> 
    <table st-table="rowCollection" id="tableId" class="table table-striped"> 
    <thead> 
     <tr> 
     <th st-sort="firstName">first name</th> 
     <th st-sort="lastName">last name</th> 
     <th st-sort="birthDate">birth date</th> 
     <th st-sort="balance" st-skip-natural="true">balance</th> 
     <th>email</th> 
     </tr> 
    </thead> 
    <tbody> 
     <!-- ADD CONDITIONAL STYLING WITH ng-class TO ASSIGN THE selected CLASS TO THE ACTIVE ROW --> 
     <tr ng-repeat="row in rowCollection track by $index" tabindex="{{$index + 1}}" ng-class="{'selected': activeRowIn($index)}"> 
     <td>{{row.firstName | uppercase}}</td> 
     <td>{{row.lastName}}</td> 
     <td>{{row.birthDate | date}}</td> 
     <td>{{row.balance | currency}}</td> 
     <td> 
      <a ng-href="mailto:{{row.email}}">email</a> 
     </td> 
     </tr> 
    </tbody> 
    </table> 
</div> 

CONTROLLER:

app.controller('navigationDemoController', [ 
    '$scope', 
    'tableNavigationService', 
    navigationDemoController 
    ]); 

    function navigationDemoController($scope, tableNavigationService) { 
    $scope.data = tableNavigationService.currentRow; 

    $scope.keyDefinitions = { 
     'UP': navigateUp, 
     'DOWN': navigateDown 
    } 

    $scope.rowCollection = [ 
     { 
     firstName: 'Chris', 
     lastName: 'Oliver', 
     birthDate: '1980-01-01', 
     balance: 100, 
     email: '[email protected]' 
     }, 
     { 
     firstName: 'John', 
     lastName: 'Smith', 
     birthDate: '1976-05-25', 
     balance: 100, 
     email: '[email protected]' 
     }, 
     { 
     firstName: 'Eric', 
     lastName: 'Beatson', 
     birthDate: '1990-06-11', 
     balance: 100, 
     email: '[email protected]' 
     }, 
     { 
     firstName: 'Mike', 
     lastName: 'Davids', 
     birthDate: '1968-12-14', 
     balance: 100, 
     email: '[email protected]' 
     } 
    ]; 

    $scope.activeRowIn = function(index) { 
     return index === tableNavigationService.currentRow; 
    }; 

    function navigateUp() { 
     tableNavigationService.navigateUp(); 
    }; 

    function navigateDown() { 
     tableNavigationService.navigateDown(); 
    }; 

    function init() { 
     tableNavigationService.setRow(0); 
    }; 

    init(); 
    }; 
})(); 

SERVIZIO E DIRETTIVA:

(function() { 
    'use strict'; 

    var app = angular.module('tableNavigation', []); 

    app.service('tableNavigationService', [ 
    '$document', 
    tableNavigationService 
    ]); 
    app.directive('keyWatch', [ 
    '$document', 
    keyWatch 
    ]); 

    // TABLE NAVIGATION SERVICE FOR NAVIGATING UP AND DOWN THE TABLE 
    function tableNavigationService($document) { 
    var service = {}; 

    // Your current selected row 
    service.currentRow = 0; 
    service.table = 'tableId'; 
    service.tableRows = $document[0].getElementById(service.table).getElementsByTagName('tbody')[0].getElementsByTagName('tr'); 

    // Exposed method for navigating up 
    service.navigateUp = function() { 
     if (service.currentRow) { 
      var index = service.currentRow - 1; 

      service.setRow(index); 
     } 
    }; 

    // Exposed method for navigating down 
    service.navigateDown = function() { 
     var index = service.currentRow + 1; 

     if (index === service.tableRows.length) return; 

     service.setRow(index); 
    }; 

    // Expose a method for altering the current row and focus on demand 
    service.setRow = function (i) { 
     service.currentRow = i; 
     scrollRow(i); 
    } 

    // Set focus to the active table row if it exists 
    function scrollRow(index) { 
     if (service.tableRows[index]) { 
      service.tableRows[index].focus(); 
     } 
    }; 

    return service; 
    }; 

    // KEY WATCH DIRECTIVE TO MONITOR KEY DOWN EVENTS 
    function keyWatch($document) { 
    return { 
     restrict: 'A', 
     link: function(scope) { 
     $document.unbind('keydown').bind('keydown', function(event) { 
      var keyDefinitions = scope.keyDefinitions; 
      var key = ''; 

      var keys = { 
       UP: 38, 
       DOWN: 40, 
      }; 

      if (event && keyDefinitions) { 

      for (var k in keys) { 
       if (keys.hasOwnProperty(k) && keys[k] === event.keyCode) { 
        key = k; 
       } 
      } 

      if (!key) return; 

      var navigationFunction = keyDefinitions[key]; 

      if (!navigationFunction) { 
       console.log('Undefined key: ' + key); 
       return; 
      } 

       event.preventDefault(); 
       scope.$apply(navigationFunction()); 
       return; 
      } 
      return; 
     }); 
     } 
    } 
    } 
})();