2013-06-30 10 views
9

Próbuję nawigować przez listę rekordów za pomocą tylko klawiatury. Po załadowaniu strony domyślny "fokus" powinien znajdować się na pierwszym rekordzie, gdy użytkownik kliknie strzałkę w dół na klawiaturze, następny rekord musi zostać skupiony. Kiedy użytkownik kliknie strzałkę w górę, poprzedni rekord powinien być skupiony. Kiedy użytkownik kliknie przycisk Enter, powinien przejść do strony szczegółów tego rekordu. WydajeNawigacja po interfejsie użytkownika przy użyciu tylko klawiatury

Here's what I have so far on Plunkr.

że ta jest obsługiwana w angularjs w 1.1.5 (niestabilny), których nie możemy wykorzystać w produkcji. Obecnie używam wersji 1.0.7. Mam nadzieję, że zrobię coś takiego - klucz powinien być obsługiwany na poziomie dokumentu. Kiedy użytkownik naciśnie określony klucz, kod powinien znaleźć się w tablicy dozwolonych kluczy. Jeśli zostanie znaleziony odpowiednik (np. Kod klawisza w dół), powinien przesunąć fokus (zastosuj .highlight css) do następnego elementu. Po naciśnięciu klawisza enter powinien pobrać rekord, który .highlight css i uzyskać identyfikator rekordu do dalszego przetwarzania.

Dziękujemy!

Odpowiedz

14

Oto przykład co można wybrać do zrobienia: 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> 

Jest to najprostszy podejście mogłem myśleć. Powoduje powiązanie dyrektywy keyTrap z body, która przechwytuje komunikat o zdarzeniu i zakresy potomne. Zakres uchwytu elementu spowoduje wychwycenie komunikatu i po prostu inkrementację lub zwiększenie wartości focusIndex lub uruchomienie funkcji open po kliknięciu na enter.

EDIT

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

obsługuje teraz, nakazał/lista filtrowane.

Część dotycząca obsługi zdarzeń nie uległa zmianie, ale teraz wykorzystuje , a także filtrowaną listę pamięci podręcznej technikę połączoną w celu śledzenia, który element jest ostry.

+0

Jest to przyjemne podejście, ale nie działa, gdy pozycje są uporządkowane ('rekord w rekordach | orderBy: '-name''). Czy masz również rozwiązanie? (nie tylko w tym przypadku, ale bardziej ogólny) – akirk

+2

Dziękuję za opinię. Zawsze jest zabawnie i prefekcyjnie, gdy trudniej radzić sobie z trudnymi przypadkami. Dodaję dodatkowy kod obsługujący listę posortowaną/filtrowaną. – Tosh

+0

Dzięki! Twoje rozwiązanie było dość inspirujące. – akirk

1

Podobny wymóg dotyczyłem nawigacji UI za pomocą klawiszy strzałek. Co ja w końcu wymyślił jest teleskopowa keydown wydarzenie Doma otoczonych angularjs dyrektywy:

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 (klucz) i getNextElement undefined .... –

+0

@SidBhalke, 'isArrowKey()' określa, czy został naciśnięty klawisz strzałki, tj. 'Key> = 37 && key <= 40'. Funkcja' getNextElement() 'zwraca element, który ma być skupiony na podstawie kierunku klawiszy i logiki nawigacji, może to być przypadek przełącznika z twardym kodowaniem lub ogólne wyszukiwanie najbliższego elementu za pomocą jego funkcji' getBoundingClientRect() '. –

+0

http://stackoverflow.com/questions/ 27956752/how-to-select-next-previous-rows-column-on-keydown-event ... możesz zobaczyć ten link, –

2

To jest dyrektywa poniżej, którą kiedyś stworzyłem dla podobnego problemu. Ta dyrektywa wykrywa zdarzenia klawiatury i zmienia wybór wiersza.

Ten link zawiera pełne wyjaśnienie, jak go zbudować. Change row selection using arrows.

Oto dyrektywy

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> 

A twój repeater

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

Wszystkie rozwiązania oferowane aż tak daleko mają jeden wspólny problem. Dyrektywy nie nadają się do ponownego użycia, wymagają znajomości zmiennych utworzonych w macierzystym zakresie $ określonym przez kontrolera. Oznacza to, że jeśli chcesz użyć tej samej dyrektywy w innym widoku, musisz ponownie wdrożyć wszystko, co zrobił poprzedni kontroler i upewnić się, że używasz tych samych nazw zmiennych do rzeczy, ponieważ dyrektywy w zasadzie zawierają zakodowane nazwy zmiennych $ scope w nich. Z pewnością nie byłbyś w stanie użyć tej samej dyrektywy dwa razy w ramach tego samego zakresu nadrzędnego.

Sposób obejścia tego polega na użyciu izolowanego zakresu w dyrektywie. W ten sposób można uczynić dyrektywę wielokrotnego użytku, niezależnie od macierzystego zakresu $, generalnie parametryzując elementy wymagane z zakresu nadrzędnego.

W moim rozwiązaniu jedyną rzeczą, którą musi wykonać kontroler, jest podanie zmiennej selectedIndex używanej przez dyrektywę do śledzenia, który wiersz w tabeli jest aktualnie wybrany. Mogłem wyodrębnić odpowiedzialność tej zmiennej do dyrektywy, ale dzięki temu, że kontroler dostarczy zmienną, można manipulować aktualnie wybranym rzędem w tabeli poza dyrektywą. Można na przykład zaimplementować "w wierszu polecenia" w sterowniku, a jednocześnie używać klawiszy strzałek do nawigacji w dyrektywie.

dyrektywy:

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

Dyrektywa ta nie tylko pozwala na poruszanie tabeli za pomocą klawiszy strzałek, ale pozwala powiązać metody wywołania zwrotnego do klawisza Enter. Tak więc po naciśnięciu klawisza Enter aktualnie wybrany wiersz zostanie dołączony jako argument do metody wywołania zwrotnego zarejestrowanej w dyrektywie (onEnter).

Jako dodatkową premię można również przekazać klasę CSS i czas trwania do dyrektywy cdArrowRow, aby po trafieniu klawisza Enter w wybranym wierszu klasa CSS przekazana zostanie do elementu wiersza, a następnie usunięto po upływie czasu trwania (w milisekundach). Zasadniczo pozwala to na zrobienie czegoś takiego, jak sprawienie, by wiersz migał w innym kolorze po naciśnięciu klawisza Enter.

Zobacz Zastosowanie:

<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> 

Kontroler:

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

Można by stworzyć usługę nawigacji tabeli, która śledzi bieżący wiersz i odsłania metody nawigacji zmodyfikować wartość bieżącego wiersza i Aktywuje rząd.

Wszystko, co musisz zrobić, to stworzyć dyrektywę wiążącą kluczowe, w której możesz śledzić zdarzenia związane z kluczem i uruchomić wyeksponowane metody z poziomu usługi nawigacji po stole, klawiszem w górę lub w dół.

Użyłem kontrolera do połączenia metod usług z dyrektywą wiążącą kluczowe za pośrednictwem obiektu konfiguracyjnego o nazwie "keyDefinitions".

można rozszerzyć keyDefinitions aby zaliczyć Wprowadź klucz (kod: 13) i zaczep do wybranej wartości indeksu $ poprzez obsługę nieruchomości „tableNavigationService.currentRow” lub „$ scope.data”, a następnie przekazać go jako parametr do własnej niestandardowej funkcji submit().

Mam nadzieję, że jest to pomocne dla kogoś.

Pisałem moje rozwiązanie tego problemu w następującej lokalizacji 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> 

Kontroler:

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 i dyrektywę:

(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; 
     }); 
     } 
    } 
    } 
})(); 
Powiązane problemy