Event - garevna/js-course GitHub Wiki

🎓 Events

В цепочке прототипов любого элемента DOM есть объект ( класс ) EventTarget

Благодаря этому все элементы DOM способны "реагировать" на события

Выведем в консоль этот объект:

console.dir ( EventTarget )

Более всего нас интересует, конечно, его свойство prototype

▼ ƒ EventTarget()
    arguments: null
    caller: null
    length: 0
    name: "EventTarget"
    ▼ prototype: EventTarget
        ► addEventListener: ƒ addEventListener()
        ► dispatchEvent: ƒ dispatchEvent()
        ► removeEventListener: ƒ removeEventListener()
        ► constructor: ƒ EventTarget()
          Symbol(Symbol.toStringTag): "EventTarget"
        ► __proto__: Object
    ► __proto__: ƒ ()

Здесь мы видим три метода, которые унаследуют все объекты, имеющие в цепочке прототипов EventTarget

Мы уже в курсе, что объект Node наследует от объекта EventTarget,

а объект Element наследует от Node,

потому что элементы DOM - это частный случай узла DOM

В свойстве prototype объекта Element мы обнаружим не слишком длинный перечень свойств, начинающихся на on

Однако объект Element является только прототипом объекта HTMLElement

А вот последний как раз и является непосредственным прототипом всех элементов DOM

Поэтому, очевидно, искать события, общие для всех элементов DOM, нужно именно в его свойстве prototype

for ( var prop in HTMLElement.prototype ) {
    if ( prop.indexOf ( 'on' ) !== 0 ) continue
    console.info ( `Event: ${prop.slice(2)}` ) 
}

Однако элементы DOM значительно отличаются друг от друга

У каждого html-элемента есть собственный конструктор, который "добавляет" специфические" для этого элемента события

( например, события input и change могут произойти только на элементах форм )


☕ DOMNodeInserted
document.body.ondomnodeinserted = 
    function ( event ) {
        console.log ( event )
    }

document.body.appendChild (
    document.createElement ( "div" )
)
Результат:
▼ MutationEvent {isTrusted: true, relatedNode: body, prevValue: "", newValue: "", attrName: "", …}
    attrChange: 0
    attrName: ""
    bubbles: true
    cancelBubble: false
    cancelable: false
    composed: false
    currentTarget: null
    defaultPrevented: false
    eventPhase: 0
    isTrusted: true
    newValue: ""
  ► path: (5) [div, body, html, document, Window]
    prevValue: ""
  ► relatedNode: body
    returnValue: true
  ► srcElement: div
  ► target: div
    timeStamp: 12720.100000005914
    type: "DOMNodeInserted"
  ► __proto__: MutationEvent

⚠️ Событие - это объект 😉

каждое событие создается конструктором Event

У каждого события есть свойство type ( строка ):

✅ click
✅ mouseover
✅ mouseout
✅ mouseenter
✅ mouseleave
✅ mousedown
✅ mouseup
✅ keydown
✅ keyup
...

В примере выше тип события - DOMNodeInserted

Кроме того, у каждого объекта события есть свойство target, которое является ссылкой на элемент, на котором произошло событие

В большинстве случаев это тот элемент, на который был "повешен" обработчик

⚠️ Однако в примере выше обработчик был повешен на элемент body,
а свойство target указывает на добавленный в DOM элемент

В следующем примере свойство target будет ссылкой на тот элемент, на котором произошел клик

☕ event.target
const pictures = [
    "https://www.insidescience.org/sites/default/files/5_heic1808a_crop.jpg",
    "https://gobelmont.ca/Portals/0/xBlog/uploads/2017/9/6/dancing-156041_960_720.png",
    "https://i2-prod.mirror.co.uk/incoming/article11840943.ece/ALTERNATES/s615/PAY-MATING-BUGS.jpg",
    "https://i.redd.it/otqqqga0ip211.jpg"
]

const divs = pictures.map (
    picture => {
        let div = document.body.appendChild (
            document.createElement ( "div" )
        )
        div.style = `
            width: 100px;
            height: 100px;
            border: solid 1px gray;
        `
        div.onclick = function ( event ) {
            let img = event.target.appendChild (
                document.createElement ( "img" )
            )
            img.src = picture
            img.width = 100
        }
        return div
})

Полный перечень событий DOM можно найти в спецификации:

🔗 1️⃣ 🔗 2️⃣


🎓 host-объект Event


Конструктор, с помощью которого создаются все события DOM

Свойство prototype конструктора Event содержит свойства, которые будут унаследованы всеми событиями


var userEvent = new Event( 'user' )

🎓 dispatchEvent

Метод dispatchEvent "отправляет" событие элементу

☕ dispatchEvent
document.body.onclick = function ( event ) {
    this.style.backgroundColor = "#fa0"
}
document.body.dispatchEvent ( new Event ( 'click' ) )

🎓 CustomEvent

Конструктор CustomEvent создает кастомное событие c дополнительными параметрами

☕ CustomEvent
function addElement ( tagName, container ) {
    var _container = 
        container && container.nodeType === 1 ? 
                    container : document.body
    return _container.appendChild (
         document.createElement ( tagName )
    )
}

var obj = addElement ( "h1" )

obj.innerText = "Hi"

obj.addEventListener ( "listen", listenHandler )

function listenHandler ( event ) {
    this.innerText = event.detail
}

var btn = addElement ( "button" )
btn.innerText = "Change"

btn.onclick = function ( event ) {
    var inp = addElement ( "input" )
    inp.onchange = function ( event ) {
        obj.dispatchEvent ( 
             new CustomEvent ( "listen",
                  { 
                       detail: this.value
                  } 
             ) 
        )
        this.parentNode.removeChild ( this )
    }
}

🎓 event handler


У всех элементов есть свойства с именами, начинающимися с "on"

Значения этих свойств должны быть ссылкой на функцию, которая будет вызвана при возникновении события ( event handler )

Обработчик события - это функция, которая вызывается тогда, когда событие произошло

Если определить значение свойства onclick элемента как функцию clickCallback, то в момент, когда пользователь кликнет левой кнопкой мышки на этом элементе, будет вызвана функция clickCallback

Функция clickCallback станет обработчиком события click элемента

⚠️ Обработчики события в момент их вызова получают в качестве первого аргумента объект события

Поэтому при объявлении функции-обработчика настоятельно рекомендуется в качестве первого параметра указывать имя переменной, в которую будет помещена ссылка на объект события:

elem.onclick = function ( event ) { ... }
elem.onmouseover = function ( mev ) { ... }

Таким образом объект события становится доступным внутри обработчика


🎓 event.screenX | event.screenY

Координаты указателя мышки относительно левого верхнего угла физического экрана


🎓 event.clientX | event.clientY

Координаты указателя мышки относительно верхнего левого края видимой части окна браузера ( viewport )

Эти координаты не зависят от положения полосы прокрутки окна браузера


🎓 event.pageX | event.pageY


Координаты указателя мышки относительно верхнего левого края страницы

Эти координаты зависят от положения полосы прокрутки окна браузера


🎓 eventPhase


🎓 eventListener

Методы добавления и удаления прослушивателей событий:

✅ addEventListener

✅ removeEventListener

👀 Свойства "on..." позволяют "повесить" только одного обработчика данного события на данный элемент

👂 eventListener-ов может быть сколько угодно для одного и того же элемента и одного и того же события

Предположим, мы вешаем обработчика события mousemove на все элементы div

Затем вешаем "персонального" обработчика события mousemove на div#sample

На элементе div#sample "сработают" оба обработчика при наведении указателя мышки


🎓 addEventListener


Первый аргумент метода addEventListener - это тип события ( строка ), например: "mouseover" "mouseout" "input" "change" ...

Второй аргумент - ссылка на функцию ( обработчика события )


☕ 1️⃣

document.getElementById ( '#sample' )
    .addEventListener ( 'click', function ( event ) {
         console.log ( 'sample click event: ', event )
     })

☕ 2️⃣

var elem = document.body.appendChild (
    document.createElement ( "p" )
)
elem.innerText = "Hello"
function clickdHandler ( event ) {
    this.innerHTML = `
        <small>
            My content was changed!
        </small>
    `
}
elem.addEventListener ( 'click', clickdHandler )

Третий аргумент - логическое значение - будучи установленным в true, позволяет перехватить событие на фазу погружения ( capturing )


☕ 3️⃣

var btn = document.createElement ( 'button' )
btn.innerText = "OK"
btn.style = `
    background-image: url(https://cdn2.iconfinder.com/data/icons/user-23/512/User_Yuppie_2.png);
    background-size: contain;
    background-repeat: no-repeat;
    background-position: left center;
    padding: 5px 10px 5px 30px;
`
document.body.appendChild ( btn )

btn.addEventListener ( 'click', function ( event ) {
    console.log ( event.currentTarget.tagName, event.eventPhase )
}, true )

document.body.addEventListener ( 'click', function ( event ) {
    console.info ( event.currentTarget.tagName, event.eventPhase )
}, true )

🎓 removeEventListener


Прослушивателей событий нужно удалять, поскольку они не убираются автоматически при удалении элемента

При удалении нужно передавать точно такие же аргументы, какие были переданы методу addEventListener при создании прослушивателя


☕ 4️⃣

Такой вариант удаления не сработает:

document.getElementById ( 'sample' )
    .addEventListener ( 'click', function ( event ) {
         console.log ( 'sample click event: ', event )
     })
document.getElementById ( 'sample' )
    .removeEventListener ( 'click', function ( event ) {
         console.log ( 'sample click event: ', event )
    })

☕ 5️⃣

А такой - да:

function clickHandler ( event ) {
   this.innerHTML = '<small>My content was changed!</small>'
}
elem.addEventListener ( 'click', clickHandler )
elem.removeEventListener ( 'click', clickHandler )

☕ 6️⃣

Разметка
<div id="main-frame" class="wrapper">
    <div id="main-content">
        <div id="main-message">
            <h1>Event Listener</h1>
            <p>Тестируем работу eventListener</p>
            <div id="list">
               <p>Что нужно помнить:</p>
               <ul class="single">
                  <li>eventListener-ов нужно удалять</li>
                  <li>Для этого есть метод removeEventListener</li>
               </ul>
           </div>
           <div class="error">Error was detected</div>
           <div id="diagnose">Печалька</div>
        </div>
    </div>
</div>
<div id="error">
   <p id="details">____________________</p>
</div>
Скрипт
var collection = document.querySelectorAll ( 'p ~ *' )
collection.forEach ( x => {
   if ( x.nodeType === 1 )
       x.addEventListener ( 'mouseover', function ( event ) {
           console.warn ( 
              event.target.tagName + (
              event.target.id ? '#' + event.target.id : 
              event.target.className ?
              '.' + event.target.className :
              ' content: ' + event.target.innerHTML )
           )
      } )
} )

var elem = document.querySelector ( '#list' )
elem.addEventListener ( 'mouseover', function ( event ) {
   event.target.innerHTML = `event: ${new Date().toLocaleString()}`
} )

function clickHandler ( event ) {
   this.innerHTML = '<small>My content was changed!</small>'
}
elem.addEventListener ( 'click', clickHandler )

🎓 preventDefault()

Иногда мы не хотим, чтобы при наступлении события элемент HTML вел себя так, как он должен себя вести по умолчанию

В таком случае мы можем использовать метод preventDefault()

Например, по умолчанию в результате клика на гиперссылке происходит переход по адресу, указанному атрибутом href

Мы можем внутри обработчика события click элемента a вызвать метод preventDefault(), что предотвратит поведение по умолчанию, и перехода не будет


☕ 7️⃣

var elem = document.body.appendChild ( 
     document.createElement ( 'a' )
)
elem.innerText = "click me"
elem.href = "https://www.w3schools.com/charsets/ref_utf_punctuation.asp"
elem.addEventListener ( 'click', 
    function ( event ) {
        event.preventDefault()
        alert ( `href: ${this.href}` )
    }
)

🎓 stopPropagation()

Почти все события "всплывают" ( но не все, например, событие focus не всплывает )

Предотвращает "всплытие" события, т.е. срабатывание обработчиков этого события на элементах, внутри которых находится целевой элемент


☕ 8️⃣

Запустите код в консоли, кликните на самом маленьком кружке и посмотрите, что будет выведено в консоль

var elemData = {
   name: "div",
    attrs: {
       className: "container",
       title: "Контейнер",
       style: `
           position: absolute;
           top: 20px;
           left: 20px;
           border-radius: 50%;
           border: dotted 2px #789;
           background-color: #70ff9090;
       `
   }
}
function clickHandler ( event ) {
    // event.stopPropagation()
    console.info ( this.num )
}

function insertElement ( elemNum, parentElem ) {
   var elem = parentElem.appendChild (
       document.createElement ( elemData.name )
   )
   elem.num = elemNum

   for ( var attr in elemData.attrs )  
      elem [ attr ] = elemData.attrs [ attr ]
   elem.style.width = `${400 - elemNum * 50}px`
   elem.style.height = `${400 - elemNum * 50}px`

   elem.addEventListener ( 'click', clickHandler )
   return elem
}

var elems = []
elems [0] = insertElement ( 0, document.body )
for ( var x = 1; x < 5; x++ ) {
   elems [x] = insertElement ( x, elems [ x - 1 ] )
}

Теперь перезагрузите страницу, опять вставьте код, но раскомментируйте строку

event.stopPropagation()

кликните на самом маленьком кружке и посмотрите, что будет выведено в консоль


🎓 stopImmediatePropagation()

Если у элемента есть несколько прослушивателей одного и того же события, они будут вызваны в том порядке, в котором они были добавлены

Если один из обработчиков, установленных одним из этих listener-ов, вызовет метод event.stopImmediatePropagation (), то остальные listener-ы, следующие за ним, уже не сработают


☕ 9️⃣

Если выполнить следующий код в консоли:

var elem = document.body.appendChild (
    document.createElement ( 'p' )
)
elem.innerHTML = 'Click me, please'

var text = [
    'Hello',
    'are you happy?',
    'what is your favorite language?',
    'Bye'
]
elem.addEventListener ( 'click', 
   function ( event ) {
      // event.stopImmediatePropagation()
      console.log ( 'Я тут первый, остальные на фиг!' )
   }
)
for ( var txt of text ) {
    elem.addEventListener ( 'click', 
        ( function ( message ) {
            return function () {
                console.log ( message )
            }
        })( txt )
    )
}

то при клике на элементе сработают все прослушиватели собятия click элемента в той последовательности, в какой мы их определили

Однако если убрать слеши перед строчкой

event.stopImmediatePropagation()

то сработает только один прослушиватель, и выведена в консоль будет только одна строчка


🔗 W3S



Примеры в песочнице:

☕ mouseover & mouseout

☕ mouseover & mouseout vs mouseenter & mouseleave

☕ onscroll | onwheel

☕ keypress vs keydown

☕ dispatchEvent


⚠️ **GitHub.com Fallback** ⚠️