Translate

2019년 6월 18일 화요일

[Vue.js] vue-datetime 플러그인 Modal Open Event(trigger)





Vue로 제작된 가벼운 DatetimePicker 어떤게 있나 알아보다 아래의 vue-datetime을 사용하기로 결정했다.

https://github.com/mariomka/vue-datetime
1004lucifer




문제점

 - API를 살펴보니 DatetimePicker 를 따로 열 수 있는 이벤트가 보이지 않았다.

input 에 click 이벤트를 주려 했지만 그것도 안되고..

다음과 같이 input 을 클릭하지 않고 DatetimePicker 를 띄울 수 있었다.



방법

https://github.com/mariomka/vue-datetime/blob/v1.0.0-beta.10/src/Datetime.vue#L203

Datetime 컴포넌트에 (API에는 없지만) open 메소드가 있어 이용을 하니 정상적으로 컴포넌트가 띄워졌다.
1004lucifer
<template>
  <div>
    <datetime ref="datetimePicker"></datetime>
    <button @click="openDatetime"></button>
  </div>
</template>

<script>
export default {
  components: {
  },
  props: {
  },
  data () {
    return {
    }
  },
  created () {
  },
  mounted () {
  },
  computed: {},
  methods: {
    openDatetime() {
      this.$refs.datetimePicker.open(event)
    }
  }
}
</script>



[JSFiddle] npm 패키지 임포트(import) 하는 방법





블로그를 사용하며 코드의 결과물을 보여주는데 JSFiddle 사이트를 많이 이용하고 있다.



이번에도 JSFiddle 을 이용하여 코드를 작성하는데 따로 dist 파일이 배포되지 않고 npm 패키지만 있는 라이브러리가 있었다.

어떻게 포함을 하는지 찾아보니 역시 또 누군가 https://unpkg.com/ 이런 멋진 사이트를 만들어 놨네..



아래의 npm package를 JSFiddle 에 import 하려고 한다.
1004lucifer
https://www.npmjs.com/package/vue-datetime

설치방법에 vue-datetime 이외에 luxon, weekstart 가 추가로 필요하고, css파일을 import 해줘야 한다고 한다.




unpkg 에서는 https://unpkg.com/vue-datetime/ 과 같이 패키지명 마지막에 슬래시(/)를 붙이면 URL이 최신버전으로 변경되며 패키징된 파일을 디렉토리 구조로 확인이 가능하다.
1004lucifer




방법

 - 다음과 같이 패키지와 필요한 파일(css 등)을 추가해준다.

1004lucifer



다음은 위의 방법을 사용해서 vue-datetime npm package 를 JSFiddle 에 넣어서 만든 예제이다.

 - javascript 부분에 따로 import 없이 자동으로 import가 되었다.
 - 일정시간이 지나면 캐시파일이 삭제되므로 unpkg를 이용해 JSFiddle 예제를 만든경우 오랜만에 Result를 띄우면 시간이 오래(몇초) 걸린다.






참고
 - https://unpkg.com/
 - https://stackoverflow.com/questions/46845199/how-to-import-npm-package-in-jsfiddle
 - https://stackoverflow.com/questions/55340816/jsfiddle-code-sample-for-ui5-web-components-using-unpkg-com-to-avoid-dependency
 - https://jsfiddle.net/gongzza/kyp2f0sk/


2019년 6월 16일 일요일

[Vue.js] router-link 동일한 링크 클릭 시 강제로 reload 하기






현재 보고있는 화면의 메뉴를 누르면 화면이 해당 메뉴에 처음 들어갔을때와 같이 보여줘야 한다는 요구사항이 있었다.



증상
1004lucifer
 - Vue 에서 기본적으로 현재 페이지는 링크를 눌러도 router-view 가 다시 로드되지 않는다.










개선
1004lucifer
 - 현재 보고있는 페이지의 링크(router-link)를 클릭 시 강제로 router-view 를 새로 로드하도록 수정을 했다.
 - 클릭한 링크의 URL이 현재 페이지라면 뒤에 인덱스 번호를 붙여서 이동하도록 수정함.





[Regex] 정규표현식 특정 문자열 포함하지 않는 매칭 방법





특정 문자열이 들어있는 라인을 제외하고 어떻게 문자열을 추출할 수 있을까 하다가 알아보았다.



특정 문자열이 포함되지 않게 매칭하는 방법은 아래와 같다.
1004lucifer
 ^((?!단어).)*$






복수개의 단어에 대해서 문자열을 제외하고 싶다면 다음과 같이 할 수 있다.
1004lucifer
 ^((?!단어|단어).)*$



1004lucifer


복수개의 단어가 동시에 없어야 한다면 다음과 같이 할 수 있다.
(이 방법은 지난번 링크-정규표현식 AND 연산 사용방법 을 응용해 봤다.)

 ^((?!(?=.*단어)(?=.*단어)).)*$
 ^((?!(?=.*단어)(?=.*단어)(?=.*단어)(?=.*단어)).)*$









매칭되는 원리

1. negative lookahead 를 이용하면 해당 단어와 일치하지 않은 단어의 앞에있는 문자열이 매칭된다.


1004lucifer


2. 해당 매칭문자열에 점(.) 하나 붙여서 글자를 하나더 추가 매칭이 가능





3. 해당 문자열 앞에 단어 대신 시작(^)을 붙여서 해당 문자열로 시작되지 않는것만 매칭





4. 그룹화를 해서 해당 문자열 제외한 앞에까지 모두 매칭





5. 마지막까지 모두 매칭되면 추출 완료


정규식

참고
 - https://stackoverflow.com/questions/406230/regular-expression-to-match-a-line-that-doesnt-contain-a-word



[jqGrid] page, lastpage 값 셋팅 예제







jqGrid 는 page, lastpage를 일부러 셋팅하지 않더라도 일반적으로는 자동으로 셋팅이 된다.
하지만 특정 상황에서는 어쩔 수 없이 page, lastpage를 수동으로 셋팅해야 하는 경우가 발생을 하게 되는데 수동으로 셋팅하는 방법과 주의할 점에 대해서 기술을 한다.



주의사항


 jqGrid는 기본적으로 page / lastpage 의 최소값은 1 /1 이다.
 jqGrid 내부로직에서 page, lastpage 가 잘못된 값인경우 1 또는 올바른 값으로 강제 셋팅되도록 코드가 여기저기 있다.
 따라서 0 / 0 처럼 셋팅을 하려고 시도하면 경우에 따라 셋팅이 잘 안될 것이다.





셋팅방법
1004lucifer
 - setGridParam를 이용하여 page, lastpage를 셋팅 후 updatepager 메소드를 이용하여 그리드의 page, lastpage 값을 업데이트 한다.
(아래 예제에서 page, lastpage 값을 변경하고 버튼을 누르면 값이 업데이트 되는것을 확인 할 수 있다.)











데이터 로딩과 같이 셋팅하기

 - 데이터 로딩 없이 page 번호를 업데이트 하는 경우는 없을텐데, 인터넷에 있는 다른 글에서는 이런 주의사항을 알려주지 않아 한참 시간을 낭비했다.
 - 데이터 로딩과 같이 셋팅 시 페이지 셋팅하는 순서가 중요하다.
   (그리드 내부로직 여기저기서 page, lastpage 가 올바르지 않으면 강제로 값을 변경한다.)

1004lucifer

1. 잘못된 방법
 - setGridParam 메소드로 data, page, lastpage 값을 한꺼번에 수정 후 reloadGrid를 사용한다.









 - reloadGrid가 된 후 page, lastpage 값을 수정 후 updatepager 로 값을 업데이트 한다.
  (하지만 헤더 부분을 클릭하여 정렬 기능을 이용 시 page, lastpage 가 다시 바뀌게 된다.)











2. 올바른 방법
 - loadComplete 이벤트 함수 내에서 page, lastpage 값을 수정 후 업데이트 한다.
  (loadComplete 함수는 대부분의 로직의 마지막에 수행이 되므로 page, lastpage 가 강제로 변경되지 않는다.)






참고 링크
 - updatepager 메소드 설명
 - reloadGrid 메소드 설명
 - populate 메소드 설명




2019년 6월 14일 금요일

[jqGrid] addJSONData 메소드 설명







환경: jqGrid v4.7.1 이하 (무료버전)



json 데이터를 일괄적으로 업데이트하는 addJSONData 메소드는 공식문서에도 나와있어 종종 사용할텐데 모르고 사용하면 디버깅을 하느라 시간을 버리게된다.


https://github.com/tonytomov/jqGrid/blob/v4.7.1/js/jquery.jqGrid.js#L1505
1004lucifer
/**
 * addJSONData
 * @param data colModel 형식의 json object 또는 jsonReader 형식의 json object
 * @param t jqGrid
 * @param rcnt 일반적으로 1
 * @param more true인 경우 페이지를 업데이트 하지 않는다.
 * @param adjust 동적스크롤그리드인 경우 npage 숫자 값 - 일반적으로 0
 */
addJSONData = function (data, t, rcnt, more, adjust) {
  var startReq = new Date();
  if (data) {
    // 트리그리드, 동적스크롤그리드가 아닌경우
    if (ts.p.treeANode === -1 && !ts.p.scroll) {
      // 그리드에서 1번째 빈row를 남기고 모두 삭제
      emptyRows.call(ts, false, true);
      rcnt = 1;
    } else { rcnt = rcnt > 1 ? rcnt : 1; }
  } else { return; }

  var dReader, locid = "_id_", frd,
    locdata = (ts.p.datatype !== "local" && ts.p.loadonce) || ts.p.datatype === "jsonstring";
  if (locdata) { ts.p.data = []; ts.p._index = {}; ts.p.localReader.id = locid; }
  ts.p.reccount = 0;
  if (ts.p.datatype === "local") {
    dReader = ts.p.localReader;
    frd = 'local';
  } else {
    dReader = ts.p.jsonReader;
    frd = 'json';
  }
  var self = $(ts), ir = 0, v, i, j, f = [], cur, gi = ts.p.multiselect ? 1 : 0, si = ts.p.subGrid === true ? 1 : 0, addSubGridCell, ni = ts.p.rownumbers === true ? 1 : 0, arrayReader = orderedCols(gi + si + ni), objectReader = reader(frd), rowReader, len, drows, idn, rd = {}, fpos, idr, rowData = [], cn = (ts.p.altRows === true) ? ts.p.altclass : "", cn1;
  ts.p.page = intNum($.jgrid.getAccessor(data, dReader.page), ts.p.page);
  ts.p.lastpage = intNum($.jgrid.getAccessor(data, dReader.total), 1);
  ts.p.records = intNum($.jgrid.getAccessor(data, dReader.records));
  ts.p.userData = $.jgrid.getAccessor(data, dReader.userdata) || {};
  
  // 서브그리드 사용하는경우
  if (si) {
    addSubGridCell = $.jgrid.getMethod("addSubGridCell");
  }
  
  if (ts.p.keyName === false) {
    // keyName 옵션을 사용하지 않으면 Reader의 id가 index로 사용(기본값: id)
    idn = $.isFunction(dReader.id) ? dReader.id.call(ts, data) : dReader.id;
  } else {
    idn = ts.p.keyName;
  }

  // data객체에서 row 추출
  drows = $.jgrid.getAccessor(data, dReader.root); // jsonReader 형식인 경우
  if (drows == null && $.isArray(data)) { drows = data; } // colModel 형식인 경우
  if (!drows) { drows = []; }

  // 데이터가 1개이상 있으면서 page 번호가 0보다 작으면 강제로 page를 1로 셋팅
  len = drows.length; i = 0;
  if (len > 0 && ts.p.page <= 0) { ts.p.page = 1; }

  var rn = parseInt(ts.p.rowNum, 10), br = ts.p.scroll ? $.jgrid.randId() : 1, altr, selected = false, selr;
  if (adjust) { rn *= adjust + 1; }
  
  // deselectAfterSort 옵션 - 소팅을 적용시키면 선택된 row가 선택해제 됨 (기본값: true)
  if (ts.p.datatype === "local" && !ts.p.deselectAfterSort) {
    selected = true;
  }
  var afterInsRow = $.isFunction(ts.p.afterInsertRow), grpdata = [], hiderow = false, groupingPrepare;

  // 데이터 그룹화 사용하는경우
  if (ts.p.grouping) {
    hiderow = ts.p.groupingView.groupCollapse === true;
    groupingPrepare = $.jgrid.getMethod("groupingPrepare");
  }
  while (i < len) {
    cur = drows[i];
    idr = $.jgrid.getAccessor(cur, idn);
    if (idr === undefined) {
      if (typeof idn === "number" && ts.p.colModel[idn + gi + si + ni] != null) {
        // reread id by name
        idr = $.jgrid.getAccessor(cur, ts.p.colModel[idn + gi + si + ni].name);
      }
      if (idr === undefined) {
        idr = br + i;
        if (f.length === 0) {
          if (dReader.cell) {
            var ccur = $.jgrid.getAccessor(cur, dReader.cell) || cur;
            idr = ccur != null && ccur[idn] !== undefined ? ccur[idn] : idr;
            ccur = null;
          }
        }
      }
    }
    idr = ts.p.idPrefix + idr;
    altr = rcnt === 1 ? 0 : rcnt;
    cn1 = (altr + i) % 2 === 1 ? cn : '';
    if (selected) {
      if (ts.p.multiselect) {
        selr = ($.inArray(idr, ts.p.selarrrow) !== -1);
      } else {
        selr = (idr === ts.p.selrow);
      }
    }
    var iStartTrTag = rowData.length;
    rowData.push("");
    if (ni) {
      rowData.push(addRowNum(0, i, ts.p.page, ts.p.rowNum));
    }
    if (gi) {
      rowData.push(addMulti(idr, ni, i, selr));
    }
    if (si) {
      rowData.push(addSubGridCell.call(self, gi + ni, i + rcnt));
    }
    rowReader = objectReader;
    if (dReader.repeatitems) {
      if (dReader.cell) { cur = $.jgrid.getAccessor(cur, dReader.cell) || cur; }
      if ($.isArray(cur)) { rowReader = arrayReader; }
    }
    for (j = 0; j < rowReader.length; j++) {
      v = $.jgrid.getAccessor(cur, rowReader[j]);
      rd[ts.p.colModel[j + gi + si + ni].name] = v;
      rowData.push(addCell(idr, v, j + gi + si + ni, i + rcnt, cur, rd));
    }
    rowData[iStartTrTag] = constructTr(idr, hiderow, cn1, rd, cur, selr);
    rowData.push("</tr>");

    if (ts.p.grouping) {
      grpdata.push(rowData);
      if (!ts.p.groupingView._locgr) {
        groupingPrepare.call(self, rd, i);
      }
      rowData = [];
    }
    if (locdata || ts.p.treeGrid === true) {
      rd[locid] = $.jgrid.stripPref(ts.p.idPrefix, idr);
      ts.p.data.push(rd);
      ts.p._index[rd[locid]] = ts.p.data.length - 1;
    }

    // gridview 옵션 false 인경우 (기본값 false)
    if (ts.p.gridview === false) {
      // 그리드 body에 row(tr) 추가
      $("#" + $.jgrid.jqID(ts.p.id) + " tbody:first").append(rowData.join(''));
      // jqGridAfterInsertRow 트리거 이벤트 호출
      self.triggerHandler("jqGridAfterInsertRow", [idr, rd, cur]);
      // afterInsertRow 이벤트 호출
      if (afterInsRow) { ts.p.afterInsertRow.call(ts, idr, rd, cur); }
      rowData = [];//ari=0;
    }
    rd = {};
    ir++;
    i++;
    if (ir === rn) { break; }
  }

  // gridview 옵션 true 인경우 (기본값 false)
  if (ts.p.gridview === true) {
    fpos = ts.p.treeANode > -1 ? ts.p.treeANode : 0;
    if (ts.p.grouping) {
      if (!locdata) {
        self.jqGrid('groupingRender', grpdata, ts.p.colModel.length, ts.p.page, rn);
        grpdata = null;
      }
    } else if (ts.p.treeGrid === true && fpos > 0) {
      $(ts.rows[fpos]).after(rowData.join(''));
    } else {
      // 그리드에 row(tr) 추가
      ts.firstElementChild.innerHTML += rowData.join(''); // append to innerHTML of tbody which contains the first row (.jqgfirstrow)
      ts.grid.cols = ts.rows[0].cells; // update cached first row
    }
  }

  // subGrid 사용하는경우
  if (ts.p.subGrid === true) {
    try { self.jqGrid("addSubGrid", gi + ni); } catch (_) { }
  }
  ts.p.totaltime = new Date() - startReq;

  // records 옵션이 0인경우 데이터의 갯수 값으로 변경
  if (ir > 0) {
    if (ts.p.records === 0) { ts.p.records = len; }
  }
  rowData = null;

  // 트리그리드 사용하는경우
  if (ts.p.treeGrid === true) {
    try { self.jqGrid("setTreeNode", fpos + 1, ir + fpos + 1); } catch (e) { }
  }
  ts.p.reccount = ir;
  ts.p.treeANode = -1;

  // userDataOnFooter 옵션 - true 설정 시 userData를 footer에 배치시킴 (기본값: false)
  if (ts.p.userDataOnFooter) { self.jqGrid("footerData", "set", ts.p.userData, true); }

  // 위에서 지정한 변수 locdata = (ts.p.datatype !== "local" && ts.p.loadonce) || ts.p.datatype === "jsonstring";
  if (locdata) {
    ts.p.records = len;
    ts.p.lastpage = Math.ceil(len / rn);
  }

  // pager 업데이트
  if (!more) { ts.updatepager(false, true); }

  // 위에서 지정한 변수 locdata = (ts.p.datatype !== "local" && ts.p.loadonce) || ts.p.datatype === "jsonstring";
  if (locdata) {
    while (ir < len && drows[ir]) {
      cur = drows[ir];
      idr = $.jgrid.getAccessor(cur, idn);
      if (idr === undefined) {
        if (typeof idn === "number" && ts.p.colModel[idn + gi + si + ni] != null) {
          // reread id by name
          idr = $.jgrid.getAccessor(cur, ts.p.colModel[idn + gi + si + ni].name);
        }
        if (idr === undefined) {
          idr = br + ir;
          if (f.length === 0) {
            if (dReader.cell) {
              var ccur2 = $.jgrid.getAccessor(cur, dReader.cell) || cur;
              idr = ccur2 != null && ccur2[idn] !== undefined ? ccur2[idn] : idr;
              ccur2 = null;
            }
          }
        }
      }
      if (cur) {
        idr = ts.p.idPrefix + idr;
        rowReader = objectReader;
        if (dReader.repeatitems) {
          if (dReader.cell) { cur = $.jgrid.getAccessor(cur, dReader.cell) || cur; }
          if ($.isArray(cur)) { rowReader = arrayReader; }
        }

        for (j = 0; j < rowReader.length; j++) {
          rd[ts.p.colModel[j + gi + si + ni].name] = $.jgrid.getAccessor(cur, rowReader[j]);
        }
        rd[locid] = $.jgrid.stripPref(ts.p.idPrefix, idr);
        if (ts.p.grouping) {
          groupingPrepare.call(self, rd, ir);
        }
        ts.p.data.push(rd);
        ts.p._index[rd[locid]] = ts.p.data.length - 1;
        rd = {};
      }
      ir++;
    }
    if (ts.p.grouping) {
      ts.p.groupingView._locgr = true;
      self.jqGrid('groupingRender', grpdata, ts.p.colModel.length, ts.p.page, rn);
      grpdata = null;
    }
  }
}


위의 로직을 보면 addJSONData 메소드는 다음 두가지 기능을 한다.
 1) 넘겨받은 데이터를 그리드 body에 보여줌
 2) Pager 업데이트


하지만 아쉽게도 데이터를 그리드 내부에 저장하는 로직이 들어있지 않아 페이지 이동을 하거나 컬럼헤더를 클릭하여 정렬 기능을 사용하면 원래 데이터로 돌아가 버린다.
(처음에 데이터가 없다면 빈그리드가 나타날 것이다.)

위의 주의해야 할 사항때문에 특별한 경우에만 이 메소드를 사용할 수 있을것 같다.

datatype:'local' 인 경우 addJSONData 보다는 아래 두가지 방법중에 하나를 사용하는게 더 편하지 않을까 싶다.
 - addRowData 메소드로 row를 하나씩 추가
 - setGridParams 메소드로 data를 업데이트하고 reload



2019년 6월 13일 목요일

[jqGrid] populate 메소드 설명







환경: jqGrid v4.7.1 이하 (무료버전)



개발자가 해당 메소드를 직접 호출할일은 일반적으로 없겠지만 jqGrid 내부로직을 공부하다보면 필수적으로 실행되며, 중요한 역할을 하는 메소드라서 이렇게 알아본다.

populate 메소드는 다음의 상황에서 사용되어진다.
 1) jqGrid 초기화(initialization) 시점
 2) reloadGrid - 링크
 3) sortGrid 메소드 (sortData 메소드 수행)
 4) onPaging 이벤트 - 링크
 5) 그리드의 헤더(th) 클릭 시 (sortData 메소드 수행)


https://github.com/tonytomov/jqGrid/blob/v4.7.1/js/jquery.jqGrid.js#L1990
1004lucifer
/**
 * populate
 * @param npage 동적스크롤 그리드 사용하는 경우 값이 넘어옴
 */
populate = function (npage) {
  // Request progressBar 가 보여지지 않을 때
  if (!ts.grid.hDiv.loading) {
    var pvis = ts.p.scroll && npage === false,
      prm = {}, dt, dstr, pN = ts.p.prmNames;
    
    // page값이 1보다 적은경우 1로 셋팅
    if (ts.p.page <= 0) { ts.p.page = Math.min(1, ts.p.lastpage); }
    
    if (pN.search !== null) { prm[pN.search] = ts.p.search; } if (pN.nd !== null) { prm[pN.nd] = new Date().getTime(); }
    if (pN.rows !== null) { prm[pN.rows] = ts.p.rowNum; } if (pN.page !== null) { prm[pN.page] = ts.p.page; }
    if (pN.sort !== null) { prm[pN.sort] = ts.p.sortname; } if (pN.order !== null) { prm[pN.order] = ts.p.sortorder; }
    if (ts.p.rowTotal !== null && pN.totalrows !== null) { prm[pN.totalrows] = ts.p.rowTotal; }

    // lc 변수에 loadComplete 이벤트 저장
    var lcf = $.isFunction(ts.p.loadComplete), lc = lcf ? ts.p.loadComplete : null;

    var adjust = 0;
    npage = npage || 1;

    // 동적스크롤 그리드 사용하는 경우
    if (npage > 1) {
      if (pN.npage !== null) {
        prm[pN.npage] = npage;
        adjust = npage - 1;
        npage = 1;
      } else {
        lc = function (req) {
          ts.p.page++;
          ts.grid.hDiv.loading = false;
          if (lcf) {
            ts.p.loadComplete.call(ts, req);
          }
          populate(npage - 1);
        };
      }
    } else if (pN.npage !== null) {
      delete ts.p.postData[pN.npage];
    }

    // 데이터 그룹화(Data grouping) 그리드 사용하는 경우
    if (ts.p.grouping) {
      $(ts).jqGrid('groupingSetup');
      var grp = ts.p.groupingView, gi, gs = "";
      for (gi = 0; gi < grp.groupField.length; gi++) {
        var index = grp.groupField[gi];
        $.each(ts.p.colModel, function (cmIndex, cmValue) {
          if (cmValue.name === index && cmValue.index) {
            index = cmValue.index;
          }
        });
        gs += index + " " + grp.groupOrder[gi] + ", ";
      }
      prm[pN.sort] = gs + prm[pN.sort];
    }
    $.extend(ts.p.postData, prm);
    var rcnt = !ts.p.scroll ? 1 : ts.rows.length - 1;

    // jqGridBeforeRequest 트리거 이벤트 호출
    var bfr = $(ts).triggerHandler("jqGridBeforeRequest");
    if (bfr === false || bfr === 'stop') { return; }

    // datatype: function 타입인경우 호출
    if ($.isFunction(ts.p.datatype)) { ts.p.datatype.call(ts, ts.p.postData, "load_" + ts.p.id, rcnt, npage, adjust); return; }

    // beforeRequest 이벤트 호출
    if ($.isFunction(ts.p.beforeRequest)) {
      bfr = ts.p.beforeRequest.call(ts);
      if (bfr === undefined) { bfr = true; }
      if (bfr === false) { return; }
    }

    // datatype 종류에 따라 로직 분류
    dt = ts.p.datatype.toLowerCase();
    switch (dt) {
      case "json":
      case "jsonp":
      case "xml":
      case "script":
        // Ajax 호출
        $.ajax($.extend({
          url: ts.p.url,
          type: ts.p.mtype,
          dataType: dt,
          data: $.isFunction(ts.p.serializeGridData) ? ts.p.serializeGridData.call(ts, ts.p.postData) : ts.p.postData,
          success: function (data, st, xhr) {
            // beforeProcessing 이벤트 호출
            if ($.isFunction(ts.p.beforeProcessing)) {
              if (ts.p.beforeProcessing.call(ts, data, st, xhr) === false) {
                endReq();
                return;
              }
            }
            // 데이터를 그리드에 추가
            if (dt === "xml") { addXmlData(data, ts.grid.bDiv, rcnt, npage > 1, adjust); }
            else { addJSONData(data, ts.grid.bDiv, rcnt, npage > 1, adjust); }
            // jqGridLoadComplete 트리거 이벤트 호출
            $(ts).triggerHandler("jqGridLoadComplete", [data]);
            // loadComplete 이벤트 호출
            if (lc) { lc.call(ts, data); }
            // jqGridAfterLoadComplete 트리거 이벤트 호출
            $(ts).triggerHandler("jqGridAfterLoadComplete", [data]);
            if (pvis) { ts.grid.populateVisible(); }
            if (ts.p.loadonce || ts.p.treeGrid) { ts.p.datatype = "local"; }
            data = null;
            if (npage === 1) { endReq(); }
          },
          error: function (xhr, st, err) {
            // loadError 이벤트 호출
            if ($.isFunction(ts.p.loadError)) { ts.p.loadError.call(ts, xhr, st, err); }
            if (npage === 1) { endReq(); }
            xhr = null;
          },
          beforeSend: function (xhr, settings) {
            var gotoreq = true;
            // loadBeforeSend 이벤트 호출
            if ($.isFunction(ts.p.loadBeforeSend)) {
              gotoreq = ts.p.loadBeforeSend.call(ts, xhr, settings);
            }
            if (gotoreq === undefined) { gotoreq = true; }
            if (gotoreq === false) {
              return false;
            }
            beginReq();
          }
        }, $.jgrid.ajaxOptions, ts.p.ajaxGridOptions));
        break;
      case "xmlstring":
        beginReq();
        dstr = typeof ts.p.datastr !== 'string' ? ts.p.datastr : $.parseXML(ts.p.datastr);
        // 데이터를 그리드에 추가
        addXmlData(dstr, ts.grid.bDiv);
        // jqGridLoadComplete 트리거 이벤트 호출
        $(ts).triggerHandler("jqGridLoadComplete", [dstr]);
        // loadComplete 이벤트 호출
        if (lcf) { ts.p.loadComplete.call(ts, dstr); }
        // jqGridAfterLoadComplete 트리거 이벤트 호출
        $(ts).triggerHandler("jqGridAfterLoadComplete", [dstr]);
        ts.p.datatype = "local";
        ts.p.datastr = null;
        endReq();
        break;
      case "jsonstring":
        beginReq();
        if (typeof ts.p.datastr === 'string') { dstr = $.jgrid.parse(ts.p.datastr); }
        else { dstr = ts.p.datastr; }
        // 데이터를 그리드에 추가
        addJSONData(dstr, ts.grid.bDiv);
        // jqGridLoadComplete 트리거 이벤트 호출
        $(ts).triggerHandler("jqGridLoadComplete", [dstr]);
        // loadComplete 이벤트 호출
        if (lcf) { ts.p.loadComplete.call(ts, dstr); }
        // jqGridAfterLoadComplete 트리거 이벤트 호출
        $(ts).triggerHandler("jqGridAfterLoadComplete", [dstr]);
        ts.p.datatype = "local";
        ts.p.datastr = null;
        endReq();
        break;
      case "local":
      case "clientside":
        beginReq();
        ts.p.datatype = "local";
        var req = addLocalData();
        // 데이터를 그리드에 추가
        addJSONData(req, ts.grid.bDiv, rcnt, npage > 1, adjust);
        // jqGridLoadComplete 트리거 이벤트 호출
        $(ts).triggerHandler("jqGridLoadComplete", [req]);
        // loadComplete 이벤트 호출
        if (lc) { lc.call(ts, req); }
        // jqGridAfterLoadComplete 트리거 이벤트 호출
        $(ts).triggerHandler("jqGridAfterLoadComplete", [req]);
        if (pvis) { ts.grid.populateVisible(); }
        endReq();
        break;
    }
  }
}


여기서 주의깊게 봐야 하는부분은 datatype 에 상관없이
beforeRequest, loadComplete 이벤트가 무조건 호출된다는 점이다.

특히 loadComplete 이벤트는 그리드에 데이터가 모두 채워진 다음에 수행이 되기 때문에 페이징 번호를 수동으로 처리하는 경우에 loadComplete에 넣어두는게 좋다.



2019년 6월 11일 화요일

[jqGrid] setPager 로직 설명 (Pager 초기화)







환경: jqGrid v4.7.1 이하 (무료버전)



아래 모습에서 페이지를 이동 및 페이지 갯수 설정을 위한 액션을 취할 때 발생하는 이벤트를 호출하는 Pager의 초기화 로직을 알아본다.





jqGrid 가 초기화 될 때 다음과 같은 로직이 수행된다.
1004lucifer
https://github.com/tonytomov/jqGrid/blob/v4.7.1/js/jquery.jqGrid.js#L2136


// Pager 초기화 로직
setPager = function (pgid, tp) {
  var sep = "<td class='ui-pg-button ui-state-disabled' style='width:4px;'><span class='ui-separator'></span></td>",
    pginp = "",
    pgl = "<table cellspacing='0' cellpadding='0' border='0' style='table-layout:auto;' class='ui-pg-table'><tbody><tr>",
    str = "", pgcnt, lft, cent, rgt, twd, tdw, i,
    clearVals = function (onpaging) {
      var ret;
      // (문서에서 공식적으로 제공하는) onPaging 이벤트 함수를 호출
      if ($.isFunction(ts.p.onPaging)) { ret = ts.p.onPaging.call(ts, onpaging); }
      if (ret === 'stop') { return false; }
      ts.p.selrow = null;
      if (ts.p.multiselect) { ts.p.selarrrow = []; setHeadCheckBox(false); }
      ts.p.savedRow = [];
      return true;
    };
  pgid = pgid.substr(1);
  tp += "_" + pgid;
  pgcnt = "pg_" + pgid;
  lft = pgid + "_left"; cent = pgid + "_center"; rgt = pgid + "_right";
  $("#" + $.jgrid.jqID(pgid))
    .append("<div id='" + pgcnt + "' class='ui-pager-control' role='group'><table cellspacing='0' cellpadding='0' border='0' class='ui-pg-table' style='width:100%;table-layout:fixed;height:100%;' role='row'><tbody><tr><td id='" + lft + "' align='left'></td><td id='" + cent + "' align='center' style='white-space:pre;'></td><td id='" + rgt + "' align='right'></td></tr></tbody></table></div>")
    .attr("dir", "ltr");
  if (ts.p.rowList.length > 0) {
    str = "<td dir='" + dir + "'>";
    str += "<select class='ui-pg-selbox' role='listbox' " + (ts.p.pgrecs ? "title='" + ts.p.pgrecs + "'" : "") + ">";
    var strnm;
    for (i = 0; i < ts.p.rowList.length; i++) {
      strnm = ts.p.rowList[i].toString().split(":");
      if (strnm.length === 1) {
        strnm[1] = strnm[0];
      }
      str += "<option role=\"option\" value=\"" + strnm[0] + "\"" + ((intNum(ts.p.rowNum, 0) === intNum(strnm[0], 0)) ? " selected=\"selected\"" : "") + ">" + strnm[1] + "</option>";
    }
    str += "</select></td>";
  }
  if (dir === "rtl") { pgl += str; }
  if (ts.p.pginput === true) { pginp = "<td dir='" + dir + "'>" + $.jgrid.format(ts.p.pgtext || "", "<input class='ui-pg-input' type='text' size='2' maxlength='7' value='0' role='textbox'/>", "<span id='sp_1_" + $.jgrid.jqID(pgid) + "'></span>") + "</td>"; }
  if (ts.p.pgbuttons === true) {
    var po = ["first" + tp, "prev" + tp, "next" + tp, "last" + tp]; if (dir === "rtl") { po.reverse(); }
    pgl += "<td id='" + po[0] + "' class='ui-pg-button ui-corner-all' " + (ts.p.pgfirst ? "title='" + ts.p.pgfirst + "'" : "") + "><span class='ui-icon ui-icon-seek-first'></span></td>";
    pgl += "<td id='" + po[1] + "' class='ui-pg-button ui-corner-all' " + (ts.p.pgprev ? "title='" + ts.p.pgprev + "'" : "") + "><span class='ui-icon ui-icon-seek-prev'></span></td>";
    pgl += pginp !== "" ? sep + pginp + sep : "";
    pgl += "<td id='" + po[2] + "' class='ui-pg-button ui-corner-all' " + (ts.p.pgnext ? "title='" + ts.p.pgnext + "'" : "") + "><span class='ui-icon ui-icon-seek-next'></span></td>";
    pgl += "<td id='" + po[3] + "' class='ui-pg-button ui-corner-all' " + (ts.p.pglast ? "title='" + ts.p.pglast + "'" : "") + "><span class='ui-icon ui-icon-seek-end'></span></td>";
  } else if (pginp !== "") { pgl += pginp; }
  if (dir === "ltr") { pgl += str; }
  pgl += "</tr></tbody></table>";
  if (ts.p.viewrecords === true) { $("td#" + pgid + "_" + ts.p.recordpos, "#" + pgcnt).append("<div dir='" + dir + "' style='text-align:" + ts.p.recordpos + "' class='ui-paging-info'></div>"); }
  $("td#" + pgid + "_" + ts.p.pagerpos, "#" + pgcnt).append(pgl);
  tdw = $(".ui-jqgrid").css("font-size") || "11px";
  $(document.body).append("<div id='testpg' class='ui-jqgrid ui-widget ui-widget-content' style='font-size:" + tdw + ";visibility:hidden;' ></div>");
  twd = $(pgl).clone().appendTo("#testpg").width();
  $("#testpg").remove();
  if (twd > 0) {
    if (pginp !== "") { twd += 50; }
    $("td#" + pgid + "_" + ts.p.pagerpos, "#" + pgcnt).width(twd);
  }
  ts.p._nvtd = [];
  ts.p._nvtd[0] = twd ? Math.floor((ts.p.width - twd) / 2) : Math.floor(ts.p.width / 3);
  ts.p._nvtd[1] = 0;
  pgl = null;
  // SelectBox(노출갯수) 변경 시 발생하는 이벤트
  $('.ui-pg-selbox', "#" + pgcnt).bind('change', function () {
    if (!clearVals('records')) { return false; }
    ts.p.page = Math.round(ts.p.rowNum * (ts.p.page - 1) / this.value - 0.5) + 1;
    ts.p.rowNum = this.value;
    if (ts.p.pager) { $('.ui-pg-selbox', ts.p.pager).val(this.value); }
    if (ts.p.toppager) { $('.ui-pg-selbox', ts.p.toppager).val(this.value); }
    populate();
    return false;
  });
  if (ts.p.pgbuttons === true) {
    // Pager 버튼에 마우스 오버 했을 때 발생하는 이벤트
    $(".ui-pg-button", "#" + pgcnt).hover(function () {
      if ($(this).hasClass('ui-state-disabled')) {
        this.style.cursor = 'default';
      } else {
        $(this).addClass('ui-state-hover');
        this.style.cursor = 'pointer';
      }
    }, function () {
      if (!$(this).hasClass('ui-state-disabled')) {
        $(this).removeClass('ui-state-hover');
        this.style.cursor = "default";
      }
    });
    // 처음, 이전, 다음, 마지막 버튼 클릭 시 발생하는 이벤트
    $("#first" + $.jgrid.jqID(tp) + ", #prev" + $.jgrid.jqID(tp) + ", #next" + $.jgrid.jqID(tp) + ", #last" + $.jgrid.jqID(tp)).click(function () {
      if ($(this).hasClass("ui-state-disabled")) {
        return false;
      }
      var cp = intNum(ts.p.page, 1),
        last = intNum(ts.p.lastpage, 1), selclick = false,
        fp = true, pp = true, np = true, lp = true;
      if (last === 0 || last === 1) { fp = false; pp = false; np = false; lp = false; }
      else if (last > 1 && cp >= 1) {
        if (cp === 1) { fp = false; pp = false; }
        else if (cp === last) { np = false; lp = false; }
      } else if (last > 1 && cp === 0) { np = false; lp = false; cp = last - 1; }
      if (!clearVals(this.id)) { return false; }
      if (this.id === 'first' + tp && fp) { ts.p.page = 1; selclick = true; }
      if (this.id === 'prev' + tp && pp) { ts.p.page = (cp - 1); selclick = true; }
      if (this.id === 'next' + tp && np) { ts.p.page = (cp + 1); selclick = true; }
      if (this.id === 'last' + tp && lp) { ts.p.page = last; selclick = true; }
      if (selclick) {
        populate();
      }
      return false;
    });
  }
  if (ts.p.pginput === true) {
    // input 요소에서 엔터 클릭 시 이벤트 발생
    $('input.ui-pg-input', "#" + pgcnt).keypress(function (e) {
      var key = e.charCode || e.keyCode || 0;
      if (key === 13) {
        if (!clearVals('user')) { return false; }
        $(this).val(intNum($(this).val(), 1));
        ts.p.page = ($(this).val() > 0) ? $(this).val() : ts.p.page;
        populate();
        return false;
      }
      return this;
    });
  }
}



위와 같이 모든 Pager 액션에 대한 이벤트는 결과적으로 onPaging 이벤트 함수를 호출하게되며, onPaging 에서 문자열 'stop' 반환 시 paging 로직은 중단된다.

setPager는 jqGrid가 초기화 될 때 한번만 수행되는 로직이다보니 개발자가 직접 호출할일이 없다.

2019년 6월 10일 월요일

[jqGrid] reloadGrid 메소드 설명







환경: jqGrid v4.7.1 이하 (무료버전)



개별적으로 사용도 가능하며, jqGrid 내부에서 빈번하게 사용이 이루어지는 reloadGrid 메소드에 대해서 알아본다.
1004lucifer
https://github.com/tonytomov/jqGrid/blob/v4.7.1/js/jquery.jqGrid.js#L2821


}).bind('reloadGrid', function (e, opts) {
  if (ts.p.treeGrid === true) { ts.p.datatype = ts.p.treedatatype; }

  // opts.current 값이 true인경우 selectionPreserver 함수를 호출한다.
  // selectionPreserver 함수는 multiselect 모드인경우 자동으로 호출된다.
  // http://www.trirand.com/blog/?page_id=393/help/true-scrolling-not-working-and-need-to-keep-selected#p18068
  if (opts && opts.current) {
    ts.grid.selectionPreserver(ts);
  }
  // datatype:'local' 이면 모든 row를 선택해제한다.
  if (ts.p.datatype === "local") { $(ts).jqGrid("resetSelection");

  // 데이터가 있다면 normalizeData, refreshIndex 함수를 호출한다.
  // normalizeData - 데이터정상화
  // refreshIndex - row의 index(기본값:id) 초기화
  if (ts.p.data.length) { normalizeData(); refreshIndex(); } }
  // 데이터가 없고 treeGrid가 아니라면 데이터를 초기화 한다.
  else if (!ts.p.treeGrid) {
    ts.p.selrow = null;
    if (ts.p.multiselect) { ts.p.selarrrow = []; setHeadCheckBox(false); }
    ts.p.savedRow = [];
  }

  // 동적 스크롤 그리드 사용 시 데이터(row) 초기화
  if (ts.p.scroll) { emptyRows.call(ts, true, false); }

  // opts.page 값이 있는경우
  // [주의사항]
  // page의 값을 0으로 설정할 수 없으며, reloadGrid 메소드 호출전에
  // setGridParam을 이용해 page 값을 0으로 설정하더라도 이부분에서 page가 1로 변경된다.
  if (opts && opts.page) {
    var page = opts.page;
    if (page > ts.p.lastpage) { page = ts.p.lastpage; }
    if (page < 1) { page = 1; }
    ts.p.page = page;
    if (ts.grid.prevRowHeight) {
      ts.grid.bDiv.scrollTop = (page - 1) * ts.grid.prevRowHeight * ts.p.rowNum;
    } else {
      ts.grid.bDiv.scrollTop = 0;
    }
  }

  // 그리드의 데이터를 셋팅한다.
  // (서버에 요청을 하거나 로컬인경우 해당 페이지의 데이터가 보여지도록 셋팅)
  if (ts.grid.prevRowHeight && ts.p.scroll) {
    delete ts.p.lastpage;
    ts.grid.populateVisible();
  } else {
    ts.grid.populate();
  }

  // inlineNav(네비게이터에서 addRow, editRow, saveRow, restoreRow 버튼)
  // 사용 시 해당 버튼들을 보여준다.
  if (ts.p._inlinenav === true) { $(ts).jqGrid('showAddEditButtons'); }
  return false;
})



공식문서에는 trigger("reloadGrid") 방법으로 사용하라고 기술되어있지만 다음과 같이 사용이 가능하다.

trigger("reloadGrid", { current:true, page:3 })

페이지를 업데이트 할 수 있으나 0값으로는 업데이트 할 수 없다.
전체적인 소스를 보니 jqGrid 에서는 page, lastpage 의 최소값의 기준을 1로 설정되어있다.




2019년 6월 9일 일요일

[jqGrid] updatepager 메소드 설명







환경: jqGrid v4.7.1 이하 (무료버전)



개별적으로 사용도 가능하며, jqGrid 내부에서 빈번하게 사용이 이루어지는 updatepager 메소드에 대해서 알아본다.
1004lucifer
https://github.com/tonytomov/jqGrid/blob/v4.7.1/js/jquery.jqGrid.js#L1903


updatepager = function (rn, dnd) {
  var cp, last, base, from, to, tot, fmt, pgboxes = "", sppg,
    tspg = ts.p.pager ? "_" + $.jgrid.jqID(ts.p.pager.substr(1)) : "",
    tspg_t = ts.p.toppager ? "_" + ts.p.toppager.substr(1) : "";
  base = parseInt(ts.p.page, 10) - 1;
  if (base < 0) { base = 0; }
  base = base * parseInt(ts.p.rowNum, 10);
  to = base + ts.p.reccount;

  // 동적 그리드 스크롤 사용시 로직
  if (ts.p.scroll) {
    var rows = $("tbody:first > tr:gt(0)", ts.grid.bDiv);
    base = to - rows.length;
    ts.p.reccount = rows.length;
    var rh = rows.outerHeight() || ts.grid.prevRowHeight;
    if (rh) {
      var top = base * rh;
      var height = parseInt(ts.p.records, 10) * rh;
      $(">div:first", ts.grid.bDiv).css({ height: height }).children("div:first").css({ height: top, display: top ? "" : "none" });
      if (ts.grid.bDiv.scrollTop == 0 && ts.p.page > 1) {
        ts.grid.bDiv.scrollTop = ts.p.rowNum * (ts.p.page - 1) * rh;
      }
    }
    ts.grid.bDiv.scrollLeft = ts.grid.hDiv.scrollLeft;
  }

  // Pager가 있는경우
  pgboxes = ts.p.pager || "";
  pgboxes += ts.p.toppager ? (pgboxes ? "," + ts.p.toppager : ts.p.toppager) : "";
  if (pgboxes) {
    fmt = $.jgrid.formatter.integer || {};
    cp = intNum(ts.p.page);
    last = intNum(ts.p.lastpage);
    $(".selbox", pgboxes)[this.p.useProp ? 'prop' : 'attr']("disabled", false);

    // 페이지 input 항목이 있는경우 해당 값 업데이트
    if (ts.p.pginput === true) {
      $('.ui-pg-input', pgboxes).val(ts.p.page);
      sppg = ts.p.toppager ? '#sp_1' + tspg + ",#sp_1" + tspg_t : '#sp_1' + tspg;
      $(sppg).html($.fmatter ? $.fmatter.util.NumberFormat(ts.p.lastpage, fmt) : ts.p.lastpage);

    }

    // (시작/끝 레코드번호) viewrecords 항목이 있는경우 해당 값 업데이트
    if (ts.p.viewrecords) {
      if (ts.p.reccount === 0) {
        $(".ui-paging-info", pgboxes).html(ts.p.emptyrecords);
      } else {
        from = base + 1;
        tot = ts.p.records;
        if ($.fmatter) {
          from = $.fmatter.util.NumberFormat(from, fmt);
          to = $.fmatter.util.NumberFormat(to, fmt);
          tot = $.fmatter.util.NumberFormat(tot, fmt);
        }
        $(".ui-paging-info", pgboxes).html($.jgrid.format(ts.p.recordtext, from, to, tot));
      }
    }

    // Pager 버튼 (기본값:true)사용하면 모습 업데이트
    if (ts.p.pgbuttons === true) {
      if (cp <= 0) { cp = last = 0; }
      if (cp === 1 || cp === 0) {
        $("#first" + tspg + ", #prev" + tspg).addClass('ui-state-disabled').removeClass('ui-state-hover');
        if (ts.p.toppager) { $("#first_t" + tspg_t + ", #prev_t" + tspg_t).addClass('ui-state-disabled').removeClass('ui-state-hover'); }
      } else {
        $("#first" + tspg + ", #prev" + tspg).removeClass('ui-state-disabled');
        if (ts.p.toppager) { $("#first_t" + tspg_t + ", #prev_t" + tspg_t).removeClass('ui-state-disabled'); }
      }
      if (cp === last || cp === 0) {
        $("#next" + tspg + ", #last" + tspg).addClass('ui-state-disabled').removeClass('ui-state-hover');
        if (ts.p.toppager) { $("#next_t" + tspg_t + ", #last_t" + tspg_t).addClass('ui-state-disabled').removeClass('ui-state-hover'); }
      } else {
        $("#next" + tspg + ", #last" + tspg).removeClass('ui-state-disabled');
        if (ts.p.toppager) { $("#next_t" + tspg_t + ", #last_t" + tspg_t).removeClass('ui-state-disabled'); }
      }
    }
  }

  // rownumbers 값 업데이트
  if (rn === true && ts.p.rownumbers === true) {
    $(">td.jqgrid-rownum", ts.rows).each(function (i) {
      $(this).html(base + 1 + i);
    });
  }

  // (그리드간 row 드래그&드롭) gridDnD 업데이트
  if (dnd && ts.p.jqgdnd) { $(ts).jqGrid('gridDnD', 'updateDnD'); }

  // jqGridGridComplete 이벤트함수 호출
  $(ts).triggerHandler("jqGridGridComplete");

  // gridComplete 이벤트 함수가 구현된경우 해당 함수를 호출 - 그리드 body(Table)을 this로 넘겨줌
  if ($.isFunction(ts.p.gridComplete)) { ts.p.gridComplete.call(ts); }

  // jqGridAfterGridComplete 이벤트함수 호출
  $(ts).triggerHandler("jqGridAfterGridComplete");
}



소스를 보니 왜 공식 문서에서 updatepager 를 포함시키지 않았는지 이해가 갔다.


jqGridGridComplete / gridComplete / jqGridAfterGridComplete

위 이벤트함수가 호출되기 때문에 무분별하게 사용하면 위 함수의 사용목적에 맞지 않는 상황에서 호출이 될 수 있기 때문에 사용하지 말라고 공식문서에 포함 시키지 않은것으로 생각이 된다.



2019년 6월 8일 토요일

[jqGrid] clearGridData 메소드 설명







환경: jqGrid v4.7.1 이하 (무료버전)



datatype: 'local' 인 경우에 특히 많이 사용이 되는 clearGridData 메소드에 대해서 알아보겠다.
1004lucifer
https://github.com/tonytomov/jqGrid/blob/v4.7.1/js/jquery.jqGrid.js#L3855


clearGridData: function(clearfooter) {
  return this.each(function () {
    var $t = this;
    if (!$t.grid) { return; }
    if (typeof clearfooter !== 'boolean') { clearfooter = false; }
    if ($t.p.deepempty) { $("#" + $.jgrid.jqID($t.p.id) + " tbody:first tr:gt(0)").remove(); }
    else {
      // emtry tr을 가져옴 (첫번째 tr은 항상 비어있는 row)
      var trf = $("#" + $.jgrid.jqID($t.p.id) + " tbody:first tr:first")[0];
      // 그리드의 tbody에 데이터를 모두 버리고 empty tr 만 넣는다.
      $("#" + $.jgrid.jqID($t.p.id) + " tbody:first").empty().append(trf);
    }
    // footerrow가 사용중이며 clearfooter 옵션이 true인경우 footerrow 영역의 데이터를 비운다.
    if ($t.p.footerrow && clearfooter) { $(".ui-jqgrid-ftable td", $t.grid.sDiv).html("&#160;"); }
    $t.p.selrow = null; $t.p.selarrrow = []; $t.p.savedRow = [];
    $t.p.records = 0; $t.p.page = 1; $t.p.lastpage = 0; $t.p.reccount = 0;
    $t.p.data = []; $t.p._index = {};
    // Pager 번호를 업데이트 한다.
    $t.updatepager(true, false);
  });
}


아래 개발자도구에서 본 모습과 같이 그리드의 첫번째 tr 은 jqGrid 에서 사용하는 empty tr 이 있는것을 확인할 수 있었다.




위의 로직처럼 clearGridData 메소드가 하는 역할은 두가지가 있다.

1. 그리드의 데이터(tr) 삭제
2. Pager(페이지번호) 업데이트







[jqGrid] onPaging 이벤트 설명







환경: jqGrid v4.7.1 이하 (무료버전)



아래 모습에서 페이지를 이동 및 페이지 갯수 설정을 위한 액션을 취할 때 발생하는 이벤트로 Pager 에서 발생하는 Event 에 대해서 정리를 하려 한다.





jqGrid 가 초기화 될 때 setPager 로직이 수행되며 onPaging 이벤트를 등록한다.
(링크 - setPager 설명)
1004lucifer


PropertyTypeDescription
onPagingpgButton이 이벤트는 '페이지버튼'을 클릭하고 데이터를 채우기전에 실행된다. 또한 사용자가 페이지 input box에 입력 후 'Enter'를 누르면 발생하고 select box를 변경했을 때에도 작동한다.
first, last, prev, next 중의 버튼을 클릭하는 경우 pgButton(string)으로 한개의 파라메터를 전달한다.
요청된 행의 수가 변경되거나 사용자가 페이지의 번호를 변경했을때를 기록한다.
'stop'을 리턴하는경우 해당 페이징 함수는 멈춘다.



아래 데모에서 보듯이 pgButton 문자열이 넘어오는 것을 볼 수 있으며, first, last, prev, next 같은 경우에는 해당 문자열 뒤에 '_{PAGER_ID}' 문자열이 추가로 붙어서 넘어온다.










 주의할 점은 onPaging 함수 내에서

 사용자가 InputBox 에 값을 입력 후 엔터를 입력 시 (현재페이지)
 $('#grid_id').getGridParam('page') 를 이용시 변경전 값을 가져오며
 $('.ui-pg-input').val() 를 이용시 새로 입력한 값을 가져올 수 있다.

 사용자가 SelectBox 의 값을 변경 시 (노출 갯수)
 $('.ui-pg-selbox option:selected').val() 를 이용하여 새로 변경한 값을 가져올 수 있다.


아래 데모는 inputbox에 입력된 값이 올바르지 않은 값인경우 입력한 값을 원래대로 돌리고 paging 기능을 중단한다.





2019년 6월 6일 목요일

[Hyper-V] RemoteFX 적용여부 확인방법 및 그래픽 성능테스트







실습환경: Windows 10 pro (빌드버전 1903)


Hyper-V 의 (게스트)가상PC 에서 RemoteFX 가 제대로 설정되었는지 아래와 같이 확인이 가능하다.

컴퓨터관리 => 시스템도구 => 장치관리자


RemoteFX 미적용

1004lucifer

RemoteFX 적용



그리고 실제 RemoteFX 적용여부에 대해서 간단한 그래픽카드 성능 테스트는 아래 사이트에서 확인이 가능하다.
 - https://testdrive-archive.azurewebsites.net/performance/fishbowl/

RemoteFX 적용 전에는 물고기 1~5마리정도 되었는데..
RemoteFX 적용 후 30~50마리 정도로 늘어나고
호스트PC의 작업관리자의 GPU사용량이 기존 15% => 50% 정도 늘어났다.




PS.
하드웨어 가속을 이용하여 월등한 성능향상을 기대했지만 RDP(Remote Desktop Protocol) 로 접속하여 이정도 퍼포먼스면 그냥 봐줄만 한것 같기도 하다.
예전에 Windows 2000 시절의 RDP에 비하면 많은 좋아지긴 했다.ㅎ

그래픽카드가 노트북 내장형이라 퍼포먼스가 많이 안나온것으로 보여지며,
게임용 PC의 좋은 그래픽카드의 경우에는 퍼포먼스가 월등히 올라갈 것으로 보여진다.



[Hyper-V] RD(Remote Desktop) 접속 시 RemoteFX 사용하도록 설정







실습환경: Windows 10 pro


RD(Remote Desktop) 접속 시 하드웨어 GPU를 이용하여(RemoteFX) 접속하기위해 다음과 같이 설정을 해준다.

실행 -> gpedit.msc -> 로컬 그룹 정책 편집기 -> 컴퓨터 구성 -> 관리 템플릿 -> Windows 구성 요소 -> 터미널 서비스 -> 원격 데스크톱 세션 호스트 -> 원격 세션 환경 -> RemoteFX 폴더 안의 내용 활성화
1004lucifer



PS.
Configure RemoteFX 항목의 설명을 읽어보면
구성되지 않은경우 기본적으로 RD(Remote Desktop) 가상화 호스트용 RemoteFX는 사용되도록 설정되며, RD세션 호스트용 RemoteFX는 사용되지 않도록 설정된다고 한다.



설명대로라면 아래 모습과 같이 Hyper-V 게스트PC 접속시 위의 설정을 하지 않아도 RemoteFX로 접속이 되어야 하지만 만일 RemoteFX를 사용하지 않고 접속하는 것같이 찝찝한 경우엔 위의 설정을 사용으로 변경하고 접속하면 된다.
1004lucifer


PS.
처음에 RemoteFX로 접속이 안되는것 같아서 위의 설정을 '사용'으로 변경 후 잘 되길래 이거였구나 싶은데 설명을 읽어보고 다시 '구성되지 않음' 으로 돌렸는데도 이제는 RemoteFX로 접속이 잘 된다. (그래픽 퍼포먼스로 확인)
기분탓인지.. 한번 사용으로 바꾸고나서 제대로 활성화가 된건지 모르겠다..;;



참고
 - https://ggojang.com/96
 - https://social.technet.microsoft.com/Forums/en-US/e139ec92-90d0-4639-a3b5-5a5a4a5b6a79/hardware-support-gpu-on-rdp-connection-to-windows-10-professional?forum=winRDc

[Hyper-V] 컴퓨터 Hyper-V 설정에서 '실제 GPU' 항목이 보여지지 않는경우







실습환경: Windows 10 pro (빌드버전 1903)


증상

RemoteFX를 사용하기 위해 컴퓨터의 Hyper-V 설정 확인 시 다음과 같이 '실제 GPU'(Physical GPUs) 항목이 보여지지 않는다.
1004lucifer





원인

 - Windows 10 버전1809 이상부터는 해당 항목이 보여지지 않게 변경되었다.







해결방법

 - 결론: 딱히 문제가 되는게 없어서 해야하는 작업은 없다.





원인을 찾아보기 위해 다음과 같이 작업을 했다.

1. 최신버전 그래픽 드라이버설치
2. 윈도우 빌드버전 업데이트 (1809 => 1903)
(위의 2가지 방법과 이것저것 설정을 만져봤지만 별다른 소득은 없었다.)
1004lucifer
3. 포멧 후 빌드버전 1803 으로 Windows10 재설치




윈도우 새로 설치 후 Hyper-V를 바로 추가하고 설정을 확인하니 아래와 같이 '실제 GPU' 항목이 있었다.
(하지만 GPU 항목이 비활성화 되어있었다.)



이것저것 만지면서 다시한번 확인해보니 갑자기 활성화가 되었다.
(그래픽카드 드라이버가 (자동으로)설치되니 바로 실제 사용하는 GPU가 잡히게 되었다.)
1004lucifer



이 상태에서 윈도우 빌드버전 업데이트(링크) 후(1803 => 1903)
컴퓨터의 Hyper-V 설정을 열어보니 '실제 GPU' 항목이 없어졌다.

1004lucifer

'실제 GPU' 항목이 보이지 않는것에 대해서..
단순히 보이지 않게 변경된건지, 사용을 하지 앟는 것인지 확인을 해보니
단순히 보이지 않게만 변경이 된것이며, 내부적으로는 실제 GPU 항목을 기본적으로 사용을 하고 있었다.
(고사양 일부 노트북처럼 서로 다른 그래픽카드 2개이상 PC에 꼽혀있는 경우에는 어떻게 나올지는 모르겠다.)



PS.
'실제 GPU' 항목은 보이지만 비활성화 된경우에는 그래픽 드라이버가 설치되지 않은경우이며, 대부분 윈도우 설치 시 자동으로 그래픽 드라이버가 설치되겠지만 일부 고사양 그래픽 드라이버의 경우 자동으로 설치되지 않아서 아래와 같은 상황이 발생할 수 있다
 - 해당 항목이 비활성화로 보여짐
 - 위와같이 보여지지않으며 내부적으로 사용하지 못함.

자신에게 맞는 그래픽카드를 설치하면 보이지 않더라도 내부적으로 사용하니 안심하고 있어도 된다.


2019년 6월 2일 일요일

[jqGrid][Error] Syntax error, unrecognized expression: # tbody:first







실습 jqGrid 버전: CDN 제공하는 v4.6 버전으로 테스트



증상

 - Vue 에서 제공하는 ref 기능을 이용해서 Table Element 에 id를 넣지 않고 작업을 하려 했는데 다음과 같이 에러가 발생하며 jqGrid 가 생성되지 않았다.

1004lucifer

Error: Syntax error, unrecognized expression: # tbody:first
    at Function.Sizzle.error (jquery-1.9.1.js:4421)
    at tokenize (jquery-1.9.1.js:5076)
    at select (jquery-1.9.1.js:5460)
    at Function.Sizzle [as find] (jquery-1.9.1.js:3998)
    at init.find (jquery-1.9.1.js:5576)
    at new init (jquery-1.9.1.js:196)
    at jQuery (jquery-1.9.1.js:62)
    at Y (jquery.jqGrid.min.js:66)
    at Q (jquery.jqGrid.min.js:83)
    at HTMLTableElement.<anonymous> (jquery.jqGrid.min.js:136)






원인 및 해결방법

 - Table 태그에 무조건 id 값을 넣어준다.
 - Vue의 ref 기능을 이용해 $(this.$refs.list) 이렇게 table 태그를 가져와도 별 상관 없겠지 생각했는데 table 태그에 id 값이 무조건 필요했었다.
  (jqGrid 자체에서 그리드 생성을 할때 내부적으로 selector로 id를 사용하는게 아닐까 싶다.)






[jqGrid] jQuery UI 통합 설명







실습 jqGrid 버전: CDN 제공하는 v4.6 버전으로 테스트


정렬 가능한 컬럼(Columns)

 - 이 메소드는 jqGrid에 통합되어있어 별다른 작업을 할 필요가 없다. 메소드를 사용하면 마우스를 사용하여 그리드 컬럼을 재정렬 할 수 있다.
이 경우에 필요한 설정은 jqGrid의 sortable 옵션을 true로 해주면 된다.

<script type="text/javascript">
jQuery(document).ready(function(){ 
  jQuery("#list").jqGrid({
    url:'example.php',
    datatype: 'xml',
    mtype: 'GET',
    colNames:['Inv No','Date', 'Amount','Tax','Total','Notes'],
    colModel :[ 
      {name:'invid', index:'invid', width:55}, 
      {name:'invdate', index:'invdate', width:90}, 
      {name:'amount', index:'amount', width:80, align:'right'}, 
      {name:'tax', index:'tax', width:80, align:'right'}, 
      {name:'total', index:'total', width:80, align:'right'}, 
      {name:'note', index:'note', width:150, sortable:false} 
    ],
    pager: '#pager',
    rowNum:10,
    rowList:[10,20,30],
    sortname: 'invid',
    sortorder: 'desc',
    viewrecords: true,
    caption: 'My first grid',
    sortable: true
  }); 
}); 
</script>

사용된 jQuery UI 위젯 및 기타 플러그인
 - jQuery UI core
 - jQuery UI sortable widget





컬럼 선택기 (Column Chooser)

 - 이 메소드를 사용하여 그리드에서 컬럼의 순서를 바꾸거나 보이기/감추기 설정을 할수 있다.

old_API
jQuery("#list").columnChooser(options);
new_API
jQuery("#list").jqGrid('columnChooser', options);
options - 아래 나열된 속성을 가진 객체 (하단 옵션 설명 참고)
1004lucifer
* 이 메소드에서 정렬 기능을 사용하려면 jqGrid 보다 jQuery multiselect 플러그인이 우선적으로 로드되어야 한다.


이 함수를 호출한 후 모달 대화상자가 열리고 사용자는 컬럼을 보이거나 숨길수 있고 순서도 바꿀 수 있다.


옵션
1004lucifer
Name Type Description Default
title string 모달 대화상자의 타이틀 See $.jgrid.col.title in language file
width number 다이얼로그의 가로길이 (픽셀) 420
height number 다이얼로그의 세로길이 (픽셀) 240
classname string 그리드 생성되는 선택자(selector)에 추가될 class null
done function 사용자가 Ok 버튼을 누를 때 호출될 함수.
현재는 컬럼을 재정렬 하기위해 remapColumns 메소드를 호출하도록 구현되어있다.
msel mixed msel 은 아래의 값을 설정할 수 있다.
- multiselect 를 확장한 ui 위젯 class의 이름
- multiselect 객체 생성 함수 (매개변수가 없거나 object를 전달)
- 소멸 ('destroy' 문자열 설정 시)
multiselect
dlog mixed dlog 는 아래의 값을 설정할 수 있다.
- 대화상자와 같은 방식으로 작동하는 ui 위젯 class의 이름
- 대화상자 생성 함수 (dlog_opts 설정 시)
- 소멸 ('destroy' 문자열 설정 시)
dialog
dlog_opts mixed dlog_opts 는 dlog에 전달할 옵션객체 이거나 옵션객체를 생성하는 함수일 수 있다.
기본값은 ui.dialog 객체에 적당한 옵션을 생성한다.
cleanup function 대화상자를 정리하고 선택하는 함수.
또한 다른 변경없이 종료함수(done function)를 호출한다. (columnChooser 가 중단되었음을 나타낸다)

위에서 정의된 종료함수(done function)는 jqGrid코드에서 다음과 같이 정의되어있다.
opts = $.extend({
...
"done" : function(perm) { if (perm) self.jqGrid("remapColumns", perm, true) },
...
});
재정렬 후 다른 작업을 수행하려면 이 옵션을 재정의 할 수 있다.
예를들어 사용자가 일부 컬럼을 보여주거나 숨긴 후 페이지의 일부 요소 너비를 다시 계산한다고 가정하면 다음과 같이 할 수 있다.
jQuery("#list").jqGrid('columnChooser', {
   done : function (perm) {
      if (perm) {
          // "OK" button are clicked
          this.jqGrid("remapColumns", perm, true);
          // the grid width is probably changed co we can get new width
          // and adjust the width of other elements on the page
          //var gwdth = this.jqGrid("getGridParam","width");
          //this.jqGrid("setGridWidth",gwdth);
      } else {
          // we can do some action in case of "Cancel" button clicked
      }
   }
});
1004lucifer
사용된 jQuery UI 위젯 및 기타 플러그인
 - jQuery UI core
 - jQuery UI sortable widget
 - jQuery UI dialog
 - jQuery multiselect plugin

jQuery multiselect 플러그인은 링크 에서 볼 수 있으며, 또한 이 플러그인은 jqGrid 빌드의 plugins 디렉토리에 포함되어 있다.







정렬 가능한 행(Rows)

 - 이 메소드를 사용하면 마우스를 사용하여 시각적으로 그리드를 재정렬(정렬) 할 수 있다.

old_API
jQuery("#list").sortableRows(options);
new_API
jQuery("#list").jqGrid('sortableRows', options);
options - sortable 위젯의 옵션 (http://jqueryui.com/sortable/)

이 메소드는 jQuery UI sortable 위젯과 완벽하게 호환된다.
즉, 이 위젯에서 사용할 수 있는 옵션 및 이벤트를 설정할 수 있다.


사용된 jQuery UI 위젯 및 기타 플러그인
 - jQuery UI core
 - jQuery UI sortable widget

알려진 문제
 - 현재 이 방법은 sortable 위젯의 문제로 인해 FireFox v3.0.x, Chrome, Safari 브라우저에서 작동하지 않으며 추후 jQuery UI의 다음 버전에서 수정되길 바란다.
 - treeGrid가 활성화된 경우 메소드가 작동하지 않음 - (트리의 row를 재정렬 할 수 없음)









그리드 리사이즈

 - 이 메소드를 사용하면 마우스를 사용하여 그리드 너비/높이를 조정할 수 있다.

old_API
jQuery("#list").gridResize(options);
new_API
jQuery("#list").jqGrid('gridResize', options);
options - resizable 위젯의 옵션 (http://jqueryui.com/resizable/)

이 메소드는 jQuery UI resizable 위젯과 완벽하게 호환된다.
즉, 이 위젯에서 사용할 수 있는 옵션 및 이벤트를 설정할 수 있다.

사용된 jQuery UI 위젯 및 기타 플러그인
 - jQuery UI core
 - jQuery UI resizable widget

알려진 문제
 - 그리드 숨기기 버튼(캡션의 버튼)을 사용시 resizable 위젯에서 생성한 컨텐츠가 숨겨지지 않는다. 이로인해 숨겨진 이후에 그리드의 테두리(border)가 나타난다.








그리드 사이의 행(rows) 드래그앤드롭

 - 이 방법을 사용하면 마우스를 사용하여 두개 이상의 그리드 간에 행을 끌어다 놓을 수 있다.

old_API
jQuery("#list").gridDnD(options);
new_API
jQuery("#list").jqGrid('gridDnD', options);
options - 아래 나열된 속성을 가진 객체 (하단 옵션 설명 참고)


옵션
1004lucifer
Name Type Description Default
connectWith string row를 드래그 할 그리드를 지정한다.
두개 이상의 그리드가 있는경우 콤마로 구분한다.
ex) '#grid1,#grid2'
empty string
onstart function 이 이벤트는 원본 그리드에서 row를 끌기 시작할 때 발생한다.
이 이벤트에 이벤트핸들러, (준비된)ui object 가 매개변수로 전달된다.
자세한 내용은 jQuery UI draggable events
null
onstop function 이 이벤트는 드래그가 중지되면 호출된다.
이 이벤트에 이벤트핸들러, (준비된)ui object 가 매개변수로 전달된다.
자세한 내용은 jQuery UI draggable events
null
beforedrop function 이 이벤트는 connectWith 에 기술한 그리드에 row drop 전에 발생한다.
이 이벤트에 다음과 같은 파라메터가 전달된다.
1. event handler
2. (준비된)ui object,
3. data (그리드에 삽입된 name:value)
4. source grid object
5. target grid object

name:value 쌍의 객체를 반환 시 타겟 그리드에 추가된다.
null
ondrop function 이 이벤트는 connectWith 에 기술한 그리드에 row drop 후에 발생한다.
이 이벤트에 다음과 같은 파라메터가 전달된다.
1. event handler
2. (준비된)ui object,
3. data (그리드에 삽입된 name:value)
자세한 내용은 jQuery UI droppable events
null
drop_opts object drop 가능한 그리드에 적용 가능한 미리정의된 옵션 (위의 connectWith 옵션으로 지정됨)
또한 임의의 옵션 및 이벤트를 설정할 수 있다. (ondrop 이벤트로 대체되는 drop 이벤트 제외)
자세한 내용은 jQuery UI droppable
{ activeClass : “ui-state-active”,
hoverClass : “ui-state-hover”
}
drag_opts object drag 가능한 그리드에 적용 가능한 미리 정의된 옵션 (이 방법이 적용되는 옵션)
또한 임의의 옵션 및 이벤트를 설정할 수 있다.
(onstart, onstop 이벤트로 대체되는 start, stop 이벤트 제외)
자세한 내용은 jQuery UI draggable
{ revert: “invalid”,
helper: “clone”,
cursor: “move”,
appendTo : “#jqgrid_dnd”,
zIndex: 5000
}
dropbyname boolean true 설정 시 name이 같은 필드만 타겟 그리드에 추가할 수 있다.
addRowData를 사용하여 새로운 row를 삽입하며, name이 a인 필드가 소스그리드에 숨겨져있으면 다생 그리드에 표시될 수 있다.
기본값인 false는 소스의 첫번째 컬럼의 계산된 대상에 추가됨을 의미한다.
false
droppos string 새로운 row를 추가할 위치를 결정한다.
first - 그리드의 첫번째 row
last - 그리드의 마지막 row
first
autoid boolean 이 옵션은 새로운 row의 id를 생성하는 방법을 정의한다.
true 설정 시 autoidprefix 옵션으로 설정된 문자열로 시작하는 랜덤 숫자값 id 를 생성한다.
false 설정 시 id는 colModel의 key 속성에 의해 결정되는 다음 레코드 수 또는 값이 될 수 있다.
함수로 정의되는 경우 이 함수는 타겟 그리드에 id로 들어갈 값을 반환해야 하며, 이 경우 전달되는 파라메터는 타겟 그리드 row에 삽입될 데이터 배열이다.
true
autoidprefix string autoid 옵션이 true로 설정된 경우에만 id의 접두사로 결정되어 사용된다. dnd_
dragcopy boolean 소스 row를 이동시키지 않고 복사를 한다.
(GridDnD 에만 해당)
false

사용된 jQuery UI 위젯 및 기타 플러그인
 - jQuery UI core
 - jQuery UI draggable widget
 - jQuery UI droppable widget

알려진 문제
 - 이 방법은 Draggable 위젯의 버그로 Safari, Chrome 에서 제대로 작동하지 않는다. 드래그 가능한 row의 앞에 텍스트가 선택되는 효과

예제
 - 아래 예제는 id grid1, grid2, grid3 3개의 그리드를 생성 후 grid1 은 grid2, grid3으로 드래그 할 수 있고, grid2는 grid1 에서만 드래그 할 수 있다.
// Creating grid1
jQuery("#grid1").jqGrid({
    datatype: "local",
    height: 100,
    colNames: ['Id1', 'Name1', 'Values1'],
    colModel: [
       {name: 'id1', index: 'id',width: 100}, 
       {name: 'name1',index: 'name',width: 100},
       {name: 'values1',index: 'values',width: 200}
    ],
    caption: 'Grid 1',
    pager: '#pgrid1'
});
 
//Creating grid2
jQuery("#grid2").jqGrid({
    datatype: "local",
    height: 100,
    colNames: ['Id2', 'Name2', 'Values2'],
    colModel: [
       {name: 'id2',index: 'id',width: 100},
       {name: 'name2',index: 'name',width: 100},
       {name: 'values2',index: 'values',width: 200}
    }],
    caption: 'Grid 2',
    pager: '#pgrid2'
});
// Creating grid3
jQuery("#grid3").jqGrid({
    datatype: "local",
    height: 'auto',
    colNames: ['Id3', 'Name3', 'Values3'],
    colModel: [
       {name: 'id3',index: 'id',width: 100},
       {name: 'name3',index: 'name', width: 100},
       {name: 'values3',index: 'values',width: 200}
    }],
    caption: 'Grid 3',
    pager: '#pgrid3'
});
 
// Data for grid1
var mydata1 = [
    {id1:"1",name1:"test1",values1:'One'},
    {id1:"2",name1:"test2",values1:'Two'},
    {id1:"3",name1:"test3",values1:'Three'}
];
// Data for grid2
var mydata2 = [
    {id2:"11",name2:"test11",values2:'One1'},
    {id2:"21",name2:"test21",values2:'Two1'},
    {id2:"31",name2:"test31",values2:'Three1'}
];
// Data for grid3
var mydata3 = [
    {id3:"12",name3:"test12",values3:'One2'},
    {id3:"22",name3:"test22",values3:'Two2'},
    {id3:"32",name3:"test32",values3:'Three2'}
];
// Adding grid data
for (var i = 0; i <= mydata1.length; i++) {
    jQuery("#grid1").jqGrid('addRowData',i + 1, mydata1[i]);
    jQuery("#grid2").jqGrid('addRowData',i + 1, mydata2[i]);
    jQuery("#grid3").jqGrid('addRowData',i + 1, mydata3[i]);
}
// connect grid1 with grid2 and grid3
jQuery("#grid1").jqGrid('gridDnD',{connectWith:'#grid2,#grid3'});
// connect grid2 with grid1
jQuery("#grid2").jqGrid('gridDnD',{connectWith:'#grid1'});





참고
 -http://www.trirand.com/jqgridwiki/doku.php?id=wiki:jquery_ui_methods