부모 노드가 자식 리스트를 보유하는 방식은

데이터에 내 자식 리스트(배열)을 표시하면 된다.

앞서 구현한 자식 노드에서 부모 노드를 지정하는 방식은

부모가 하나이기 때문에 parent 변수 하나만 추가하면 됐지만

한 부모의 자식은 여럿이 될 수 있기 때문에

배열(children)을 사용해야 한다.

그리고, 자식의 자식의 자식의 ... 로 작성될 수 있기 때문에

배열의 배열의 배열...

과 같이 무한하게 표시될 수 있다.

이렇게 무한하게 표현되는 데이터를 처리하기 위해서는

데이터가 있는 만큼 무한하게

반복해서 처리할 수 있는 재귀 함수가 필요하다.

데이터(행)를 처리하고

해당 데이터에 자식이 있으면

다시 데이터를 처리하고

해당 데이터에 자식이 있으면 ....

이렇게 반복되는 것을 하나만 함수로 작성하고

데이터가 없을 때까지 반복해서 호출하는 것을 재귀함수 라고 한다.



sample2_1.html


먼저, 20 라인의 데이터를 보면

앞서 구현한 자식 노드에서 부모 노드를 지정하는 방식과 비슷하게

자신을 나타내는 ID가 추가되었다.

다만, 부모 대신에 자식들(children)이 배열로 구현되어 있다.

부모는 하나지만

자식은 여러 명일 수 있기 때문에 복수형으로 사용한다.

자식 필드로 children이 선언되고

부모와 동일한 구조로 데이터가 저장된다.

여기서는 처리하지 않았지만

자식의 자식일 경우

Children을 계속 넣어주면 된다.


앞서의 방식보다 코드가 깔끔해 보이는데

재귀 호출을 사용하였다.

재귀 호출, 재귀 함수라고도 하는데

자신이 자신을 부르는 것을 의미한다.

자식이 어느 정도(몇 대손)까지 있을 지 모르기 때문에

자식에 대한 처리를 위해 재귀 호출을 사용했다.


재귀 함수인 setData는

파라미터로 데이터와 부모 태그, 깊이(몇 대손)를 주게 된다.

데이터는 자식 리스트(children)를 의미하고,

부모 태그는 부모 TR을 의미하는 데

첫 데이터(단군)은 테이블(table) 태그이다.

깊이는 처음에 0 일 것이고

재귀호출 할 때 마다 1 씩 더해 준다.


주어진 데이터의 개수 만큼 반복해서 $.each( data, function( index, row)

TR을 생성하고

CSS 클래스 명으로 이름을 부여한다.

다만, 위치는

부모가 없으면 테이블에 추가하고(appendTo)

부모가 있으면 부모 다음에 넣어준다 (insertAfter).

다만 조금 더 정확하게 구현하려면 부모 다음이 아니고

해당 부모의 마지막 자식 다음이 되어야 한다.

구현 방법이 복잡해서 여기서는 부모 다음에 추가했다.

부모가 있는지 없는지는

파라미터로 넘어온 부모의 태그 타입(nodeName)이

테이블이면 부모가 없는 것으로 처리했다.


들여쓰기 하는 방식은

자식 노드에서 부모 노드를 지정하는 방법과 같이

깊이의 개수만큼 SPAN 태그를 생성해 준다.


마지막으로 자식이 있으면 if (row.children)

다시 자신(setData)을 호출하고

파라미터로 자식 리스트를 넣어주면 된다.


자식 노드에서 부모 노드를 지정하는 방식(sample2.html)과

부모 노드가 자식 리스트를 보유하는 방식(sample2_1.html)을 구현하였다.

일반적으로 대부분의 라이브러리들이 (TreeGrid jQuery, EasyTree)

부모 노드가 자식 리스트를 보유하는 방식을 사용하고 있다.


하지만, 개인적으로

자식 노드에서 부모 노드를 지정하는 방식이 더 좋다고 생각한다.

데이터 베이스에 데이터가 저장되는 방식이

자식 노드에서 부모 노드를 지정하는 방식과 같기 때문이다.

(본 블로그의 무한 댓글 참조)

즉, 데이터 베이스에 ID와 Parent 필드로 저장하고 있다.

부모 노드가 자식 리스트를 보유하는 방식을 사용하게 되면

ID와 Parent 필드로 구성된 데이터를 정리해서

자식 리스트를 작성해야 하기 때문이다.

즉, Java나 PHP등의 서버 언어에서의 처리가 있어야 한다.


따라서 JS 코드만 보면

자식 노드에서 부모 노드를 지정하는 방식이 더 복잡하지만

데이터 베이스의 데이터를 그대로 사용할 수 있기 때문에

이후의 기능 개선도

자식 노드에서 부모 노드를 지정하는 방식으로 구현할 예정이다.



라이브러리화 한다는 것은

HTML 파일에 같이 작성된 자바 스크립트(JS) 코드를

별도의 JS 파일에 함수로 구현하고

이 함수만 호출하면

TreeGrid를 사용할 수 있도록 하겠다는 것이다.

또, 사용자(개발자)가 자신의 상황에 맞추어

간단한 설정만 하면 TreeGrid를 사용할 수 있도록 하는 것이다.

즉, TreeGrid 라이브러리 파일(sample3_treegrid9.js)과

이 라이브러리를 사용하는 HTML 파일(sample3.html)로 분할 해서 구현한다.


라이브러리화한 후,

HTML 파일에 남아있는 코드를 보면

복잡한 코드는 사라지고 option과 data만 남은 것을 볼 수 있다.

option과 data는

TreeGrid 사용자의 상황에 따라 바꿔서 지정하고

사용자는 30 라인에 있는 것처럼

TreeGrid9을 실행만 시키면 된다.


sample3.html


기존의 코드는 다음과 같이 sample3_treegrid9.js파일에 작성하고

HTML(sample3.html)에서는

8 라인에서 작성한 것처럼 <script src="sample3_treegrid9.js"></script>

JS 파일을 가져오기 하면 된다.

sample3_treegrid9.js


이상의 코드와

이전 예제(sample2.html)와 차이는

1 라인의 TreeGrid9 함수뿐이다.

외부 파일로 빼면서 함수로 구성한 것이다.

모든 코드는 동일하다.

다만, HTML에서 호출할 때 함수로 호출하지 않고

New를 사용해서 클래스로 실행했다.

간단하게는 함수화해서 함수를 호출해도 되지만

앞으로 여러 가지 기능을 추가해 나갈 것이기 때문에

클래스 방식으로 사용하였다.

JS에서는 함수와 클래스가 유사하게 사용된다.

자세한 설명은 [JS 클래스]로 검색하면 찾을 수 있다.



본 예제는 모든 데이터를 화면에 출력하고

자식 노드는 부모 노드 보다 뒤로 들여쓰기 하여 구현했다.

이번 단계에서는 부모 노드를 클릭하면

자식 노드를 감추고 보여주는 기능을 구현할 예정인데

쉽게 코드를 설명하기 위해

숨기는 기능부터 먼저 구현해 본다.


기능 구현을 위해 고려해야 할 대상은 다음과 같다.

  • 클릭 대상 선택
  • 자식이 있는 부모와 없는 부모, 자식을 보여주고 있는지 숨기고 있는지 구분하는 방법


둘 다 같은 결과를 필요로 하는데,

하나의 노드(행)를 클릭해서 자식을 숨기게 되면

현재 어떤 노드가 선택되었는지 보여 주는 기능을 구현하기 어렵다.

(행 선택 기능으로 여기서는 구현하지 않는다.)

그리고 두 번째 기능을 구현하기 위해

대부분 노드 앞에 특정 기호를 사용하게 된다.

즉, 특정 기호로 자식이 있는지 없는지 표시하고

특정 기호를 클릭하면 자식을 보이거나 숨기게 구현다.


윈도우 탐색기를 예로 하면

하위 폴더가 있는 경우 + 기호가 나오고

부모 폴더의 + 기호를 클릭하면

하위 폴더가 나타나며 – 기호로 바뀌게 된다.

여기서는 이 기호를 Expander라고 하고

Expander를 클릭하면 자식을 숨기거나

보이도록 구현한다.


다음으로 처리해야 할 것은

Expander를 클릭하면

내 자식 노드들을 찾아서 숨기는 것이다.

나의 자식은 나를 부모로 지정한 노드들을 의미하고

나를 부모로 지정한 노드들은

클래스명에 "treegrid9-parent-"를 이용하여

나(나의 ID)를 지정한 노드들을 의미한다.


이상의 개념을

다음과 같이 코드로 구현하면 되는데

코드를 보기 전에

위 개념을 토대로 직접 작성 해보는 것이 실력향상에 도움이 될 수 있다.


sample4.html

HTML 파일에서 11 라인에 CSS 클래스 하나를 추가 했다.

자식 노드를 숨기고 보여주기 위한 expander와 관련된 것으로

treegrid9-indent와 같은 기능을 하지만

expander 기호에 마우스를 올리면

클릭할 수 있는 기호로 바꾸는 스타일이 추가된 것이다.


sample4_treegrid9.js

먼저 30 라인을 보면 If문 조건이 바뀌었다.

이전에는 첫 번째 필드(i===0)이고

깊이가 0 이상 (depth>0)이면 들여쓰기를 하도록 했는데,

현재는 깊이 조건이 빠져서

모든 노드에서 들여쓰기 span이 생성된다.

자식이 있는 부모에 expander 기호를 붙이게 되면

다음과 같이 자식이 없는 부모와 줄 맞춤이 맞지 않게 된다.

           + Job1
           Job3

따라서 모든 노드에 span을 생성하고

자식이 있는 부모 노드만 span에 expander 기호와

CSS 스타일(treegrid9-expander)를 넣어서 보기 좋게 만들었다.


다음으로 기존 코드에 추가된

13 라인부터 22 라인을 이해하면 된다.

자식이 부모를 지정하는 방식이기 때문에

자식을 생성하다가

부모 ID(row.parent)로 부모를 찾아서 $(".treegrid9-" + row.parent)

부모의 클래스 명에 treegrid9-expanded라는

이름이 있으면 부모 표시가 된 것이니 넘어가고

없으면 부모 표시가 되지 않은 것이니 !parent.hasClass("treegrid9-expanded")

부모 표시와 처리를 진행하게 된다.

클래스명 treegrid9-expanded는 식별자로 사용되는 이름이고

별도의 CSS 지정을 하지 않았다.

이렇게도 많이 사용하기 때문에 기억해두면 좋을 것이다.


부모 처리가 되지 않았으며

먼저, expander로 사용할 span 태그를 찾는다.

여기서는 부모(parent[0] 즉 TR)의

첫 번째 셀(TD) 중에서 첫 번째 자식(span)을

expander 기호를 넣을 태그로 보고 .addClass("treegrid9-expander")

– 기호를 넣어준다. expander.html("-")


expander(기호)를 클릭하면

현재 노드의 자식 노드를 찾아서 숨겨주는 처리를 해야 한다.

클릭한 개체(expander 즉 this)에

가장 가까운(closest) TR을 찾아서 $(this).closest("tr")

현재 노드를 파악하고,

현재 노드의 ID을 추출한 뒤 getNodeId(tr)

해당 ID을 부모로 지정한 "tr.treegrid9-parent-" + id

모든 자식(TR)을 찾아서 find

보이지 않게 (hide) 처리하면 된다.

JQuery의 closest, find, hide를 활용한 코드로

자세한 설명은 찾아 보길 바란다.


마지막으로 현재 노드의 ID 값을 찾는 getNodeId 함수는

getDepth 함수와 같이 정규식을 사용해서

ID 값(숫자)만 남겨서 반환하는 함수이다.


sample4.html 파일을 실행한 후

expander(-)를 클릭해서

다음과 같이 Job2가 숨겨지는지 확인해 보자




앞서의 예제에서는 expander를 클릭하면

자식 노드가 숨겨지게 코드를 작성하였다.

여기서는 expander를 다시 클릭하면

감춰진 자식 노드가 보이는 기능을 구현한다.

즉, expander를 클릭하면 숨겨지고,

다시 클릭하면 나타나고

다시 클릭하면 숨겨지도록 구현한다.


개념부터 정리하면 모든 노드는 펼쳐져 있다.

이때 부모 노드의 expander 기호는 모두 – 이다.

부모 노드의 expander를 클릭하면

기호는 –에서 열수 있다는 의미인 + 기호로 바꾸고

자식 노드들을 보이지 않게(Hide) 처리한다.

다시 expander를 클릭하면

열렸다는 의미로 기호는 – 로 바꾸어 주고

자식 노드들을 보이게(show) 처리하면 된다.


간단한 개념이기 때문에

직접 구현해 본 후 다음 코드를 보는 것이 좋다.


기존 코드에 다음과 같이 몇 라인만 바꾸어 주면 된다.

            if (!parent.hasClass("treegrid9-expanded")){
                parent.addClass("treegrid9-expanded");
                var expander = $(parent[0].cells[0].firstChild).addClass("treegrid9-expander");
                expander.html("-");
                expander.click(function() {
                    var isexpanded = $(this).html() === "-";
                    if (isexpanded) {
                        $(this).html("+");
                    } else {
                        $(this).html("-");
                    }
                    var tr = $(this).closest("tr");
                    var id = getNodeId(tr);
                    table.find("tr.treegrid9-parent-" + id).each(function(i, e){
                        if (isexpanded)
                            $(this).hide();
                        else {
                            $(this).show();
                        }
                    });
                });
            }

sample5_treegrid9.js

expander를 클릭하면

expander의 기호가 무엇인지 판단해서 $(this).html() === "-"

자식 노드를 보일 것인지(show) 숨길 것인지(hide)를 처리한다.


자식노드를 찾는 것도 앞서의 코드와 동일하다.


실행해 보면 트리 라이브러리처럼

자식이 보였다 안보였다 하는 것을 확인 할 수 있다.


이제 데이터를 바꿔서 실행해 보자.

var data = [
                {id:"1", "task": "job1", "startdate": "2016-10-11", "enddate": "2016-11-11"},
                {id:"2", "task": "job2", "startdate": "2016-10-11", "enddate": "2016-10-30", parent: "1"},
                {id:"3", "task": "job3", "startdate": "2016-11-12", "enddate": "2016-12-11", parent: "2"}
            ];

sample5.html

이전 예제에서는 계층 구조(depth)가 1 단계였는데

Job3을 Job2의 자식으로 변경하여 실행해 보자.

다음 그림과 같이 실행된다.

몇 가지 문제가 발생하는 것을 알 수 있다.

먼저, 계층이 많아지니 expander 기호의 위치가

모두 행의 맨 앞에 있어서 적절하지 않아 보인다.

그리고, 마우스로 클릭해보면

바로 아래의 자식은 숨겨지는데

자식의 자식은 숨겨지지 않는다.


어떻게 수정해야 하는지 생각해 보고

다음 예제에서 이 문제를 해결하는 방법을 정리한다.





다음과 같이 데이터를 구성하고 실행하면

var data = [
                {id:"1", "task": "job1", "startdate": "2016-10-11", "enddate": "2016-12-11"},
                {id:"2", "task": "job2", "startdate": "2016-10-11", "enddate": "2016-10-30", parent: "1"},
                {id:"3", "task": "job3", "startdate": "2016-11-12", "enddate": "2016-12-11", parent: "2"},
                {id:"4", "task": "job4", "startdate": "2016-12-12", "enddate": "2016-12-31"}
            ];

sample6.html

다음과 같이 2 가지 문제가 있는 것을 찾았다.

1. expander 기호의 위치가 행의 맨 앞에 있어서 적절하지 않아 보인다.

2. job1의 expander 기호를 클릭하면 job2만 숨겨진다.

  즉, 손자(job3)은 숨겨지지 않는다.


이 버그도 쉽게 해결할 수 있으니

먼저 해보고 다음 설명을 읽는 것이 좋을 것 이다.


            if (!parent.hasClass("treegrid9-expanded")){
                parent.addClass("treegrid9-expanded");
                var expander = $(parent[0].cells[0]).find("span").last().addClass("treegrid9-expander");
                expander.html("-");
                expander.click(function() {
                    var isexpanded = $(this).html() === "-";
                    if (isexpanded) {
                        $(this).html("+");
                    } else {
                        $(this).html("-");
                    }
                    var tr = $(this).closest("tr");
                    getChildren(getNodeId(tr), isexpanded);
                    function getChildren(id, isexpanded) {
                            table.find("tr.treegrid9-parent-" + id).each(function(i, e){
                                 if (isexpanded)
                                    $(this).hide();
                                 else {
                                     $(this).show();
                                 }
                                id = getNodeId($(this));
                            if (table.find("tr.treegrid9-parent-" + id).length>0){
                                getChildren(id, isexpanded);
                            } ;
                        });
                    }
                });
            }

sample6_treegrid9.js


먼저 expander 기호의 위치 문제는

노드의 맨 앞에 있는 기호를

뒤로 보내서 task 필드의 값 바로 앞에 두면 된다.

들여쓰기와 expander 기호 표시를 위해

task 필드 앞에 span을 생성하였는데,

기존에는 expander 기호를 맨 앞 span에 넣었다.

이것을 맨뒤 span에 넣으면 된다.


따라서 다음과 같이 코드를 수정한다.

기존    var expander = $(parent[0].cells[0].firstChild).addClass("treegrid9-expander");

변경    var expander = $(parent[0].cells[0]).find("span").last().addClass("treegrid9-expander");

기존에는 첫 번째 TD(task 필드)의

첫번째 자식(span)에 기호를 넣었고,

수정된 코드는 첫 번째 TD(task 필드)의자식 중

span을 찾아서 마지막(last) 자식에 기호를 넣게 작성하였다.

그러면 다음 그림과 같이 task 필드의 값 바로 앞에

expander 기호가 위치하게 된다.


두번째 문제는

job1의 expander 기호를 클릭하면 job2만 숨겨지고

손자(job3)은 숨겨지지 않는 것이다.


이것은 자식 찾기를 한번만 했기 때문에 발생한다.

앞서 코드 구성에서

바로 위 부모만(직계 존손) 자신의

자신의 노드 클래스명에 표시하도록 했고(treegrid9-parent-)

부모는 자신을 부모로 표기한 이 값만을 찾도록 하고 있다. table.find("tr.treegrid9-parent-" + id).each(function(i, e)

즉, 모든 자식을 찾아야 하는데

모든 자식을 찾으려면

나(클릭한 노드)를 중심으로

나를 부모로 하는 자식 노드를 찾아서 숨기거나 보이도록 처리한다.

그리고, 각 자식 노드를

부모로 하는 노드가 있는 지 확인해서 if (table.find("tr.treegrid9-parent-" + id).length>0)

있으면 다시 자식 노드를 찾아서 숨기거나 보이도록 처리한다.

그리고, 다시 각 자식 노드를

부모로 하는 노드가 있는 지 확인해서 if (table.find("tr.treegrid9-parent-" + id).length>0)

이렇게 무한 반복으로

자식 노드를 찾아서 처리해야 한다.

복잡해 보이지만

이것은 재귀 함수를 사용하면 간단하게 처리된다.

기존 코드를 getChildren 함수에 넣고

이 함수를 자식이 있으면 호출하도록 작성하였다.


또, getChildren를 함수내 내장 함수로 작성했다.

개인적으로 JS의 재미있는 기능이라고 생각하며

자세한 내용은 찾아보길 바란다.


지금까지 간단한 TreeGrid를 제작하는 예제를 통해서

자바스크립트, JQuery, JSon 등의 사용법을 익혀보았다.

노드 선택, Ajax 처리 등의 기능을 추가하면

제법 그럴듯한 TreeGrid를 만들 수 있을 것이다.

누군가 제대로 된 오픈 소스 라이브러리를 개발하길 기대하며……


자바스크립트(JS)의 객체 지향 프로그래밍은

기존의 객체 지향 언어들과는 조금 다르면서 재미 있게 제공되고 있다.

자바스크립트의 객체 지향 프로그래밍에 대한 간단한 설명은 많지만

실제적인 사용법에 대한 설명은 없는 것 같아서

개인적으로 최근에 재미있게 본 NicEdit를 대상으로

자바스크립트의 객체 지향 프로그래밍에 대한 정리를 하였다.


2015년에 ECMAScript 6에서 class란 개념을 소개하여

현재는 쉽게 사용할 수 있지만,

과거 방식도 알아두면 자바 스크립트 사용에 대한 이해에 도움이 되기에 정리한다.

NicEdit는 웹 에디터로 좋은 웹 에디터는 아니다.

상용 제품이나 다른 오픈소스(CKEditor 등)에 비해 기능도 많지 않고,

HTML 5 를 준수하지도 않는다.

JS에서 제공되는 Document 객체의 execCommand 메서드를 이용하여

단순 기능 위주로 구현되어 있다.

구글에서 간단한 웹 에디터 제작으로 검색해보면

관련 자료를 찾을 수 있다.

https://www.google.com/search?q=web+editor+execCommand&newwindow=1&source=lnt&tbs=lr:lang_1ko&lr=lang_ko&sa=X&ved=0ahUKEwiphKX3iLjPAhWBj5QKHZJ7AyQQpwUIFg&biw=1295&bih=872

https://developer.mozilla.org/ko/docs/Web/API/Document/execCommand

에디터를 제작하는 자세한 방법은 찾아보길 바란다.

그러나, 지금까지 본 수 많은 JS 오픈소스 코드 중

NicEdit가 코드 양이 가장 적으면서

JS 객체 지향 프로그래밍 특성을 가장 잘 반영한 프로그램으로 생각한다.

더욱이 코드를 살펴보면,

제법 괜찮은 JS 프레임워크라는 걸 알 수 있다.

또, 코드 작성 (명명 규칙 등)도 잘 되어 있어

별도의 문서(주석) 없이도 이해할 수 있다.

그래서, 모든 코드를 정리할 수 없지만

JS 객체 지향 프로그래밍 응용 관련된 부분과

알아두면 유용하게 사용할 수 있는 몇 가지 기능 위주로 정리하였다.

이 글은 개념 위주로 작성했기 때문에 자세한 자료는 찾아봐야 한다.


이 글을 읽기 전에

JS 객체 지향 프로그래밍에 전혀 모른다면

관련 자료를 찾아서 기본 내용 정도는 이해하고 읽어야 한다.

최소한 JSon, 함수, 내부함수(inner function),

클래스, Prototype 등은 알아야 한다.

여기서는 이에 대해 설명하지 않는다.


먼저, NicEdit 웹 사이트에서 소스를 다운로드 받아야 한다.

소스는 압축 파일 형태로 제공되고,

압축을 해제하면 예제와 에디터 소스(nicEdit.js)가 제공된다.

실행 데모는 다운 받은 파일을 실행해도 되고,

웹 사이트에서 데모 페이지를 확인해도 된다.

소스는 압축(minimize, Compressor)되어 있어

상세한 내용을 알아보기 힘든 구조로 되어 있다.


그렇지만 에디터 소스(nicEdit.js)를 적절한 편집기로 열어보면

22 개의 클래스가 있는 것을 알 수 있다.

이 소스를 적당한 방법으로 압축을 해제한다.

여기서는 웹사이트(http://jsbeautifier.org/)를 이용했다.

소스의 내용을 복사해서 사이트에 넣고 실행해주면 간단하게 변환할 수 있다.


다운로드 받을 때,

다운로드 페이지의 하단에 있는 "Uncompressed NicEdit for development"

설정을 체크하고 다운 받으면 압축되지 않은 소스를 받을 수 있다.

(http://jsbeautifier.org/ 사용법을 기록으로 남기려고...)

이렇게 해서  가려진 12개의 클래스를 확인할 수 있다.

소스를 변환해서

한 줄씩 코드를 확인해도 되고,

다음 그림과 같이 Eclipse에서 Outline으로

전체 구조를 한 눈에 확인 할 수 있다.

NicEdit의 본격적인 클래스 구조에 대해

알기 전에 기본적인 라이브러리 구조에 대해 알아야 한다.

웹 에디터는 동적으로 HTML 태그를 생성하기 때문에

관련된 많은 기능들이 필요하다.

웹 에디터는 라이브러리 형태로 제공되기 때문에

사용자(개발자)가 에디터로 사용할 Div나 TextArea를 지정하면

HTML 문서 작성을 위해

편집 기능을 제공하는 버튼,

옵션을 설정하기 위한 팝업 등의 컴포넌트(HTML)을

동적으로 만들어서 제공하게 된다.

자주 사용하는 기능들을 모아서 라이브러리 형태로 구성해 두었는데

NicEdit에서는 bk로 시작하는 클래스(함수 - bkLib, bkEvent 등) 들을 의미한다.

이러한 기능은 JQuery와 비슷한 방식으로 만들어졌고

제공하는 기능이나 사용법도 비슷하다.

다시 말하면, 빠르고 쉬운 개발을 위해서는

JQuery와 같은 프레임워크가 필요한데

자체적인 제품에 외부의 JQuery를 쓰는 것이 바람직하지 않아서

유사한 기능들을 미리 제작해 놓은 것이라 할 수 있다.

NicEdit의 기본 라이브러리를 잘 익혀서

자체적으로 다른 S/W를 개발할 경우 사용하면 유용할 것이다.



34개의 클래스들이 많아 보이지만

라이브러리, 에디터, 플러그인 클래스의

3개의 그룹으로 나누어 정리할 수 있고,

먼저 라이브러리에 대하여 정리한다.


라이브러리에서 가장 먼저 확인되는 클래스는 bkExtend이다.

JS는 클래스를 함수로 이용하여 사용하기 때문에 혼동할 수 있는데

bkExtend는 클래스라기 보다는

개체의 속성(변수와 함수)을 복사하는 함수에 가깝다.

bkClass.extend = function(C) {
   var A = function() {
       if (arguments[0] !== bkClass) {
           return this.construct.apply(this, arguments)
       }
   };
   var B = new this(bkClass);
   bkExtend(B, C);
   A.prototype = B;
   A.extend = this.extend;
   return A
};  

bkClass는 bkExtend와 prototype으로

클래스 상속 메서드(extend)를 구현한 것이다.

다른 언어에서 클래스 상속이란

부모의 속성을 자식이 물려 받는 것이고,로

JS는 클래스 속성을 복사(또는 클래스 둘을 합친다)하는 개념으로 사용한다.

JS는 상속의 개념이 없기 때문에

복사 개념(extend)을 이용하여 흉내 낸 것으로 볼 수 있다.

JQuery의 extend와 비슷하다.

프로토타입(prototype)에 대한 내용은 http://www.nextree.co.kr/p7323/ 참조


bkClass의 두 가지 기능 중 하나가 이상에서 언급한 상속(extend)이고

나머지 하나는 생성자(construct) 이다.

JS는 클래스가 없기 때문에 생성자도 없다.

하지만 클래스가 생성될 때 여러가지 처리를 해야 할 필요가 있다 (생성자).

따라서, NicEdit는 extend 메서드를 호출할 때를

생성자가 실행되는 시점으로 보고 construct 함수를 호출한다.

다음 코드에서 보듯이 빈 construct 함수를 생성하고

extend 에서 호출한다.

function bkClass() {}
bkClass.prototype.construct = function() {};

bkClass.extend = function(C) {
   var A = function() {
       if (arguments[0] !== bkClass) {
           return this.construct.apply(this, arguments)
       }
   };
   var B = new this(bkClass);
   bkExtend(B, C);
   A.prototype = B;
   A.extend = this.extend;
   return A
}; 

이 내용을 복사해서 가진 클래스(자식, 예: nicEditor)에서는

construct에 필요한 내용(코드)를 추가하여,

생성자처럼 사용하는 것이다.

var nicEditor = bkClass.extend({
   construct: function(C) {
       this.options = new nicEditorConfig();
       bkExtend(this.options, C);
       this.nicInstances = new Array();
       this.loadedPlugins = new Array();
       var A = nicEditors.nicPlugins;
       for (var B = 0; B < A.length; B++) {
           this.loadedPlugins.push(new A[B].p(this, A[B].o))
       }
       nicEditors.editors.push(this);
       bkLib.addEvent(document.body, "mousedown", this.selectCheck.closureListener(this))
   }, 

bkClass.extend가 실행되면

해당 클래스의 생성자가(construct) 실행되고,

해당 클래스(함수)내에 있는 변수와 함수가 복사 되게 된다.

클래스 상속(복사)과 같은 일이 발생한다.

따라서, bkClass가 가장 상위의 클래스 같은 역할을 하게 된다.

이 글에서는 상속 받는다는 표현(bkClass.extend)을 사용할 것이다.


BkClass에서 상속받은 (var bkElement = bkClass.extend(...) )

bkElement는 NicEdit에서

동적으로 생성하는 클래스(버튼, 판넬 등 HTML 태그)들을 감싸는 클래스이다.

예로, 버튼(button)에 CSS를 적용할 경우 addStyle이라는 함수를 만들고

그냥 addStyle를 호출할 경우 addStyle(해당버튼, 스타일값)으로 지정해야 한다.

이렇게 사용하는 것을 구조화 프로그래밍이라고 한다.

그렇지 않고

해당버튼.addStyle(스타일값)

이렇게 클래스의 메서드로 사용하면

객체지향 프로그래밍 방식으로 개발한다고 할 수 있다.

bkElement는 많이 사용하는 함수들을 메서드로 가지고 있어

해당 HTML 태그에서 자유롭게 사용할 수 있게 한다.


addStyle함수는 JQuery에서는 $(해당버튼).css(스타일값)로 사용한다 (addStyle도 있음).

JQuery의 $와 비슷한 기능은 $BK( + bkElement)함수이다. 


bkElement의 주요 메서드는

이벤트 추가(addEvent), CSS 클래스 추가(addClass) / 삭제(removeClass),

CSS 속성 지정(setStyle), HTML 속성 지정(setAttributes) 등으로

모두 HTML 태그 지정에 유용한 것들로 구성되어 있다.


구체적으로 살펴보면,

bkElement의 addEvent는 실제로는 bkLib의 addEvent를 호출한다.

bkLib의 addEvent를 클래스 내에서 편리하게 사용하기 위해

bkElement의 메서드로 구성한 것이다.

즉, bkElement를 상속한 클래스는 this.addEvent()로 호출하고

그렇지 않은 클래스나 함수에서는

bkLib.addEvent()를 호출하여 사용하면 된다.

어떤 방식이 좋다고 말하기 어렵고,

객체 지향과 구조화 프로그램의 차이라고 정리할 뿐이다.


bkElemen의 사용 예를 확인해 보면 다음과 같다.


var A = new bkElement("DIV").setStyle({width: (parseInt(B.getStyle("width")) || B.clientWidth) + "px"}).appendBefore(B); 


bkElement 파라메터로 DIV가 주어지는데

bkElement는 bkClass에서 상속 받았기 때문에

생성자(construct)가 실행된다.

bkElement의 생성자에서는 주어진 파라메터를

document의 createElement를 이용하여 생성한다.

var bkElement = bkClass.extend({
    construct: function(B, A) {
        if (typeof(B) == "string") {
            B = (A || document).createElement(B)
        }
        B = $BK(B);
        return B
    },

즉, 위 코드는 HTML DIV 태그를 생성하고,

bkElement로 포장해서(WRAP- $BK) 반환한다.

bkElement에서 제공되는 메서드로 스타일(setStyle)을 지정하고

B 클래스 다음 위치(appendBefore)에 배치하게 된다.

(JQuery-insertBefore와 매우 유사하게 사용한다. )

위 코드를 일반 JS 코드로 구현하면 다음과 같다.

(실제로는 조금 더 복잡하게 처리 해야 한다.)

           
            var A = document.createElement("DIV");
           
            A.style.width = (parseInt(B.style.width) || B.clientWidth) + "px";
           
            B.parentNode.insertBefore(A, B); 



bkLib는 많이 사용되는 함수의 묶음 클래스로 볼 수 있다.

자바의 Static 클래스처럼 bkLib.메서드()로 호출해서 사용한다.

배열화 하거나(toArray), 배열에서 값을 찾기(inArray),

이벤트 추가(addEvent) / 삭제(cancelEvent) 등의 기능을 가지고 있다.


bkEvent 클래스는

이벤트를 추가하고(addEvent), 실행하는(fireEvent) 클래스 이다.

실제 JS의 이벤트와는 차이가 있다.

앞서의 이벤트들은 모두 HTML 태그들에

이벤트를 적용해서 발생하는 것이고,

bkEvent의 addEvent는

프로그램(NicEdit)이 지정된 이벤트들을 가지고 있다가

실행하라고 하면 그때 서야 프로그램 상에서 호출되는 이벤트이다.

즉, bkElement와 bkLib에서 지정된 이벤트 함수는

사용자의 행동에 의해 시스템(웹 브라우저)에서 호출되고,

bkEvent에서 지정된 함수는

함수 리스트(eventList)에 보관하고 있다가

프로그램 상에서 호출된다.

예로,

에디터에는 굵게, 이탤릭체 등 다양한 편집 버튼이 있다.

이 버튼들을 클릭하는 것은 bkElement의 addEvent를 이용하여 함수를 지정하고,

사용자가 마우스로 선택하면 해당 함수가 실행이 된다.

하지만, 사용자가 에디터에서 작업하다 다른 곳을 클릭한 경우

에디터는 버튼을 누를 수 없게 만들어야 한다 (Disable).

에디터에는 많은 버튼들이 있는데

이 모든 버튼 클래스에 Disable 이벤트를 만들고 등록했다가 (addEvent)

사용자가 에디터에서 작업하다 다른 곳을 클릭한 경우 이 이벤트들을

호출하여(fireEvent) 버튼을 사용할 수 없게 만들어 준다.

이 방식을 쓰지 않으면

모든 컨트롤의 함수를 직접 호출해야 하는 불편함이 있다.


$BK는 함수로 보는 것이 적당한데

파라메터가 문자열이면 해당 문자열을 ID로 HTML 태그를 찾아서 반환하고 (getElementById)

아니면 HTML 태그 클래스로 보고 bkElement로 포장하여 반환한다.

bkElement로 포장한다는 말은

HTML 태그이지만 bkElement의 메서드들을 사용할 수 있다는 것이다.


마지막으로 두 개의 내부 함수(closure)를 사용한 예가 있는 데,

내부 함수에 대한 개념은 찾아보기 바라며

prototype으로 함수에 두 개의 기능을 추가 하였다.

closure, closureListener이다.

이름이 내부 함수(closure)이지 기능이 내부 함수이지는 않다.

내부 함수 기법을 사용해서 이름을 그렇게 붙인 것 같고,

실제 용도는 전혀 다르다.

둘 다 이벤트 호출시 this를 특정 클래스로 나타내기 위해 사용하는 것이다.

즉, 버튼을 클릭(click)하면 해당 이벤트 내에서 this는 버튼이다.

버튼을 bkElement로 생성한 경우

this는 버튼이 아닌 버튼을 생성한 bkElement가 되어야 한다.

둘의 차이점은 closure는 파라메터 없이 호출되는 경우이고,

closureListener는 파라메터를 필요로 하는 경우에 사용한다.

예로, JS에서는 mousedown이벤트에서 event개체가 파라메터로 넘어온다.

function mousedown(event) {

처리 ~

}

이 event개체를 파라메터로 받아서 사용할 경우 closureListener,

없이 사용할 때는 closure로 사용하면 된다.

파라메터로 지정할 필요 없이

window.event로 사용해도 되지만, 이 개체는 Firefox에서 지원되지 않는다.


사용예

      bkLib.addEvent(document.body, "mousedown", this.selectCheck.closureListener(this))

      bkLib.addEvent(F, "submit", this.saveContent.closure(this)) 


selectCheck와 saveContent는 함수로 이밴트 핸들러이고

Function.prototype로 함수 원형을 수정하였기 때문에

이상과 같이 사용자 함수 뒤에 추가하여 지정하는 식으로 사용한다.


정리하면,

NicEdit를 구성하는 기본 라이브러리(클래스)는

클래스의 첫 부모(시조) bkClass(Object),

NicEdit용 HTML 태그 컨트롤을 위한 부모 bkElement,

NicEdit용 HTML 태그 컨트등에 이벤트를 보내기 위한 bkEvent,

잡다하게 사용하는 라이브러리 bkLib,

마지막으로 이벤트의 발생자를 바꾸는 closure가 있다.

구현 방법이나 사용법이 JQuery와 같거나 유사하다는 것을 알 수 있는데,

NicEdit의 라이브러리들은 복사해서

자체 제품 개발시 사용한다면 큰 도움이 될 것이다











다음 내용을 읽기 전에 웹 에디터와 관련된

몇 가지 용어에 대해서 이해해야 한다.

편집 기능을 제공하는 버튼,

버튼들을 배치하는 툴바

실제 문서를 편집하는 에디터에 대해서 알고 있어야 한다 (그림 참조).


NicEdit의 에디터 기능과 관련된 클래스 구조만

다음 그림과 같이 정리했다.


NicEdit는 에디터 하나를 의미하는 클래스 nicEditor와

여러 개의 에디터(nicEditor)들을 관리하는 클래스인 nicEditors가 기본 클래스이다.

하나의 웹 페이지에 여러 개의 웹 에디터가 사용될 경우를 위한 클래스 이다.

nicEditors는 클래스라기 보다는 Json 구조체의 느낌이 강하고

다수의 에디터와 플러그인 관리를 주 목적으로 하고 있다.

플러그인은 확장 기능을 의미하는 것으로

nicEditors에서 Json형태로 등록(registerPlugin)해서 가지고 있다가

nicEditor의 생성자에서 실행하여 보관한다.

NicEditors 플러그인 정보 관리 외에

다수의 nicEditor 사용시 정보를 보관하고 있으면서

ID로 해당 nicEditor를 찾는 기능(findEditor) 등을 제공한다.


nicEditor는 bkClass에서 상속받아

생성자에서 옵션과 플러그인 생성 등의 기능을 시작한다.

(bkEvent에서도 상속 받았는데, 별도로 정리한다.)

nicEditor는 제공되는 데모 파일의 소스를 보면 알 수 있지만 (demo02.html)

nicEditor를 생성함(new)으로써 웹 에디터를 실행한다.

<script type="text/javascript">
bkLib.onDomLoaded(function() {
    new nicEditor().panelInstance('area1');
    new nicEditor({fullPanel : true}).panelInstance('area2');
    new nicEditor({iconsPath : '../nicEditorIcons.gif'}).panelInstance('area3');
    new nicEditor({buttonList: ['fontSize','bold','italic','underline','strikeThrough','subscript','superscript','html','image']}).panelInstance('area4');
    new nicEditor({maxHeight : 100}).panelInstance('area5');
});
           
</script> 


하지만

nicEditor는 인스턴스(instance) 관리와 판넬(Panel) 관리를 주로 한다.

nicEditor의 판넬(nicEditorPanel)은

편집에 사용되는 버튼들을 놓는 장소(toolbar)를 의미하고,

판넬 생성(setPanel)과 삭제(removePanel) 기능이 있다.

판넬 생성시(construct) 버튼을 놓을 장소(toolbar)를 생성하고,

사용할 버튼 리스트(buttonList : 위 코드 중 4번째 new)를 확인해서

버튼들을 생성(addButton)한다.


nicEditor의 에디터 관련(문서편집) 기능은

실제로는 nicEditorInstance와 nicEditorIFrameInstance에 구현 되어 있다.

nicEditor는 웹 브라우저가 오페라이면 nicEditorIFrameInstance를 사용하고

아니면 (IE, FireFox 등) nicEditorInstance를 사용한다.

nicEditorIFrameInstance는 nicEditorInstance에서 상속 받은 클래스로

Iframe 태그를 사용하는 차이가 있다.


nicEditor는 인스턴스를 관리한다는 측면에서 nicEditors와 유사하다.

하나의 툴바에 여러 개의 편집창을 쓸 수 있다는 것으로

여기에 정리하지 않고 넘어간다(데모 4 참조).


nicEditor의 nicCommand는

실제로는 nicEditorInstance의 nicCommand를 호출하고

nicEditorInstance의 nicCommand에서는

JS의 document.execCommand를 호출하여

편집 기능을 구현하고 있다.


정리하면, 편집 영역을 관리하는 nicEditorInstance,

편집 기능을 사용자가 사용할 수 있게 하는 버튼은 nicEditorButton,

버튼이 놓이는 툴바는 nicEditorPanel,

이들을 관리하는 nicEditor로 구성된다.


nicEditorInstance에는 편집 기능 설정과 명령어 실행 외에도

웹 브라우저에서 제공하는 Selection, Range을

메서드(getSel, getRng)로 구현하였다.

이 개체는 편집기에서 현재 선택된 영역(클래스)을 의미하는 것으로

제대로 된 웹 에디터를 구현하기 위해서는 이 개념을 잘 알고 활용해야 하는데,

NicEdit에서는 많이 활용되지 않았다.

제대로 웹 에디터를 제작하려면 찾아보길 바라고 여기서는 넘어간다.


편집 기능에서 마지막으로 알아야 할 것은 nicEditorConfig이다.

nicEditorConfig는 bkClass에서 상속 받지만

Json 구조의 데이터로 보는 것이 적절하다.

nicEditorConfig는 NicEdit의 실행을 위한 환경 설정 클래스로,

buttons, iconsPath, buttonList, iconList의 변수를 가지고 있다.

var nicEditorConfig = bkClass.extend({
    buttons: {
        'bold': {
            name: __('Click to Bold'),
            command: 'Bold',
            tags: ['B', 'STRONG'],
            css: {
                'font-weight': 'bold'
            },
            key: 'b'
        },
        'italic': {
            name: __('Click to Italic'),
            command: 'Italic',
            tags: ['EM', 'I'],
            css: {
                'font-style': 'italic'
            },
            key: 'i'
        },  

          생략 ~~

buttons는 NicEdit에서 지원되는 모든 버튼들의 정보를 가지고 있다.

그리고, 각 버튼에 대하여 고유 이름(bold, italic, underline등)을

나타내는 문자열을 키로 하여 Json형식으로

세부 정보(name, command, tags, css, key)를 가지고 있다.

세부 정보 중 name은 이름을 의미하지만,

버튼 위에 마우스가 올라오면

해당 버튼의 기능을 설명하는 툴팁 문장(예: Click to Bold)으로 활용된다.

command는 실제 명령어를 의미하고

document.execCommand에 사용할 명령어로

이 명령어에 의해 편집이 실행된다.

예: document.execCommand('bold', false, null)

이 명령어는 NicEdit에서 nicEditor의 nicCommand에 의해 실행된다.

tags는 편집된 문장에 입력 커서가 위치하면 선택된 문장에 적용된 tag에 따라

버튼을 활성/비활성 시키기 위해 사용되는 태그다.


예로, 굵게(bold)가 적용된 문장에 입력 커서가 있는 경우

bold 버튼이 눌러지고, 다른 버튼은 그대로 있게 된다.

이 기능을 구현하기 위해 현재 선택된 문장의 태그를 확인해서

태그가 B이거나 strong이면 bold 버튼이 눌러지게 만든다.

css도 같은 역할을 하는 것으로,

예전 HTML은 태그(B, STRONG)로,

최근에는 CSS('font-weight': 'bold')로 작성된다.

NicEdit는 CSS와 같이 설정은 있지만 기능은 개발되지 않았다.

글자를 굵게 만드는 bold의 경우

NicEdit는 execCommand룰 사용하기 때문에

웹 브라우저에 따라 B또는 STRONG 태그가 생긴다.

태그 인식도 이렇게 인식한다.

하지만 최근의 웹 에디터는

생성은 CSS('font-weight': 'bold')로 하고,

인식도 CSS로 한다.

key는 단축키를 의미하는 것으로 여겨지고,

구현은 되어 있지 않은 것 같다.


buttonList는 이상에서 정의된 버튼들 리스트로 웹 에디터에서 사용 여부를 나타낸다.

즉, buttonList에 이름이 나열된 버튼만 생성이 된다.

nicEditorConfig에서는 기본값들로 거의 모든 버튼 이름을 가지고 있다.

앞서의 nicEditor생성 코드(demo02.html)의 4번째 예처럼

New nicEditor({buttonList : ['fontSize','bold','italic','underline','strikeThrough','subscript','superscript','html','image']}

         ).panelInstance('area4');

nicEditor 생성시 파라메터로 사용할 버튼만 buttonList에 넣어서 전달하면 해당 버튼만 생성된다.

이러한 기법도 알아두면 좋다.

nicEditor의 생성자에서

options 변수에 nicEditorConfig가 생성되어 할당된다.


var nicEditor = bkClass.extend({
      construct: function(C) {
         this.options = new nicEditorConfig();
   bkExtend(this.options, C);
             

즉, 이것은 기본값 역할을 한다.

nicEditorConfig는 4개의 메인 키(변수)를 가지는 Json 데이터이고,

options에 할당되었기 때문에 options도 동일한 값을 가지고 있게 된다.

bkExtend는 값을 복사(상속)하는 기능을 하는 것으로

파라메터 C에 4개의 메인 키가 있으면 해당 키로 값이 복사되고,

없으면 넘어가기 때문에 기존 값이 유지 된다.

앞서의 nicEditor 생성 코드를 예로 하면,

nicEditor 파라메터로 Json({}) 값이 주어진 것은 buttonList이다.

따라서, 나머지 3개의 키는 nicEditorConfig에 지정된 값이 사용되고,

버튼은 파라메터에 지정된 buttonList 의 8개 버튼만이 사용된다.

demo02.html을 실행해서 4번째 NicEdit를 보면 확인 할 수 있다.


IconsPath는 버튼에 사용되는 이미지 파일의 위치(디렉토리 경로)를 의미하는 것으로

NicEdit의 모든 버튼은 다음 그림과 같이 하나의 이미지로 저장되어 있다.

        

nicEditorIcons.gif

iconList는 이미지 파일에서 버튼별 이미지 위치를 의미한다.

예로, "bgcolor": 1 으로 지정된 것은

bgcolor 버튼이 첫번째(0부터 시작하므로 실제론 2번째) 이미지를 사용한다는 의미이다.

버튼은 HTML button 태그로 생성하는 것이 아닌

DIV에 음영을 주어서 생성하고,

배경 이미지(backgroundImage)로 IconsPath의 경로를 지정해 준 뒤

이미지 좌표(backgroundPosition)를 버튼 위치 * -18(버튼크기)로 계산해서 지정한다.

bgcolor 버튼은 1이니 1 * -18 로 계산해서

TOP은 고정이고 left 위치가 -18이 되고,

div 크기(width)가 18이니,

해당 이미지만 버튼 위에 표시되어 보이게 된다.

다음 그림과 같이 [DOM탐색기]로 보면 쉽게 알 수 있다.

많은 이미지를 네트워크를 통해 전달하고, 처리하는 것 보다

하나의 이미지를 전송하고, 메모리에 올려서 처리하는 이 방식이 흔하게 사용된다.

이 처리는 nicEditor클래스의 getIcon 메소드에 구현되어 있다.



앞서 NicEdit 라이브러리 설명 중 

fireEvent에 대하여 간단하게 정리하였다.

여기서는 nicEditorInstance 클래스를 중심으로

보다 상세하게 구현 방법을(사용방법) 정리한다.


정리하기 전에 다음 흐름을 확인하고 넘어간다.

이 장에서 정리한 내용을 그림으로 그린 것으로 

익숙하지 않은 사람에게는 

그림과 정리한 내용을 같이 보는 것이 더 쉬울 수 있다.


먼저, nicEditorInstance 클래스 생성 후

mousedown과 keyup 이벤트 핸들러로 selected를 지정했다. 

var nicEditorInstance = bkClass.extend({
    ~~ 생략 ~~
    init: function() {
        this.elm.setAttribute("contentEditable", "true");
        if (this.getContent() == "") {
            this.setContent("<br />")
        }
        this.instanceDoc = document.defaultView;
        this.elm.addEvent("mousedown", this.selected.closureListener(this)).addEvent(생략).addEvent("keyup", this.selected.closure(this));
        this.ne.fireEvent("add", this)
    },
    ~~ 생략 ~~
     

사용자가 편집기에서 동작을 할 때 마다

selected로 받아서 무엇인가를 처리하겠다는 것이다.

selected: function(C, A) {
    if (!A && !(A = this.selElm)) {
        A = this.selElm()
    }
    if (!C.ctrlKey) {
        var B = this.ne.selectedInstance;
        if (B != this) {
            if (B) {
                this.ne.fireEvent("blur", B, A)
            }
            this.ne.selectedInstance = this;
            this.ne.fireEvent("focus", B, A)
        }
        this.ne.fireEvent("selected", B, A);
        this.isFocused = true;
        this.elm.addClass("selected")
    }
    return false
}, 

지정된 selected 핸들러의 코드를 보면

특정 변화가 있으면 3가지 이벤트를 발생 시키라고

niceditor에게 요구 한다(fireEvent).

마우스나 키보드를 눌러서 이벤트를 발생 시킨 대상

편집기인 나(this)와 현재 선택되었던 편집기(이전 편집기 - B)가 다르면

이전 편집기(B)에게는 포커스가 나갔다고(blur) 알리고(fireEvent),

이벤트를 발생시킨 나(this)에게는 포커스가 들어왔다고(focus) 알린다(fireEvent),

이것은 사용자가 나(this)를 마우스나 키보드로 클릭했다는 당연한 것을 의미한다.


fireEvent를 niceditor(ne)의 메서드로 호출하는데

bkEvent 클래스에서 상속 받은 메서드이다.

niceditor는 bkClass, bkEvent 클래스에서 상속 받았다.

bkEvent 클래스의 fireEvent에서는

다음 코드와 같이 등록된 이벤트들(eventList)에게

지정된 이벤트를 실행하라고 알림을 준다(apply).

함수를 실행하는 것과 같은데,

this를 해당 개체로 하기 위해 apply를 사용한다.

apply에 대한 자세한 내용은 찾아보길 바란다.

var bkEvent = {
    addEvent: function(A, B) {
      ~~ 생략 ~~
    fireEvent: function() {
        var A = bkLib.toArray(arguments),
            C = A.shift();
        if (this.eventList && this.eventList[C]) {
            for (var B = 0; B < this.eventList[C].length; B++) {
                this.eventList[C][B].apply(this, A)
            }
        }
    }
}; 


niceditor에 이벤트를 등록하는 대상들은 모두 버튼들로

편집기가 포커스를 가지면 버튼을 활성화 시키고

편집기가 포커스를 잃으면 버튼을 비활성화 시키는 것이다.

따라서, bkEvent(nicEditor)의 addEvent를 호출하는것은 

nicEditorButton와 nicEditorSelect

그리고 이들로부터 상속 받은 자식들이다.

nicEditorButton 생성자에서 다음 코드를 확인할 수 있다.

파라미터 B는 눈치로 nicEditor(ne)임을 알 수 있다.

선택되면(포커스가 가면 selected) enable함수를 호출하도록,

포커스를 잃으면 disable 함수를 호출하도록 지정하였다.


 var nicEditorButton = bkClass.extend({
    construct: function(D, A, C, B) {
        this.options = C.buttons[A];
        this.name = A;
        this.ne = B;
        this.elm = D;
        ~~ 생략 ~~
        B.addEvent("selected", this.enable.closure(this)).addEvent("blur", this.disable.closure(this)).addEvent("key", this.key.closure(this));
        this.disable();
        this.init()
    },

nicEditorInstance에서 focus 이벤트가 있었는데,

버튼에서 사용하지 않는다.

버튼에서는 focus 대신에 selected를 사용한다.

focus가 아닌 selected로 사용한 이유는

(이름은 붙이기 나름이지만)

버튼 활성화 외에 현재 선택된 영역의 HTML 태그를 확인하여

해당 버튼을 누른 상태(실행 여부 표시)로

보여 주기 위한 기능(checkNodes)도 있기 때문인 것으로 추측한다.




마지막은 NicEdit의 버튼과 플러그인 클래스이다.

버튼과 플러그인 클래스들이 JS의 객체지향을 가장 잘 보여준다고 생각한다.

다음 그림을 보면서 설명을 읽기 바란다.

NicEdit의 플러그인은 

크게 확장 버튼(nicEditorAdvancedButton)과

선택 상자(nicEditorSelect) 두 가지 이다.

버튼을 누르면 편집 명령이 실행되는

일반적인 버튼은 nicEditorButton으로 생성되고,

버튼을 누르면 별도의 창이 뜨는 것은 모두 플러그인으로 구현되었다.


즉, 버튼을 누르면 해당 버튼 아래에 

세부 설정 판넬이 나타나는 nicEditorAdvancedButton 클래스가 있고,

일반 버튼과 달리 사용자가 특정 값을 리스트에서 선택하는

HTML SELECT 태그를 구현한 nicEditorSelect 클래스가 있다.

확장 버튼(nicEditorAdvancedButton)은

일반 버튼과 동일하게 생성되고 기능하지만

사용자가 클릭하면 세부 설정 판넬이 나타나는 차이를 가지기 때문에

nicEditorButton에서 상속된다.

nicEditorAdvancedButton은 설정 판넬을 보여주고 감추는 기능 위주로 구현되고

nicEditorAdvancedButton를 상속받아

설정 판넬에 나타나는 것들(그림)에 따라 각 클래스가 다시 만들어 진다.

즉, 설정 판넬에

이미지 파일을 선택하게 하면 nicImageButton,

웹사이트 주소 정보를 입력 받으면 nicLinkButton,

글자색을 선택하게 하면 nicEditorColorButton,

배경색을 선택하게 하면 nicEditorBgColorButton 클래스를 구성하였다.


HTML select 태그를 이용하지 않고 div를 이용하여 직접 개발하였다.

HTML select 태그는 설정 판넬에 리스트들이 한 라인씩 출력되고,

사용자가 선택하면 

버튼 부분에 해당 내용이 출력되면서 설정 판넬이 사라진다.

nicEditorSelect로 이 기능을 구현하고

설정 판넬의 내용에 따라 3가지 하위 클래스를 개발하였다.

폰트 이름이 나열되는 nicEditorFontFamilySelect,

폰트 크기가 나열되는 nicEditorFontSizeSelect,

폰트 포맷(스타일)이 나열되는 nicEditorFontFormatSelect 클래스를 개발하였다.

먼저, NicEdit에서 사용되는 (일반적인) 버튼의 클래스이자

다른 버튼들의 가장 상위의 부모인 nicEditorButton을 살펴 본다.

nicEditorButton은 bkClass에서 상속 받아

생성자에서 DIV를 이용하여 버튼을 만들어 낸다.

버튼의 플랫(flat) 기능을 위해 active, deactive 함수가 있다.

마우스가 버튼 위(hover)에 오면 버튼 외곽선을 그리고(active),

벗어나면 원 상태로 돌아가는 기능을 가지고 있다(deactive).

이러한 상태 변화는 updateState에서 이루어 진다.

NicEdit에 포커스가 있으면 버튼이 눌러질 수 있지만 (enable)

포커스가 다른 곳으로 이동하면 버튼을 누를 수 없게 만든다(disable).

버튼을 선택하면(mouseClick),

nicEditorConfig에서 지정된 command를 실행한다.


nicEditorPane은 판넬(div)를

보였다가 숨기는 기능이 전부 이다(toggle).

이 판넬에 컬러 박스들을 보이거나,

리스트들을 보여서 사용자가 선택하게 하는 역할을 한다.

nicEditorAdvancedButton은

nicEditorPane을 마우스로 버튼을 클릭하면 보이고, 

다시 버튼을 클릭하면 숨기는 기능이 첫번째이다 (mouseClick).

그리고, 아주 재미있는 기능이 addForm()함수에 있다.

Json 데이터를 파라메터로 받아서

nicEditorPane에 HTML form과 입력 상자 등을 자동으로 생성한다.

nicEditorAdvancedButton의

addForm() 함수 코드를 살펴보는 것은 다소 어렵기 때문에

자식 클래스에서 호출하는 사용법 위주로 살펴본다.

자식 클래스의 역할은

nicEditorPane에 각자에게 필요한 정보를 입력하도록 설정하고

입력 받은 값 데로 실행하는 것이다.

nicLinkButton은 URL 링크 정보를 필요로 한다.

따라서, URL 링크를 입력하는 입력 상자(text-href),

링크의 제목(text - title),

링크를 사용자가 클릭했을 때,

현재 창에 해당 링크 내용을 출력할 것인지,

새로운 창에 출력할 것인지 선택하게 하는 선택창(select - target)의

3개 HTML 태그가 필요하다.

폼 작성 완료를 위한 버튼(submit)도 있어야 한다.


nicLinkButton은 addPane과 submit메소드 두가지로 구성되었다.

addPane은 앞서 언급한 입력들을 위해

nicEditorPane에 설정할 내용들을 설정하는 일종의 이벤트로

부모인 nicEditorAdvancedButton에서

nicEditorPane이 보일 때 호출하고 있다.

부모에서 호출하기 때문에

자식에서는 오버라이드(override)한 것처럼 사용한다.

submit도 유사한데

부모에게서 선언하고, 구체적인 코드를 작성하지 않았다.

구체적인 코드는 자식에게서 작성하여 (override)

객체 지향 프로그래밍처럼 구현하였다.


nicLinkButton의 addPane에서 사용된 addForm 코드와

실행 화면을 비교하며 사용법을 정리한다.

var nicLinkButton = nicEditorAdvancedButton.extend({
    addPane: function() {
        this.ln = this.ne.selectedInstance.selElm().parentTag("A");
        this.addForm({
            "": {
                type: "title",
                txt: "Add/Edit Link"
            },
            href: {
                type: "text",
                txt: "URL",
                value: "http://",
                style: {
                    width: "150px"
                }
            },
            title: {
                type: "text",
                txt: "Title"
            },
            target: {
                type: "select",
                txt: "Open In",
                options: {
                    "": "Current Window",
                    _blank: "New Window"
                },
                style: {
                    width: "100px"
                }
            }
        }, this.ln)
    },

 

빈문자열, href, title, target 4가지의 Json 키가 있고

각각이 다시 Json 값을 가지고 있다.

이 4가지가 실행 화면에서 각각 한 행 인 걸 알 수 있다.

이 키는 각 입력 컨트롤의 아이디(HTML ID 속성)가 되는 것으로

빈 문자열은 입력 받는 것이 없다는 것을 의미한다.

그리고 type은 입력 컨트롤의 종류로

title은 입력 컨트롤이 아닌 단순 문자열(txt) 출력을 의미하고,

text는 입력 상자(Text 태그),

select는 선택 상자(Select 태그)

contents는 다중 입력 상자(TextArea 태그)를 생성한다.

먼저, 빈 문자열은 입력 받는 컨트롤이 없다는 걸 의미한다.

따라서 하위 속성에서 type은 단순 문자열 출력인 title이고,

추가할 링크를 입력하라(Add/Edit Link)는 제목이 주어진다.

다음의 href는 입력 상자(type:text)를 생성하고

기본값(value)을 "http://" 준 뒤 사용자의 입력을 대기한다.

submit후에 아이디가 href 인 입력 상자의 값을 가져와 사용한다.

Txt 속성은 일종의 label 역할을 한다.

하나의 행은 안내문 역할을 하는 txt와

사용자의 입력을 나타내는 type로 구성되었다.

세 번째로 title키는

제목을 입력 받는데, url의 제목을 의미한다.

마지막으로 target은 type이 select로

HTML 선택 박스(select)를 생성한다.

선택할 항목은 options하위에 json으로 지정한다.

addForm을 호출하면

마지막에 submit 버튼(질의보내기)이 무조건 생성된다.

이 버튼이 submit()와 연동되어 있다.

nicLinkButton에서 오버라이드한 submit()의 내용은

사용자가 입력한 url 값이 없으면 오류 메시지를 출력하고

값이 있으면 먼저 판넬부터 숨긴다 (제거-removePane).

그리고 사용자가 입력한 값을 파라미터로

nicCommand를 실행하여 html link (a href)를 생성하게 된다.


정리하면

nicEditorButton, nicEditorAdvancedButton, nicLinkButton으로 

이어지는 계층 구조를 이해하고

왜 계층이 생겼는지 이해해야 한다.

모든 버튼에 필요한 기능은 nicEditorButton 클래스,

이 중에 버튼을 누르면 

상세 정보를 입력할 수 있도록 하는 nicLinkButton 클래스가 있다.

NicLinkButton 이 외에도

nicEditorColorButton, nicEditorBgColorButton, nicImageButton이 있는데

이들의 공통점은 설정 판넬(nicEditorPane)이 필요하다는 것이고

차이점은 설정 판넬에서 입력 받는 값들과

실행 방식(command)은 각각 모두 다르다는 것이다.

공통적인 것은 모아서 구조화 하거나 객체지향으로 개발하게 되는데,

여기서는 부모화(추상화 - nicEditorAdvancedButton)해서 객체지향 프로그래밍을 하였다.

차이점은 각각의 클래스에서 구현하게 된다.

나머지 3개의 클래스에 대한 설명은 하지 않지만

어떤 차이를 어떻게 구현했는지 알면 도움이 많이 되니 확인해 보길 바란다.


마지막으로

nicEditorSelect 클래스를 정리한다.

nicEditorSelect의 자식들은

nicEditorAdvancedButton의 자식들과 달라서 모두 유사하다.

즉, 버튼과 선택된 내용을 보여주는 라벨이 있는 select 태그로 구성되었다.


nicEditorSelect는 nicEditorButton과

nicEditorAdvancedButton를 합친 기능을 하고 있다.

nicEditorSelect는 버튼을 누르면 설정 판넬(nicEditorPane)이 나오는 것으로 

nicEditorAdvancedButton과 유사하다.

차이점은 버튼 보다 너비가 넓고, 

넓은 공간에 사용자가 선택한 값을 출력한다는 차이가 있다.

그리고, 설정 판넬의 내용도 사용자 입력 값이 아니라

사용자가 선택할 수 있는 리스트들 

(폰트명-Arial, Comic Sans, Courier New 등, 

폰트크기-P, pre, h6, h5 등, 

폰트 형식-8pt, 10pt, 12pt, 14pt 등)이 나열되는 것으로 

HTML SELECT 태그와 동일하다.

차이점은 리스트에서 

폰트명일 경우 폰트명이 적용되어 값이 출력되고,

폰트 크기일 경우 폰트 크기가 적용되어 출력된다는 것이다 (위 그림 참조).

nicEditorSelect 하위에 3개의 자식이 있는 것은

폰트명, 폰트크기, 폰트 형식이 적용된 것을 보여 주기 위해서 이다.

따라서, 자식 클래스인 

폰트명(nicEditorFontFamilySelect), 

폰트크기(nicEditorFontFormatSelect), 

폰트형식(nicEditorFontSizeSelect)에는

리스트 항목을 나타내는 Json 데이터인 sel 변수와

항목 개수 만큼(for (itm in this.sel) )

폰트명(font face), 폰트 크기(font size), 폰트 형식(p, pre, h6…) 태그를

적용하는 코드가 있을 뿐이다.

이렇게 HTML 적용된 리스트가

nicEditorSelect 클래스 자식들이다.

var nicEditorFontSizeSelect = nicEditorSelect.extend({
    sel: {
        1: "1&nbsp;(8pt)",
    ~~ 생략 ~~
    },
    init: function() {
        this.setDisplay("Font&nbsp;Size...");
        for (itm in this.sel) {
            this.add(itm, '<font size="' + itm + '">' + this.sel[itm] + "</font>")
        }
    }
});
var nicEditorFontFamilySelect = nicEditorSelect.extend({
    sel: {
        arial: "Arial",
    ~~ 생략 ~~
    },
    init: function() {
        this.setDisplay("Font&nbsp;Family...");
        for (itm in this.sel) {
            this.add(itm, '<font face="' + itm + '">' + this.sel[itm] + "</font>")
        }
    }
});
var nicEditorFontFormatSelect = nicEditorSelect.extend({
    sel: {
        p: "Paragraph",
    ~~ 생략 ~~
    },
    init: function() {
        this.setDisplay("Font&nbsp;Format...");
        for (itm in this.sel) {
            var A = itm.toUpperCase();
            this.add("<" + A + ">", "<" + itm + ' style="padding: 0px; margin: 0px;">' + this.sel[itm] + "</" + A + ">")
        }
    }
}); 


계층 구조 중심으로 정리하면서

nicEditorButton의 메서드에 대한 정리가 부족하다.

nicEditorButton의 주요 메서드 중

버튼 활성과 관련된 것은 라이브러리에서 언급했다.

대부분 마우스가 버튼 위에 올라오면 (mousemove, hover)

평평한 버튼 주변에 선이 생기게 하는 기능,

마우스로 클릭한 기능(mousedown - activate/deactivate),

에디터가 포커스를 잃으면 버튼을 누를 수 없게 하는 기능(disable, enable) 등이 구현되어 있다.

모두 평범한 코드인데

checkNodes는 HTML DOM 구조를 조금 활용한 코드이다.

nicEditorInstance에서 언급한 Selection, Range로

현재 커서의 위치에 있는 HTML 개체를 가져와서

HTML 태그(CSS)가 무엇인지 판단하여 버튼을 활성화 시킨다.

do {
    if (this.options.tags && bkLib.inArray(this.options.tags, B.nodeName)) {
        this.activate();
        return true
    }
} while (B = B.parentNode && B.className != "nicEdit");
B = $BK(A);
while (B.nodeType == 3) {
    B = $BK(B.parentNode)
}
if (this.options.css) {
    for (itm in this.options.css) {
        if (B.getStyle(itm, this.ne.selectedInstance.instanceDoc) == this.options.css[itm]) {
            this.activate();
            return true
        }
    }

위 코드에서

앞서의 do 문에서는 nicEditorConfig에서 지정되어

각 버튼에게 주어진 tags값과 태그의 nodeName이 같은지(inArray) 비교하여

버튼을 활성화(activate) 시킨다.

현재 선택된 노드(B) 뿐 아니라 부모(parentNode) 노드까지 찾아보게(do) 구현하였다.

또는 CSS로 작성된 태그를 검출할 수 있도록

현재 선택된 노드(B)로 부터 CSS를 추출하여(getStyle) 버튼에 지정된 CSS와 비교한다.

checkNodes의 기능은 에디터에서 제법 중요한 기능이고

노드 탐색(parentNode)등의 코드는

JS 라이브러리(도구)에서 많이 사용되는 방법이라 잘 익혀두는 것이 좋다.




+ Recent posts