개발자's Life

[JavaScript] Slick Grid 를 이용하여 Col, Row 고정 시키기 본문

Front-end

[JavaScript] Slick Grid 를 이용하여 Col, Row 고정 시키기

Rowen Jobs 2023. 3. 2. 20:44
728x90
반응형

오늘은 회사 신규모듈 예산 기능 개발중에 틀 고정 기능이 있으면 좋을 듯 하여 시간은 빠듯하지만 Slick Grid 의 틀 고정 기능을 사용하여 완성하였습니다. 

 

확실히 컬럼이 많은 테이블일 경우 틀 고정 기능은 필수라 생각이 드네요. 

 

https://github.com/6pac/SlickGrid/blob/master/examples/example-frozen-columns-and-rows.html#L363

 

GitHub - 6pac/SlickGrid: A lightning fast JavaScript grid/spreadsheet

A lightning fast JavaScript grid/spreadsheet. Contribute to 6pac/SlickGrid development by creating an account on GitHub.

github.com

해당 GitHub 참고 하여 진행 하였고 

http://6pac.github.io/SlickGrid/examples/example-frozen-columns-and-rows.html

 

SlickGrid example: Frozen Columns and Rows

 

6pac.github.io

데모 페이지 참고하여 완성하였습니다. 

 

<!DOCTYPE HTML>
<html>
<head>
<!-- css 와 스타일도 동일하게 해줘야 합니다. -->
  <meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1">
  <link rel="shortcut icon" type="image/ico" href="favicon.ico" />
  <title>SlickGrid example: Frozen Columns and Rows</title>
  <link rel="stylesheet" href="../slick.grid.css" type="text/css" media="screen" charset="utf-8"/>
  <link rel="stylesheet" href="../controls/slick.pager.css" type="text/css" media="screen" charset="utf-8"/>
  <link rel="stylesheet" href="../css/smoothness/jquery-ui.css" type="text/css"/>
  <link rel="stylesheet" href="examples.css" type="text/css" media="screen" charset="utf-8"/>
  <link rel="stylesheet" href="../controls/slick.columnpicker.css" type="text/css" media="screen" charset="utf-8"/>
  <style>
    .cell-title {
      font-weight: bold;
    }

    .cell-effort-driven {
      text-align: center;
    }

    .cell-selection {
      border-right-color: silver;
      border-right-style: solid;
      background: #f5f5f5;
      color: gray;
      text-align: right;
      font-size: 10px;
    }

    .slick-row.selected .cell-selection {
      background-color: transparent; /* show default selected row background */
    }

    .slick-headerrow-column {
      background: #87ceeb;
      text-overflow: clip;
      -moz-box-sizing: border-box;
      box-sizing: border-box;
    }

    .slick-headerrow-column input {
      margin: 0;
      padding: 0;
      width: 100%;
      height: 100%;
      -moz-box-sizing: border-box;
      box-sizing: border-box;
    }

    .slick-row .slick-cell.frozen:last-child, 
    .slick-header-column.frozen:last-child, 
    .slick-headerrow-column.frozen:last-child,
    .slick-footerrow-column.frozen:last-child {
      border-right: 1px solid #999;
    }

    .slick-row.frozen:last-child .slick-cell {
      border-bottom: 1px solid #999;
    }
  </style>
</head>
<body>
<div style="position:relative">
  <div style="width:600px;">
    <div class="grid-header" style="width:100%">
      <label>SlickGrid</label>
      <span style="float:right" class="ui-icon ui-icon-search" title="Toggle search panel"
            onclick="toggleFilterRow()"></span>
    </div>
    <div id="myGrid" style="width:100%;height:350px;"></div>
    <div id="pager" style="width:100%;height:20px;"></div>
  </div>

  <div class="options-panel">
    <b>Search:</b>
    <hr/>
    <div style="padding:6px;">
      <label style="width:200px;float:left">Frozen Row:</label>
      <input type=text id="frozenRow" style="width:60px;" value="5">
      <button id="setFrozenRow">Set</button>
      <br/><br/>
      <label style="width:200px;float:left">Frozen Column:</label>
      <input type=text id="frozenColumn" style="width:60px;" value="2">
      <button id="setFrozenColumn">Set</button>
      <br/><br/>
      <button id="btnSelectRows">Select first 10 rows</button>

      <br/>

      <h2>Demonstrates:</h2>
      <ul>
        <li>a filtered Model (DataView) as a data source instead of a simple array</li>
        <li>grid reacting to model events (onRowCountChanged, onRowsChanged)</li>
        <li>
          <b>FAST</b> DataView recalculation and <b>real-time</b> grid updating in response to data changes.<br/>
          The grid holds <b>50'000</b> rows, yet you are able to sort, filter, scroll, navigate and edit as if it had 50
          rows.
        </li>
        <li>adding new rows, bidirectional sorting</li>
        <li>column options: cannotTriggerInsert</li>
        <li>events: onCellChange, onAddNewRow, onKeyDown, onSelectedRowsChanged, onSort</li>
        <li><font color=red>NOTE:</font> all filters are immediately applied to new/edited rows</li>
        <li>Handling row selection against model changes.</li>
        <li>Paging.</li>
        <li>inline filter panel</li>
      </ul>
    </div>
  </div>
</div>

<div id="inlineFilterPanelL" style="display:none;background:#dddddd;padding:3px;color:black;">
  Show tasks with title including <input type="text" id="txtSearch2">
</div>

<div id="inlineFilterPanelR" style="display:none;background:#dddddd;padding:6px;color:black;">
  and % at least &nbsp;
  <div style="width:100px;display:inline-block;" id="pcSlider2"></div>
</div>

<!-- 아래 스크립트 경로 지정 -->
<script language="JavaScript" src="../lib/firebugx.js"></script>

<script src="../lib/jquery-3.1.0.js"></script>
<script src="../lib/jquery-ui.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/sortablejs/Sortable.min.js"></script>

<script src="../slick.core.js"></script>
<script src="../slick.interactions.js"></script>
<script src="../slick.editors.js"></script>
<script src="../slick.formatters.js"></script>
<script src="../plugins/slick.rowselectionmodel.js"></script>
<script src="../slick.grid.js"></script>
<script src="../slick.dataview.js"></script>
<script src="../controls/slick.pager.js"></script>
<script src="../controls/slick.columnpicker.js"></script>

각자 프로젝트 경로에 맞게 플러그인을 해줍니다.

여기서 주의해야할 부분이 전 이전 버전으로 적용해서 사용하다가 틀 고정 기능을 사용하기 위해 있는 스크립트 제외하고

추가해줘야 할 스크립트 셋팅 후 가볍게 실행하였는데 에러 발생해서 보니 옛 스크립트에 없는 기능들이 많이 채워졌더라구요... 이미 사용하시고 계신 분들은 참고해서 진행해주세요! 

 

여기까지는 JSP 셋팅 부분이고 

 

$(function () {
  // 이 부분이 기존 입력 Options 으로 지정 해놓은 컬럼 Input 태그에 뿌려줍니다.
  $('#frozenColumn').val(options.frozenColumn);
});

function isIEPreVer9() { var v = navigator.appVersion.match(/MSIE ([\d.]+)/i); return (v ? v[1] < 9 : false); }

var dataView;
var grid;
var data = [];

// Grid 에 그려 질 컬럼들 하나하나 셋팅 작업
var columns = [
  {
    id: "sel",
    name: "#",
    field: "num",
    behavior: "select",
    cssClass: "cell-selection",
    width: 40,
    cannotTriggerInsert: true,
    resizable: false,
    unselectable: true
  },
  {
    id: "title",
    name: "Title",
    field: "title",
    cssClass: "cell-title",
    editor: Slick.Editors.Text,
    validator: requiredFieldValidator,
    sortable: true
  },
  {
    id: "duration",
    name: "Duration",
    field: "duration",
    editor: Slick.Editors.Text,
    sortable: true
  },
  {
    id: "%",
    name: "% Complete",
    field: "percentComplete",
    resizable: false,
    width: 80,
    formatter: Slick.Formatters.PercentComplete,
    editor: Slick.Editors.PercentComplete,
    sortable: true
  },
  {
    id: "start",
    name: "Start",
    field: "start",
    editor: Slick.Editors.Date,
    sortable: true
  },
  {
    id: "finish",
    name: "Finish",
    field: "finish",
    editor: Slick.Editors.Date,
    sortable: true
  },
  {
    id: "effort-driven",
    name: "Effort Driven",
    cssClass: "cell-effort-driven",
    field: "effortDriven",
    formatter: Slick.Formatters.Checkmark,
    editor: Slick.Editors.Checkbox,
    cannotTriggerInsert: true,
    sortable: true
  },
  {
    id: "title1",
    name: "Title1",
    field: "title1",
    cssClass: "cell-title",
    editor: Slick.Editors.Text,
    validator: requiredFieldValidator,
    sortable: true
  },
  {
    id: "title2",
    name: "Title2",
    field: "title2",
    cssClass: "cell-title",
    editor: Slick.Editors.Text,
    validator: requiredFieldValidator,
    sortable: true
  },
  {
    id: "title3",
    name: "Title3",
    field: "title3",
    cssClass: "cell-title",
    editor: Slick.Editors.Text,
    validator: requiredFieldValidator,
    sortable: true
  },
  {
    id: "title4",
    name: "Title4",
    field: "title4",
    cssClass: "cell-title",
    editor: Slick.Editors.Text,
    validator: requiredFieldValidator,
    sortable: true
  }
];

var columnFilters = {};

// 아래 frozenColumn, frozenRow 에서 설정이 가능하고 -1 로 지정하면 
// 틀 고정 없는 순수 Grid 를 그려줍니다.
var options = {
  editable: true,
  enableAddRow: true,
  enableCellNavigation: true,
  asyncEditorLoading: true,
  forceFitColumns: false,
  autoEdit: false,
  topPanelHeight: 25,
  frozenColumn: 2,
  frozenRow: 5,
  showHeaderRow: true,
  syncColumnCellResize: false
};

var sortcol = "title";
var sortdir = 1;
var percentCompleteThreshold = 0;
var searchString = "";

function requiredFieldValidator(value) {
  if (value == null || value == undefined || !value.length) return {
    valid: false,
    msg: "This is a required field"
  };
  else
    return {
      valid: true,
      msg: null
    };
}

// 틀 고정 Input 값에 값을 넣고 업데이틀 해주면 해당 함수가 실행이 되어 Grid 셋팅이 됩니다.
function updateHeaderRow() {
  for (var i = 0; i < columns.length; i++) {
    if (columns[i].id !== "selector") {
      var header = grid.getHeaderRowColumn(columns[i].id);
      $(header).empty();
      $("<input type='text'>")
        .data("columnId", columns[i].id)
        .val(columnFilters[columns[i].id])
        .appendTo(header);
    }
  }
}

function myFilter(item) {
  for (var columnId in columnFilters) {
    if (columnId !== undefined && columnFilters[columnId] !== "") {
      var c = grid.getColumns()[grid.getColumnIndex(columnId)];
      if (item[c.field] != columnFilters[columnId]) {
        return false;
      }
    }
  }

  return true;
}

function percentCompleteSort(a, b) {
  return a["percentComplete"] - b["percentComplete"];
}

function comparer(a, b) {
  var x = a[sortcol],
    y = b[sortcol];
  return (x == y ? 0 : (x > y ? 1 : -1));
}

function addItem(newItem, columnDef) {
  var item = {
    "num": data.length,
    "id": "new_" + (Math.round(Math.random() * 10000)),
    "title": "New task",
    "duration": "1 day",
    "percentComplete": 0,
    "start": "01/01/2009",
    "finish": "01/01/2009",
    "effortDriven": false
  };
  $.extend(item, newItem);
  dataView.addItem(item);
}


function toggleFilterRow() {
  grid.setTopPanelVisibility(!grid.getOptions().showTopPanel);
}


$(".grid-header .ui-icon").addClass("ui-state-default ui-corner-all").mouseover(function (e) {
  $(e.target).addClass("ui-state-hover")
}).mouseout(function (e) {
  $(e.target).removeClass("ui-state-hover")
});

$(function () {
  // prepare the data
  for (var i = 0; i < 50000; i++) {
    var d = (data[i] = {});

    d["id"] = "id_" + i;
    d["num"] = i;
    d["title"] = "Task " + i;
    d["duration"] = "5 days";
    d["percentComplete"] = Math.round(Math.random() * 100);
    d["start"] = "01/01/2009";
    d["finish"] = "01/05/2009";
    d["effortDriven"] = (i % 5 == 0);
    d["title1"] = i;
    d["title2"] = i;
    d["title3"] = i;
    d["title4"] = i;
  }


  dataView = new Slick.Data.DataView();
  grid = new Slick.Grid("#myGrid", dataView, columns, options);
  grid.setSelectionModel(new Slick.RowSelectionModel());

  var pager = new Slick.Controls.Pager(dataView, grid, $("#pager"));
  var columnpicker = new Slick.Controls.ColumnPicker(columns, grid, options);


  // move the filter panel defined in a hidden div into an inline secondary grid header row
  var $secondaryRow = grid.getTopPanel();

  $("#inlineFilterPanelL").appendTo($secondaryRow[0]).show();

  $("#inlineFilterPanelR").appendTo($secondaryRow[1]).show();

  grid.onCellChange.subscribe(function (e, args) {
    dataView.updateItem(args.item.id, args.item);
  });

  grid.onAddNewRow.subscribe(function (e, args) {
    var item = {
      "num": data.length,
      "id": "new_" + (Math.round(Math.random() * 10000)),
      "title": "New task",
      "duration": "1 day",
      "percentComplete": 0,
      "start": "01/01/2009",
      "finish": "01/01/2009",
      "effortDriven": false
    };
    $.extend(item, args.item);
    dataView.addItem(item);
  });

  grid.onKeyDown.subscribe(function (e) {
    // select all rows on ctrl-a
    if (e.which != 65 || !e.ctrlKey) return false;

    var rows = [];

    for (var i = 0; i < dataView.getLength(); i++) {
      rows.push(i);
    }

    grid.setSelectedRows(rows);
    e.preventDefault();
  });

  grid.onMouseEnter.subscribe(function (e) {
    var cell = this.getCellFromEvent(e);

    this.setSelectedRows([cell.row]);
    e.preventDefault();
  });

  grid.onMouseLeave.subscribe(function (e) {
    this.setSelectedRows([]);
    e.preventDefault();
  });

  grid.onSort.subscribe(function (e, args) {
    sortdir = args.sortAsc ? 1 : -1;
    sortcol = args.sortCol.field;

    if (isIEPreVer9()) {
      // using temporary Object.prototype.toString override
      // more limited and does lexicographic sort only by default, but can be much faster

      var percentCompleteValueFn = function () {
        var val = this["percentComplete"];
        if (val < 10) {
          return "00" + val;
        } else if (val < 100) {
          return "0" + val;
        } else {
          return val;
        }
      };

      // use numeric sort of % and lexicographic for everything else
      dataView.fastSort((sortcol == "percentComplete") ? percentCompleteValueFn : sortcol, args.sortAsc);
    } else {
      // using native sort with comparer
      // preferred method but can be very slow in IE with huge datasets
      dataView.sort(comparer, args.sortAsc);
    }
  });

  // wire up model events to drive the grid
  dataView.onRowCountChanged.subscribe(function (e, args) {
    grid.updateRowCount();
    grid.render();
  });

  dataView.onRowsChanged.subscribe(function (e, args) {
    grid.invalidateRows(args.rows);
    grid.render();
  });

  dataView.onPagingInfoChanged.subscribe(function (e, pagingInfo) {
    var isLastPage = pagingInfo.pageSize * (pagingInfo.pageNum + 1) - 1 >= pagingInfo.totalRows;
    var enableAddRow = isLastPage || pagingInfo.pageSize == 0;
    var options = grid.getOptions();

    if (options.enableAddRow != enableAddRow) grid.setOptions({
      enableAddRow: enableAddRow
    });
  });

  $(grid.getHeaderRow()).delegate(":input", "change keyup", function (e) {
    columnFilters[$(this).data("columnId")] = $.trim($(this).val());
    dataView.refresh();
  });


  grid.onColumnsReordered.subscribe(function (e, args) {
    updateHeaderRow();
  });

  grid.onColumnsResized.subscribe(function (e, args) {
    updateHeaderRow();
  });

  var h_runfilters = null;

  // wire up the slider to apply the filter to the model
  // wire up the slider to apply the filter to the model
  $("#pcSlider,#pcSlider2").slider({
    "range": "min",
    "slide": function (event, ui) {
      Slick.GlobalEditorLock.cancelCurrentEdit();

      if (percentCompleteThreshold != ui.value) {
        window.clearTimeout(h_runfilters);
        h_runfilters = window.setTimeout(dataView.refresh, 10);
        percentCompleteThreshold = ui.value;
      }
    }
  });

  // wire up the search textbox to apply the filter to the model
  $("#txtSearch,#txtSearch2").keyup(function (e) {
    Slick.GlobalEditorLock.cancelCurrentEdit();

    // clear on Esc
    if (e.which == 27) this.value = "";

    searchString = this.value;
    dataView.refresh();
  });

  $("#btnSelectRows").click(function () {
    if (!Slick.GlobalEditorLock.commitCurrentEdit()) {
      return;
    }

    var rows = [];

    for (var i = 0; i < 10 && i < dataView.getLength(); i++) {
      rows.push(i);
    }

    grid.setSelectedRows(rows);
  });

  // initialize the model after all the events have been hooked up
  dataView.beginUpdate();
  dataView.setItems(data);
  dataView.setFilter(myFilter);
  dataView.endUpdate();

  // if you don't want the items that are not visible (due to being filtered out
  // or being on a different page) to stay selected, pass 'false' to the second arg
  dataView.syncGridSelection(grid, true);

  updateHeaderRow();

  $("#gridContainer").resizable();

// Row 고정 해주는 버튼.
  $('#setFrozenRow').click(function () {
    var val = -1;

    if ($('#frozenRow').val() != '') {
      val = parseInt($('#frozenRow').val());
    }

    grid.setOptions({
      'frozenRow': val
    });

    updateHeaderRow();
  });

// Col 고정 해주는 버튼.
  $('#setFrozenColumn').click(function () {
    var val = -1;

    if ($('#frozenColumn').val() != '') {
      val = parseInt($('#frozenColumn').val());
    }

    grid.setOptions({
      'frozenColumn': val
    });

    updateHeaderRow();
  });
})

위 코드에서 주석 달린 부분이 행, 열 고정 시킬때 주의깊게 보셔야 할 부분입니다. 

 

다들 오늘도 수고하셨습니다. 

728x90
Comments