2009년 8월 20일 목요일

jQuery와 a 태그를 이용한 button 구현

이미 정찬명님의 텍스트 버튼 구현cross-browsing을 위한 시도들이 있었지만, 가장 호환성이 좋고 무엇보다 자바스크립트 동작 말고 말 그대로 링크를 위해 버튼을 쓰는 경우까지 깔끔하게 커버할 수 있는 a 태그를 이용할 때의 치명적인 단점은 버튼의 enabled/disabled 상태를 바꿀 수 없다는 점이다.

회사에서 작업 중인 웹어플리케이션에서 가상머신 인스턴스들을 관리하기 위한 일종의 웹콘솔 UI를 만드는데, 어떤 버튼은 AJAX를 통해 실행되는 기능이기 때문에 자바스크립트 동작을 연결하지만, 같은 형태로 같은 도구모음 영역 안에 들어가는 다른 버튼은 어떤 다른 페이지로의 링크를 담고 있다거나 한 경우가 있었다. 그리고 현재 설정이나 사용자 조작에 따라 버튼이 활성화/비활성화되어야 할 필요도 있었다. 처음에는 일단 a 태그에 아이콘 표시를 위한 span 하나 넣는 정도로 하였지만, 비활성화 처리도 해야겠고 하다보니 button 태그를 쓰게 되었다.

하지만 이미 여러 곳에서 지적하고 있는 것처럼 button 태그로 완벽하게 cross-browsing을 구현하는 건 거의 불가능에 가까울 뿐더러(하더라도 매우 지저분-_-), 어떤 건 말 그대로 링크를 위한 버튼용으로 a 태그를 쓰고 또 어떤 건 button 태그를 쓰고 이러면서 이 둘이 동시에 하나의 영역 안에서 일관성 있게 표현되도록 하자니까 또 그만한 CSS 노가다질이 없었다. (접근성 측면에서는 의미에 맞게 각각 써주는 것이 좋겠지만.)

그래서 a 태그를 그냥 쓰고, 링크만 한 경우는 링크 동작을 막고 click 이벤트 핸들러를 지정한 경우는 해당 이벤트 핸들러만 잠시 detach하는 식으로 disabled 상태를 구현하기로 마음먹었다. 시각적 효과 면에서 a 태그를 쓰는 것이 disabled 상태는 어차피 클래스 하나 먹여주면 간단히 해결할 수 있는 문제고, :hover 상태 표현도 IE6 같은 옛날 브라우저에서도 잘 먹기 때문에 가장 좋다. 그래서 약간의 검색 끝에 jQuery에서 비공식적으로 jQuery.data(element, 'events')라는 속성에 각 이벤트 이름별로 핸들러들을 배열로 저장하고 있다는 사실을 발견하여 이런 코드를 짜게 되었다.

$.fn.disable = function(options) {
    var defaults = {
        className: 'ui-state-disabled'
    };
    var settings = $.extend({}, defaults, options);
    return this.each(function() {
        if (this.tagName == 'button' || this.tagName == 'input')
            $(this).attr('disabled', 'disabled').addClass(settings.className);
        else {
            var $elem = $(this);
            var current_events = $elem.data('events');
            var removed_events = []; // store all existing click events bound via jQuery
            if (!$elem.data('disabled')) {
                if (current_events) {
                    $.each(current_events.click, function(index, item) {
                        removed_events.push(item);
                    });
                }
                $elem.unbind('click') // unbind all click events
                .bind('click', function(ev) { ev.preventDefault(); })
                .data('removed_events', removed_events)
                .data('disabled', true)
                .addClass(settings.className);
            }
        }
    });
};
$.fn.enable = function(options) {
    var defaults = {
        className: 'ui-state-disabled'
    };
    var settings = $.extend({}, defaults, options);
    return this.each(function() {
        if (this.tagName == 'button' || this.tagName == 'input')
            $(this).removeAttr('disabled').removeClass(settings.className);
        else {
            var $elem= $(this);
            var removed_events = $elem.data('removed_events');
            if ($elem.data('disabled')) {// restore saved click events
                $elem.unbind('click')
                .removeData('removed_events')
                .data('disabled', false)
                .removeClass(settings.className);
                if (removed_events) {
                    $.each(removed_events, function(index, item) {
                        $elem.bind('click', item);
                    });
                }
            }
        }
    });
};

 사용법은 간단히 $(selector).disable(), $(selector).enable() 이러면 된다. 단, 아직 중복해서 disable/enable 하는 경우에 대한 테스트는 해보지 않았으므로 잘 안 될 수 있다. (대충 보기에 아마 disable을 중복하면 기존 이벤트를 잃어버리는 문제가 생길 듯하고, enable은 여러 번 해도 상관 없을 듯하...지만 확인은 직접. ㅋㅋ) 아직 disable하지 않은 것을 enable하는 경우나 disable/enable 반복되는 경우도 모두 처리한 버전으로 업데이트. :)

댓글 1개:

  1. 안녕하세요~
    저두 이번에 회사에서 가상서버에 웹페이지에서 접근할수 있는 콘솔기능을 만들려고 하는데 머가먼지 잘모르겠더라구여.
    죄송하지만 자료가 있으시면 공유를 부탁드려도 되는지요..

    답글삭제