Глава 14
Обработка на събития

Вие имате власт над съзнанието - не върху събитията. Осъзнайте това и ще намерите сили.”

Marcus Aurelius, Медитации

Някои програми работят с директен вход на потребителя, като например взаимодействие с мишка и клавиатура. Графикът и редът на такова въвеждане не може да се предвиди по-рано. Това изисква по-различен подход за контрол на потока, от този който използвахме досега.

Манипулиране на събития

Представете си интерфейс, където единственият начин да разберете дали е натиснат клавиш от клавиатурата, е да прочетете текущото състояние на този клавиш. За да можете да реагирате на натискане на клавиш, ще трябва постоянно да следите състоянието на клавиша, така че да можете да го хванете преди да е освободен отново. Това би било опасно за извършване на други интензивни времеви изчисления, тъй като рискувате да пропуснете натискане на клавиш.

Това е редът, по който се обработва входа с по-примитивни машини. Стъпка на горе за хардуера или операционната система е да забележи натискане на клавиш и поставянето му в режим на изчакване. Една програма може след това периодично да проверява чакащите на опашката, за нови събития и да реагира на това, което намира там.

Разбира се, тя трябва да помни, че трябва да прегледа опашката и да го прави често, защото всяко време между натиснатия клавиш и забелязването му от програмата, ще накара софтуера да чувства липса на реакция. Този подход се нарича polling (избирателен). Повечето програмисти го избягват, когато е възможно.

По-добър механизъм за основна система е да дадем шанс на кода да реагира на събития, когато те се появят. Браузърите ни позволяват да регистрираме функции, като handlers (манипулатори) за конкретни събития.

<p>Click this document to activate the handler.</p>
<script>
  addEventListener("click", function() {
    console.log("You clicked!");
  });
</script>

Функцията addEventListener регистрира втория си аргумент да се извиква всеки път, когато събитието описано в първия аргумент възникне.

Събития и DOM разклонения

Всеки манипулатор на събитие в браузъра се регистрира в контекста. Когато извикате addEventListener, както показахме по-рано, вие го извиквате, като метод за целия прозорец, защото в браузъра глобалният обхват е еквивалентен на window обект. Всеки DOM елемент има свой addEventListener метод, който ви позволява да слушате специално за този елемент.

<button>Click me</button>
<p>No handler here.</p>
<script>
  var button = document.querySelector("button");
  button.addEventListener("click", function() {
    console.log("Button clicked.");
  });
</script>

Примерът прикрепва манипулатор към разклонението на бутона. По този начин, кликването върху бутона причинява стартиране на този манипулатор, докато кликванията върху останалата част от документа не го прави.

Давайки на разклонение onclick атрибут има подобен ефект. Но едно разклонение може да има само един onclick атрибут, така че можете да регистрирате само един манипулатор на разклонение по този начин. Метода addEventListener ви позволява да добавите произволен брой манипулатори, така че да не можете случайно да заместите манипулатор, който вече е бил регистриран.

Метода removeEventListener се извиква с аргументи, подобни на addEventListener, за премахване на манипулатор.

<button>Act-once button</button>
<script>
  var button = document.querySelector("button");
  function once() {
    console.log("Done.");
    button.removeEventListener("click", once);
  }
  button.addEventListener("click", once);
</script>

За да можем да опишем функцията манипулатор, ние и даваме име (като, once), така че да можем да я подадем, както на addEventListener, така и на removeEventListener.

Обекти на събития

Върпреки, че го игнорирахме в предишните примери, функцията на манипулатора на събитие приема аргумент: обкет на събитието. Този обект ни дава допълнителна информация за събитието. Например, ако искаме да знаем, кой бутон на мишката е натиснат, можем да разгледаме обекта на събитието със which свойство.

<button>Click me any way you want</button>
<script>
  var button = document.querySelector("button");
  button.addEventListener("mousedown", function(event) {
    if (event.which == 1)
      console.log("Left button");
    else if (event.which == 2)
      console.log("Middle button");
    else if (event.which == 3)
      console.log("Right button");
  });
</script>

Информацията съхранена в обекта на събитието се различава за всеки тип събитие. Ние ще обсъдим различните видове по-късно в тази глава. Свойството type на обекта винаги съдържа string за индентифициране на събитието (например, "click" или "mousedown").

Разпростиране

Манипулаторите на събития регистрирани на разклонения с деца също ще получат някои събития, които се случват на децата. Ако един бутон вътре в параграф се натисне, манипулатора на събитието на параграфа също ще получи клик събитие.

Но ако бутона и параграфа имат манипулатор, тогава по-специфичния манипулатор - този на бутона, ще отиде на първо място. Събитията се разпространяват навън от разклонението, където се е случило към родителското разклонение и до корена на документа. Накрая, след като всички манипулатори регистрирани на конкретно разклонение са минали своя ред, тогава манипулаторите регистрирани на целия прозорец получават шанс да реагират на събитието.

Във всеки един момент можем да извикаме метода stopPropagation на обекта на събитието, за да предотвратим манипулаторите по-нагоре да получат събитието. Това може да бъде полезно, например, когато имаме бутон в друг кликнат елемент и не искаме с кликванията върху бутона да активират клик поведение на външния елемент.

Следващият пример регистрира "mousedown" манипулатори на бутон и параграфът около него. Когато кликнете с десния бутон на мишката, се извиква stopPropagation, което ще попречи на манипулатора на параграфа да реагира. При натискане на друг бутон на мишката, манипулаторите и на двата ще работят.

<p>A paragraph with a <button>button</button>.</p>
<script>
  var para = document.querySelector("p");
  var button = document.querySelector("button");
  para.addEventListener("mousedown", function() {
    console.log("Handler for paragraph.");
  });
  button.addEventListener("mousedown", function(event) {
    console.log("Handler for button.");
    if (event.which == 3)
      event.stopPropagation();
  });
</script>

Повечето обекти на събития имат target свойство, което се отнася до разклонението от което произхождат. Можем да използваме това свойство за да гарантираме, че при случайно боравене с нещо, ще спре разпространението навън от това разклонение.

Също така е възможно да се използва метода target да хвърли широка мрежа за определяне на типа на събитие. Например, ако имаме едно разклонение съдържащо дълъг списък от бутони, може да е по-удобно да регистрираме един клик-манипулатор на външното разклонение и да използваме target свойството, за да разберем, кога е натиснат един бутон, а не да регистрираме индивидуални манипулатори за всеки бутон.

<button>A</button>
<button>B</button>
<button>C</button>
<script>
  document.body.addEventListener("click", function(event) {
    if (event.target.nodeName == "BUTTON")
      console.log("Clicked", event.target.textContent);
  });
</script>

Действие по подразбиране

Много събития имат действие по подразбиране свързано с тях. Ако кликнете върху линк връзка, ще бъдете заведени към целата на връзката. Ако натиснете стрелката надолу, браузърът ще превърти страницата на долу. Ако щракнете с десния бутон, ще получите контекстно меню и т.н.

За повечето видове събития, манипулаторите на събития в JavaScript се извикват преди да се извърши поведението по подразбиране. Ако манипулатора не иска нормалното поведение да се случи, обикновено, защото той вече се е погрижил за обработката на събитието, можем да извикаме метода preventDefault на обекта на събитието.

Той може да се използва за прилагане на собствени клавишни комбинации или контекстно меню. Може също така да бъде използван неприятно за да се меси противно на поведението, което потребителите очакват. Например, това е линк, който не може да бъде последван:

<a href="https://developer.mozilla.org/">MDN</a>
<script>
  var link = document.querySelector("a");
  link.addEventListener("click", function(event) {
    console.log("Nope.");
    event.preventDefault();
  });
</script>

Опитайте се да не правите такива неща, освен ако нямате основателна причина за това. За хората, които използват вашата страница може да бъде неприятно, когато поведението, което очакват се окаже счупено.

В зависимост от браузъра, някои събития не могат да бъдат засечени. На Chrome, например, клавишни комбинации за затваряне на текущия раздел (като, Cthl-W или Command-W) не могат да се обработват от JavaScript.

Клавишни събития

Когато е натиснат клавиш от клавиатурата, на браузъра се предизвиква "keydown" събитие. Когато е освободен - "keyup" събитие.

<p>This page turns violet when you hold the V key.</p>
<script>
  addEventListener("keydown", function(event) {
    if (event.keyCode == 86)
      document.body.style.background = "violet";
  });
  addEventListener("keyup", function(event) {
    if (event.keyCode == 86)
      document.body.style.background = "";
  });
</script>

Въпреки името си "keydown", ефекта не е само, когато клавишът е физически натиснат. Когато клавиша е натиснат и задържан, ефекта на събитието се предизвиква отново всеки път с клавишното повторение. По някога, например, ако искате да се увеличи ускорението на герой от игра с натискането на клавиша със стрелка и да го намалите с пускането на този клавиш, вие трябва да внимавате да не го увеличите прекалено много с всяко клавишно повторение и да се окажете пред неволно огромни стойности.

В предишния пример използвахме свойството keyCode на обекта на събитието. Това е начина, по който можете да определите, кой клавиш е натиснат или освободен. За съжаление, не винаги е ясно, как се превежда номера на клавишния код към действителния клавиш.

За буквени и цифрови клавиши, асоциирания клавишен код ще бъде характер от Unicode таблицата, свързан с (главната) буква или цифрата изписани върху него. Метода charCodeAt на strings ни дава възможност да намерим този код.

console.log("Violet".charCodeAt(0));
// → 86
console.log("1".charCodeAt(0));
// → 49

Други клавиши имат по-малко предсказуеми клавишни кодове. Най добрият начин да намерите нужния ви код е да експериментирате - направете регистър на манипулатора за клавишното събитие, където да събирате кодовете на клавишите и натиснете клавиша, който ви интересува.

Модифициращи клавиши, като Shift, Ctrl, Alt и Meta (Command на Mac) генерират клавишни събития, точно както нормални клaвиши. Но когато търсите клавишни комбинации, може да разберете дали тези клавиши сa натиснати, като погледнете в shiftKey, ctrlKey, altKey и metaKey свойствата на клавиатурата и на мишка събитията.

<p>Press Ctrl-Space to continue.</p>
<script>
  addEventListener("keydown", function(event) {
    if (event.keyCode == 32 && event.ctrlKey)
      console.log("Continuing!");
  });
</script>

Събитията "keydown" и "keyup" дават информация за физическия клавиш, който е натиснат. Но какво става, ако се интересуваме от действително въведения текст? Вземане на този текст от клавишните кодове е неудобно. Вместо това съществува друго събитие "keypress" чийто ефект е, като "keydown" ( и многократно, както при "keydown", когато е задържан клавиша), но само за клавиши, които въвеждат характер. Свойството charCode в обекта на събитието съдържа код, който може да се тълкува, като код с Unicode характер. Можем да използваме String.fromCharCode функцията за да превърнем този код в действителен единичен string характер

<p>Focus this page and type something.</p>
<script>
  addEventListener("keypress", function(event) {
    console.log(String.fromCharCode(event.charCode));
  });
</script>

DOM разклонението, от където идва ключовото събитие зависи от елемента, който е фокусиран, когато клавиша е натиснат. Нормални разклонения не могат да бъдат фокусирани (освен ако не им дадем tabindex атрибут), но такива неща, като линкове, бутони и форми -полета могат. Ще се върнем на форми - полета в Глава 18. Когато нищо по-специално не е фокусирано, document.body действа, като целево разклонение на клавишното събитие.

Кликвания на мишката

Натискането на бутона на мишката, също води до редица ефекти на събития. Събитията "mousedown" и "mouseup" са подобни на "keydown" и "keyup" и ефектите, когато бутона е натиснат или освободен. Това се случва на DOM разклонения, които са непосредствено под показалеца на мишката, когато възникне събитието.

След "mouseup" събитието, ефекта на "click" събитието е върху по -специфично разклонение, което съдържа и двете: натискнето и освобождаването на бутона. Например, ако натиснем бутона на мишката върху един параграф, а след това преместим показалеца върху друг параграф и отпуснем бутона, "click" събитието ще се случи на елемента, които съдържа тези два параграфа.

Ако две кликвания се случат близо едно до друго, ефекта на събитието "dblclick" (двойно кликване) ще се случи след второто кликване.

За да получите точна информация за мястото, където дадено събитие на мишката се случва, може да погледнете неговите pageX и pageY свойства, които съдържат координатите на събитието(в пиксели), отнасящи се към горния ляв ъгъл на документа.

Следващият пример, изпълнява една проста чертожна програма. Всеки път, когато щракнете на документа, той добавя една точка под показалеца на мишката. Вижте Глава 19 за по-сложна чертожна програма.

<style>
  body {
    height: 200px;
    background: beige;
  }
  .dot {
    height: 8px; width: 8px;
    border-radius: 4px; /* rounds corners */
    background: blue;
    position: absolute;
  }
</style>
<script>
  addEventListener("click", function(event) {
    var dot = document.createElement("div");
    dot.className = "dot";
    dot.style.left = (event.pageX - 4) + "px";
    dot.style.top = (event.pageY - 4) + "px";
    document.body.appendChild(dot);
  });
</script>

Свойствата clientX и clientY са подобни на pageX и pageY, но относно тази част на документа, която в момента се движи заедно с изгледа. Това може да бъде полезно, когато се сравняват координатите на мишката с координатите върнати от getBoundingClientRect, който също връща демонстрационен прозорец с относителни координати.

Движение на мишката

Всеки път, когато показалецът на мишката се движи се създават ефекти на "mousemove" събития. Това събитие може да се използва за проследяване на позицията на мишката. Една обща ситуация, в която това е полезно е при изпълнението на някаква форма на плъзгаща (drag) функционалност за мишката.

Както например, следната програма показва бар и създава събитие манипулатор, така че при плъзване наляво или на дясно прави този бар по-дълъг или по-къс.

<p>Drag the bar to change its width:</p>
<div style="background: orange; width: 60px; height: 20px">
</div>
<script>
  var lastX; // Tracks the last observed mouse X position
  var rect = document.querySelector("div");
  rect.addEventListener("mousedown", function(event) {
    if (event.which == 1) {
      lastX = event.pageX;
      addEventListener("mousemove", moved);
      event.preventDefault(); // Prevent selection
    }
  });

  function buttonPressed(event) {
    if (event.buttons == null)
      return event.which != 0;
    else
      return event.buttons != 0;
  }
  function moved(event) {
    if (!buttonPressed(event)) {
      removeEventListener("mousemove", moved);
    } else {
      var dist = event.pageX - lastX;
      var newWidth = Math.max(10, rect.offsetWidth + dist);
      rect.style.width = newWidth + "px";
      lastX = event.pageX;
    }
  }
</script>

Имайте в предвид, че манипулатора "mousemove" е регистриран на целия прозорец. Дори ако мишката отиде извън бара по време на плъзгането, ние все още искаме да обновим размера му и да спрем плъзгането, когато мишката се освободи.

Трябва да спрем преоразмеряването на бара , когато бутона на мишката е свободен. За съжаление, не всички браузъри поддържат "mousemove" събития за смислени which свойства. Налице е стандартното свойство buttons, което осигурява подобна информация, но това също не се поддържа от всички браузъри. За щастие всички основни браузъри поддържат или buttons или which, така че buttonPressed функцията в примера първо прави опит с buttons и ако не се поддържа се прехвърля към which.

Всеки път, когато показалеца на мишката влезе или излезе от разклонението, се създават ефектите на "mouseover" или "mouseout" събитията. Тези две събития могат да се използват наред с други неща, за да създадете “Hover” ефект, показващ или оформящ нещо, когато мишката е над даден елемент.

За съжаление създаването на такъв ефект не е толкова просто, като се започне с "mouseover" и да се завърши със "mouseout". Когато мишката се движи от едно разклонение към неговите деца, ефекта на "mouseout" остава на родителското разклонение, въпреки че мишката вече не е в неговия обхват. За да направим нещата по-лоши, тези събития се разпространяват точно, както и другите събития и по този начин ще получите "mouseout" събитие, когато мишката напусне едно от детските разклонения, върху което манипулатора е регистриран.

За да заобиколите този проблем, можете да използвате свойството relatedTarget на обекта на събитието, създадено за тези събития. То ви казва в случай на "mouseover", над какъв елемент е показалеца и в случай на "mouseout", от кой елемент излиза. Ние искаме да променим нашия “hover” ефект, само когато relatedTarget е извън целевото разклонение. Само в този случай това събитие представлява преминаването отвън на вътре в разклонението (или обратното).

<p>Hover over this <strong>paragraph</strong>.</p>
<script>
  var para = document.querySelector("p");
  function isInside(node, target) {
    for (; node != null; node = node.parentNode)
      if (node == target) return true;
  }
  para.addEventListener("mouseover", function(event) {
    if (!isInside(event.relatedTarget, para))
      para.style.color = "red";
  });
  para.addEventListener("mouseout", function(event) {
    if (!isInside(event.relatedTarget, para))
      para.style.color = "";
  });
</script>

Функцията isInside следва връзките на дадено родителско разклонение докато или достигне горната част на документа (където node става нула) или намира родителя, който търсим.

Трябва да добавя, че “hover” ефекта, може да бъде постигнат много по-лесно с помоща на CSS :hover, както следващия пример показва. Но ако “hover” ефекта включва нещо по-сложно, отколкото смяна на стила на целевото разклонение, трябва да използвате начина с "mouseover" и "mouseout" събитията.

<style>
  p:hover { color: red }
</style>
<p>Hover over this <strong>paragraph</strong>.</p>

Scroll събития (превъртане)

Всеки път, когато даден елемент се превърта, се използва ефекта на "scroll" събитието върху него. Той има различни цели, като да знае, какво потребителя гледа в момента (за забраняване на анимации на екрана или изпращане на шпионски доклади към централата на злите) или показва известни признаци на прогрес (като маркираме част от съдържанието на таблица или появяване на номер на страница).

Следващият пример чертае прогреса на бар в горния десен ъгъл на документа и го актуализира за да се запълни, когато превъртаме на долу.

<style>
  .progress {
    border: 1px solid blue;
    width: 100px;
    position: fixed;
    top: 10px; right: 10px;
  }
  .progress > div {
    height: 12px;
    background: blue;
    width: 0%;
  }
  body {
    height: 2000px;
  }
</style>
<div class="progress"><div></div></div>
<p>Scroll me...</p>
<script>
  var bar = document.querySelector(".progress div");
  addEventListener("scroll", function() {
    var max = document.body.scrollHeight - innerHeight;
    var percent = (pageYOffset / max) * 100;
    bar.style.width = percent + "%";
  });
</script>

Позициониране на елемента с position : fixed, много прилича на absolute позиция, но разликата е, че го предпазва от превъртане заедно с останалата част от документа. Ефекта е, че правим нашия бар неподвижен в своя ъгъл. Вътре в него е друг елемент, който се преоразмерява, за да посочи текущия напредък. Използваме % вместо px, като мерна единица при определяне на ширината на елемента, така че елемента да е съразмерен спрямо цялата лента.

Глобалната променлива innerHeight ни дава височината на прозореца, която трябва да извадим от общата скрол - височина scrollHeight, за да не можем да продължим да превъртаме, докато стигнем края на документа.(Има също и innerWidth, който се мести заедно с innerHeight.) Чрез разделяне на pageYOffset (текущата позиция на превъртане) с максималната позиция на превъртане и се умножи по 100, получаваме процента за прогреса на лентата.

Извикването на preventDefault върху скрол събитието не пречи на превъртането да се случи. В действителност, манипулатора на събитието се извиква само след, като превъртането се състои.

Фокус събития

Когато един елемент се фокусира, браузърът използва ефекта на "focus" събитие върху него. Когато губи фокус използва ефекта на "blur" събитие.

За разлика от събитията, обсъдени по рано, тези две събития не се разпространяват. Манипулатора на родителския елемент, не се уведомява, когато дъщерният елемент е фокусиран или губи фокус.

Следващият пример показва помощен текст в текстово поле, което сме фокусирали в момента.

<p>Name: <input type="text" data-help="Your full name"></p>
<p>Age: <input type="text" data-help="Age in years"></p>
<p id="help"></p>

<script>
  var help = document.querySelector("#help");
  var fields = document.querySelectorAll("input");
  for (var i = 0; i < fields.length; i++) {
    fields[i].addEventListener("focus", function(event) {
      var text = event.target.getAttribute("data-help");
      help.textContent = text;
    });
    fields[i].addEventListener("blur", function(event) {
      help.textContent = "";
    });
  }
</script>

Обекта на window ще получи "focus" и "blur" събитията, когато потребителя се движи във браузъра или в прозореца, в който се показва документа.

Load събитие

Когато една страница завърши зареждането, използва ефекта на "load" събитието върху прозореца и обектите в тялото на документа. Това често се използва за планиране на действията за инициализиране, които изискват целият документ да бъде построен. Не забравяйте , че съдържанието на <script> таговете се стартират в момента, когато се срещне тага. Това често е твърде рано, например, когато скрипта трябва да направи нещо с част от документа, който се появява след <script> тага.

Елементи, като изображения и скрипт тагове, които се зареждат от външен файл също имат "load" събитие, което показва, че файловете са заредени. Подобно на събитията свързани с фокуса, load събитията не се разпространяват.

Когато страницата е затворена или навигацията е далеч от там (например, следването на линк), се използва ефекта на "beforeunload" събитието. Основната употреба на това събитие е да предотврати отказването на потребителя (да загуби интерес и да затвори прозореца) поради дългото зареждане на страницата. Предотвратяване на страницата от unloading не се прави, както се очаква с preventDefault метода. Вместо това се прави чрез връщане на string от манипулатора. String-а ще се използва в диалогов прозорец, който пита потребителя дали иска да остане на страницата или да я напусне. Този механизъм гарантира, че потребителя е в състояние да напусне страницата, дори ако тя изпълнява злонамерен скрипт, който би предпочел да го задържи там завинаги, за го принуди да гледа свързани с отслабване реклами.

Изпълнение на скрипт по график

Има различни неща, които да са причина скрипта да започне да се изпълнява. Четенето на <script> таг е едно такова нещо. Ефект на събитие е съвсем друго нещо. В Глава 13 обсъдихме функцията requestAnimationFrame, чиято функция за графика да бъде извикана преди следващата страница да се преначертае. Това е още един начин, по който един скрипт може да започне да работи.

Важно е да се разбере, че макар и събитията да могат да имат ефекти по всяко време, няма два скрипта в един документ, които да се изпълняват в един и същи момент. Ако скрипт вече се изпълнява, манипулаторите за събития и парчета код, планирани по друг начин трябва да чакат реда си. Това е причината поради , която един документ замръзва, когато даден скрипт работи по-дълго време. Браузърът не може да реагира на кликвания и други събития в рамките на документа, тъй като не може да работи със събития, докато текущия скрипт не приключи работа.

Някои програмни среди позволяват множество нишки за изпълнение, които да се движат по едно и също време. Правенето на няколко неща едновременно, може да се използва да се направи една програма по-бърза. Но когато имате множество участници, които действат по едно и също време в едни и същи части на системата, мисленето на програмата става най-малко една идея по-трудно.

Фактът, че програмите на JavaScript могат да правят само едно нещо в даден момент прави живота ни по-лесен. За случаите, в които някои неща наистина отнемат време, може да се пусне потребителски фонов режим без замръзване на страницата, браузърите предоставят нещо наречено web workers. Worker е изолирана от JavaScript среда, която работи с основната програма на документа и може да комуникира с него само чрез изпращане и приемане на съобщения.

Да приемем, че имаме следния код във файла, наречен code/squareworker.js:

addEventListener("message", function(event) {
  postMessage(event.data * event.data);
});

Представете си, че квадратния номер е тежко продължително изчисление, за това ние искаме да се изпълни във фонов режим. Този код създава worker, който ще изпрати няколко съобщения и ще изведе отговорите.

var squareWorker = new Worker("code/squareworker.js");
squareWorker.addEventListener("message", function(event) {
  console.log("The worker responded:", event.data);
});
squareWorker.postMessage(10);
squareWorker.postMessage(24);

Функцията postMessage изпраща съобщение, което ще доведе до ефекта на "message" събитие в приемника. Скрипта, който създава worker изпраща и получава съобщения чрез обекта на worker, докато worker разговаря със скрипта, който го е създал чрез изпращане и слушане направо от неговия глобален обхват, което е нов глобален обхват, а не споделен с оригиналния скрипт.

Настройка на таймерите

Функцията setTimeout е подобна на requestAnimationFrame. Тя планира друга функция да бъде извикана по-късно. Но вместо да извика другата функция на следващото преначертаване, тя изчаква дадено количество милисекунди. Тази страница сменя фона си от син в жълт след две секунди.

<script>
  document.body.style.background = "blue";
  setTimeout(function() {
    document.body.style.background = "yellow";
  }, 2000);
</script>

Понякога трябва да се отмени планираната функция. Това се прави чрез съхраняване на стойността върната от setTimeout и извикване на clearTimeout върху нея.

var bombTimer = setTimeout(function() {
  console.log("BOOM!");
}, 500);

if (Math.random() < 0.5) { // 50% chance
  console.log("Defused.");
  clearTimeout(bombTimer);
}

Функцията cancelAnimationFrame работи по същия начин, както clearTimeout - извикана върху върнатата стойност от requestAnimationFrame ще отмени тази рамка.

Подобен набор от функции setInterval и clearInterval се използват за задаване на таймери, които трябва да се повтарят на всеки X милисекунди.

var ticks = 0;
var clock = setInterval(function() {
  console.log("tick", ticks++);
  if (ticks == 10) {
    clearInterval(clock);
    console.log("stop.");
  }
}, 200);

Debouncing

Някои видове събития имат потенциала да се изстрелват много бързо и много пъти в един ред (като "mousemove" и "scroll" събитията). Когато обработвате такива събития, вие трябва да бъдете внимателни да не правите нещо твърде дълго или вашия манипулатор да отнема толкова много време, че взаимодействието с документа да се чувства бавно и накъсано.

Ако все пак трябва да направите нещо nontrivial с такъв манипулатор, можете да използвате setTimeout, за да се уверите, че не го прави твърде често. Това обикновено се нарича debouncing на събитието. Има няколко леко различаващи се подхода към това.

В първия пример, искаме да направим нещо, когато потребителя е въвел нещо, но не искаме да го изпълним веднага за всяко клавишно събитие. Когато се пише бързо, ние просто искаме да изчакаме докато настъпи пауза. Вместо незабавно извършване на действия в манипулатора на събитието, ще създадем таймаут. Също така ще изчистим предишното изчакване (ако има такова), така че когато се случват събития близо едно до друго (по-близо от нашето изчакване на закъснение), времето за изчакване от предишното събитие ще бъде анулирано.

<textarea>Type something here...</textarea>
<script>
  var textarea = document.querySelector("textarea");
  var timeout;
  textarea.addEventListener("keydown", function() {
    clearTimeout(timeout);
    timeout = setTimeout(function() {
      console.log("You stopped typing.");
    }, 500);
  });
</script>

Давайки недефинирана стойност на clearTimeout или да го наречем изчакване, събитието вече няма ефект. Затова, трябва да бъдем внимателни, когато го извикаме, като просто да го прави за всяко събитие.

Можем да използваме малко по-различен подход, ако искаме отговори на интервали, така че да са разделени поне за известен период от време, но искаме ефекта през серията от събития, а не само по-късно. Например, можем да искаме да се отговори на "mousemove" събитията, като показваме текущите координати на мишката, но само на всеки 250 милисекунди.

<script>
  function displayCoords(event) {
    document.body.textContent =
      "Mouse at " + event.pageX + ", " + event.pageY;
  }

  var scheduled = false, lastEvent;
  addEventListener("mousemove", function(event) {
    lastEvent = event;
    if (!scheduled) {
      scheduled = true;
      setTimeout(function() {
        scheduled = false;
        displayCoords(lastEvent);
      }, 250);
    }
  });
</script>

Резюме

Манипулаторите на събития дават възможност да се открят и реагира на събития, върху които нямаме пряк контрол. Метода addEventListener се използва, за да се регистрира такъв манипулатор.

Всяко събитие има тип ("keydown", "focus" и т.н.), който го идентифицира. Повечето събития се извикват върху конкретен DOM елемент и след това се разпространяват към родителските елементи, позволявайки на манипулаторите свързани с тези елементи да ги ръководят.

Когато се извика манипулатор на събитие, той се предава на обекта на събитието с допълнителна информация за това събитие. Този обект също има методи, които ни позволяват да спрем по-нататъшното разпространение със (stopPropagation) и предотвратяват по подразбиране работата на браузъра със събитието с (preventDefault).

Натискането на клавиши, предизвикват ефекти на "keydown", "keypress" и "keyup" събитията. Натискането на бутон на мишката, предизвиква ефекти на "mousedown", "mouseup" и "click" събитията. Движението на мишката, предизвиква ефекти на "mousemove", "mouseenter" и "mouseout" събитията.

Превъртането може да се установи със "scroll" събитието, а фокус промени могат да бъдат установени с "focus" и "blur" събитията. Когато документа завърши зареждането, "load" събитието предизвиква ефекти в прозореца.

Само една част от програма на JavaScript може да работи в даден момент. По този начин, манипулатори на събития и други планирани скриптове трябва да изчакат другите скриптове да завършат преди да дойде техния ред.

Упражнения

Цензурирана клавиатура

Между 1928г. и 2013г. турското законодателство забранява употребата на буквите Q, W и X в официални документи. Това е част от по-широка инициатива за задушаване на кюрдската култура, където тези букви се срещат в езика използван от кюрдите, но не и в Инстанбулския турски.

Като упражнение с тези нелепи неща от технологията, искам да програмирате текстово поле (<input type="text">), така че тези букви да не могат да бъдат въведени вътре.

(Не се притеснявайте за копиране и поставяне и други подобни вратички).

<input type="text">
<script>
  var field = document.querySelector("input");
  // Your code here.
</script>

Решението на това упражнение включва предотвратяване на поведението по подразбиране за клавишни събития. Може да използвате "keypress" или "keydown". Ако някое от тях извика preventDefault буквата няма да се появи.

Идентифицирането на буква при въвеждане изисква преглеждането и с keyCode или charCode свойствата и сравняване с кодовете на буквите, които искате да филтрирате. В "keydown" не е нужно да се притеснявате за малки или главни букви, тъй като той определя само дали клавиша е натиснат. Ако решите да ползвате "keypress", който идентифицира действително въведения характер, трябва да се уверите, че тества и двата случая. Един от начините да се направи това е:

/[qwx]/i.test(String.fromCharCode(event.charCode))

Mouse пътека

В ранните дни на JavaScript, което беше време на крещящи начални страници с много анимирани изображения, хората излязоха с някои наистина вдъхновяващи начини за използване на езика.

Един от тях е “миша пътека”- серия от снимки, които следват показалеца на мишката, като го преместим на другата страна на страницата.

В това упражнение, искам да приложите една пътека на мишката. Използвайте абсолютно позиционирани <div> елементи с фиксиран размер и цвят на фона( направете справка в секцията Кликвания на мишката за пример). Създайте едни куп такива елементи и когато мишката се движи, да се показват в началото на показалеца на мишката.

Има няколко различни възможни подхода тук. Можете да направите вашето решение толкова просто или толкова сложно, колкото си искате. Просто решение е да поддържате фиксиран брой елементи в пътеката и цикъл над тях за преместване на следващия от тях в текущата позиция на мишката всеки път, когато "mousemove" събитието се случва.

<style>
  .trail { /* className for the trail elements */
    position: absolute;
    height: 6px; width: 6px;
    border-radius: 3px;
    background: teal;
  }
  body {
    height: 300px;
  }
</style>

<script>
  // Your code here.
</script>

Създаването на елементите е най-добре с цикъл. Добавете ги към документа, за да ги покаже. За да може да получите достъп до тях по-късно за промяна на позицията им, ги съхранете в масив.

Търкалянето чрез тях може да се направи с брояч променлива и добавяне на 1 към нея всеки път, когато има "mousemove" събитие. Оператора за остатък (% 10) може да се ползва за получаване на валиден масив с индекси, за да изберете елемента, който искате да се позиционира по време на дадено събитие.

Друг интересен ефект може да се постигне чрез моделиране на проста физична система. Използвайте "mousemove" събитието, само за да актуализира един чифт променливи, които следят положението на мишката. След това използвайте requestAnimationFrame за задните елементи, да симулират привличане от показалеца на мишката. На всяка стъпка анимация актуализирайте позицията на базата на тяхното положение спрямо показалеца (и евентуално скороста, която се съхранява за всеки елемент). Измислянето на добър начин да направите това е до вас.

Етикети

Етикетиран интерфейс е общ модел на дизайна. Той ви позволява да изберете интерфейсен панел, като избирате от редица номерирани етикети прикрепени към даден елемент.

В това упражнение ще приложите просто етикетиране на интерфейс. Напишете функция asTabs, която взема DOM разклонение и създава етикетиран интерфейс, показващ децата елементи на това разклонение. Трябва да въведете списък <button> елементи в горната част от разклонението, по един за всяко дете елемент, съдържащ текст извлечен от data-tabname атрибутa на детето. Всички освен едно от оригиналните деца трябва да бъдат скрити (даден стил - display: none) и видими в момента на избиране на родителското разклонение, когато кликнете върху бутоните.

Когато това работи, може да го разширите, като добавите style към активния в момента бутон по различен начин.

<div id="wrapper">
  <div data-tabname="one">Tab one</div>
  <div data-tabname="two">Tab two</div>
  <div data-tabname="three">Tab three</div>
</div>
<script>
  function asTabs(node) {
    // Your code here.
  }
  asTabs(document.querySelector("#wrapper"));
</script>

Има един недостатък, с който най-вероятно ще се сблъскате е, че не може да използвате директно childNodes свойството върху разклоненията, като колекция от етикети на разклонения. От една страна, когато добавите бутони те ще отидат също към децата на разклоненията и в крайна сметка в този обект, защото той е жив. От друга страна, текстовите разклонения, създадени за интервал между разклоненията също са там и те трябва да получат свои собствени етикети.

За да се справите с това, започнете с изграждането на истински масив от всички деца в обвивка, които имат nodeType от 1.

Когато се регистрира събитие от манипулаторите на бутоните, функцията на манипулатора трябва да знае с кой етикет на елемент е свързан бутона. Ако те са създадени в един нормален цикъл, може да получите достъп до индекса на променливата от вътрешноста на функцията, но това няма да ви даде правилният номер, защото тази променлива ще е допълнително променена от цикъла.

Едно просто временно решение е да се използва forEach метода и създаване на функция манипулатор от вътрешноста на функцията подадена към forEach. Индекса на цикъла, който се подава като втори аргумент на тази функция, ще бъде нормална локална променлива и така няма да бъде презаписана от по-нататъшни повторения.