2013-06-30 8 views
9

Я пытаюсь перейти через список записей, используя только клавиатуру. Когда страница загружается, по умолчанию «фокус» должен быть на первой записи, когда пользователь нажимает стрелку вниз на клавиатуре, следующая запись должна быть сфокусирована. Когда пользователь нажимает стрелку вверх, предыдущая запись должна быть сфокусирована. Когда пользователь нажимает кнопку «Ввод», он должен отнести их на страницу сведений этой записи.Перейдите к пользовательскому интерфейсу, используя только клавиатуру

Here's what I have so far on Plunkr.

Оказывается, что это поддерживается в AngularJS в 1.1.5 (неустойчивого), который мы не можем использовать в производстве. В настоящее время я использую 1.0.7. Я надеюсь сделать что-то вроде этого - ключ должен быть обработан на уровне документа. Когда пользователь нажимает определенный ключ, код должен искать в массиве разрешенных ключей. Если совпадение найдено (например, код клавиши вниз), он должен переместить фокус (применить .highlight css) к следующему элементу. Когда вводится ввод, он должен захватить запись, которая .highlight css и получить идентификатор записи для дальнейшей обработки.

Спасибо!

ответ

14

Вот пример того, что вы можете выбрать, чтобы сделать: 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> 

Это самый простой подход, который я мог думать. Он связывает директиву keyTrap с номером body, который захватывает событие keydown и $broadcast сообщение для дочерних областей. Область держателя элементов будет захватывать сообщение и просто увеличивать или уменьшать focusIndex или запускать функцию open при ударе enter.

EDIT

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

теперь поддерживает, упорядоченный/отфильтрованный список.

Обработка событий не изменилась, но теперь используется $index, а также отфильтрованное список кеширования техника, комбинированная для отслеживания того, что элемент фокусируется.

+0

Это хороший подход, но он не работает, когда записи упорядочены ('запись в записях | orderBy: '-name''). У вас есть решение для этого? (не только для этого случая, но более общего) – akirk

+2

Благодарим вас за отзыв. Это всегда забавно и преданность, с которыми сталкиваются более сложные случаи использования. Я добавляю дополнительный код, который поддерживает отсортированный/отфильтрованный список. – Tosh

+0

Спасибо! Ваше решение было довольно вдохновляющим. – akirk

1

У меня было аналогичное требование для поддержки навигации с помощью клавиш со стрелками. То, что я наконец придумал инкапсулируется обработчик keydown события DOM в пределах 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 (ключ) и getNextElement undefined .... –

+0

@SidBhalke, 'isArrowKey()' определяет, была ли нажата клавиша со стрелкой, то есть 'key> = 37 && key <= 40'. Функция' getNextElement() 'возвращает элемент, который будет сфокусирован, в зависимости от направления ключа и вашей логики навигации. Это могут быть случаи жесткого кодирования или вообще поиск ближайшего элемента с использованием его' getBoundingClientRect() '. –

+0

http://stackoverflow.com/questions/ 27956752/how-to-select-next-previous-rows-column-on-keydown-event ... вы можете увидеть эту ссылку, –

2

Это директива ниже, которую я когда-то строил для аналогичной проблемы. Эта директива прослушивает события клавиатуры и изменяет выбор строки.

Эта ссылка содержит полное объяснение того, как ее построить. Change row selection using arrows.

Вот директива

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> 

И ваш ретранслятор

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

Все решения, предлагаемые до сих пор имеют одну общую проблему. Директивы не подлежат повторному использованию, они требуют знания переменных, созданных в исходной переменной $ scope, предоставляемой контроллером. Это означает, что если вы хотите использовать одну и ту же директиву в другом представлении, вам нужно будет повторно реализовать все, что вы сделали предыдущий контроллер, и убедитесь, что вы используете одни и те же имена переменных для вещей, поскольку директивы в основном имеют жестко закодированные имена переменных $ scope в них. Вы определенно не сможете использовать одну и ту же директиву дважды в пределах одной и той же родительской области.

Путь вокруг этого заключается в использовании изолированного объема в директиве. Поступая таким образом, вы можете сделать директиву повторно используемой независимо от родительской переменной $ scope, в основном параметризуя элементы, требуемые из родительской области.

В моем решении единственное, что нужно сделать контроллеру, это предоставить переменную selectedIndex, которую эта директива использует для отслеживания выбранной строки в таблице. Я мог бы изолировать ответственность этой переменной от директивы, но, если контроллер предоставит переменную, она позволяет вам манипулировать текущей выбранной строкой в ​​таблице вне директивы. Например, вы можете реализовать «на строке выбора щелчка» в своем контроллере, все еще используя клавиши со стрелками для навигации в директиве.

Директива:

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

Эта директива не только позволяет осуществлять навигацию по таблице с помощью клавиш со стрелками, но это позволяет связать метод обратного вызова клавишей Enter. Так что, когда нажата клавиша ввода, выбранная строка будет включена как аргумент метода обратного вызова, зарегистрированного в директиве (onEnter).

В качестве дополнительного бонуса вы также можете передать класс CSS и продолжительность в директиву cdArrowRow, чтобы при нажатии клавиши ввода на выбранную строку класс CSS, который прошел, будет применен к элементу строки, тогда удаляется после прошедшего времени (в миллисекундах). Это в основном позволяет вам делать что-то вроде того, что при нажатии клавиши ввода происходит смещение строки.

Вид использования:

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

Контроллер:

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

Вы можете создать таблицу навигации сервис, который отслеживает текущую строку и предоставляет методы навигации, чтобы изменить значение текущей строки и устанавливает фокус на ряд.

Тогда вам нужно будет создать директиву привязки ключей, в которой вы можете отслеживать события нажатия клавиш и запускать открытые методы из службы навигации по таблице, на клавиатуре или клавише вниз.

Я использовал контроллер, чтобы связать методы службы с директивой привязки ключей через объект конфигурации, называемый «keyDefinitions».

Вы можете расширить keyDefinitions чтобы включить в него Введите ключ (код: 13) и подключить к выбранному значению индекса $ с помощью свойства сервиса «tableNavigationService.currentRow» или «$ scope.data», а затем передать его как параметр вашей собственной функции submit().

Я надеюсь, что это кому-то поможет.

Я отправил мое решение этого вопроса на следующем месте 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(); 
    }; 
})(); 

ОБСЛУЖИВАНИЕ И ДИРЕКТИВА:

(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; 
     }); 
     } 
    } 
    } 
})(); 
Смежные вопросы