2014년 1월 10일 금요일

html 에서 엑셀 틀고정 구성하기

html 로 엑셀과 같은 그리드를 만드는 일은 그리 간단한 일은 아니지만, 그렇다고 안되는 일도 아닙니다.   다만, 상당히 번잡한 스크립트 코드와 디자인이 수반되어야 하는 시간이 걸리는 일이라고 생각합니다.

연산이 필요한 항목은 AJAX의 방법을 이용하여 서버에서 계산하게 한다면, 약간 복잡한 계산도 경우에 따라서는 차트관련한 내용도 구성할 수 있을 듯 합니다. (물론 클라이언트에서만 동작하도록 하여도 가능할 수는 있습니다.)  - 데이터가 많아 진다면 ... 한계는 있을 것 같습니다.

이 글에서는 html 에서 틀고정 그리드를 구성하는 방법에 대하여서만 생각해 보고자 합니다.

일단 div 로 그리드의 모든 영역을 구성할 지 아니면 table 구조를 사용할지는 결정하여야 합니다.  디자인과 더 많은 수고를 들일 용의가 있다면, div로 구성하는 것이 좋을지도 모르지만 개념적인 것을 살펴보기 위해 이 글에서는 table 구조를 사용합니다.   물론 틀은 div 로 구성할 예정입니다.

예전에 이런 저런 방법으로 구성해 본적이 있었는데 div 틀로 나누어 처리하는 것이 디자인뿐만 아니라 모듈을 구성하는데에도 훨씬 직관적인것 같습니다.  몇몇 사이트나 블로그에서도 그렇게 구성하는것 같기도 하고요 ...

아래의 내용은 다양한 기능을 모두 배제한 가장 기본적인 내용으로만 기술 하였습니다.

대략 6개의 div가 필요합니다.

첫번째 div 는 전체를 감싸는 grid div 입니다. padding, margin, border 등을 사용할 수 있게 하려면 그에 따른 크기조정을 미리 감안하여야 합니다.

두번째 div는 고정되어 움직이지 않는 영역을 구성합니다.  이 구성영역의 크기에 따라 왼쪽 영역과 오른쪽 영역의 각 넓이와 높이가 결정됩니다.

세번째 div 는 header 중 좌우로만 스크롤될 수 있는 영역입니다.
네번째 div 는 기능은 중요하지 않지만 디자인적으로 의미가 있는 scroll 할 때 사용하는 영역입니다. browser 에 따라 차이가 있지만, IE 에서는 17px 정도의 크기를 가지고 있습니다.

다섯번째 div 는 왼쪽에 고정되어 위 아래로 스크롤하는 div 입니다.
여섯번째 div 는 본문으로 좌우 상하 스크롤바가 달린 본문 영역입니다.

전체 감싸는 블록을 제외 하고는 div 의 style 에서 float:left 라는 항목이 있어야 합니다.

이런 6(내용은 실질적으로 4개)개로 나뉘어진 div에 table을 구성하여 그려 주면 틀고정 효과가 있는 내용을 생성할 수 있습니다.

아래와 같은 내용을 구성할 수 있습니다.

 <body onResize="return resizeGridSize();">
 <DIV class="__def__frame__block" id='gridBlock'>
     <DIV class="__def__header__fix__block" id='headerFixBlock'></DIV>
     <DIV class="__def__header__contents__block" id='headerCtnBlock'></DIV>
     <DIV class="__def__header__scroll__block" id='headerScrollBlock'> &nbsp; </DIV>          <DIV class="__def__main__fix__block" id='mainFixBlock'> </DIV>
     <DIV class="__def__main__contents__block" id='mainCtnBlock'> </DIV>
 </DIV>
 </body>

style 은 아래와 같습니다.

<style type="text/css">
html,body {
width:100%;
height:100%;
padding:0px;
margin:0px;
border:0px;
}

.__def__frame__block {
margin:0px;
padding:0px;
border:0px solid black;
width:100%;
height:100%;
background-color:#FFFF00;
overflow:hidden;
}

.__def__header__fix__block {
float:left;
margin:0px;
padding:0px;
border:0px solid black;
background-color:#FFFFFF;
overflow:hidden;
}

.__def__header__contents__block {
float:left;
margin:0px;
padding:0px;
border:0px solid black;
background-color:#FF0000;
overflow:hidden;
}

.__def__header__scroll__block {
float:left;
margin:0px;
padding:0px;
border:0px solid black;
width:17px;
background-color:#999999;
overflow:hidden;
font-size:1px;
}

.__def__main__fix__block {
float:left;
margin:0px;
padding:0px;
border:0px solid black;
background-color:#009900;
overflow:hidden;
}

.__def__main__contents__block {
float:left;
margin:0px;
padding:0px;
border:0px solid black;
background-color:#000080;
overflow-x:scroll;
overflow-y:scroll;
scrollbar:17px;
}

.__def__main__contents__tbl {
table-layout:fixed;
padding:0px;
margin:0px;
border:0px;
text-align:center;
font-size:11px;
}

.__def__main__contents__tbl TD {
width:120px;
}
</style>

script 는 아래와 같습니다.

<script type="text/javascript">

function setScrollPosition() {
var hCtnBlock = document.getElementById("headerCtnBlock");
var mFixBlock = document.getElementById("mainFixBlock");
var mCtnBlock = document.getElementById("mainCtnBlock");

if ( !hCtnBlock || !mFixBlock || !mCtnBlock ) {
return false;
}

hCtnBlock.scrollLeft = mCtnBlock.scrollLeft;
mFixBlock.scrollTop = mCtnBlock.scrollTop;
}

function resizeGridSize() {
var mainGrid = document.getElementById("gridBlock");
var hFixBlock = document.getElementById("headerFixBlock");
var hCtnBlock = document.getElementById("headerCtnBlock");
var hSclBlock = document.getElementById("headerScrollBlock");
var mFixBlock = document.getElementById("mainFixBlock");
var mCtnBlock = document.getElementById("mainCtnBlock");

if ( !mainGrid || !hFixBlock || !hCtnBlock || !hSclBlock || !mFixBlock || !mCtnBlock ) {
alert("Grid 구성을 위한 객체를 찾을수 없습니다.");
return;
}

var mWidth = parseInt(mainGrid.clientWidth);
var mHeight = parseInt(mainGrid.clientHeight);
var hFixWidth = parseInt(hFixBlock.clientWidth);
var hFixHeight = parseInt(hFixBlock.clientHeight);

// browser 별 달리 설정 혹은 script 로 확인
var scrollSize = 17;

hCtnBlock.style.width = (mWidth-hFixWidth-scrollSize)+"px";
hCtnBlock.style.height = (hFixHeight)+"px";
hSclBlock.style.height = (hFixHeight)+"px";

mFixBlock.style.width =  (hFixWidth)+"px";
mFixBlock.style.height =  (mHeight-hFixHeight-scrollSize)+"px";

mCtnBlock.style.width =  (mWidth-hFixWidth)+"px";
mCtnBlock.style.height =  (mHeight-hFixHeight)+"px";

return false;
}

function appendEventObject(obj,type,fnHandler,isBubble) {
if ( !obj ) {
return false;
}
if ( obj.attachEvent ) {
obj.attachEvent("on"+type,fnHandler);
} else if ( obj.addEventListener ) {
obj.addEventListener(type,fnHandler,isBubble);
}
}

function makeRandomDistandMatrix(maxSize,connSize,maxDistance) {
// check arguments ... skip
var result = [];
var seed = Math.round(maxDistance/1000);
for ( var y = 0; y < maxSize; y++ ) {
var arr = [];
for ( var x = 0; x < maxSize; x++ ) {
var cPos = Math.round(x/2);
if ( (y-cPos) >= 0 && (y+cPos) < maxSize ) {
if ( x%2 == 0) {
cPos = y+cPos;
} else {
cPos = y-cPos;
}
} else if ( (y-cPos) < 0 ) {
cPos = x;
} else {
cPos = (maxSize-x-1);
}

if ( cPos == y ) {
arr[cPos] = 0;
} else {
if ( x < connSize ) {
arr[cPos] = Math.round(Math.random()*seed)*x;
} else {
arr[cPos] = maxDistance;
}
}
}
result.push(arr);
}
return result;
}

function drawTableTmp(matrixTbl,parentObj,classNameStr) {
var tableObj = document.createElement("TABLE");
var size = matrixTbl.length;
var cSize = matrixTbl[0].length;
tableObj.style.width = cSize*120+"px";
//parentObj.appendChild(tableObj);
tableObj.className = classNameStr;
tableObj.style.backgroundColor = "#000080";
tableObj.setAttribute("cellSpacing","1");
tableObj.setAttribute("cellPadding","0");
var tBodyObj = document.createElement("TBODY");
tableObj.appendChild(tBodyObj);
for ( var y = 0; y < size; y++ ) {
var trObj = document.createElement("TR");
trObj.style.height = "22px";
tBodyObj.appendChild(trObj);
for ( var x = 0,xSize=matrixTbl[y].length; x < xSize; x++ ) {
var tdObj = document.createElement("TD");
trObj.appendChild(tdObj);
tdObj.style.backgroundColor = "#FFFFFF";
tdObj.innerText = matrixTbl[y][x];
}
}
parentObj.innerHTML = tableObj.outerHTML;
}


function initResource() {
var hColumnSize = 3;
var hRowSize = 3;
var maxSize = 50;
var connSize = 8;
var maxDistance = 99999999;
var matrix = makeRandomDistandMatrix(maxSize,connSize,maxDistance);

var hFixMatrix = [];
var hCtnMatrix = [];
var mainLeftMatrix = [];
for ( var i = 0; i < hRowSize; i++ ) {
hFixMatrix[i] = [];
hCtnMatrix[i] = [];
for ( var j = 0; j < hColumnSize; j++ ) {
hFixMatrix[i].push("H_F_" + i + "_" + j);
}

for ( var j = 0; j < maxSize; j++ ) {
hCtnMatrix[i].push("H_C_" + i + "_" + j);
}
}

for ( var i = 0; i < maxSize; i++ ) {
mainLeftMatrix[i] = [];
for ( var j = 0; j < hColumnSize; j++ ) {
mainLeftMatrix[i].push("NUM_"+i + "_" +j);
}
}

var hFixBlock = document.getElementById("headerFixBlock");
var hCtnBlock = document.getElementById("headerCtnBlock");
var hSclBlock = document.getElementById("headerScrollBlock");
var mFixBlock = document.getElementById("mainFixBlock");
var mCtnBlock = document.getElementById("mainCtnBlock");

drawTableTmp(hFixMatrix,hFixBlock,"__def__main__contents__tbl");
drawTableTmp(hCtnMatrix,hCtnBlock,"__def__main__contents__tbl");
drawTableTmp(mainLeftMatrix,mFixBlock,"__def__main__contents__tbl");
drawTableTmp(matrix,mCtnBlock,"__def__main__contents__tbl");

resizeGridSize();
appendEventObject(document.getElementById("mainCtnBlock"),"scroll",setScrollPosition,false);
}

window.onload = initResource;

</script>

table은 임의로 4개의 영역으로 구성하였습니다.
영역이 구성되면 div를 resize 해서 스크롤이 필요한 영역을 활성화 합니다.
본문 영역에만 scroll event 를 구성하여 필요한 화면이 나오도록 구성하였습니다.

contenteditable 속성과 chart 기능을 추가하고 event handling 과 디자인적 요소를 더한 다면 ... (할게 너무 많은가요? )

구성된 코드는 IE와 Chrome 에서 동작합니다. 다만 scrollbar size 때문에 크롬에서는 디자인이 약간 깨지고 있습니다.

크롬에서 resize 이벤트가 잘 작동하지 않기 때문에 body 부분에 이벤트를 걸어 놓았습니다.

GWT, SmartGWT 등에서는 Grid 유형에 대한 지원이 많습니다.
자유로운 틀고정 까지는 아니라도 사용자가 Column의 크기를 조정하거나, 소팅기능을 제공하거나 하는 등요 .. 물로 Client 단에서 처리하는 내용이 많아지면 많아질 수록 반응속도는 느려지기 마련이고요 ....


관심있는 분에게 조금이라도 도움이 되었으면 합니다.





댓글 1개: