2013-06-30 6 views
9

Ich versuche, durch eine Liste von Datensätzen mit nur Tastatur zu navigieren. Wenn die Seite geladen wird, sollte der Standardfokus auf dem ersten Datensatz liegen. Wenn der Benutzer auf den Abwärtspfeil auf der Tastatur klickt, muss der nächste Datensatz fokussiert werden. Wenn der Benutzer auf den Pfeil nach oben klickt, sollte der vorherige Datensatz fokussiert sein. Wenn der Benutzer auf die Schaltfläche "Enter" klickt, sollte er sie zur Detailseite dieses Datensatzes bringen.Navigieren Sie die Benutzeroberfläche nur mit der Tastatur

Here's what I have so far on Plunkr.

Es scheint, dass dies in AngularJS in 1.1.5 (instabil) unterstützt wird, die wir nicht in der Produktion verwendet werden können. Ich verwende derzeit 1.0.7. Ich hoffe, so etwas zu machen - der Schlüssel sollte auf Dokumentebene behandelt werden. Wenn der Benutzer einen bestimmten Schlüssel drückt, sollte der Code in einem Array zulässiger Schlüssel nachschlagen. Wenn eine Übereinstimmung gefunden wird (z. B. Schlüsselcode nach unten), sollte sie den Fokus (Anwendung der .highlight css) auf das nächste Element verschieben. Wenn Enter gedrückt wird, sollte es den Datensatz aufnehmen, der Css highlight und die Datensatz-ID für die weitere Verarbeitung abrufen.

Vielen Dank! Hier

Antwort

14

ist das Beispiel, was Sie wählen könnten zu tun: 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> 

Dies ist der einfachste Ansatz, den ich denken konnte. Es bindet eine Richtlinie keyTrap an die body, die fängt den keydown Ereignis und $broadcast Nachricht an untergeordneten Bereiche. Der Elementhalterbereich fängt die Nachricht ab und erhöht oder verringert einfach den focusIndex oder löst eine open-Funktion aus, wenn er enter erreicht.

EDIT

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

unterstützt nun ordnete/gefilterten Liste.

Event-Handling-Teil hat sich nicht geändert, aber jetzt verwendet $index und gefilterte Listen-Caching Technik kombiniert, um zu verfolgen, welches Element wird konzentriert.

+0

Es ist ein netter Ansatz, aber es funktioniert nicht, wenn die Einträge geordnet sind ('record in records | orderBy: '-name''). Haben Sie auch dafür eine Lösung? (nicht nur für diesen Fall, sondern ein generischer) – akirk

+2

Vielen Dank für das Feedback. Es macht immer Spaß und Druck, mit schwierigeren Anwendungsfällen herausgefordert zu werden. Ich füge den zusätzlichen Code hinzu, der sortierte/gefilterte Liste unterstützt. – Tosh

+0

Danke! Ihre Lösung war sehr inspirierend. – akirk

1

Ich hatte eine ähnliche Anforderung, um UI Navigation mit Pfeiltasten zu unterstützen. Was ich kam schließlich mit ist keydown DOM Event-Handler innerhalb einer AngularJS Richtlinie verkapselt:

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 (key) und getNextElement undefined .... –

+0

@SidBhalke, 'isArrowKey()' bestimmt, ob eine Pfeiltaste gedrückt wurde, zB 'key> = 37 && key <= 40'. Die Funktion' getNextElement() 'gibt das zu fokussierende Element basierend auf der Schlüsselrichtung und Ihrer Navigationslogik zurück. Es kann sich dabei um fest codierte Switch-Fälle handeln oder im Allgemeinen nach dem nächsten Element mit seinem' getBoundingClientRect() '. –

+0

http://stackoverflow.com/questions/ 27956752/how-to-select-next-Vorherige-Zeilen-Spalte-auf-Keydown-Ereignis ... können Sie bitte diesen Link sehen, –

2

Dies ist die Richtlinie unten, die ich einmal für ein ähnliches Problem erstellt habe. Diese Direktive überwacht die Tastaturereignisse und ändert die Zeilenauswahl.

Dieser Link enthält eine vollständige Erklärung zum Erstellen. Change row selection using arrows.Hier

ist die Richtlinie

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> 

Und Ihre Repeater

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

Alle Lösungen haben dargebracht bisher ein einziges gemeinsames Problem. Die Direktiven sind nicht wiederverwendbar und erfordern Kenntnisse über Variablen, die im übergeordneten Bereich $ vom Controller erstellt wurden. Das heißt, wenn Sie die gleiche Anweisung in einer anderen Ansicht verwenden möchten, müssen Sie alles, was Sie den vorherigen Controller getan haben, erneut implementieren und sicherstellen, dass Sie die gleichen Variablennamen für Dinge verwenden, da die Direktiven grundsätzlich fest codierte $ Scope-Variablennamen haben in ihnen. Sie wären definitiv nicht in der Lage, dieselbe Anweisung zweimal innerhalb des gleichen übergeordneten Bereichs zu verwenden.

Die Art und Weise, um dies ist isoliert Anwendungsbereich der Richtlinie zu verwenden. Auf diese Weise können Sie die Richtlinie machen wiederverwendbar, unabhängig von der Mutter $ Umfang von allgemein Parametrier- Elementen aus dem übergeordneten Bereich erforderlich.

In meiner Lösung der einzige Sache, dass der Controller eine selectedIndex Variable tun muss, ist, dass die Richtlinie, welche Zeile in der Tabelle verfolgen verwendet, wird zur Zeit ausgewählt. Ich hätte die Verantwortlichkeit dieser Variable für die Direktive isolieren können, aber indem ich den Controller dazu veranlasse, die Variable zur Verfügung zu stellen, die es erlaubt, die aktuell ausgewählte Zeile in der Tabelle außerhalb der Direktive zu manipulieren. Zum Beispiel könnten Sie „auf Klick wählen Sie Reihe“ in Ihrem Controller implementieren, während noch die Pfeiltasten für die Navigation in der Direktive.

Die Richtlinie:

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); 
       } 
      }); 
     } 
    } 
} 

Diese Richtlinie ermöglicht Ihnen nicht nur eine Tabelle navigieren mit den Pfeiltasten, aber es erlaubt Ihnen, eine Callback-Methode auf die Enter-Taste zu binden. So dass, wenn die Enter-Taste gedrückt wird, um die Zeile, die derzeit als ein Argument an der Callback-Methode mit der Richtlinie (onEnter) registriert aufgenommen wird ausgewählt.

Als kleines Plus können Sie auch eine CSS-Klasse und -Dauer an die cdArrowRow-Direktive übergeben, so dass die übergebene CSS-Klasse dann auf das Zeilenelement angewendet wird, wenn die Eingabetaste auf einer ausgewählten Zeile gedrückt wird nach Ablauf der Dauer (in Millisekunden) entfernt. Dies ermöglicht es Ihnen, etwas zu tun, wenn die Zeile beim Drücken der Eingabetaste eine andere Farbe erhält.

Ansicht Verbrauch:

<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

Sie könnten einen Tisch Navigationsdienst schaffen, der die aktuelle Zeile verfolgt und stellt Methoden Navigation konzentrieren sich die aktuelle Zeilenwert und setzt auf das ändern Reihe.

Dann müssen Sie lediglich eine Schlüsselbindungsanweisung erstellen, mit der Sie Schlüsselausfallereignisse verfolgen und die offengelegten Methoden vom Schlüsselnachricht- oder Schlüsseltastenschlüssel des Tabellennavigationsdiensts auslösen können.

Ich habe einen Controller verwendet, um die Service-Methoden über ein Konfigurationsobjekt namens 'keyDefinitions' mit der Schlüsselbindungsanweisung zu verknüpfen.

Sie können die keyDefinitions erweitern umfassen die Enter-Taste (Code: 13) und Haken an den ausgewählten $ Indexwert über die Service-Eigenschaft ‚tableNavigationService.currentRow‘ oder ‚$ scope.data‘, dann ist es passieren als Parameter für Ihre eigene benutzerdefinierte submit() -Funktion.

Ich hoffe, dass dies für jemanden hilfreich ist.

Ich habe meine Lösung für dieses Problem auf dem folgenden Plunker Standort geschrieben:

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> 

REGLER:

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(); 
    }; 
})(); 

SERVICE UND DIE RICHTLINIE:

(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; 
     }); 
     } 
    } 
    } 
})(); 
Verwandte Themen