본문 바로가기

3.구현/HTML5&Javascript

[jquery] multiselect 플러그인 제작하기

들어가기

jQuery로 플러기인을 만들었던 그 경험을 공유하려고 글을 작성하였다. 만들려는 UI 컴포넌트는 multiselect로 select 컨트롤에 체크박스를 추가하여 여러 값을 선택할 수 있는 select이다. 물론 기능적으로는 문제는 없지만 플러그인을 작성하기 위해 매우 간단하게 작성하였으므로 실제로 사용하다보면 수정이 많이 필요하다. 그렇기에 실제 multiselect은 다른 플러그인을 사용하고 여기 multiselect은 간단한 플러그인 제작을 위해서만 참고하기 바란다.

작성자: ospace114@empal.com, http://ospace.tistory.com/

Multiselect 요구사항

Multiselect은 여러 체크박스로 동시에 여러 항목을 선택하는 컨트롤이다. 여기서는 이런 체크 박스를 사용한 셀렉트 박스와 몇가지 기능을 추가하여 작성하려고 한다. 먼저 기존 셀렉트 박스를 살펴보고 어떻게 사용하는지 살펴보자. 기존의 select가 다음과 같다.

<div style="width:300px">
<select id="foo" style="width:100%">
    <option value="1">one</option>
    <option value="2">two</option>
    <option value="3">three</option>
</select>
</div>

Fig 01. Select 큰트롤

여기에서 multiple 속성을 지정하면 여러 개 값을 선택할 수 있는 기능을 제공할 수 있다. 그러나 익숙하지 않은 형태의 컨트롤이 보일 것이다.

<div style="width:300px">
<select id="foo" style="width:100%" multiple>
    <option value="1">one</option>
    <option value="2">two</option>
    <option value="3">three</option>
</select>
</div>

Fig 02. Multiselect 속성이 적요된 Select 컨트롤

선택가능한 값이 리스트 형태로 표시되고 컨트롤키와 같이 클릭해야 여러 개 값을 선택할 수 있다. 그다지 편리하거나 직관적인 인터페이스가 아니가 그렇기 때문에 체크박스 형태의 셀렉트 박스를 많이 사용된다.
위의 select의 설정을 그대로 사용하여 multiselect을 구성하도록 만들 것이다. 간단하게 기능을 정리하면 다음과 같다.

  • Multiselect을 표시하기 위해 기존 select은 구성을 그대로 사용하며 별도로 mutiselect을 초기화를 위한 추가 값 업음
  • Multiselect의 값을 선택으로 인한 변경 이벤트를 기존 select을 처리하는 일반적인 jquery로 이벤트 처리
  • Multiselect의 선택된 값을 획득하려면 기본 select을 값을 획득하는 방법 동일하게 지원

이렇게 한번 구성하면 별도로 multiselect에 대해 몰라도 간단한 값을 변경에 대한 이벤트와 값을 획득하는 것이 기존 jQuery와 동일하게 처리된다. 즉, 기존에 jQuery로 사용하고 있다면 별다른 수정 없이 multiselect 플러그인만 적용하면 바로 사용할 수 있다.

$('#foo').on('change', function() {
    console.log('changed', $(this).val());
});
$('#foo').multiselect({height:'100px'});

플러그인 기본 구조

다음은 나름 대로 정리한 가장 기본적은 jQuery 플러그인 기본 구조이다. 특히, 컨트롤 형태의 플러그인 구조에 맞춰서 작성되었다. 단순히 UI 형태의 플러그인이 아닌 경우는 적합하지 않을 수 있다. 이 구조에서 플러그인 제작을 시작하겠다.

(function($){
  const PLUGIN_NAME = 'bar';
  const DEFAULTS = {/*기본설정값*/}; //(1)

  function Plugin(elem, opts) {    //(2)
    this.elem = $(elem); 
    this.opts = $.extend(DEFAULTS, opts);
    //구현
    this.init();
  }

  Plugin.prototype = {             //(3)
    init: function() {
        //함수 구현
      });
    }
  }

  $.fn[PLUGIN_NAME] = function(opts) { //(4)
    var args = arguments;
    if(undefined == opts || 'object' === typeof opts) {
      return this.each(function () {
        if($.data(this, PLUGIN_NAME)) return;
        $.data(this, PLUGIN_NAME, new Plugin(this, opts));
      });
    } else if('string' === typeof opts && 'init' != opts) {
      this.each(function() {
        var inst = $.data(this, PLUGIN_NAME);
        if ('function' === typeof inst[opts]) {
          inst[opts].apply(inst, Array.prototype.slice.call(args, 1));
        } else if ('unload' === opts) {
          $.data(this, PLUGIN_NAME, null);
        }
      });
    }
  };
})(jQuery); 

간단하게 설명하면 가장 먼저 $를 사용할 수 있는 jQuery을 매개변수를 갖는 익명함수를 정의한다. (1)은 처음 부분에 사용할 변수를 저장하는 영역이다. (2)는 플러그인 처음 로딩할 때 기본 구조 구성하는 Plugin 함수이다. (3)은 프로토타입으로 플러그인 초기화 및 지원되는 명령을 구현한다. (4)는 플러그인을 jQuery에 등록하고 기본적은 초기화 및 명령 수행하는 영역이다.

위의 기본 구성에서 작업해야하는 부분은 (1) 부분의 기본 설정 값(defaults)과 플러그인 이름(plugin_name)이다. 별도로 플러그인 이름을 입력받도록 되어 있지만 굳이 필요없다면 plugin_name 변수 들에 사용할 플러그인 이름을 직접 입력해도 된다.
여기서 사용할 플러그인 이름은 "multiselect_j"이고 기본 설정값을 별도로 사용하지 않았다. 이를 적용한 코드는 아래와 같다. 굳이 필요는 없지만 구분을 위한 Plugin() 대신에 Multiselect()을 사용했다.

(function($){
  var PLUGIN_NAME = 'multiselect_j';
  var DEFAULTS = {/*기본설정값*/};

  function Multiselect(elem, opts) {
    this.elem = $(elem); 
    this.opts = $.extend(DEFAULTS, opts);
    // 구현
    this.init();
  }

  Multiselect.prototype = {
    init: function() {
        // 함수 구현
      });
    }
  }
  // ...
})(jQuery); 

Multiselect을 위한 기본 레이아웃 구성

플러그인 구현 전에 작성할 플러그인의 레이아웃을 먼저살펴보자.

Fig 03. Multislect 컨트롤 레이아웃

현재 여기서 구현할 multiselect 컨트롤의 레이아웃이다. 전체를 감싸는 msj-options-base 클래스가 존재한다. 그 내부의 상단에 리스트 박스를 열고 닫을 버튼(button)이 있고, 아래에 옵션 값들이 표시될 div인 msj-options 클래스가 있다. 옵션 값들 중에 처음에 검색입력 텍스트 박스가 표시되는 msj-search가 있고, 옵션이 추가되는 msj-option이 존재한다. msj-option안에는 체크박스와 이름을 표시할 라벨이 존재한다. 그리고 화면을 표현 형식을 변경할 CSS은 별도 파일로 구성해서 제공되며, 위의 레이아웃에 있는 태그명과 클래스명을 기준으로 적용하게 했다.
먼저 기본적인 레이아웃을 Multiselect()에서 1차적으로 구현해보자. 가변적으로 변화하거나 값을 처리하는 부분은 나중에 init()에서 구현할 예정이다. 먼저 고정으로 표시되는 레이아웃이다.

function Multiselect(elem, opts) {
    var self = this;
    this.elem = $(elem);
    this.opts = $.extend(defaults, opts);
    // 유효한 대상인지 확인
    if(!('SELECT' === this.elem[0].nodeName && this.elem.attr('multiple'))) {
        throw 'multiselct must be a <select> with multiple attribute';
    }
    this.elem.hide(); //기존 요소 숨김
    //버튼 추가
    this.msj_btn = $('<button/>').text('Not Selected').css({'max-height':this.opts.height, width:this.opts.width});
    //옵션들이 추가할 영역
    this.msj_opt = $('<div/>', {class:'msj-options'}).css({'max-height':this.opts.height, width:this.opts.width});
    //앞의 내용을 기본 베이스에 추가
    this.msj_opt_base = $('<div/>', {class:'msj-options-base'})
        .append(this.msj_btn)
        .append(this.msj_opt);
    //검색 기능이 있는 경우 옵션영역에 추가
    if(this.opts.searchable) {
        this.msj_opt.append($('<div/>',{class:'msj-search'})
            .append($('<input/>', {type:'text', placeholder:'Search Keyword'})
            .on('change', function(e, v) {
                var val = $(this).val();
                self.msj_opt.find('div.msj-option').each(function() {
                    ~$(this).text().indexOf(val) ? $(this).show():$(this).hide();
                });

            })));
    };
    //화면 요소에 등록
    this.elem.after(this.msj_opt_base);
    //버튼 클릭시 옵션 목록을 열고 닫기 구현
    this.msj_btn.on('click', function() {
        self.msj_opt.is(':visible')?self.msj_opt.hide():self.msj_opt.show();
    });
    this.init();
}

조금 내용이 많아 보이자만 주석과 앞의 레이아웃을 참고하면 이해하는데 어렵지 않다. 여기서 재미 있는 부분은 새로운 객체를 추가는 부분을 보면 문자열 형태로 나열해서 추가하는 방식이 아니다.

this.msj_opt = $('<div class="msj-options"></div>');

위의 방식처럼 문자열로 나열해서 필요한 속성들을 지정할 수도 있다. 그러나 jQuery에서는 아래 방식을 권장하고 있다.

this.msj_opt = $('<div/>', {class:'msj-options'});

중간에 이벤트도 처리하고 있다. 하나는 버튼이고 다른 하나는 검색이다. 둘 다 고정된 컨트롤이기 때문에 생성시 이벤트 핸들러도 등록하였다. 버튼에 대한 이벤트는 옵션 리스트를 표시하고 숨기는 처리를 한다.

self.msj_opt.is(':visible')?self.msj_opt.hide():self.msj_opt.show();

로직은 복잡하지 않다. 현재 보여지고 있다면 숨기고, 아니면 표시한다.
검색 처리하는 핸들러는 조금 복잡하다. 그렇다고 엄청 복잡한 코드는 아니다.

function(e, v) {
    var val = $(this).val();
    self.msj_opt.find('div.msj-option').each(function() {
        ~$(this).text().indexOf(val) ? $(this).show():$(this).hide();
    });
}

로직 상으로는 옵션들에서 텍스트를 출력해서 현재 검색어가 포함되어 있는지 확인해서 포함되면 표시하고, 그렇지 안다면 숨기도록 한다.
여기까지는 기본 셀렉트 버튼 정도만 표시되고 내용은 아무 것도 없는 상태이다. 다음으로 선택할 값을 표시해보자.

Select의 옵션값을 체크박스로 표시하기

Multiselct()가 생성되고 다음에 this.init()에 의해서 초기화된다. 여기서는 기존 select 옵션값들을 multiselect의 체크박스로 추가하는 초기화 로직이 포함되어 있다.
로직을 간단하게 설명하면 기존 select 컨트롤에서 option들을 추출해서 해당 텍스트와 값을 획득해서 multiselect에서 라벨과 체크박스로 추가한다. 여기까지 설명하면 이해하는데 어렵지 않을거라 생각한다. 실제 코드를 살펴보자.

var opt_label_for = 1; //라벨과 체크박스가 연동되도록 사용할 ID 인덱스

function multiselect_add(self, text, value) {
    self.msj_opt.append(
        $('<div/>', {class:'msj-option'}).append(
            $('<label/>', {for:'msj-opt-'+opt_label_for}).append(
                $('<input/>', {id:'msj-opt-'+opt_label_for, type:'checkbox', value:value}).on('change', function() {
                    self.elem
                        .find('option[value="'+this.value+'"]')
                        .prop('selected', $(this).prop('checked'))
                        .trigger('change');
                    var checkedVal = self.msj_opt.find('input:checked').map(function(){ return $(this).parent().text(); }).toArray();
                    self.msj_btn.text(checkedVal.length > 0 ? checkedVal.join(',') : 'Not Selected');
                })
            ).append(text)
        )
    );
    ++opt_label_for;
}

Multiselect.prototype = {
  init: function() {
    var self = this;
    self.elem.children().each(function() {
      if('OPTION' == this.nodeName) {
        multiselect_add(self, this.text, this.value);
      }
    });
  }
};

먼저 init()을 살펴보면

  1. 기존 select 컨트롤에서 자식을 모두 추출
  2. 자식 중에서 노드명이 OPTION인 경우는 multiselect_add()로 체크박스 추가

multiselect_add()은 multiselect에 새로운 선택 값을 추가하는 함수이다. 앞의 레이아웃에도 다루었지만 아래와 같은 형태로 구성된다.

  • .msj-option
  • label[for=msj-opt-##]
  • input[type=text][id=msj-opt-##]

"##"은 숫자로 opt_label_for에서 계산된 값을 사용하며 라벨의 for와 텍스트 입력의 id는 같은 값을 가진다. 이 부분은 라벨 클릭해도 체크박스가 체크되도록 하기위한 부분이다. 여기서 중요한 부분이 체크박스의 선택 값이 변경되는 경우의 이벤트 핸들러이다.

  • 체크박스에서 클릭시 처리하는 이벤트 핸들러의 역활은 원본 셀렉트 박스의 같은 값을 가지는 옵션을 선택하도록 한다. 이렇게 하면 multiselect에서 선택된 체크박스의 값이 원본 셀렉트 박스의 선택된 값으로 획득하도록 한다.
  • 선택된 값들을 버튼 텍스트으로 표시한다.

코드 이해하는데는 어렵지 않을 것이다.

self.elem
    .find('option[value="'+this.value+'"]')
    .prop('selected', $(this).prop('checked'))
    .trigger('change');
var checkedVal = self.msj_opt.find('input:checked').map(function(){ return $(this).parent().text(); }).toArray();
self.msj_btn.text(checkedVal.length > 0 ? checkedVal.join(',') : 'Not Selected');

Multiselect에서 새로운 체크박스를 동적으로 추가

앞의 체크박스를 추가하는 부분은 기존에 있는 값들을 그대로 복제해서 사용하였다. 실제 동적으로 동작 중에 새로운 선택값을 추가할 경우가 생긴다. 이럴때 단순히 multiselect에 체크박스를 추가하는 작업만으로 끝나지 않는다. 여기서 구현하는 플러그인의 특징으로 인해 원래 셀렉트 박스에도 변경사항을 반영해야 한다. Multiselect에 추가된 체크박스의 값과 텍스트를 원래 셀렉트박스의 옵션을 추가하는 로직이 추가로 구현해야한다.

추가로 multiselect에 추가에 대한 명령이 새로 만들어졌다. 플러그인에서는 명령 처리하는 것을 어떻게할지 간단하게 살펴보자. 기존에 새로운 기능에 대해서 함수로 정의하고 함수를 호출함으로서 원하는 기능을 동작시켰다. 이렇게 할 경우 jQuery에 등록되는 함수 개수가 많아지거나, 트리형태 함수구조가 만들어질 수 있다. 또한 기존 jQuery의 연속적적인 호출 형식을 유지하려면 트리형태의 함수구조를 사용할 수 없게된다. 그렇기에 jQuery에서 추가 명령에 대한 구현을 함수 형태로 호출하는게 아니라 문자열 메시지 호출하는 방식을 권장한다.

$.fn[plugin_name] = function(opts) {
    var args = arguments;
    if(undefined == opts || 'object' === typeof opts) {
      return this.each(function () {
        if($.data(this, PLUGIN_NAME)) return;
        $.data(this, PLUGIN_NAME, new Plugin(this, opts));
      });
    } else if('string' === typeof opts && 'init' != opts) {
      this.each(function() {
        var inst = $.data(this, PLUGIN_NAME);
        if ('function' === typeof inst[opts]) {
          inst[opts].apply(inst, Array.prototype.slice.call(args, 1));
        } else if ('unload' === opts) {
          $.data(this, PLUGIN_NAME, null);
        }
      });
    }
};

위의 명령 처리에서 보면 unload라고 해서 현재 셀렉트 박스에 로딩된 multiselect 객체를 해제한다. 이 명령은 간단하게 만든 것이기에 완벽한 해제를 하기 위해서 추가적은 처리가 필요한다.
위의 명령에서 첫번재 매개변수는 명령 문자열이고, 다음에 오는 매개변수들은 해당 명령의 매개변수로 넘겨지게 된다. 이런 상황에서 새로운 값을 추가하는 'add' 명령에 대해서 아래와 같이 구현할 수 있다.

Multiselect.prototype = {
    add: function(text, value) {
        select_add(this, text, value);
        multiselect_add(this, text, value);
    }
};
function select_add(self, text, value) {
    self.elem.append($('<option/>', {value:value}).text(text));
}

새로운 함수인 select_add()가 구현되었다. 단순히 셀렉트 박스에 옵션을 추가했다. 생각보다 단순하다.

마지막: CSS를 적용

앞의 HTML만으로는 원하는 multiselect 모양을 얻을 수 없다. 여기서는 CSS을 별도로 분리해서 정의하였다. 물론 CSS를 HTML 내에 포함해서 플러그인이 동작하도록 할 수도 있지만, 그렇게 되면 확장성과 유연성이 떨어진다고 판단했기에 분리했다. 사용한 CSS는 다음과 같다.

.msj-options-base {
    box-sizing: border-box;
}
.msj-options-base > button {
    position: relative;
    width: 100%;
    text-align: left;
    border: 1px solid #aaa;
    background-color: #fff;
    white-space: nowrap;
    font-size: 13px;
    outline-offset: -2px;
    color: #aaa;
}
.msj-options-base > button[disabled] {
    background-color: #e5e9ed;
    color: #808080;
    opacity: 0.6;
}
.msj-options-base > button:before,
.msj-options-base > button:after {
    position: absolute;
    content: '';
    width: 0.7em;
    height: 3px;
    top: 50%;
    background: #999;
}
.msj-options-base > button:before {
    right: 9px;
    border-radius: 2px 1px 1px 2px;
    transform-origin: 50% 50%;
    transform: rotate(45deg);
}
.msj-options-base > button:after {
    right: 5px;
    border-radius: 1px 2px 2px 1px;
    transform-origin: 50% 50%;
    transform: rotate(-45deg);
}
.msj-options-base > .msj-options {
    -webkit-box-sizing: border-box; /* Safari/Chrome, other WebKit */
    -moz-box-sizing: border-box;    /* Firefox, other Gecko */
    box-sizing: border-box;         /* Opera/IE 8+ */
    overflow-x: hidden;
    overflow-y: auto;
    border: 1px solid #aaa;
    background-color: white;
    z-index: 2;
    display:none;
}
.msj-options-base > .msj-options label {
    padding: 0px 6px 0px 6px;
    font-size: 13px;
}
.msj-options-base > .msj-options > .msj-search input {
    width:100%;
    border: none;
    outline: none;
    padding: 0px 5px 3px 5px;
    border-bottom: 1px groove;
}

Github

https://github.com/ospace/javascript-works/tree/main/multiselect_j

결론

지금까지 jQuery의 플러그인을 제작해보았다. 앞의 플러그인은 UI에 관련된 것으로 DOM 객체를 처리하고 연동하는 방식에 대해 주로 다뤘다. 그러나 jQuery의 플러그인을 UI 뿐만 아니라 유틸성 프로그램이나 확장을 구현할 수 있다. 앞의 플러그인은 처음 플러그인을 제작하기 위한 가이드 예제이기에 기능적으로 미흡하거나 문제가 발생할 수 있기 때문에 실무에 부적합할 수 있다. 또는 이런 기본적은 UI에 해당하는 플러그인은 훌륭한 분들이 이미 제작해놓았기에 갔다가 사용하면 된다. 단지 이른 UI 플러그인이 부족하거나 새로운 UI를 제작하고 싶은 분들에게 도움이 되었기를 바란다. 모드 즐거운 코딩생활되세요. ^^ ospace.

참고

[1] nobleclem, jQuery MultiSelect, https://github.com/nobleclem/jQuery-MultiSelect

반응형