테이블 정렬(table sorting)

자바 스크립트 기본 문법을 이해하기 위한 예제였고

테이블 컬럼 이동 (Drag and Drop Table Columns) 예제를 구현하면서

자바 스크립트의 이벤트(마우스) 처리와 CSS 이용에 대한 방법을 정리한다.

테이블 컬럼 이동은

특정 컬럼(A)을 마우스 버튼을 누른 체 움직여서 (Drag)

다른 컬럼(B)에서 멈출 경우 (Drop)

특정 컬럼(A)을 해당 컬럼(B)의 앞으로 이동시키는 것을 의미한다.

개발 진행 과정은 이해를 쉽게 하기 위해

다음과 같이 단계별로 진행한다.

  1. 마우스 이벤트 기본 구현
  2. CSS 클래스 문제 해결
  3. 컬럼 이동(Drag and Drop) 처리
  4. 컬럼 이동(Drag and Drop) 중에 이동 컬럼 헤드 보기
  5. 컬럼 이동(Drag and Drop) 중에 이동 컬럼 전체 보기
  6. JQuery


예제 코드는 다음 사이트의 코드를

단계별로 풀어서 쉬운 부문만 정리하였으니

실제로 사용할 경우에는

다음 사이트의 코드를 활용하거나

dragtable같은 라이브러리를 사용하는 것이 좋다.

JQuery의 Sortable 예제도(웹페이지 중간의 Sort tables)

좋은 참고가 될 수 있다.


테이블 컬럼 이동 (Drag and Drop)은

4가지 마우스 액션에 의해 이루어 진다.

먼저, 이동하려는 컬럼의 헤드를 마우스로 누르고(Mouse Down)

이동할 위치의 컬럼으로 마우스를 움직여서(Mouse Move)

클릭한 마우스 버튼을 놓으면 (Mouse Up)

이동하려는 컬럼을 이동할 컬럼의 앞으로 옮겨준다.

이때, 이동하려는 컬럼과 이동할 위치의 컬럼을

CSS를 이용하여 구별해주는 것이 좋다.

특히, 이동할 위치의 컬럼으로

마우스가 올라오면(Mouse Over) 배경색을 지정하고,

다른 컬럼으로 이동하면

즉, 마우스가 현재 컬럼에서 떠나면(Mouse Out) 원상태로 되돌린다.

따라서 Mouse Move 이벤트는

Mouse Over와 Out으로 구분하여 처리한다.


이상의 개념으로 직접 구현해 본 후,

다음 코드를 보는 것이

실력 향상에 도움이 되니 시도해 보길 바란다.


위 코드에서 CSS 부분은 [라인 5 ~25]

Table 태그에 대한 스타일과

이동하려는 헤드(TH)의 CSS 클래스(dragging - 라인 16)와

이동한 새로운 위치의 CSS 클래스(hovering - 라인 21)이다.


다음으로 자바 스크립트에서는

문서가 로드 되고(onload) 나면 [라인 28]

모든 헤드(TH)를 찾아서 (getElementsByTagName – 라인 29)

사용할 마우스 이벤트에 이벤트 핸들러들을 지정한다.

여기서 컬럼의 헤드로 TH 태그를 사용했지만

TH 태그를 사용하지 않고 TD 태그를 사용할 경우에는

첫 번째 행(TR)의 TD가 헤드가 된다.


웹페이지에서 마우스를 누르고 움직이면(드래그하면)

기본적으로 해당 범위 안에 있는 내용들이 선택(select)된다.

여기서는 드래그하면 컬럼을 이동시킬 것이기 때문에

내용이 선택되지 못하게(return false)

onselectstart 이벤트에서 처리해 준다 [라인 31].


마지막 부분은 HTML 부분으로

테이블(Table) 태그를 생성하고

3개의 행(TR)과 10개의 컬럼(TH, TD)을 생성하였다 [라인 64~].


마우스 이벤트에 대해서 좀더 상세하게 정리하면

특정 컬럼을 마우스로 누르면(MouseDown – 라인 40),

현재 눌려진 컬럼(this)이 이동할 컬럼이라고 여겨

dragTD 변수에 넣어서 보관한다 [라인 41].

그리고 시각적인 표시를 위해

이 컬럼에 CSS dragging 클래스를 지정한다. [라인 42]

이 컬럼은 다른 컬럼보다 조금 어둡게(background:#eee) 처리한다.


이동할 대상 컬럼의 헤드에 마우스가 올라오면(Over – 라인 45)

Hovering 클래스를 지정해서

다른 색상(background:#ccc)으로 표시한다 [라인 47].


이 컬럼에서 마우스가 떠나면(Out – 라인 - 50)

Hovering 클래스를 지정해서 배경색을 바꾸었던 것을

Hovering 클래스를 지워서 원상태로 돌린다 [라인 52].


옮기려는 컬럼에서

눌렀던 마우스 버튼을 놓으면 (Mouse Up – 라인 55)

현재 컬럼에 지정된 Hovering 클래스를 지우고 [라인 56]

드레그 하던 컬럼(dragTD)에 지정된

dragging 클래스를 지워주고 [라인 57]

드레그 하는 필드를 초기화 한다 [라인 58].

컬럼에 마우스가 오거나(over), 떠나면(out)

CSS 클래스를 이용하여

배경색을 지정하거나 제거하게 되는데,

마우스가 눌러진 상태에서만 작동하고

그냥 마우스가 올라오거나 떠났을 때는 아무 것도 하지 않아야 한다.

즉, 마우스가 눌러진 상태를 확인하기 위해

dragTD 변수의 값이 있는지 확인한다 [라인 46, 51].

dragTD 변수의 값은 마우스 버튼이 눌러졌을 때는

해당 컬럼의 헤드를 가지고 있고 [라인41],

놓았을 때는(Up) null을 가지고 있다 [라인58].


마우스 이벤트와 관련된 상세한 정보는 자료를 찾아보기 바라고

여기에서는 사용하지 않았지만

각 이벤트 핸들러에서 파라미터로 사용된 ev에 대해서 정리한다.

ev 변수는 Event 객체를 나타내는 것으로

현재 발생한 이벤트의 각종 정보를 가지고 있다.

클릭 이벤트의 경우 클릭된 개체를

event.target (ev.target)으로 알 수 있다.

여기서는 this를 사용하였다.

이외에 클릭된 위치 좌표나,

클릭된 마우스 버튼(왼쪽, 오른쪽)등의 정보를 알 수 있다.


마우스 이벤트 외에도 많은 이벤트 들이 있으니

잘 익혀두어야 하고,

이상의 마우스 동작은 마우스를 이용하여 움직이는

대부분의 프로그램에서 사용되는 방식이니

잘 이해해 두는 것이 좋다.

(페이지 마지막에 있는 예제 참조)


정리하면,

마우스 버튼을 누르면(down)

해당 개체 정보를 보관하고 표시를 한다.

마우스 버튼을 놓으면(up)

보관된 정보를 초기화하고

작업 대상으로 표시한 것을 원래대로 되돌린다.

작업 대상이 있을 경우에만,

마우스의 움직임(Move-over, out)에 따라

필요한 행동을 하도록 작성한다.



두 번째 구현으로

테이블 헤드(TH)의 배경색(background:#aaa)을 지정해 본다.

기존 코드의 HTML에서 Style로 지정해도 되지만

다음과 같이 tableHead라는 클래스를 추가해서 테스트 한다.

.tableHead{
    background:#aaa;
    cursor:pointer
}
</style>   
<script>
window.onload = function() {
    var head = document.getElementsByTagName("th");
    for (i=0; i<head.length; i++) {
        head[i].onselectstart = function() { return false }
        head[i].onmousedown = mousedown;
        head[i].onmouseover = mouseover;
        head[i].onmouseout = mouseout;
        head[i].onmouseup   = mouseup;
        head[i].className = "tableHead";
    }

}


이렇게 추가하고

드레그를 해보면 버그가 발생하는 것을 알 수 있다.

tableHead에 의해 배경색이 어두운 색으로 지정되었는데,

드레그를 하고 난 뒤에

배경색들이 모두 사라지는 것을 볼 수 있다.

이 버그를 해결한다.

function mousedown(ev){
    dragTD = this;
    addClass(this, "dragging");
}

function mouseover(ev){
    if (dragTD === null) { return;}
    addClass(this, "hovering");
}

function mouseout(ev){
    if (dragTD === null) { return;}
    removeClass(this, "hovering");
}

function mouseup(ev){
    removeClass(this, "hovering");
    removeClass(dragTD, "dragging");
    dragTD = null;
}

function addClass(src, classname) {
    if (src.className.indexOf(classname) === -1 ) {
        src.className += " " + classname;
    }
}

function removeClass(src, classname) {
    src.className = src.className.replace(" " + classname, "");
}

전체 코드

버그가 발생한 것은

CSS 클래스를 사용할 때

className에 값을 바로 지정했기 때문에 생긴 것이다.

테이블 헤더에 CSS 클래스가 없을 때는

다른 CSS 클래스를 넣고 빼도 (className = "") 문제가 없었다.

하지만 head[i].className = "tableHead"로

이미 CSS 클래스가 지정되어 있는데

this.className = "dragging"나 "hovering"으로 지정하면

기존 것이 지워지고 새로운 CSS 클래스가 설정된다.

초기화 하기 위해 className = "" 로 지정하면

기존 CSS 클래스가 지워지게 된다.

기존에 있던 tableHead를

추가해 주는 것이 방법이 될 수 있겠지만,

여러 개가 사용되면 다시 문제가 생기기 때문에 좋지 않다.

웹 개발시 많은 CSS 클래스가 사용되는데

매번 추가해주는 작업을 할 수 없다.


일반적으로 많이 사용하는 방식은 다음과 같다.

         className += " " + classname

대입(=)하는 것이 아니고,

문자열을 추가(+)하는 방식으로 사용한다.

다만, 기존에 있는 클래스명과 연결되는 것을 막기 위해

공백 하나(" ")를 추가해 준다.

즉, className속성에 "A B C D ..." 으로 이어지게 구성한다.

반대로 CSS 클래스를 제거할 때는

className.replace(" " + classname, "")와 같이

해당 클래스 이름을 찾아서 공백으로 바꾸어(replace) 준다.

이것을 좀더 쉽게 사용하기 위해

addClass(), removeClass()로 함수화 해서 사용했다.


세번째로 실제 컬럼 이동(Drag and Drop) 을 구현해 본다.

마우스 버튼을 누른 상태에서 이동한 후

마우스 버튼을 놓으면 (up)

눌렀을 때의 컬럼(dragTD)을

놓았을 때의 컬럼 앞으로 옮겨주면 된다.

컬럼(열)을 이동하는 방법은

앞서 행을 정렬하면서 사용한 것처럼

insertBefore 함수를 사용하지만

구현이 조금 더 어렵다.


테이블 태그는

하나의 행에 모든 컬럼의 정보가 있기 때문에

테이블 정렬에서는 하나의 행(row)을 이동시켰다.

하지만 컬럼을 이동하는 것은

하나의 컬럼에 모든 행의 정보가 있는 것이 아니기 때문에

처리가 복잡해 진다.

컬럼의 정보는 행에만 있기 때문이다.

즉, 컬럼을 이동시키려면

모든 행의 해당 컬럼을 찾아서 이동시켜야 한다.

따라서 이동시킬 컬럼을 찾기 위해

행 내의 컬럼의 위치 정보를 이용한다.

function mouseup(ev){
    removeClass(this, "hovering");
    removeClass(dragTD, "dragging");
   
    var srcInx = dragTD.cellIndex;
    var tarInx = this.cellIndex;
    var table = document.getElementById("tableOne");
    var rows = table.rows;
   
    for (var x=0; x<rows.length; x++) {
        tds = rows[x].cells;
        rows[x].insertBefore(tds[srcInx], tds[tarInx])
    }
   
    dragTD = null;
}

전체 코드

컬럼의 위치를 나타내는 속성은 cellIndex이다.

즉, 이동시키고자 하는 컬럼을 cellIndex로 찾고 (srcInx)

이동할 위치의 컬럼을 cellIndex로 찾아서 (tarInx)

insertBefore로 옮겨 주면 된다.

         rows[x].insertBefore(tds[srcInx], tds[tarInx])

이것을 모든 행에 대해서 진행해야 하기 때문에

행의 개수(table.rows.length) 만큼 반복한다.


지금까지는 컬럼을 드레그 할 때

선택한 컬럼의 배경색을 바꾸도록 표시하였다.

네번째로 좀더 쉽고 편히 볼 수 있도록

이동하는 마우스 옆에

현재 선택된 컬럼의 내용이 나타나도록 구현한다.

구현 방법은

마우스가 움직일 때 마다(MouseMove)

커서의 좌표를 구해서

컬럼의 내용을 보여준 개체(div)에 지정한다.

<style>
.draggedDiv {
    width:auto;
    height:auto;
    padding:2px 8px;
    border:1px solid #000;
    position:absolute;
    background:#eee;
}
</style>   
<script>
window.onload = function() {
    ~~ 생략 ~~
    document.documentElement.onmouseup = documentMouseup;
    document.documentElement.onmousemove = documentMouseMove;
}
function documentMouseup(ev){
    if (dragTD) {
        removeClass(dragTD, "dragging");
        dragTD = null;
        draggedDiv.parentNode.removeChild(draggedDiv);
        draggedDiv = null;   
    }
}
function documentMouseMove(ev){
    if (!draggedDiv) { return;}
   
    draggedDiv.style.top = ev.pageY + 5 + "px";
    draggedDiv.style.left = ev.pageX + 10 + "px";
}
var dragTD = null, draggedDiv=null;
function mousedown(ev){
    dragTD = this;
    addClass(this, "dragging");
   
    draggedDiv = document.createElement("div")
    draggedDiv.innerHTML = this.innerHTML;
    draggedDiv.className = "draggedDiv";
    draggedDiv.style.top = ev.pageY + 5 + "px";
    draggedDiv.style.left = ev.pageX + 10 + "px";
    document.body.appendChild(draggedDiv);
}
function mouseup(ev){
    draggedDiv.parentNode.removeChild(draggedDiv);
    draggedDiv = null;
    ~~ 생략 ~~
}

전체코드

이동하려는 컬럼을

별도의 창에서 보여주기 위해서

별도의 창을 동적으로 생성하고 삭제하도록 구현했다.

별도 창(DIV)을 생성하는 것은 (createElement("div"))

이동할 컬럼을 선택하는 시점

즉, 마우스 버튼이 눌려질 때이다 (Mouse Down).

삭제(removeChild)되는 시점은

이동하려는 위치(컬럼)을 선택한 시점

즉, 눌려진 마우스 버튼을 놓았을 때이다 (Mouse Up).

이상의 코드를 확인해 보면

전역으로 선언된 draggedDiv 변수에

mousedown, mouseup 함수에서

div를 생성하고 삭제하는 것을 확인 할 수 있다.


onload 이벤트에서 별도 창을 생성하고

mousedown, mouseup 함수에서

보이거나 숨기는 방식으로 구현 할 수도 있으니 직접 구현해 보길 바란다.


별도 창을 생성한 뒤,

이동할 컬럼의 내용(this.innerHTML)을

별도 창에 넣어주고(draggedDiv.innerHTML),

마우스의 움직임(Mouse Move)에 따라서

별도 창의 좌표(left, top)을 바꾸어 준다.


컬럼의 이동은 테이블의 헤드 컬럼으로 제한을 하기 때문에

모든 이벤트를 테이블 헤드에 대하여 적용하였다.

하지만, 별도 창은 마우스가

테이블 태그에 있거나 웹 페이지에 있으나

마우스를 누르고 움직일 때는 항상 보여야 하기 때문에

문서(document)의 Mouse Move 이벤트에 적용한다 (documentMouseMove).

Event (ev) 클래스에서 제공하는

pageX, pageY 속성을 통해서

현재 마우스 커서의 위치 값을 찾을 수 있다.


문서(document)의 Mouse Up 이벤트를 사용한 것은 (documentMouseup)

컬럼을 이동하기 위해서 마우스 버튼을 누른 상태에서

테이블 태그의 헤더가 아닌

다른 태그나 웹 페이지에서

마우스 버튼을 놓았을 경우를 처리하기 위한 것이다.

이 경우 기존의 mouseup()에서 작성한

드래그 종료(취소) 코드와 동일하게 작성한다.

이동하겠다고 한 컬럼에 적용된

CSS클래스(dragging)를 제거하고

이동 컬럼(dragTD) 변수에 Null을 지정하여

다른 이벤트(over, out)이 작동하지 않게 한다.


마지막 구현으로 별도 창에 컬럼의 헤드뿐 아니라

컬럼 전체의 내용이 나오게 구현해 본다.

하나의 컬럼에 10개의 행이 있다면 모두 나타나도록 구현한다.


구현 방법은 네 번째로 구현한 것과 동일하다.

다만, 컬럼의 헤드 내용을 별도 창에 넣어주었던 것을

테이블 태그를 생성하여

모든 행의 해당 컬럼을 복사해서 넣어준다.

function mousedown(ev){
    dragTD = this;
    addClass(this, "dragging");
   
    draggedDiv = document.createElement("div")
    draggedDiv.className = "draggedDiv";
    draggedDiv.style.top = ev.pageY + 5 + "px";
    draggedDiv.style.left = ev.pageX + 10 + "px";
    document.body.appendChild(draggedDiv);
   
    var dragTable = document.createElement("table")
    draggedDiv.appendChild(dragTable);
    var srcInx = dragTD.cellIndex;
    var table = document.getElementById("tableOne");
    var rows = table.rows;
   
    for (var x=0; x<rows.length; x++) {
        var tr = rows[x].cloneNode(false);
        dragTable.appendChild(tr);
        var tds = rows[x].cells[srcInx].cloneNode(true);
        tr.appendChild(tds);
    }   
}

전체코드

먼저, 별도 창에 새로운 테이블 태그를

생성하여 추가한다.

그리고, 이동하기 위해 선택한 컬럼의 위치(cellIndex)를 파악하고(srcInx),

기존 테이블의 행 정보를 가지고 와서

해당 컬럼(srcInx)의 위치(cellIndex)에 있는

컬럼만 복제한다(cloneNode).


모든 행을 복제해야 하기 때문에

행의 개수(rows.length)만큼 반복하고,

각 행(TR - rows[x])을 먼저 복제한다.

새로운 테이블에 행이 없기 때문에

행을 먼저 만들어야 열을 추가할 수 있다.

행을 복제하고

행의 이동할 컬럼만(rows[x].cells[srcInx])만 복제해서 추가한다.


새로 생성하지 않고 복제를 하는 이유는

각 행과 열(컬럼)이 가지는 정보(내용, CSS등)들을

그대로 복제해야 실제 테이블에서 보이는 것과

별도 창에서 보이는 것이 같아 지기 때문이다.


복제에 사용한 cloneNode 메소드의 파라이터로

행을 복제할 때는 false를

열을 복제할 때는 true를 지정했다.

false는 자식 정보를 복제하지 않겠다는 것을 의미하는 것으로

행을 복제할 때 자식 정보를 복제하면

모든 열(컬럼)이 복제되기 때문이다.

행의 스타일만 복제하고

열은 지정된 열(srcInx)에 대한 모든 것을 복제한다.


JQuery 변환 방법에 대해서는

테이블 정렬(table Sorting)등에서 정리했기 때문에

별도로 정리하지 않는다.

다만, 앞서 두 번째 구현에서

정리한 addClass(), removeClass() 함수는 사용하지 않는다.

동일한 이름의 동일한 기능이 JQuery에서 제공되고 있다.

함수가 아니라 메소드로 사용하는 차이가 있다.

    AAA.addClass("dragging")
    AAA.removeClass("dragging")

전체코드



응용

이상의 MouseDown, MouseMove, MouseUp 등의

마우스 이벤트는 다양하게 활용될 수 있다.


조금 작은 예제로 팝업 윈도우를 구현 할 수 있다.

최근의 웹 개발에서는 팝업 윈도우를 DIV로 구현한다.

그림과 같이 팝업 윈도우의 헤드를

마우스를 누르고, 움직이고, 놓으면

해당 팝업 윈도우가 따라 움직이도록 구현한다.


전체 코드

좀 큰 예제는 파워포인트를 구현하는 것이다.

파워포인트의 슬라이드 위에서 

마우스를 누르고, 움직이고, 놓으면

움직인 만큼을 크기로 하는 도형이 생성된다.

다시 해당 도형을

마우스를 누르고, 움직이고, 놓으면

놓은 위치로 도형을 이동시켜 준다.

이러한 기능을 잘 구현한 예제가 다음 주소에 있다.

(SVG를 사용한 것으로 구현 방법에 차이가 있다.)

https://jgraph.github.io/mxgraph/javascript/


단계 1: 마우스 드레그 1 -  좌에서 우로

단계 2: 마우스 드레그 2 - 우에서 좌로 추가

단계 3: 도형 생성

단계 4: 도형 이동



+ Recent posts