web-dev-qa-db-ja.com

キーボードのみを使用してUIをナビゲートする

キーボードのみを使用してレコードのリストをナビゲートしようとしています。ページが読み込まれると、デフォルトの「フォーカス」は最初のレコードにあるはずです。ユーザーがキーボードの下矢印をクリックすると、次のレコードにフォーカスする必要があります。ユーザーが上矢印をクリックすると、前のレコードにフォーカスが移されます。ユーザーがEnterボタンをクリックすると、そのレコードの詳細ページに移動します。

これがPlunkrでこれまでに持っているものです。

これは1.1.5(不安定)のAngularJSでサポートされているようですが、本番環境では使用できません。私は現在1.0.7を使用しています。私はこのようなことをしたいと思っています-キーはドキュメントレベルで処理する必要があります。ユーザーが特定のキーを押すと、コードは許可されたキーの配列を検索する必要があります。一致するものが見つかった場合(ダウンキーコードなど)、フォーカスを次の要素に移動する必要があります(.highlight cssを適用します)。 Enterキーを押すと、cssを強調表示するレコードを取得し、さらに処理するためにレコードIDを取得する必要があります。

ありがとうございました!

9
tempid

これがあなたがすることを選ぶことができる例です: 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>

これは私が考えることができる最も簡単なアプローチです。ディレクティブkeyTrapbodyにバインドし、keydownイベントと$broadcastメッセージを子スコープにキャッチします。要素ホルダースコープはメッセージをキャッチし、focusIndexをインクリメントまたはデクリメントするか、openを押すとenter関数を起動します。

編集

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

順序付き/フィルタリングされたリストをサポートするようになりました。

イベント処理部分は変更されていませんが、$indexを使用し、フィルターされたリストキャッシュ手法を組み合わせて、フォーカスされているアイテムを追跡するようになりました。

14
Tosh

これまでに提供されたすべてのソリューションには、1つの共通の問題があります。ディレクティブは再利用できません。コントローラーによって提供される親$ scopeで作成された変数の知識が必要です。つまり、同じディレクティブを別のビューで使用する場合は、前のコントローラーで行ったすべてを再実装し、同じ変数名を使用していることを確認する必要があります。これは、ディレクティブには基本的にハードコードされた$ scope変数名があるためです。それらの中で。同じ親スコープ内で同じディレクティブを2回使用することは絶対にできません。

これを回避する方法は、ディレクティブで分離スコープを使用することです。これを行うことにより、親スコープから必要な項目を一般的にパラメーター化することにより、親$ 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キーにバインドできます。そのため、Enterキーを押すと、現在選択されている行が、ディレクティブ(onEnter)に登録されているコールバックメソッドの引数として含まれます。

追加のボーナスとして、CSSクラスと期間をcdArrowRowディレクティブに渡すこともできます。これにより、選択した行でEnterキーを押すと、渡されたCSSクラスが行要素に適用され、渡された後に削除されます。期間(ミリ秒単位)。これにより、基本的に、Enterキーが押されたときに行を別の色で点滅させるようなことができます。

使用法の表示:

<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
        }
    }
5
user1042361

これは、私がかつて同様の問題のために作成した以下のディレクティブです。このディレクティブは、キーボードイベントをリッスンし、行の選択を変更します。

このリンクには、それを構築する方法についての完全な説明があります。 矢印を使用して行の選択を変更します

これがディレクティブです

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}">
2
rahil471

矢印キーを使用したUIナビゲーションをサポートするための同様の要件がありました。私が最終的に思いついたのは、AngularJSディレクティブ内にカプセル化されたDOMのkeydownイベントハンドラーです。

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
}
1
Herman Kan

現在の行を追跡し、ナビゲーションメソッドを公開して現在の行の値を変更し、その行にフォーカスを設定するテーブルナビゲーションサービスを作成できます。

次に、キーダウンイベントを追跡し、キーアップまたはキーダウン時にテーブルナビゲーションサービスから公開されたメソッドを起動できるキーバインディングディレクティブを作成するだけです。

コントローラーを使用して、「keyDefinitions」という構成オブジェクトを介してサービスメソッドをキーバインディングディレクティブにリンクしました。

KeyDefinitionsを拡張して、 Enter キー(コード:13)を選択し、サービスプロパティ「tableNavigationService.currentRow」または「$ scope.data」を介して選択した$ index値にフックし、それをパラメーターとして独自のカスタムsubmit()関数に渡します。

これが誰かに役立つことを願っています。

この問題の解決策を次のプランカーの場所に投稿しました。

キーボードナビゲーションサービスのデモ

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>

コントローラ:

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