在 AngularJS 中跟踪网格内的位置

Track a position within a grid in AngularJS

本文关键字:位置 网格 跟踪 AngularJS      更新时间:2023-09-26

我有一个问题:

我想在 AngularJS 的网格中跟踪一个位置,该位置可以通过使用箭头键进行更改,例如 10x10 网格。我最初以为我会在指令中执行此操作,所以我有以下代码:

angular.module('mymodule').directive('grid', function() {
    return {
        restrict : 'A',
        compile : function($target, $attrs) {
            var rows = $attrs.rows || 10;
            var cols = $attrs.cols || 10;
            for(var y = 0; y < rows; y++) {
                var row = angular.element('<div>');
                row.addClass('row');
                for(var x = 0; x < cols; x++) {
                    var col = angular.element('<div>');
                    col.addClass('item');
                    row.append(col);
                }
                $target.append(row);
            }
            return function($scope, $elem, $attrs) {
                // Maintain the position here
            }
        }
    }
});

这将创建一个漂亮的网格,但是我怀疑实际将用于管理头寸的代码放在哪里。我还希望能够在两个网格之间保持位置,所以我认为需要在存储网格的位置创建类似 PositionManager 的东西?

您对在我的模块中在哪里执行此操作有什么想法?

TL;DR
工作演示(但它甚至更长,并且没有大量文档:D(


这是一个很长的解决方案,所以我不打算在这里重现所有代码。
我将提供代码的大纲和方法的简短描述。
(完整的代码可以在上面的链接中找到(也捆绑为一个模块,以便于重用(。


概述

该方法利用以下组件:

  1. grid指令
    在漂亮的网格中显示数据,并维护数据/元素的 Grid 对象表示形式。

  2. gridContainer指令
    顾名思义,此元素包含网格。它将网格组合在一起,这样容器内一次只能有一个网格处于活动状态("活动">意味着响应箭头键事件(。
    在此元素上注册键事件侦听器(然后将箭头按下委托给活动网格(;

  3. Position服务
    这用于创建新的 Position 对象,以保存和操作网格中活动单元格的轨迹。

  4. Grid服务
    这用于创建新的 Grid 对象,这些对象能够跟踪其活动单元位置并提供实用程序功能用于四处移动,选择一个单元格,确定活动离开其边界后"焦点"应该去哪里等。

  5. GridContainer服务
    这用于"隔离"网格,即防止导航离开容器。
    它还允许在页面上具有相同ID的多个网格。只要每个容器驻留在不同的容器中,所有容器都会按预期工作。

  6. 一个DIRECTION常数
    基本上是可能的相邻方向(北、南、东、西(的"枚举"。

  7. 一些 CSS
    使我们的网格显示漂亮和"网格"。

以下是它的工作原理(或多或少(:

  1. 定义一个包含多个<grid>元素之一的 <grid-container> 元素。
    您可以指定所有必要的数据(ID,大小,数据,位置,"邻居"等(。
    每个<grid-container>元素都有一个GridContainer对象备份它,每个<grid>元素都有Grid对象备份它。
    (为 grid 指令提供了默认模板,但用户可以使用 template-url 属性指定其他模板。

  2. 一旦<grid-container>具有键盘焦点并按下箭头键,它将在活动网格上调用相应的方法(例如向上/向下/向左/向右移动(。

  3. 网格计算新位置并更改其内部位置表示。
    如果新位置超过自身的边界,它会在相应的方向上寻找邻居。
    如果没有在该方向注册的邻居,它会"环绕"(例如,如果您在顶部并按"向上",则从底部开始(。

  4. 单击单元格时,父 Grid 将自动变为活动状态,并且其位置将更改为单击的单元格。

像馅饼一样简单,对吧!


观景

下面是示例 4 网格视图的样子:

<grid-container>
    <grid grid-id="grid-1" data="data1"
          width="{{data1[0].length}}" height="{{data1.length}}" x="0" y="0"
          neighbor-e="grid-2" neighbor-s="grid-3">
    </grid>
    <grid grid-id="grid-2" data="data2"
          width="{{data2[0].length}}" height="{{data2.length}}"
          neighbor-w="grid-1" neighbor-s="grid-4">
    </grid>
    <grid grid-id="grid-3" data="data3"
          width="{{data3[0].length}}" height="{{data3.length}}"
          neighbor-n="grid-1" neighbor-e="grid-4">
    </grid>
    <grid grid-id="grid-4" data="data4"
          width="{{data4[0].length}}" height="{{data4.length}}"
          neighbor-n="grid-2" neighbor-w="grid-3">
    </grid>
</grid-container>

以下是用作gridgridContainer指令模板的部分:

grid-container.tmpl.html

<div tabindex="-1" ng-transclude=""></div>

网格.tmpl.html

<div class="grid">
    <div class="row" ng-repeat="row in data" ng-init="rowIdx=$index">
        <div class="cell" ng-class="{active:isActive(colIdx,rowIdx)}"
                ng-repeat="cell in row" ng-init="colIdx=$index">
            {{rowIdx+1}}.{{colIdx+1}}:<br />cell-{{cell.text}}
        </div>
    </div>
</div>

守则

下面是代码的大纲。为了简洁起见,我删除了实际的导入细节,只保留了代码组织的高级功能,例如对象的属性和方法、指令定义对象的属性、指令控制器/链接函数中的注释等:

var app = angular.module('myApp', []);
app.controller('mainCtrl', function ($scope) {
    var data = [
        [{text:  1}, {text:  2}, {text:  3}, {text:  4}, {text:  5}],
        [{text:  6}, {text:  7}, {text:  8}, {text:  9}, {text: 10}],
        [{text: 11}, {text: 12}, {text: 13}, {text: 14}, {text: 15}],
        [{text: 16}, {text: 17}, {text: 18}, {text: 19}, {text: 20}],
        [{text: 21}, {text: 22}, {text: 23}, {text: 24}, {text: 25}]
    ];
    $scope.data1 = $scope.data2 = $scope.data3 = $scope.data4 = data;
});
app.directive('gridContainer', function (GridContainer) {
    return {
        restrict: 'E',
        templateUrl: 'partials/grid-container.tmpl.html',
        transclude: true,
        scope: {},
        controller: function gridContainerCtrl($element, $scope) {
            // Create a new GridContainer and maintain a reference to the currently active Grid,
            // so key-events change the position in that Grid
            // Delegate key-events to the active Grid
            // Deregister the event-listener upon removing the element
        }
    };
});
    templateUrl: function (tElem, tAttrs) {
        var templateUrl = tAttrs.templateUrl;
        if (!templateUrl) {
            if (!$templateCache.get(defaultTmplKey)) {
                $templateCache.put(defaultTmplKey, defaultTmplStr);
            }
            templateUrl = defaultTmplKey;
        }
        return templateUrl;
    },
app.directive('grid', function ($templateCache, DIRECTION) {
    ...
    return {
        restrict: 'E',
        require: '^gridContainer',
        templateUrl: function (tElem, tAttrs) {
            // Return `tAttrs.templateUrl` if it is defined.
            // If not, put the default template into the `$templateCache`
            // and return the key.
        },
        scope: {
            data:   '=',
            gridId: '@',
            width:  '@',
            height: '@',
            x:      '@',
            y:      '@',
            neighborN: '@',
            neighborS: '@',
            neighborE: '@',
            neighborW: '@'
        },
        link: function gridPostLink(scope, elem, attrs, ctrl) {
            // Initialize a Grid
            // Utility function to check if a cell is the active cell
            // Upon clicking on a cell, set the current Grid as active
            // and change its position to the clicked cell
            // Deregister the event-listener upon removing the element
            // If the position is initialized [i.e. both !== -1],
            // set this Grid as the active grid
        }
    };
});
app.constant('DIRECTION', {...});
app.factory('Grid', function (Position, DIRECTION) {
    ...
    function Grid(siblingGrids, id, rows, cols, x, y) {
        this._id           = ...;
        this._rows         = ...;
        this._cols         = ...;
        this._position     = new Position(...);
        this._neighbors    = ...;
        this._siblingGrids = ...;
        ...
    }
    Grid.prototype.getRows       = function () {...};
    Grid.prototype.getColumns    = function () {...};
    Grid.prototype.getX          = function () {...};
    Grid.prototype.getY          = function () {...};
    Grid.prototype.setXY         = function (x, y) {...};
    Grid.prototype.getNeighbor   = function (dir) {...};
    Grid.prototype.setNeighbor   = function (dir, neighborID) {...};
    Grid.prototype.indexToXY     = function (idx) {...};
    Grid.prototype.xyToIndex     = function (x, y) {...};
    Grid.prototype.moveUp        = function () {...};
    Grid.prototype.moveDown      = function () {...};
    Grid.prototype.moveLeft      = function () {...};
    Grid.prototype.moveRight     = function () {...};
    Grid.prototype._findNeighbor = function (direction) {...};
    return Grid;
});
app.factory('GridContainer', function (Grid) {
    function GridContainer() {
        this._grids = ...;
    }
    GridContainer.prototype.newGrid = function (id, rows, cols, x, y) {...};
    return GridContainer;
});
app.factory('Position', function () {
    function Position(x, y) {
        this._x = ...;
        this._y = ...;
    }
    Position.prototype.getX  = function () {...};
    Position.prototype.getY  = function () {...};
    Position.prototype.setXY = function (x, y) {...};
    return Position;
});

完整的代码和工作演示可以在这里找到。

它被捆绑为一个模块(esGrid(,所有组件(控制器/指令/服务等(都es.名称间隔(出于可重用性的原因(。
为了在您的应用程序中使用它:

1. 在脚本中[MODULE: esGrid]下包含 IIFE。
2.esGrid作为依赖项添加到模块中。
3. 使用视图中的指令:

<es-grid-container><es.grid ... ></es.grid>...<es-grid-container>4. 定义自己的模板,如下所示: <es.grid ... template-url="some.url"></es.grid>
5. 如果你想引用CSS(或任何类似CSS的选择器(中的元素,不要忘记转义.,因为它是一个特殊字符:CSS: es'.grid { border: ... }


免責聲明:
为了使建议的解决方案与Angular 1.2.+支持的所有浏览器兼容,没有采取任何措施。(是的,我正在看你们的旧IE。
它与现代浏览器兼容,可以轻松修改以在蹩脚的...呃旧浏览器(例如,用angular.forEach()之类的东西替换Array.forEach()(。

应该使用编译函数的情况极为罕见。您应该能够仅使用ng-repeat完成相同的事情。例如:

<div ng-repeat="row in ctrl.range(rows)">
    <span 
            ng-repeat="col in ctrl.range(cols)" 
            ng-class="{selected: ctrl.curRow === row && ctrl.curCol === col}">
        cell
    </span>
</div>

有无数种方法可以做到这一点,但这里有一个工作示例应该可以帮助您入门:http://jsfiddle.net/robianmcd/9otpqbg8/1/

如果单击网格,则可以使用箭头键在其周围导航