티스토리 뷰

728x90
SMALL

이벤트 타입

오래된 이벤트 타입

폼 관련 이벤트

폼이 전송될때 submit, 초기화될때 reset 이벤트

버튼과 유사한 폼 요소는 사용자와 상호작용시에 click 이벤트

어떤 상태를 유지하는 폼 요소는 사용자가 상태를 바굴 때 change 이벤트

폼 요소가 포커스를 갖거나 잃을 때 focus, blur 이벤트

submit, reset 이벤트는 이벤트 핸들러에서 취소할 수 있는 기본 동작이 있고, 이는 몇 가지 클릭 관련 이벤트에서도 마찬가지다. focus, blur 이벤트에서는 버블링이 일어나지 않지만, 다른 폼 관련 이벤트는 발생한다.

사용자가 요소 안에 텍스트를 입력할 수 있는 모든텍스트 입력 폼 요소와<textarea>에서 inut 이벤트가 발생한다. input 이벤트는 매 입력마다 발생한다. 하지만 input 이벤트의 이벤트 객체는 텍스트가 입력되는 매 순간을 가리키지는 않는다.

창 관련 이벤트

창 관련 이벤트는 브라우저 창에 출력된 어떤 특정 문서의 내용이 아닌 창 자체와 관련된 실행을 나타낸다.

이중에서 가장 중요한 이벤트가 load 이벤트

unload 이벤트는 사용자가 문서에서 떠날 때 발생, 취소 안돼

beforeunload는 페이지를 정말 떠나길 원하는지 확인하기 위해 사용자에게 물어볼 기회를 제공한다. beforeunload 이벤트 핸들러가 문자열을 반환하면, 새로운 페이지를 불러오기 전에 confirm() 대화상자를 통해 이 문자열이 사용자에게 출력되고, 사용자는 confirm() 대화상자에서 페이지 이동을 취소하고 현재 페이지에 남을 수도 있다.

onerror 프로퍼티는 이벤트 핸들러와 유사한 존재이고 자바스크립트 에러에 대한 응답에서 발생한다. 그러나 onerror는 실행시 전달인자가 다르므로 실제로 이벤트 핸들러는 아니다.(14장)

<img> 요소 같은 각 문서 요소에도 load와 errror 이벤트 핸들러를 등록할 수 있다. 몇몇 브라우저는 abort 이벤트도 지원하는데, 이는 이미지나 여러 종류의 네트워크 자원을 사용자가 중지해서 불러올 수 없을 때 발생한다.

focus, blur 이벤트는 window 관련 이벤트로도 사용된다.

resize, scroll 이벤트는 사용자가 브라우저 창의 크기를 조절하거나 스크롤할 때 발생한다.

마우스 관련 이벤트

마우스 관련 이벤트는 사용자가 문서 위에서 마우스를 클릭하거나 움직일 때 발생한다. 이런 이벤트는 마우스 포인터가 올려져 있는 가장 깊숙한 안쪽 요소에서 발생하지만, 문서 계층 구조를 따라서 버블링된다. 마우스 이벤트 핸들러에 전달되는 이벤트 객체에는 마우스의 위치와 버튼 상태, 이벤트 핸들러에 전달되는 이벤트 객체에는 마우스의 위치와 버튼 상태, 이벤트 발생시 옵션키가 같이 눌렸는지를 나타내는 프로퍼티 모음이 들어있다.

clientX, clientY 프로퍼티는 눈에 보이는 창 기준 좌표계로 마우스 위치를 명시한다.

button, which 프로퍼티는 마우스의 어떤 버튼을 어디서 눌렀는지를 나타낸다.

altKey, ctrlKey, metaKey, shiftKey 프로퍼티는 일치하는 옵션키가 눌려 있으면 true

클릭 관련 이벤트에는 마우스 클릭이 몇 번인지를 나타내는 detail 프로퍼티도 존재한다.

mousemove 이벤트는 사용자가 마우스를 움직이거나 드래그하는 내내 발생한다. 이벤트가 지속적으로 발생하므로 지나친 연산 작업은 피하자

mousedown, mouseup 이벤트는 사용자가 마우스 버튼을 누르고 뗄 때 발생한다. 마우스 드래그를 감지하고 반응하려면 mousemove 핸들러를 등록하는 mousedown 핸들러를 등록하면 된다. 이런 프로퍼티를 상요하면 마우스 움직임이 처음 시작된 요소의 바깥으로 움직인 후에도 mousemove 이벤트를 계속 받게 된다.

브라우저에서는 mousedown, mouseup 이벤트가 순서대로 발생한 다음에 click 이벤트도 발생한다. click 이벤트는 장치 독립적 폼 이벤트이지만 폼 요소만이 아닌 어떤 문서 요소에서든 발생한다. 그리고 앞에서 설명한 모든 마우스 관련 확장 프로퍼티가 있는 이벤트 객체를 핸들러에 전달한다.

만약 한 곳에서 충분히 짧은 시간에 마우스를 두 번 클릭하면, 두 번째 클릭 이벤트로 dbclick 이벤트가 뒤따라서 발생한다. 브라우저는 마우스 오른쪽 버튼을 클릭하면 일반적으로 상황에 맞는 context 메뉴를 출력한다. 메뉴가 출력되기 전에 contextmenu 이벤트가 발생하는데, 이 이벤트를 취소하면 메뉴 출력을 막을 수 있다. 마우스 오른쪽 버튼 클릭을 알 수 있는 손쉬운 방법이기도 하다.

사용자가 마우스를 움직여서 새로운 요소 위로 이동하면, 브라우저가 이 요소에서 mouseover 이벤트를 발생시킨다. 마우스가 이 요소에 있지 않으면 mouseout 이벤트를 발생시킨다. 이런 마우스 관련 이벤트에서는 핸들러에 전달할 이벤트 객체에 마우스 이동 과정과 관련된 다른 요소를 나타내는 relatedTarget 프로퍼티가 존재한다. mouseover과 mouseout 이벤트는 여기서 설명한 모든 마우스 관련 이벤트처럼 버블링된다. 하지만 버블링때문에 문제가 되기도 한다.

사용자가 마우스 휠을 돌리면 브라우저가 mousewheel 이벤트를 일으킨다. 

키보드 관련 이벤트

웹브라우저가 키보드 포커스를 가지고 있을 때, 사용자가 키보드를 누르거나 뗄 때마다 이벤트가 발생한다. 키보드 관련 이벤트는 종류에 관계없이 문서 요소에 키보드 포커스가 있으면 발생한다. 그리고 Document와 Window 객체에까지 버블링된다. 키보드 이벤트 핸들러는 누르거나 뗀 키를 명시하는 keycode 프로퍼티가 있는 이벤트 객체를 받는다. keyCode 말고도 키 이벤트의 이벤트 객체에는 옵션 키의 상태를 나타내는 altKeyctrlKey, metaKey, shiftKey가 존재

keydonwkeyup 이벤트는 저수준 키보드 이벤트다. 그래서 옵션 키를 포함한 어떤 키라도 누르거나 떼면 발생한다. keydown 이벤트가 화면에 출력할 문자를 생성할 때, 이어지는 keypress 이벤트는 keydown과 keyup 사이에 발생한다.

keypress 이벤트는 고수준의 텍스트 이벤트다. 따라서 keypress 이벤트의 이벤트 객체는 키보드에서 누른 키가 아니라, keydown 이벤트 결과로 화면상에 출력된 문자를 가리킨다.

DOM 관련 이벤트

focusin, focusout : 버블링발생

mouseenter, mouseleave

DOM 레벨 3 이벤트 명세에서 새로 추가된 내용은 휠 이벤트를 통한 2차원 마우스 지원과 텍스트 입력 관련 이벤트 지원이다. 특히 텍스트 입력 관련 이벤트를 위해서 textinput 이벤트와 KeyboardEvent 객체가 제공되는데, KeyboardEvent 객체는 keydown과 keyup, keypress 이벤트 핸들러에 전달인자로 넘겨지는 객체다. textinput 때문에 keypress는 권장 사항이 아니다.

HTML5 관련 이벤트

HTML5나 이와 관련된 표준은 웹 애플리케이션을 위한 많은 신규 API들을 정의한다. 이 API의 대부분은 이벤트를 정의한다. 널리 알려진 HTML5의 기능 중 하나는 소리와 동영상을 재생하는 <audio>와 <vidoe> 요소다. 이 요소에는 네트워크 이벤트나 데이터 버퍼링 상태, 재생 상태를 알리기 위해 발생하는 정말 많은 이벤트가 존재한다. 이런 미디어 관련 이벤트의 이벤트 객체에는 별다른 추가 프로퍼티가 없다. 하지만 이벤트 객체의 target 프로퍼티가 <audio>나 <video> 요소를 식별하는데, 이 요소에 미디어와 관련된 많은 프로퍼티와 메서드가 있다. 

자바스크립트 애플리케이션은 HTML5의 드래그 앤 드롭 API를 이용해서 OS 기반의 드래그 앤 드롭 작업과 연계할 수 있다. 즉 웹 애플리케이션과 일반 애플리케이션 사이에 데이터 전송이 가능하다. 이 API는 다음과 같은 일곱 개의 이벤트 타입을 정의한다.

dragstart   drag        dragend

dragenter  dragover  dragleave  drop

드래그 앤 드롭 관련 이벤트는 마우스 이벤트처럼 이벤트 객체를 가지고 발생한다. dataTransfer라는 하나의 프로퍼티가 추가되는데, 이 프로퍼티는 전송된 데이터와 이를 사용할 수 있는 형식에 대한 정보를 담고 있다.

HTML5는 웹 애플리케이션이 브라우저의 뒤로가기와 앞으로 가기 버튼과 상호작용할 수 있게 하는 히스토리 관리 메커니즘을 정의한다. 이 메커니즘으로 hashchange와 popstate라는 이름의 이벤트가 생겨났고, 이 이벤트는 load와 unload 같은 이벤트의 생명 주기를 알리며 개별 문서 요소가 아닌 Window 객체에서 발생한다.

유효성 검사 메커니즘도 정의하고 잇다.

HTML5는 로컬의 애플리케이션 캐시에 설치될 수 있는 오프라인 웹 애플리케이션을 지원한다. 따라서 브라우저가 오프라인일 때도 실행할 수 있다. 이와 관련된 가장 중요한 이벤트는 offline과 online 이벤트인데, 이 이벤트는 브라우저 네트워크 연결이 끊기거나 연결될 때마다 window 객체에서 발생한다. 그 밖의 이벤트들은 애플리케이션 다운로드 진행 상태와 애플리케이션 캐시 갱신을 알리는 이벤트다.

cached  checking  downloading  error

noupdate  obsolete  progress  updateready

새로운 웹 애플리케이션 API들은 비동기 통신을 위해 message 이벤트를 사용한다. 문서 간 메시지 교환 API는 한 서버의 문서 내 스크립트가 다른 서버의 문서 내 스크립트와 메시지를 교환할 수 있게 한다. 각 메시지 전송 때마다 수신 문서의 Window 객체에서는 message 이벤트가 발생한다. 핸들러에 전달된 이벤트 객체에는 메시지 발신자를 식별하기 위한 source와 origin 프로퍼티, 메시지 내용을 담은 data 프로퍼티가 포함된다. message 이벤트는 유사한 방법을 통해 웹 워커와는 로컬 통신을, 서버 전송 이벤트와 웹 소켓을 통해서는 네트워크 통신을 한다.

웹 스토리지 API는 저장된 데이터 변경을 알리는 storage 이벤트가 Window 객체에서 발생하도록 정의한다. beforeprint, afterprint 이벤트는 Window 객체에서 문서가 출력되기 직전 혹은 직후에 발생하며, 덕분에 출력 일시와 같은 내용을 추가하거나 삭제할 수 있다.

터치스크린과 모바일 관련 이벤트

생략

이벤트 핸들러 등록하기

이벤트 핸들럴르 등록하는 방법은 두 가지다. 첫째는 이벤트 타깃이 되는 객체나 문서 요소에 프로퍼티를 지정하는 방법이고, 둘째는 객체나 요소의 메서드에 이벤트 핸들러를 전달하는 방법이다.

이벤트 핸들러 프로퍼티로 핸들러 지정하기

이벤트 핸들러를 등록하는 가장 간단한 방법은 원하는 이벤트 핸들러 함수를 이벤트 타깃의 프로퍼티에 지정하는 것이다. 이벤트 핸들러 프로퍼티는 관습적으로 앞에 on이 붙은 이벤트명을 이름으로 쓴다.

window.onload=function(){
  var elt=document.getElementById("shipping_address");
  elt.onsubmit=function(){return validate(this);}
}

이벤트 핸들러의 프로퍼티의 단점은 각 이벤트 타입에 대해 하나의 핸들러만 등록된다는 가정하에 이벤트 타깃이 디자인 되었다는 점이다.

이벤트 핸들러 속성으로 핸들러 지정하기

문서 요소의 이벤트 핸들러 프로퍼티는 상응하는 HTML 태그의 속성을 통해서도 지정할 수 있다.

<button onclick="alert('Thank you');">Click Here</button>

프로그래밍 스타일을 따르는 개발자라면 HTML 이벤트 핸들러 속성을 사용하는 방법은 가급적 피해야 한다.

addEventListener()

타깃 객체에 대한 이벤트 핸들러를 등록해서 사용할 수 있는 addEventListener() 메서드가 존재한다. 여기서 타깃 객체는 Wndow 객체와 Document 객체, 문서 요소가 될 수 있다. addEventListener()에는 세 개의 전달인자가 있다. 첫째 인자는 핸들러를 등록할 이벤트명이다. 이벤트명은 문자열이고 이벤트 핸들러 프로퍼티를 지정할 때 사용한 'on' 접두사는 제거해야 한다. 둘째 인자는 지정한 이벤트 타입이 발생할 때 실행할 함수다. 마지막 인자는 불리언 값이다. 일반적으로 이 인자 값은 false를 쓴다. 만약 true를 넘기면, 함수는 캡처링 이벤트로 핸들러를 등록하고 버블링과 다른 이벤트 전파 단계에서 호출된다. 

<button id="mybutton">Click me</button>
<script>
  var b=document.getElementById("mybutton");
  b.onclick=function(){alert("Thanks for clicking me!");};
  b.addEventListener("click",function(){alert("Thanks again!");},false);
</script>

addEventListener는 onclick 프로퍼티의 값에 영향을 주지 않는다. 따라서 alert 2개 생성

addEventListener는 중복이 되고 첫번째 인자가 동일 인자라면 해당 핸들러 함수는 한번만 등록되므로 상관 없다.

removeEventListener() 메서드는 똑같이 세 개의 전달인자가 존재한다. 이벤트 핸들러를 임시로 등록했다가 잠시 후 지울 때 자주 쓰인다. 예를 들어, mousedown 이벤트가 일어날 때 mousemove와 mouseup 이벤트의 임시 캡처링 이벤트 핸들러를 강제로 등록한다.

document.removeEventListener("mousemove",handleMouseMove,true);

document.removeEventListener("mouseup",handleMouseUp,true);

이벤트 핸들러 호출

이벤트 핸들러가 등록된 후에는 지정한 객체에서 지정한 타입의 이벤트가 이렁날 때 웹브라우저가 자동으로 핸들러를 호출한다.

이벤트 핸들러의 전달인자

이벤트 핸들러는 일반적으로 단일 인자로 이벤트 객체를 가지고 호출된다. 이 이벤트 객체는 프로퍼티를 통해서 이벤트에 대한 상세 내용을 제공한다.

이벤트 핸들러 컨텍스트

이벤트 프로퍼티를 이용한 이벤트 핸들러 함수 등록은 문서 요소 객체에 신규 메서드를 정의하는 과정과 유사하다.

e.onclick=function(){~~};

즉 이벤트 핸들러 안에서 this 키워드는 이벤트 타깃을 참조한다.

addEventListener()를 이용해서 등록할 때도 핸들러는 이벤트 타깃에서 호출되고, this 값은 이벤트 타깃이 된다.

이벤트 핸들러의 유효범위

모든 자바스크립트의 함수처럼, 이벤트 핸들러는 유효범위가 정적이다. 핸들러는 호출된 위치가 아닌 정의된 위치의 유효범위 안에서 실행된다. 그리고 이 유효범위에 있는 모든 지역 변수에 접근할 수 있다. 하지만 HTML 속성으로 등록한 이벤트 핸들러는 예외다. 이렇게 등록된 핸들러는 전역 변수에 접근할 수 있는 최상위 함수로 변환된다. 그러나 과거의 여러 복잡한 이유로 인해서 이 경우 핸들러의 유효범위 체인이 수정되어 실행된다.   HTML의 속성은 긴 문자열의 코드를 넣기에 적합한 위치가 아니다보니, 변경된 유효범위 체인을 사용한다.

this.tagName => tagName

document.getElementById => getElementById

this.form.zipcod => zipcode

핸들러의 반환 값

객체의 프로퍼티나 HTML 속성으로 등록된 이벤트 핸들러의 반환 값은 때로는 중요한 의미를 가진다. 일반적으로는 반환 값이 false이면 브라우저가 이벤트와 관련된 기본 동작을 중지한다.

false 같은 이벤트 핸들러의 반환 값이 객체의 프로퍼티나 HTML 속성으로 등록된 핸들러에서만 의미가 있고

addEventListener()로 등록된 이벤트 핸들러는 반환 값 대신 이벤트 객체의 preventDefault() 메서드를 호출해야 한다.

호출 순서

1. 객체의 프로퍼티와 HTML 속성으로 등록된 핸들러가 존재하면 가장 먼저 호출한다.

2. addEventListener()로 등록한 핸들러는 등록된 순서대로 호출한다.

이벤트 전파

이벤트 타깃이 Window 객체나 XMLHttpRequest 같은 독립적인 단일 객체라면, 브라우저는 그저 해당 객체에서 이벤트 응답으로 적절한 핸들러를 호출한다. 하지만 이벤트 타깃이 Docuument 객체 혹은 문서 요소라면 상황이 좀더 복잡하다.

이벤트 버블링은 이벤트 전파 단계에서 순서상 세번째다. 타깃 객체의 이벤트 핸들러 호출이 두번째고, 첫번째는 캡처링이다. addEventListener()의 세번째 인자가 true라면 이벤트 핸들러는 이벤트 전파 단계에서 가장 먼저 호출되는 캡처링 이벤트 핸들러로 등록된다. 이벤트 캡처링은 true일때만 작동한다.

이벤트 캡처링은 해당 타깃 객체에 이벤트를 전달하기 전에 이벤트를 살펴볼 기회를 제공한다.

이벤트 취소

프로퍼티로 등록된 이벤트 핸들러의 반환 값이 이벤트 브라우저 기본 동작을 취소하는데 사용될 수 있고, e.preventDefault로 addEventListener()를 취소할 수 있다.

하지만 이벤트에서 취소할 수 있는 작업이 기본 동작만 있는 건 아니다. 이벤트 전파도 취소할 수 있다. 이벤트 전파가 진행되는 것을 막기 위한 stopPropagation() 메서드가 이벤트 객체에 존재한다. 동일 객체에 등록된 다른 핸들러가 있다면, stopPropagation() 메서드를 호출한 후 나머지 핸들러가 호출되지만, 다른 객체에 있는 이벤트 핸들러는 이벤트 전파를 멈추었으므로 호출되지 않는다. stopPropagation() 메서드는 이벤트가 전파되는 동안 언제든지 호출될 수 있다. stopImmediatePropagation() 메서드는 다른 객체로 이벤트가 전파되는 것을 막을 뿐 아니라, 동일 객체에 등록된 다른 이벤트 핸들러의 호출도 막는다.

문서 로딩 관련 이벤트

대부분의 웹 애플리케이션은 문서를 모두 불러왔고 조작해도 상관없는 상태가 되면 브라우저가 알려주길 바란다. load 이벤트는 문서와 문서 안의 모든 이미지를 전부 불러오기 전에는 실행되지 않는다. 그러나 보통은 문서는 모두 파싱되었지만 이미지는 다운로드되기 전 시점도 스크립트를 실행하기에 적절하다. 만약 load가 아닌 이벤트에서 스크립트를 실행할 수 있다면 웹 애플리케이션의 초기 실행 시간을 줄일 수 있다.

문서를 불러와서 모두 파싱하고 defer 속성을 사용한 연기된 스크립트가 있다면 실행한 이후에 DOMContentLoaded 이벤트가 발생한다. 이미지와 async 속성이 있는 스크립트가 아직 로딩중일 수 있지만, 문서는 이미 조작 가능한 상태다.

ex) 문서 조작이 가능할 때 함수 호출하기

/*
 * Pass a function to whenReady() and it will be invoked (as a method of the
 * document) when the document is parsed and ready for manipulation. Registered
 * functions are triggered by the first DOMContentLoaded, readystatechange, or
 * load event that occurs. Once the document is ready and all functions have
 * been invoked, any functions passed to whenReady() will be invoked 
 * immediately.
 */
var whenReady = (function() { // This function returns the whenReady() function
    var funcs = [];    // The functions to run when we get an event
    var ready = false; // Switches to true when the handler is triggered

    // The event handler invoked when the document becomes ready
    function handler(e) {
        // If we've already run once, just return
        if (ready) return;

        // If this was a readystatechange event where the state changed to
        // something other than "complete", then we're not ready yet
        if (e.type === "readystatechange" && document.readyState !== "complete")
            return;
        
        // Run all registered functions.
        // Note that we look up funcs.length each time, in case calling
        // one of these functions causes more functions to be registered.
        for(var i = 0; i < funcs.length; i++) 
            funcs[i].call(document);

        // Now set the ready flag to true and forget the functions
        ready = true;
        funcs = null;
    }

    // Register the handler for any event we might receive
    if (document.addEventListener) {
        document.addEventListener("DOMContentLoaded", handler, false);
        document.addEventListener("readystatechange", handler, false);
        window.addEventListener("load", handler, false);
    }
    else if (document.attachEvent) {
        document.attachEvent("onreadystatechange", handler);
        window.attachEvent("onload", handler);
    }

    // Return the whenReady function
    return function whenReady(f) {
        if (ready) f.call(document); // If already ready, just run it
        else funcs.push(f);          // Otherwise, queue it for later.
    }
}());

마우스 관련 이벤트

mouseenter와 mouseleave를 제외한 모든 이벤트는 버블링된다. 하이퍼링크와 전송 버튼의 click 이벤트는 취소할 수 있는 기본 동작이 있다. 마우스 포인터의 좌표를 나타내는 clientX, clientY 프로퍼티가 있다. 문서 좌표계의 좌표로 변환하기 위해서는 창의 스크롤 오프셋을 합해야 한다. 

ex) 문서 요소 드래그하기

/**
 * Drag.js: drag absolutely positioned HTML elements.
 *
 * This module defines a single drag() function that is designed to be called
 * from an onmousedown event handler. Subsequent mousemove events will
 * move the specified element. A mouseup event will terminate the drag.
 * This implementation works with both the standard and IE event models.
 * It requires the getScrollOffsets() function from elsewhere in this book.
 *
 * Arguments:
 *
 *   elementToDrag: the element that received the mousedown event or
 *     some containing element. It must be absolutely positioned. Its
 *     style.left and style.top values will be changed based on the user's
 *     drag.
 *
 *   event: the Event object for the mousedown event.
 **/
function drag(elementToDrag, event) {
    // The initial mouse position, converted to document coordinates
    var scroll = getScrollOffsets();  // A utility function from elsewhere
    var startX = event.clientX + scroll.x;
    var startY = event.clientY + scroll.y;

    // The original position (in document coordinates) of the element
    // that is going to be dragged.  Since elementToDrag is absolutely
    // positioned, we assume that its offsetParent is the document body.
    var origX = elementToDrag.offsetLeft;
    var origY = elementToDrag.offsetTop;

    // Compute the distance between the mouse down event and the upper-left
    // corner of the element. We'll maintain this distance as the mouse moves.
    var deltaX = startX - origX;
    var deltaY = startY - origY;

    // Register the event handlers that will respond to the mousemove events
    // and the mouseup event that follow this mousedown event.
    if (document.addEventListener) {  // Standard event model
        // Register capturing event handlers on the document
        document.addEventListener("mousemove", moveHandler, true);
        document.addEventListener("mouseup", upHandler, true);
    }
    else if (document.attachEvent) {  // IE Event Model for IE5-8
        // In the IE event model, we capture events by calling
        // setCapture() on the element to capture them.
        elementToDrag.setCapture();
        elementToDrag.attachEvent("onmousemove", moveHandler);
        elementToDrag.attachEvent("onmouseup", upHandler);
        // Treat loss of mouse capture as a mouseup event.
        elementToDrag.attachEvent("onlosecapture", upHandler);
    }

    // We've handled this event. Don't let anybody else see it.
    if (event.stopPropagation) event.stopPropagation();  // Standard model
    else event.cancelBubble = true;                      // IE

    // Now prevent any default action.
    if (event.preventDefault) event.preventDefault();   // Standard model
    else event.returnValue = false;                     // IE

    /**
     * This is the handler that captures mousemove events when an element
     * is being dragged. It is responsible for moving the element.
     **/
    function moveHandler(e) {
        if (!e) e = window.event;  // IE event Model

        // Move the element to the current mouse position, adjusted by the
        // position of the scrollbars and the offset of the initial click.
        var scroll = getScrollOffsets();
        elementToDrag.style.left = (e.clientX + scroll.x - deltaX) + "px";
        elementToDrag.style.top = (e.clientY + scroll.y - deltaY) + "px";

        // And don't let anyone else see this event.
        if (e.stopPropagation) e.stopPropagation();  // Standard
        else e.cancelBubble = true;                  // IE
    }

    /**
     * This is the handler that captures the final mouseup event that
     * occurs at the end of a drag.
     **/
    function upHandler(e) {
        if (!e) e = window.event;  // IE Event Model

        // Unregister the capturing event handlers.
        if (document.removeEventListener) {  // DOM event model
            document.removeEventListener("mouseup", upHandler, true);
            document.removeEventListener("mousemove", moveHandler, true);
        }
        else if (document.detachEvent) {  // IE 5+ Event Model
            elementToDrag.detachEvent("onlosecapture", upHandler);
            elementToDrag.detachEvent("onmouseup", upHandler);
            elementToDrag.detachEvent("onmousemove", moveHandler);
            elementToDrag.releaseCapture();
        }

        // And don't let the event propagate any further.
        if (e.stopPropagation) e.stopPropagation();  // Standard model
        else e.cancelBubble = true;                  // IE
    }
}

마우스 휠 관련 이벤트

기본 동작을 막아서 mousewheel 이벤트를 취소할 수 있다. mousewheel 이벤트 핸들러의 이벤트 객체에는 사용자가 휠을 얼마나 돌렸는지를 명시하는 wheelDelta 프로퍼티가 존재한다. 보통 위쪽으로 마우스 휠을 한 칸 돌리면 wheelDelta의 값이 120 delta이고 아래쪽으로 한 칸 돌리면 -120 delta이다. 사파리와 클롬은 축이 두개인 트랙볼을 가진 애플 마우스를 지원하는데 이를 위해 wheelDeltaX와 wheelDeltaY(=wheelDelta) 프로퍼티가 존재한다. 

이 모든 이벤트 타입의 이벤트 객체는 mouse 관련 이벤트 객체와 유사하다. 이벤트 객체에 키보드 옵션 키의 상태 값과 마우스 포인터 좌표 관련 프로퍼티가 포함되어 있다.

ex) 마우스 힐 이벤트 제어하기

// Enclose the content element in a frame or viewport of the specified width
// and height (minimum 50x50). The optional contentX and contentY arguments
// specify the initial offset of the content relative to the frame. (If
// specified, they must be <= 0.) The frame has mousewheel event handlers that
// allow the user to pan the element, and to shrink or enlarge the frame.
function enclose(content, framewidth, frameheight, contentX, contentY) {
    // These arguments aren't just the initial values: they maintain the
    // current state and are used and modified by the mousewheel handler.
    framewidth = Math.max(framewidth, 50);
    frameheight = Math.max(frameheight, 50);
    contentX = Math.min(contentX, 0) || 0;
    contentY = Math.min(contentY, 0) || 0;

    // Create the frame element and set a CSS classname and styles
    var frame = document.createElement("div");
    frame.className = "enclosure"; // So we can define styles in a stylesheet
    frame.style.width = framewidth + "px";       // Set the frame size.
    frame.style.height = frameheight + "px";
    frame.style.overflow = "hidden";             // No scrollbars, no overflow
    frame.style.boxSizing = "border-box";        // Border-box simplifies the 
    frame.style.webkitBoxSizing = "border-box";  // calculations for resizing
    frame.style.MozBoxSizing = "border-box";     // the frame.

    // Put the frame in the document and move the content elt into the frame.
    content.parentNode.insertBefore(frame, content);
    frame.appendChild(content);

    // Position the element relative to the frame
    content.style.position = "relative";
    content.style.left = contentX + "px";
    content.style.top = contentY + "px";

    // We'll need to work around some browser-specific quirks below
    var isMacWebkit = (navigator.userAgent.indexOf("Macintosh") !== -1 &&
                       navigator.userAgent.indexOf("WebKit") !== -1);
    var isFirefox = (navigator.userAgent.indexOf("Gecko") !== -1);

    // Register mousewheel event handlers.
    frame.onwheel = wheelHandler;       // Future browsers
    frame.onmousewheel = wheelHandler;  // Most current browsers
    if (isFirefox)                      // Firefox only
        frame.addEventListener("DOMMouseScroll", wheelHandler, false);

    function wheelHandler(event) {
        var e = event || window.event;  // Standard or IE event object

        // Extract the amount of rotation from the event object, looking
        // for properties of a wheel event object, a mousewheel event object 
        // (in both its 2D and 1D forms), and the Firefox DOMMouseScroll event.
        // Scale the deltas so that one "click" toward the screen is 30 pixels.
        // If future browsers fire both "wheel" and "mousewheel" for the same
        // event, we'll end up double-counting it here. Hopefully, however,
        // cancelling the wheel event will prevent generation of mousewheel.
        var deltaX = e.deltaX*-30 ||  // wheel event
                  e.wheelDeltaX/4 ||  // mousewheel
                                0;    // property not defined
        var deltaY = e.deltaY*-30 ||  // wheel event
                  e.wheelDeltaY/4 ||  // mousewheel event in Webkit
   (e.wheelDeltaY===undefined &&      // if there is no 2D property then 
                  e.wheelDelta/4) ||  // use the 1D wheel property
                     e.detail*-10 ||  // Firefox DOMMouseScroll event
                               0;     // property not defined

        // Most browsers generate one event with delta 120 per mousewheel click.
        // On Macs, however, the mousewheels seem to be velocity-sensitive and
        // the delta values are often larger multiples of 120, at 
        // least with the Apple Mouse. Use browser-testing to defeat this.
        if (isMacWebkit) {
            deltaX /= 30;
            deltaY /= 30;
        }

        // If we ever get a mousewheel or wheel event in (a future version of)
        // Firefox, then we don't need DOMMouseScroll anymore.
        if (isFirefox && e.type !== "DOMMouseScroll")
            frame.removeEventListener("DOMMouseScroll", wheelHandler, false);

        // Get the current dimensions of the content element
        var contentbox = content.getBoundingClientRect();
        var contentwidth = contentbox.right - contentbox.left;
        var contentheight = contentbox.bottom - contentbox.top;

        if (e.altKey) {  // If Alt key is held down, resize the frame
            if (deltaX) {
                framewidth -= deltaX; // New width, but not bigger than the
                framewidth = Math.min(framwidth, contentwidth);  // content
                framewidth = Math.max(framewidth,50);   // and no less than 50.
                frame.style.width = framewidth + "px";  // Set it on frame
            }
            if (deltaY) {
                frameheight -= deltaY;  // Do the same for the frame height
                frameheight = Math.min(frameheight, contentheight);
                frameheight = Math.max(frameheight-deltaY, 50);
                frame.style.height = frameheight + "px";
            }
        }
        else { // Without the Alt modifier, pan the content within the frame
            if (deltaX) {
                // Don't scroll more than this
                var minoffset = Math.min(framewidth-contentwidth, 0);
                // Add deltaX to contentX, but don't go lower than minoffset
                contentX = Math.max(contentX + deltaX, minoffset);
                contentX = Math.min(contentX, 0);     // or higher than 0
                content.style.left = contentX + "px"; // Set new offset
            }
            if (deltaY) {
                var minoffset = Math.min(frameheight - contentheight, 0);
                // Add deltaY to contentY, but don't go lower than minoffset
                contentY = Math.max(contentY + deltaY, minoffset);
                contentY = Math.min(contentY, 0);     // Or higher than 0
                content.style.top = contentY + "px";  // Set the new offset.
            }
        }

        // Don't let this event bubble. Prevent any default action.
        // This stops the browser from using the mousewheel event to scroll
        // the document. Hopefully calling preventDefault() on a wheel event
        // will also prevent the generation of a mousewheel event for the
        // same rotation.
        if (e.preventDefault) e.preventDefault();
        if (e.stopPropagation) e.stopPropagation();
        e.cancelBubble = true;  // IE events
        e.returnValue = false;  // IE events
        return false;
    }
}

드래그 앤 드롭 관련 이벤트

위 예제에서 웹 페이지의 요소를 마우스로 끌어다 어딘가 놓을 수 있다. 하지만 진짜 드래그 앤 드롭은 약간 다르다. 드래그 앤 드롭은 한 애플리케이션 안에서 혹은 전혀 다른 애플리케이션 간에 일어나며, '드래그 할 소스'와 '드롭할 타깃' 사이에 데이터가 전송되는 사용자 인터페이스다. DnD는 사람과 컴퓨터 간의 복잡한 상호작용이므로, DnD를 구현하기 위한 API도 덩달아 복잡해졌다.

DnD는 항상 이벤트 기반으로 작동하며 이 이벤트는 두 종류로 분류할 수 있다. 드래그 소스에서 발생하는 이벤트와 드롭 타깃에서 일어나는 이벤트다. 모든 DnD 이벤트 핸들러의 이벤트 객체는 dataTransfer 프로퍼티를 제외하면 마우스 이벤트 객체와 같다. 이 프로퍼티는 Dnd API의 메서드와 프로퍼티를 정의한 DataTransfer 객체를 참조한다.

드래그 소스에서 발생하는 이벤트가 비교적 간단하니 먼저 다루겠다. HTML draggable 속성을 가진 문서 요소는 모두 드래그 소스가 될 수 있다. 사용자가 드래그 소스 위에서 마우스 드래그를 시작하면, 브라우저는 요소의 내용을 선택하는 대신 dragstart 이벤트를 발생시킨다. 이 이벤트의 핸들러에서는 드래그 소스로 사용 가능하게 만든 데이터와 그 타입을 명시하기 위해 dataTransfer.setData() 메서드를 호출해야 한다. 핸들러에서는 또한 dataTransfer.effectAllowed 프로퍼티에 "move", "copy", "link" 같은 데이터 전송 동작을 지정할 수 있고, 드래그하는 동안 시각적 표현으로 사용할 이미지나 문서 요소를 명시하기 위해 dataTransfer.setDragImage()나 dataTransfer.addElement()를 호출할 수도 있다. 

드래그가 이루어지는 동안에는 브라우저가 드래그 소스에서 drag 이벤트를 발생시킨다. dataTranfer.setDragImage()로 지정한 이미지를 갱신하거나 배달중인 데이터의 수정을 원한다면 이 이벤트에서 다룰 수 있다. 하지만 drag 이벤트 핸들러를 등록할 일은 거의 없을 것이다.

드롭을 하면 dragend 이벤트가 발생된다. 이때 드래그 소스의 dataTransfer.effectAllowed의 값이 "move"이고 실제로 드래그를 해서 move 동작이 발생했다면 dataTransfer.dropEffect 값도 "move"여야 한다. 그러면 데이터는 다른 곳으로 보내지며, 드래그 소스에서는 데이터를 지워야만 한다.

이처럼 dragstrat 이벤트만 구현하면 드래그 관련 소스 작업은 끝나며 예제가 이를 보여준다. <span> 요소 안에 hh:mm 형태로 현재 시간을 출력하고 일분 후에 갱신한다. 예제의 자바스크립트 코드는 시계가 있는 요소의 draggable 프로퍼티를 true로 지정해서, 시계를 드래그 소스로 만들어 버린다. 그리고 ondragstart 이벤트 핸들러 함수를 정의하는데, 핸들럴는 dataTranser.setData()를 사용해서 드래그되는 데이터로 사용할 타임스탬프 문자열을 명시하고, 드래그될 때의 이미지를 지정하기 위해 datatransfer.setDragIcon()도 호출한다.

<script src="whenReady.js"></script>
<script>
whenReady(function() {
    var clock = document.getElementById("clock");  // The clock element
    var icon = new Image();                        // An image to drag
    icon.src = "clock-icon.png";                   // Image URL

    // Display the time once every minute
    function displayTime() {
        var now = new Date();               // Get current time
        var hrs = now.getHours(), mins = now.getMinutes();
        if (mins < 10) mins = "0" + mins;
        clock.innerHTML = hrs + ":" + mins; // Display current time
        setTimeout(displayTime, 60000);     // Run again in 1 minute
    }
    displayTime();

    // Make the clock draggable
    // We can also do this with an HTML attribute: <span draggable="true">...
    clock.draggable = true;

    // Set up drag event handlers
    clock.ondragstart = function(event) {
        var event = event || window.event; // For IE compatability

        // The dataTransfer property is key to the drag-and-drop API
        var dt = event.dataTransfer;

        // Tell the browser what is being dragged.
        // The Date() constructor used as a function returns a timestamp string
        dt.setData("Text", Date() + "\n");

        // Tell the browser to drag our icon to represent the timestamp, in
        // browsers that support that. Without this line, the browser may
        // use an image of the clock text as the value to drag.
        if (dt.setDragImage) dt.setDragImage(icon, 0, 0);
    };
});
</script>
<style> 
#clock { /* Make the clock look nice */
    font: bold 24pt sans; background: #ddf; padding: 10px;
    border: solid black 2px; border-radius: 10px;
}
</style>
<h1>Drag timestamps from the clock</h1>
<span id="clock"></span>  <!-- The time is displayed here -->
<textarea cols=60 rows=20></textarea> <!-- You can drop timestamps here -->

드롭 타깃은 드래그 소스보다 좀더 까다롭다. 모든 문서 요소는 드롭 타깃이 될 수 있지만 드래그 소스처럼 HTML 속성을 지정할 필요는 없다. 단순한 이벤트 핸들러를 정의하기만 하면 된다. 드롭 타깃에서 발생하는 이벤트는 모두 네 개다. 먼저 dragenter를 보자. 드래그 중인 객체가 어떤 문서 요소에 놓일 때, 브라우저는 해당 요소에서 dragenter 이벤트를 일으킨다. 이때 유요한 데이터가 드래그되었는지 확인하기 위해 드롭 타깃에서 dataTransfer.type 프로퍼티를 사용한다. (드래그 소스와 드롭 타깃이 "move", "copy", "link" 같은 전송 방법 중 하나로 전달되었는지는 dataTransfer.effectAllowed로 확인할 수 있다.) 이런 확인이 끝나면, 드롭 타깃에서 드롭이 일어났음을 사용자와 브라우저에게 알려 주어야 한다. 사용자에게는 요소의 테두리와 바탕색을 바꿔서 알릴 수 있다. 놀라운 건 브라우저인데, 브라우저에게는 dragenter 이벤트를 취소해서 알린다.

드롭 타깃이 dragenter 이벤트를 취소하지 않으면 브라우저는 해당 요소를 마우스 드래그의 드롭 타깃으로 다루지 않을 테고, 더이상 어떤 이벤트도 일으키지 않는다. 하지만 드롭 타깃이 dragenter 이벤트를 취소하면, 브라우저는 사용자가 타깃 객체 위에서 계속 객체를 드래그하고 있음을 나타내는 dragover 이벤트를 일으킨다. 그리고 다음 단계로 진행하려면 또 한 번 이 이벤트 핸들러에서 취소를 해야 한다. 드롭 타깃에 "move", "copy", "link" 중 한 가지만 동작하게 지정하려면 dataTranfer.dropEffect를 dragover 이벤트 핸들러에서 지정해야 한다.

이런 이벤트 취소를 통해 활성화된 드롭 타깃 위에서 드래그 중인 객체를 그대로 밖으로 이동시키면, 드롭 타깃에서 dragleave 이벤트가 발생한다. 만약 dragenter 이벤트 반응했음을 보여주기 위해 시각적으로 변화를 주었다면, dragleave 이벤트 핸들러에서 되돌려야 한다. 예를 들어, 타깃 요소의 테두리나 배경색을 바꾸었다면 복구해야 한다. 불행히도 dragenter와 dragleave 이벤트는 버블링된다. 그러므로 만약 드롭 타깃이 어떤 요소를 포함하고 있다면, dragleave 이벤트가 발생했을 때 진짜로 타깃 바깥쪽으로 나가는 이벤트인지, 타깃의 안쪽 객체를 빠져나가서 생기는 이벤트인지 판단하기가 어렵다.

끝으로, 사용자가 드롭 타깃에 객체를 드롭하면 드롭 타깃에서 드롭 이벤트가 발생한다. 이 이벤트의 핸들러에서는 dataTransfer.getData()를 사용해 전송된 데이터를 가져와야 한다. 그리고 필요한 작업을 한다. 만약 사용자가 드롭 타깃에 여러 파일을 드래그해서 놓으면, dataTransfer.files 프로퍼티는 File 객체들을 담은 유사 배열 객체가 된다. 새로운 HTML5 API에서는 드롭 이벤트 핸들러에서 파일과 파일이 아닌 데이터를 확인하기 위해 dataTransfer.items[]의 요소를 순회할 수 있다.

ex) 드롭 타깃과 드래그 소스를 이용한 목록

/*
 * The DnD API is quite complicated, and browsers are not fully interoperable.
 * This example gets the basics right, but each browser is a little different
 * and each one seems to have its own unique bugs. This code does not attempt
 * browser-specific workarounds.
 */
whenReady(function() {  // Run this function when the document is ready

    // Find all <ul class='dnd'> elements and call the dnd() function on them
    var lists = document.getElementsByTagName("ul");
    var regexp = /\bdnd\b/;
    for(var i = 0; i < lists.length; i++)
        if (regexp.test(lists[i].className)) dnd(lists[i]);

    // Add drag-and-drop handlers to a list element
    function dnd(list) {
        var original_class = list.className;  // Remember original CSS class
        var entered = 0;                      // Track enters and leaves

        // This handler is invoked when a drag first enters the list. It checks
        // that the drag contains data in a format it can process and, if so,
        // returns false to indicate interest in a drop. In that case, it also
        // highlights the drop target to let the user know of that interest.
        list.ondragenter = function(e) {
            e = e || window.event;  // Standard or IE event
            var from = e.relatedTarget; 

            // dragenter and dragleave events bubble, which makes it tricky to
            // know when to highlight or unhighlight the element in a case like
            // this where the <ul> element has <li> children. In browsers that
            // define relatedTarget we can track that.
            // Otherwise, we count enter/leave pairs

            // If we entered from outside the list or if
            // this is the first entrance then we need to do some stuff
            entered++;
            if ((from && !ischild(from, list)) || entered == 1) {

                // All the DnD info is in this dataTransfer object
                var dt = e.dataTransfer; 

                // The dt.types object lists the types or formats that the data
                // being dragged is available in. HTML5 says the type has a
                // contains() method. In some browsers it is an array with an
                // indexOf method. In IE8 and before, it simply doesn't exist.
                var types = dt.types;    // What formats data is available in

                // If we don't have any type data or if data is
                // available in plain text format, then highlight the
                // list to let the user know we're listening for drop
                // and return false to let the browser know.
                if (!types ||                                           // IE
                    (types.contains && types.contains("text/plain")) || //HTML5
                    (types.indexOf && types.indexOf("text/plain")!=-1)) //Webkit 
                {
                    list.className = original_class + " droppable";
                    return false;
                }
                // If we don't recognize the data type, we don't want a drop
                return;   // without canceling
            }
            return false; // If not the first enter, we're still interested
        };

        // This handler is invoked as the mouse moves over the list.
        // We have to define this handler and return false or the drag
        // will be canceled.
        list.ondragover = function(e) { return false; };

        // This handler is invoked when the drag moves out of the list
        // or out of one of its children. If we are actually leaving the list
        // (not just going from one list item to another), then unhighlight it.
        list.ondragleave = function(e) {
            e = e || window.event;
            var to = e.relatedTarget;

            // If we're leaving for something outside the list or if this leave
            // balances out the enters, then unhighlight the list
            entered--;
            if ((to && !ischild(to,list)) || entered <= 0) {
                list.className = original_class;
                entered = 0;
            }
            return false;
        };

        // This handler is invoked when a drop actually happens.
        // We take the dropped text and make it into a new <li> element
        list.ondrop = function(e) {
            e = e || window.event;       // Get the event

            // Get the data that was dropped in plain text format.
            // "Text" is a nickname for "text/plain".  
            // IE does not support "text/plain", so we use "Text" here.
            var dt = e.dataTransfer;       // dataTransfer object
            var text = dt.getData("Text"); // Get dropped data as plain text.

            // If we got some text, turn it into a new item at list end.
            if (text) {
                var item = document.createElement("li"); // Create new <li>
                item.draggable = true;                   // Make it draggable
                item.appendChild(document.createTextNode(text)); // Add text
                list.appendChild(item);                  // Add it to the list

                // Restore the list's original style and reset the entered count
                list.className = original_class;
                entered = 0;

                return false;
            }
        };

        // Make all items that were originally in the list draggable
        var items = list.getElementsByTagName("li");
        for(var i = 0; i < items.length; i++)
            items[i].draggable = true;

        // And register event handlers for dragging list items.
        // Note that we put these handlers on the list and let events
        // bubble up from the items.

        // This handler is invoked when a drag is initiated within the list.
        list.ondragstart = function(e) {
            var e = e || window.event;
            var target = e.target || e.srcElement;
            // If it bubbled up from something other than a <li>, ignore it
            if (target.tagName !== "LI") return false;
            // Get the all-important dataTransfer object
            var dt = e.dataTransfer;
            // Tell it what data we have to drag and what format it is in
            dt.setData("Text", target.innerText || target.textContent);
            // Tell it we know how to allow copies or moves of the data
            dt.effectAllowed = "copyMove";
        };

        // This handler is invoked after a successful drop occurs
        list.ondragend = function(e) {
            e = e || window.event;
            var target = e.target || e.srcElement;

            // If the drop was a move, then delete the list item.
            // In IE8, this will be "none" unless you explicitly set it to 
            // move in the ondrop handler above.  But forcing it to "move" for
            // IE prevents other browsers from giving the user a choice of a
            // copy or move operation.
            if (e.dataTransfer.dropEffect === "move")
                target.parentNode.removeChild(target);
        }

        // This is the utility function we used in ondragenter and ondragleave.
        // Return true if a is a child of b.
        function ischild(a,b) {
            for(; a; a = a.parentNode) if (a === b) return true;
            return false;
        }
    }
});

텍스트 관련 이벤트

브라우저들은 오래 전부터 키보드 입력과 관련해서 세 개의 이벤트를 지원했다. keydown과 keyup 이벤트는 저수준 이벤트이며, keypress 이벤트는 출력 가능한 문자가 생성되었음을 나타내는 고수준 이벤트다. 

표준안이 제안한 textinput 이벤트와 최신 브라우저에 구현된 textInput 이벤트의 이벤트 객체는 입력한 텍스트를 갖고 있는 data 프로퍼티만 추가된 단순 이벤트 객체다. 입력이 키보드로 될 때는 문자가 하나씩 담기기 때문에 data 프로퍼티가 유용하지만, 다른 입력수단은 한 번에 문자열이 입력되는 경우가 있어서 유용하지 않다.

textinput, textInput, keypress 이벤트는 모두 문자가 입력되는 기본 동작을 막을 수 있고, 이를 통해 입력을 걸러낼 수도 있다.

/**
 * InputFilter.js: unobtrusive filtering of keystrokes for <input> elements
 *
 * This module finds all <input type="text"> elements in the document that
 * have an "data-allowed-chars" attribute. It registers keypress, textInput, and
 * textinput event handlers for any such element to restrict the user's input
 * so that only characters that appear in the value of the attribute may be
 * entered. If the <input> element also has an attribute named "data-messageid",
 * the value of that attribute is taken to be the id of another document
 * element. If the user types a character that is not allowed, the message
 * element is made visible. If the user types a character that is allowed, the
 * message element is hidden. This message id element is intended to offer
 * an explanation to the user of why her keystroke was rejected. It should
 * typically be styled with CSS so that it is initially invisible.
 *
 * Here is sample HTML that uses this module.
 *   Zipcode: <input id="zip" type="text"
 *                   data-allowed-chars="0123456789" data-messageid="zipwarn">
 *   <span id="zipwarn" style="color:red;visibility:hidden">Digits only</span>
 *
 * This module is purely unobtrusive: it does not define any symbols in
 * the global namespace.
 */
whenReady(function () {  // Run this function when the document is loaded
    // Find all <input> elements
    var inputelts = document.getElementsByTagName("input");
    // Loop through them all
    for(var i = 0 ; i < inputelts.length; i++) {
        var elt = inputelts[i];
        // Skip those that aren't text fields or that don't have
        // a data-allowed-chars attribute.
        if (elt.type != "text" || !elt.getAttribute("data-allowed-chars"))
            continue;
        
        // Register our event handler function on this input element
        // keypress is a legacy event handler that works everywhere.
        // textInput (mixed-case) is supported by Safari and Chrome in 2010.
        // textinput (lowercase) is the version in the DOM Level 3 Events draft.
        if (elt.addEventListener) {
            elt.addEventListener("keypress", filter, false);
            elt.addEventListener("textInput", filter, false);
            elt.addEventListener("textinput", filter, false);
        }
        else { // textinput not supported versions of IE w/o addEventListener()
            elt.attachEvent("onkeypress", filter); 
        }
    }

    // This is the keypress and textInput handler that filters the user's input
    function filter(event) {
        // Get the event object and the target element target
        var e = event || window.event;         // Standard or IE model
        var target = e.target || e.srcElement; // Standard or IE model
        var text = null;                       // The text that was entered

        // Get the character or text that was entered
        if (e.type === "textinput" || e.type === "textInput") text = e.data;
        else {  // This was a legacy keypress event
            // Firefox uses charCode for printable key press events
            var code = e.charCode || e.keyCode;

            // If this keystroke is a function key of any kind, do not filter it
            if (code < 32 ||           // ASCII control character
                e.charCode == 0 ||     // Function key (Firefox only)
                e.ctrlKey || e.altKey) // Modifier key held down
                return;                // Don't filter this event

            // Convert character code into a string
            var text = String.fromCharCode(code);
        }
        
        // Now look up information we need from this input element
        var allowed = target.getAttribute("data-allowed-chars"); // Legal chars
        var messageid = target.getAttribute("data-messageid");   // Message id
        if (messageid)  // If there is a message id, get the element
            var messageElement = document.getElementById(messageid);
        
        // Loop through the characters of the input text
        for(var i = 0; i < text.length; i++) {
            var c = text.charAt(i);
            if (allowed.indexOf(c) == -1) { // Is this a disallowed character?
                // Display the message element, if there is one
                if (messageElement) messageElement.style.visibility = "visible";

                // Cancel the default action so the text isn't inserted
                if (e.preventDefault) e.preventDefault();
                if (e.returnValue) e.returnValue = false;
                return false;
            }
        }

        // If all the characters were legal, hide the message if there is one.
        if (messageElement) messageElement.style.visibility = "hidden";
    }
});

keypress와 textpinput 이벤트는 포커스가 위치한 문서 요소에 새로운 텍스트가 실제로 입력되기 직전에 발생한다. 따라서 이벤트 핸들러에서 이벤트를 취소하거나 텍스트 입력을 막을 수 있다. 반면에 브라우저에는 요소에 텍스트가 입력된 후에 발생하는 input이라는 이벤트 타입도 존재하는데, 이 이벤트는 취소할 수 없고 새로 입력된 텍스트도 이벤트 객체에 담지 않는다. 하지만 input 이벤트를 쓰면 입력 방법과는 무관하게 요소 안의 텍스트 내용이 바뀌었음을 전달받을 수 있다.

<input type="text" oninput="this.value=this.value.toUpperCase();">

키보드 관련 이벤트

keydown과 keyup 이벤트는 키보드의 키를 누르고 뗄 때 발생한다. 이 이벤트는 옵션 키와 펑션 키, 문자 키에서 모두 발생한다. 만약 사용자가 키를 계속 누르고 있으면, keyup 이벤트가 발생하기 전까지 keydown 이벤트가 연속으로 발생한다. 키보드 관련 이벤트의 이벤트 객체에는 눌린 키를 나타내는 keyCode 프로퍼티가 존재한다. 키보드의 키 중에, 눌렀을 때 화면에 문자를 출력하는 키의 keyCode 값은 해당 키의 기본 문자를 유니코드로 인코딩한 숫자형 값이다. 알파벳은 언제나 대문자고 숫자도 쉬프트를 눌러도 숫자다. 하지만 문자를 출력하지 않은 키는 다르다. 이런 키의 keyCode 값은 표준화되어 있지 않지만, 큰 문제 없이 브라우저 사이에 호환성을 유지한다.

키보드 이벤트 객체에 altKey, ctrlKey, metaKey, shiftKey 프로퍼티가 존재한다. 이벤트가 발생했을 때 옵션 키를 누르고 있으면 true가 된다.

키 이름을 문자열로 담고 있는 key라는 새로운 프로퍼티는 출력된 문자를 나타내고 키가 펑션 키라면 key 프로퍼티는 "F2"나 "Home"같은 값을 나타낸다. keyIdentifier라는 프로퍼티는 값이 문자열이고, 펑션 키도 "Shift" 같이 쉽게 활용할 수 있는 값이다. 하지만 문자를 출력하는 키에 대해서는 문자를 유니코드 인코딩한 문자열을 담고 있어서 활용도가 떨어진다.

/*
 * Keymap.js: bind key events to handler functions.
 *
 * This module defines a Keymap class. An instance of this class represents a
 * mapping of key identifiers (defined below) to handler functions. A Keymap
 * can be installed on an HTML element to handle keydown events. When such an
 * event occurs, the Keymap uses its mapping to invoke the appropriate handler.
 *
 * When you create a Keymap, you can pass a JavaScript object that represents 
 * the initial set of bindings for the Keymap. The property names of this object
 * are key identifers, and the property values are the handler functions.
 * After a Keymap has been created, you can add new bindings by passing a key
 * identifer and handler function to the bind() method. You can remove a
 * binding by passing a key identifier to the unbind() method.
 *
 * To make use of a Keymap, call its install() method, passing an HTML element,
 * such as the document object. install() adds an onkeydown event handler to
 * the specified object. When this handler is invoked, it determines the key
 * identifier of the pressed key and invokes the handler function, if any,
 * bound to that key identifier. A single Keymap may be installed on more than
 * one HTML element.
 *
 * Key Identifiers
 *
 * A key identifier is a case-insensitive string representation of a key plus
 * any modifier keys that are held down at the same time. The key name is
 * usually the (unshifted) text on the key. Legal key names include "A", "7",
 * "F2", "PageUp", "Left", "Backspace", and "Esc".
 *
 * See the Keymap.keyCodeToKeyName object in this module for a list of names.
 * These are a subset of the names defined by the DOM Level 3 standard and 
 * this class will use the key property of the event object when implemented.
 *
 * A key identifier may also include modifier key prefixes. These prefixes are
 * Alt, Ctrl, Meta, and Shift. They are case-insensitive, and must be separated
 * from the key name and from each other with spaces or with an underscore,
 * hyphen, or +. For example: "SHIFT+A", "Alt_F2", "meta-v", and "ctrl alt left".
 * On Macs, Meta is the Command key and Alt is the Option key. Some browsers
 * map the Windows key to the Meta modifier.
 *
 * Handler Functions
 *
 * Handlers are invoked as methods of the document or document element on which
 * the keymap is installed and are passed two arguments:
 *   1) the event object for the keydown event
 *   2) the key identifier of the key that was pressed
 * The handler return value becomes the return value of the keydown handler.
 * If a handler function returns false, the keymap will stop bubbling and
 * cancel any default action associated with the keydown event.
 *
 * Limitations
 *
 * It is not possible to bind a handler function to all keys. The operating
 * system traps some key sequences (Alt-F4, for example). And the browser
 * itself may trap others (Ctrl-S, for example). This code is browser, OS,
 * and locale-dependent. Function keys and modified function keys work well,
 * and unmodified alphanumeric keys work well. The combination of Ctrl and Alt
 * with alphanumeric characters is less robust.
 *
 * Most punctuation characters that do not require the Shift key (`=[];',./\ 
 * but not hyphen) on standard US keyboard layouts are supported. But they are
 * not particularly portable to other keyboard layouts and should be avoided.
 */

// This is the constructor function
function Keymap(bindings) {
    this.map = {};    // Define the key identifier->handler map
    if (bindings) {   // Copy initial bindings into it
        for(name in bindings) this.bind(name, bindings[name]);
    }
}

// Bind the specified key identifier to the specified handler function
Keymap.prototype.bind = function(key, func) {
    this.map[Keymap.normalize(key)] = func;
};

// Delete the binding for the specified key identifier
Keymap.prototype.unbind = function(key) {
    delete this.map[Keymap.normalize(key)];
};

// Install this Keymap on the specified HTML element
Keymap.prototype.install = function(element) {
    // This is the event-handler function
    var keymap = this;
    function handler(event) { return keymap.dispatch(event, element); }

    // Now install it
    if (element.addEventListener)
        element.addEventListener("keydown", handler, false);
    else if (element.attachEvent) 
        element.attachEvent("onkeydown", handler);
};

// This method dispatches key events based on the keymap bindings.
Keymap.prototype.dispatch = function(event, element) {
    // We start off with no modifiers and no key name
    var modifiers = ""
    var keyname = null;

    // Build the modifier string in canonical lowercase alphabetical order.
    if (event.altKey) modifiers += "alt_";      
    if (event.ctrlKey) modifiers += "ctrl_";
    if (event.metaKey) modifiers += "meta_";
    if (event.shiftKey) modifiers += "shift_";

    // The keyname is easy if the DOM Level 3 key property is implemented:
    if (event.key) keyname = event.key;
    // Use the keyIdentifier on Safari and Chrome for function key names
    else if (event.keyIdentifier && event.keyIdentifier.substring(0,2) !== "U+")
        keyname = event.keyIdentifier;
    // Otherwise, use the keyCode property and the code-to-name map below
    else keyname = Keymap.keyCodeToKeyName[event.keyCode];

    // If we couldn't figure out a key name, just return and ignore the event.
    if (!keyname) return;

    // The canonical key id is modifiers plus lowercase key name
    var keyid = modifiers + keyname.toLowerCase();

    // Now see if the key identifier is bound to anything
    var handler = this.map[keyid];

    if (handler) {  // If there is a handler for this key, handle it
        // Invoke the handler function
        var retval = handler.call(element, event, keyid);

        // If the handler returns false, cancel default and prevent bubbling
        if (retval === false) {
            if (event.stopPropagation) event.stopPropagation();  // DOM model
            else event.cancelBubble = true;                      // IE model
            if (event.preventDefault) event.preventDefault();    // DOM
            else event.returnValue = false;                      // IE
        }

        // Return whatever the handler returned
        return retval;
    }
};

// Utility function to convert a key identifier to canonical form.
// On non-Macintosh hardware, we could map "meta" to "ctrl" here, so that
// Meta-C would be "Command-C" on the Mac and "Ctrl-C" everywhere else.
Keymap.normalize = function(keyid) {
    keyid = keyid.toLowerCase();           // Everything lowercase
    var words = keyid.split(/\s+|[\-+_]/); // Split modifiers from name
    var keyname = words.pop();             // keyname is the last word
    keyname = Keymap.aliases[keyname] || keyname; // Is it an alias?
    words.sort();                          // Sort remaining modifiers
    words.push(keyname);                   // Add the normalized name back 
    return words.join("_");                // Concatenate them all
};

Keymap.aliases = {        // Map common key aliases to their "official" 
    "escape":"esc",       // key names used by DOM Level 3 and by 
    "delete":"del",       // the key code to key name map below.
    "return":"enter",     // Both keys and values must be lowercase here.
    "ctrl":"control",
    "space":"spacebar",
    "ins":"insert"
};

// The legacy keyCode property of the keydown event object is not standardized
// But the following values seem to work for most browsers and OSes.
Keymap.keyCodeToKeyName = {
    // Keys with words or arrows on them
    8:"Backspace", 9:"Tab", 13:"Enter", 16:"Shift", 17:"Control", 18:"Alt",
    19:"Pause", 20:"CapsLock", 27:"Esc", 32:"Spacebar", 33:"PageUp",  
    34:"PageDown", 35:"End", 36:"Home", 37:"Left", 38:"Up", 39:"Right",
    40:"Down", 45:"Insert", 46:"Del",

    // Number keys on main keyboard (not keypad)
    48:"0",49:"1",50:"2",51:"3",52:"4",53:"5",54:"6",55:"7",56:"8",57:"9",

    // Letter keys. Note that we don't distinguish upper and lower case
    65:"A", 66:"B", 67:"C", 68:"D", 69:"E", 70:"F", 71:"G", 72:"H", 73:"I",
    74:"J", 75:"K", 76:"L", 77:"M", 78:"N", 79:"O", 80:"P", 81:"Q", 82:"R",
    83:"S", 84:"T", 85:"U", 86:"V", 87:"W", 88:"X", 89:"Y", 90:"Z",

    // Keypad numbers and punctuation keys. (Opera does not support these.)
    96:"0",97:"1",98:"2",99:"3",100:"4",101:"5",102:"6",103:"7",104:"8",105:"9",
    106:"Multiply", 107:"Add", 109:"Subtract", 110:"Decimal", 111:"Divide",

    // Function keys
    112:"F1", 113:"F2", 114:"F3", 115:"F4", 116:"F5", 117:"F6",
    118:"F7", 119:"F8", 120:"F9", 121:"F10", 122:"F11", 123:"F12",
    124:"F13", 125:"F14", 126:"F15", 127:"F16", 128:"F17", 129:"F18",
    130:"F19", 131:"F20", 132:"F21", 133:"F22", 134:"F23", 135:"F24",

    // Punctuation keys that don't require holding down Shift
    // Hyphen is nonportable: FF returns same code as Subtract
    59:";", 61:"=", 186:";", 187:"=", // Firefox and Opera return 59,61 
    188:",", 190:".", 191:"/", 192:"`", 219:"[", 220:"\\", 221:"]", 222:"'"
};
728x90
LIST

' > 자바스크립트 완벽 가이드' 카테고리의 다른 글

20장 클라이언트 스토리지  (0) 2020.11.28
18장 HTTP 스크립팅  (0) 2020.11.27
16장 CSS 다루기  (0) 2020.11.22
15장 문서 스크립팅  (0) 2020.11.21
14장 window 객체  (0) 2020.11.20
댓글
공지사항