Js Ui
Js Ui
Браузер: документ,
события, интерфейсы
Илья Кантор
Сборка от 2 апреля 2023 г.
1/319
●
Загрузка ресурсов: onload и onerror
●
Разное
●
MutationObserver: наблюдатель за изменениями
●
Selection и Range
● Событийный цикл: микрозадачи и макрозадачи
2/319
Изучаем работу со страницей – как получать элементы, манипулировать их
размерами, динамически создавать интерфейсы и взаимодействовать с
посетителем.
Документ
Здесь мы научимся изменять страницу при помощи JavaScript.
window
location Function
frames
…
history
XMLHttpRequest
3/319
Например, здесь мы используем window как глобальный объект:
function sayHi() {
alert("Hello");
}
А здесь мы используем window как объект окна браузера, чтобы узнать его
высоту:
4/319
DOM – не только для браузеров
Спецификация DOM описывает структуру документа и предоставляет
объекты для манипуляций со страницей. Существуют и другие, отличные от
браузеров, инструменты, использующие DOM.
Например:
●
Объект navigator даёт информацию о самом браузере и операционной
системе. Среди множества его свойств самыми известными являются:
navigator.userAgent – информация о текущем браузере, и
navigator.platform – информация о платформе (может помочь в
понимании того, в какой ОС открыт браузер – Windows/Linux/Mac и так далее).
●
Объект location позволяет получить текущий URL и перенаправить браузер
по новому адресу.
5/319
окна браузера для коммуникации с пользователем.
Спецификации
BOM является частью общей спецификации HTML .
Итого
Спецификация DOM
описывает структуру документа, манипуляции с контентом и события, подробнее
на https://dom.spec.whatwg.org .
Спецификация CSSOM
Описывает файлы стилей, правила написания стилей и манипуляций с ними, а
также то, как это всё связано со страницей, подробнее на
https://www.w3.org/TR/cssom-1/ .
Спецификация HTML
Описывает язык HTML (например, теги) и BOM (объектную модель браузера) –
разные функции браузера: setTimeout , alert , location и так далее,
подробнее на https://html.spec.whatwg.org . Тут берётся за основу
спецификация DOM и расширяется дополнительными свойствами и методами.
Пожалуйста, заметьте для себя эти ссылки, так как по ним содержится очень
много информации, которую невозможно изучить полностью и держать в уме.
Когда вам нужно будет прочитать о каком-то свойстве или методе, справочник на
сайте Mozilla https://developer.mozilla.org/ru/search тоже очень хороший ресурс,
хотя ничто не сравнится с чтением спецификации: она сложная и объёмная, но
сделает ваши знания максимально полными.
6/319
А теперь давайте перейдём к изучению DOM, так как страница – это основа
всего.
DOM-дерево
Это был лишь небольшой пример того, что может DOM. Скоро мы изучим много
способов работать с DOM, но сначала нужно познакомиться с его структурой.
Пример DOM
<!DOCTYPE HTML>
<html>
<head>
<title>О лосях</title>
</head>
<body>
Правда о лосях.
</body>
</html>
DOM – это представление HTML-документа в виде дерева тегов. Вот как оно
выглядит:
7/319
▾ HTML
▾ HEAD
#text ↵␣␣␣␣
▾ TITLE
#text О лосях
#text ↵␣␣
#text ↵␣␣
▾ BODY
#text ↵␣␣Правда о лосях.↵
8/319
В остальных случаях всё просто – если в документе есть пробелы (или любые
другие символы), они становятся текстовыми узлами дерева DOM, и если мы их
удалим, то в DOM их тоже не будет.
Здесь пробельных текстовых узлов нет:
<!DOCTYPE HTML>
<html><head><title>О лосях</title></head><body>Правда о лосях.</body></html>
▾ HTML
▾ HEAD
▾ TITLE
#text О лосях
▾ BODY
#text Правда о лосях.
Автоисправление
Например, в начале документа всегда должен быть тег <html> . Даже если его
нет в документе – он будет в дереве DOM, браузер его создаст. То же самое
касается и тега <body> .
9/319
▾ HTML
▾ HEAD
▾ BODY
#text Привет
<p>Привет
<li>Мама
<li>и
<li>Папа
…Но DOM будет нормальным, потому что браузер сам закроет теги и
восстановит отсутствующие детали:
▾ HTML
▾ HEAD
▾ BODY
▾P
#text Привет
▾ LI
#text Мама
▾ LI
#text и
▾ LI
#text Папа
10/319
Таблицы всегда содержат <tbody>
<table id="table"><tr><td>1</td></tr></table>
▾ TABLE
▾ TBODY
▾ TR
▾ TD
#text 1
Например, узел-комментарий:
<!DOCTYPE HTML>
<html>
<body>
Правда о лосях.
<ol>
<li>Лось -- животное хитрое</li>
<!-- комментарий -->
<li>...и коварное!</li>
</ol>
</body>
</html>
11/319
▾ HTML
▾ HEAD
▾ BODY
#text ↵␣␣Правда о лосях.↵␣␣␣␣
▾ OL
#text ↵␣␣␣␣␣␣
▾ LI
#text Лось -- животное хитрое
#text ↵␣␣␣␣␣␣
#comment комментарий
#text ↵␣␣␣␣␣␣
▾ LI
#text ...и коварное!
#text ↵␣␣␣␣
#text ↵␣␣↵
12/319
Поэкспериментируйте сами
13/319
В правой части инструментов разработчика находятся следующие подразделы:
● Styles – здесь мы видим CSS, применённый к текущему элементу: правило за
правилом, включая встроенные стили (выделены серым). Почти всё можно
отредактировать на месте, включая размеры, внешние и внутренние отступы.
● Computed – здесь мы видим итоговые CSS-свойства элемента, которые он
приобрёл в результате применения всего каскада стилей (в том числе
унаследованные свойства и т.д.).
● Event Listeners – в этом разделе мы видим обработчики событий,
привязанные к DOM-элементам (мы поговорим о них в следующей части
учебника).
●
… и т.д.
Взаимодействие с консолью
При работе с DOM нам часто требуется применить к нему JavaScript. Например:
получить узел и запустить какой-нибудь код для его изменения, чтобы
посмотреть результат. Вот несколько подсказок, как перемещаться между
вкладками Elements и Console.
Для начала:
1. На вкладке Elements выберите первый элемент <li> .
2. Нажмите Esc – прямо под вкладкой Elements откроется Console.
14/319
Последний элемент, выбранный во вкладке Elements, доступен в консоли как
$0 ; предыдущий, выбранный до него, как $1 и т.д.
15/319
Это может быть полезно для отладки. В следующей главе мы рассмотрим
доступ и изменение DOM при помощи JavaScript.
Инструменты разработчика браузера отлично помогают в разработке: мы можем
исследовать DOM, пробовать с ним что-то делать и смотреть, что идёт не так.
Итого
16/319
У DOM-узлов есть свойства и методы, которые позволяют выбирать любой из
элементов, изменять, перемещать их на странице и многое другое. Мы вернёмся
к ним в последующих разделах.
Навигация по DOM-элементам
document
document.documentElement <HTML>
parentNode
previousSibling nextSibling
<DIV>
childNodes
firstChild lastChild
<html> = document.documentElement
Самый верхний узел документа: document.documentElement . В DOM он
соответствует тегу <html> .
<body> = document.body
Другой часто используемый DOM-узел – узел тега <body> : document.body .
17/319
<head> = document.head
Тег <head> доступен как document.head .
<html>
<head>
<script>
alert( "Из HEAD: " + document.body ); // null, <body> ещё нет
</script>
</head>
<body>
<script>
alert( "Из BODY: " + document.body ); // HTMLBodyElement, теперь он есть
</script>
</body>
</html>
В DOM значение null значит «не существует» или «нет такого узла».
18/319
В примере ниже детьми тега <body> являются теги <div> и <ul> (и
несколько пустых текстовых узлов):
<html>
<body>
<div>Начало</div>
<ul>
<li>
<b>Информация</b>
</li>
</ul>
</body>
</html>
<html>
<body>
<div>Начало</div>
<ul>
<li>Информация</li>
</ul>
<div>Конец</div>
<script>
for (let i = 0; i < document.body.childNodes.length; i++) {
alert( document.body.childNodes[i] ); // Text, DIV, Text, UL, ..., SCRIPT
}
</script>
...какой-то HTML-код...
</body>
</html>
19/319
Свойства firstChild и lastChild обеспечивают быстрый доступ к
первому и последнему дочернему элементу.
Они, по сути, являются всего лишь сокращениями. Если у тега есть дочерние
узлы, условие ниже всегда верно:
DOM-коллекции
Как мы уже видели, childNodes похож на массив. На самом деле это не
массив, а коллекция – особый перебираемый объект-псевдомассив.
Первый пункт – это хорошо для нас. Второй – бывает неудобен, но можно
пережить. Если нам хочется использовать именно методы массива, то мы можем
создать настоящий массив из коллекции, используя Array.from :
20/319
DOM-коллекции – только для чтения
DOM-коллекции, и даже более – все навигационные свойства,
перечисленные в этой главе, доступны только для чтения.
Мы не можем заменить один дочерний узел на другой, просто написав
childNodes[i] = ... .
DOM-коллекции живые
Почти все DOM-коллекции, за небольшим исключением, живые. Другими
словами, они отражают текущее состояние DOM.
Если мы сохраним ссылку на elem.childNodes и добавим/удалим узлы в
DOM, то они появятся в сохранённой коллекции автоматически.
<body>
<script>
// выводит 0, 1, length, item, values и другие свойства.
for (let prop in document.body.childNodes) alert(prop);
</script>
</body>
Соседи и родитель
<html>
<head>...</head><body>...</body>
</html>
21/319
● говорят, что <body> – «следующий» или «правый» сосед <head>
● также можно сказать, что <head> «предыдущий» или «левый» сосед
<body> .
Например:
document.documentElement <HTML>
parent Element
children
22/319
Эти ссылки похожи на те, что раньше, только в ряде мест стоит слово Element :
●
children – коллекция детей, которые являются элементами.
●
firstElementChild , lastElementChild – первый и последний
дочерний элемент.
● previousElementSibling , nextElementSibling – соседи-элементы.
●
parentElement – родитель-элемент.
Эта деталь может быть полезна, если мы хотим пройти вверх по цепочке
родителей от произвольного элемента elem к <html> , но не до
document :
<html>
<body>
<div>Начало</div>
<ul>
<li>Информация</li>
</ul>
<div>Конец</div>
23/319
<script>
for (let elem of document.body.children) {
alert(elem); // DIV, UL, DIV, SCRIPT
}
</script>
...
</body>
</html>
<tr> :
●
tr.cells – коллекция <td> и <th> ячеек, находящихся внутри строки
<tr> .
●
tr.sectionRowIndex – номер строки <tr> в текущей секции
<thead>/<tbody>/<tfoot> .
●
tr.rowIndex – номер строки <tr> в таблице (включая все строки
таблицы).
Пример использования:
<table id="table">
24/319
<tr>
<td>один</td><td>два</td>
</tr>
<tr>
<td>три</td><td>четыре</td>
</tr>
</table>
<script>
// выводит содержимое первой строки, второй ячейки
alert( table.rows[0].cells[1].innerHTML ) // "два"
</script>
Итого
Задачи
Для страницы:
<html>
<body>
<div>Пользователи:</div>
<ul>
<li>Джон</li>
<li>Пит</li>
25/319
</ul>
</body>
</html>
● элемент <div> ?
● <ul> ?
● второй <li> (с именем Пит)?
К решению
Вопрос о соседях
важность: 5
К решению
26/319
Открыть песочницу для задачи.
К решению
Например:
<div id="elem">
<div id="elem-content">Element</div>
</div>
<script>
// получить элемент
let elem = document.getElementById('elem');
<div id="elem">
<div id="elem-content">Элемент</div>
</div>
<script>
// elem - ссылка на элемент с id="elem"
elem.style.background = 'red';
// внутри id="elem-content" есть дефис, так что такой id не может служить именем пе
// ...но мы можем обратиться к нему через квадратные скобки: window['elem-content']
</script>
27/319
<div id="elem"></div>
<script>
let elem = 5; // теперь elem равен 5, а не <div id="elem">
alert(elem); // 5
</script>
querySelectorAll
28/319
Следующий запрос получает все элементы <li> , которые являются
последними потомками в <ul> :
<ul>
<li>Этот</li>
<li>тест</li>
</ul>
<ul>
<li>полностью</li>
<li>пройден</li>
</ul>
<script>
let elements = document.querySelectorAll('ul > li:last-child');
Этот метод действительно мощный, потому что можно использовать любой CSS-
селектор.
querySelector
matches
29/319
Этот метод удобен, когда мы перебираем элементы (например, в массиве или в
чём-то подобном) и пытаемся выбрать те из них, которые нас интересуют.
Например:
<a href="http://example.com/file.zip">...</a>
<a href="http://ya.ru">...</a>
<script>
// может быть любая коллекция вместо document.body.children
for (let elem of document.body.children) {
if (elem.matches('a[href$="zip"]')) {
alert("Ссылка на архив: " + elem.href );
}
}
</script>
closest
Например:
<h1>Содержание</h1>
<div class="contents">
<ul class="book">
<li class="chapter">Глава 1</li>
<li class="chapter">Глава 2</li>
</ul>
</div>
<script>
let chapter = document.querySelector('.chapter'); // LI
alert(chapter.closest('.book')); // UL
alert(chapter.closest('.contents')); // DIV
30/319
getElementsBy*
Существуют также другие методы поиска элементов по тегу, классу и так далее.
На данный момент, они скорее исторические, так как querySelector более
чем эффективен.
Например:
<table id="table">
<tr>
<td>Ваш возраст:</td>
<td>
<label>
<input type="radio" name="age" value="young" checked> младше 18
</label>
<label>
<input type="radio" name="age" value="mature"> от 18 до 50
</label>
<label>
<input type="radio" name="age" value="senior"> старше 60
</label>
</td>
</tr>
</table>
<script>
let inputs = table.getElementsByTagName('input');
31/319
}
</script>
// не работает
document.getElementsByTagName('input').value = 5;
<form name="my-form">
<div class="article">Article</div>
<div class="long article">Long article</div>
</form>
<script>
// ищем по имени атрибута
let form = document.getElementsByName('my-form')[0];
32/319
Живые коллекции
<div>First div</div>
<script>
let divs = document.getElementsByTagName('div');
alert(divs.length); // 1
</script>
<div>Second div</div>
<script>
alert(divs.length); // 2
</script>
Если мы будем использовать его в примере выше, то оба скрипта вернут длину
коллекции, равную 1 :
<div>First div</div>
<script>
let divs = document.querySelectorAll('div');
alert(divs.length); // 1
</script>
<div>Second div</div>
<script>
alert(divs.length); // 1
</script>
33/319
Итого
querySelector CSS-selector ✔ -
querySelectorAll CSS-selector ✔ -
getElementById id - -
getElementsByName name - ✔
getElementsByClassName class ✔ ✔
Кроме того:
● Есть метод elem.matches(css) , который проверяет, удовлетворяет ли
элемент CSS-селектору.
●
Метод elem.closest(css) ищет ближайшего по иерархии предка,
соответствующему данному CSS-селектору. Сам элемент также включён в
поиск.
Задачи
Поиск элементов
важность: 4
Как найти?…
1. Таблицу с id="age-table" .
2. Все элементы label внутри этой таблицы (их три).
3. Первый td в этой таблице (со словом «Age»).
34/319
4. Форму form с именем name="search" .
5. Первый input в этой форме.
6. Последний input в этой форме.
К решению
Классы DOM-узлов
35/319
EventTarget
Node
Document CharacterData
Element
36/319
Он обеспечивает навигацию на уровне элементов: nextElementSibling ,
children . А также и методы поиска элементов: getElementsByTagName ,
querySelector .
Для того, чтобы узнать имя класса DOM-узла, вспомним, что обычно у объекта
есть свойство constructor . Оно ссылается на конструктор класса, и в
свойстве constructor.name содержится его имя:
37/319
alert( document.body.constructor.name ); // HTMLBodyElement
Как видно, DOM-узлы – это обычные JavaScript объекты. Для наследования они
используют классы, основанные на прототипах.
В этом легко убедиться, если вывести в консоли браузера любой элемент через
console.dir(elem) . Или даже напрямую обратиться к методам, которые
хранятся в HTMLElement.prototype , Element.prototype и т.д.
console.dir(elem) и console.log(elem)
38/319
Спецификация IDL
В спецификации для описания классов DOM используется не JavaScript, а
специальный язык Interface description language (IDL), с которым
достаточно легко разобраться.
// Объявление HTMLInputElement
// Двоеточие ":" после HTMLInputElement означает, что он наследует от HTMLElement
interface HTMLInputElement: HTMLElement {
// далее идут свойства и методы элемента <input>
Свойство «nodeType»
Например:
<body>
<script>
let elem = document.body;
39/319
// давайте разберёмся: какой тип узла находится в elem?
alert(elem.nodeType); // 1 => элемент
Например:
40/319
<script>
// для комментария
alert( document.body.firstChild.tagName ); // undefined (не элемент)
alert( document.body.firstChild.nodeName ); // #comment
// for document
alert( document.tagName ); // undefined (не элемент)
alert( document.nodeName ); // #document
</script>
</body>
<body>
<p>Параграф</p>
<div>DIV</div>
<script>
alert( document.body.innerHTML ); // читаем текущее содержимое
document.body.innerHTML = 'Новый BODY!'; // заменяем содержимое
</script>
</body>
41/319
Мы можем попробовать вставить некорректный HTML, браузер исправит наши
ошибки:
<body>
<script>
document.body.innerHTML = '<b>тест'; // забыли закрыть тег
alert( document.body.innerHTML ); // <b>тест</b> (исправлено)
</script>
</body>
Скрипты не выполнятся
Если innerHTML вставляет в документ тег <script> – он становится
частью HTML, но не запускается.
Вот так:
elem.innerHTML += "...";
// это более короткая запись для:
elem.innerHTML = elem.innerHTML + "..."
42/319
В примере chatDiv выше строка chatDiv.innerHTML+="Как дела?"
заново создаёт содержимое HTML и перезагружает smile.gif (надеемся,
картинка закеширована). Если в chatDiv много текста и изображений, то эта
перезагрузка будет очень заметна.
Посмотрим на пример:
<script>
alert(elem.outerHTML); // <div id="elem">Привет <b>Мир</b></div>
</script>
Рассмотрим пример:
<div>Привет, мир!</div>
<script>
let div = document.querySelector('div');
43/319
Какая-то магия, да?
<body>
Привет
<!-- Комментарий -->
<script>
let text = document.body.firstChild;
alert(text.data); // Привет
44/319
Мы можем представить, для чего нам может понадобиться читать или изменять
текстовый узел, но комментарии?
Например:
<div id="news">
<h1>Срочно в номер!</h1>
<p>Марсиане атаковали человечество!</p>
</div>
<script>
// Срочно в номер! Марсиане атаковали человечество!
alert(news.textContent);
</script>
Как мы видим, возвращается только текст, как если бы все <теги> были
вырезаны, но текст в них остался.
45/319
<div id="elem1"></div>
<div id="elem2"></div>
<script>
let name = prompt("Введите ваше имя?", "<b>Винни-пух!</b>");
elem1.innerHTML = name;
elem2.textContent = name;
</script>
1. В первый <div> имя приходит «как HTML»: все теги стали именно тегами,
поэтому мы видим имя, выделенное жирным шрифтом.
2. Во второй <div> имя приходит «как текст», поэтому мы видим <b>Винни-
пух!</b> .
Свойство «hidden»
<script>
elem.hidden = true;
</script>
Мигающий элемент:
<script>
46/319
setInterval(() => elem.hidden = !elem.hidden, 1000);
</script>
Другие свойства
Например:
<script>
alert(elem.type); // "text"
alert(elem.id); // "elem"
alert(elem.value); // значение
</script>
Если же нам нужно быстро что-либо узнать или нас интересует специфика
определённого браузера – мы всегда можем вывести элемент в консоль,
используя console.dir(elem) , и прочитать все свойства. Или исследовать
«свойства DOM» во вкладке Elements браузерных инструментов разработчика.
Итого
nodeType
47/319
Свойство nodeType позволяет узнать тип DOM-узла. Его значение – числовое:
1 для элементов, 3 для текстовых узлов, и т.д. Только для чтения.
nodeName/tagName
Для элементов это свойство возвращает название тега (записывается в верхнем
регистре, за исключением XML-режима). Для узлов-неэлементов nodeName
описывает, что это за узел. Только для чтения.
innerHTML
Внутреннее HTML-содержимое узла-элемента. Можно изменять.
outerHTML
Полный HTML узла-элемента. Запись в elem.outerHTML не меняет elem .
Вместо этого она заменяет его во внешнем контексте.
nodeValue/data
Содержимое узла-неэлемента (текст, комментарий). Эти свойства практически
одинаковые, обычно мы используем data . Можно изменять.
textContent
Текст внутри элемента: HTML за вычетом всех <тегов> . Запись в него
помещает текст в элемент, при этом все специальные символы и теги
интерпретируются как текст. Можно использовать для защиты от вставки
произвольного HTML кода.
hidden
Когда значение установлено в true , делает то же самое, что и CSS
display:none .
Задачи
Считаем потомков
важность: 5
48/319
Напишите код, который выведет каждый элемент списка <li> :
К решению
<html>
<body>
<script>
alert(document.body.lastChild.nodeType);
</script>
</body>
</html>
К решению
Тег в комментарии
важность: 3
<script>
let body = document.body;
К решению
49/319
важность: 4
К решению
Атрибуты и свойства
Когда браузер загружает страницу, он «читает» (также говорят: «парсит») HTML и
генерирует из него DOM-объекты. Для узлов-элементов большинство
стандартных HTML-атрибутов автоматически становятся свойствами DOM-
объектов.
Например, для такого тега <body id="page"> у DOM-объекта будет такое
свойство body.id="page" .
DOM-свойства
document.body.myData = {
name: 'Caesar',
title: 'Imperator'
};
alert(document.body.myData.title); // Imperator
document.body.sayTagName = function() {
alert(this.tagName);
};
50/319
document.body.sayTagName(); // BODY (значением "this" в этом методе будет document.bod
Element.prototype.sayHi = function() {
alert(`Hello, I'm ${this.tagName}`);
};
Итак, DOM-свойства и методы ведут себя так же, как и обычные объекты
JavaScript:
●
Им можно присвоить любое значение.
●
Они регистрозависимы (нужно писать elem.nodeType , не
elem.NoDeTyPe ).
HTML-атрибуты
В HTML у тегов могут быть атрибуты. Когда браузер парсит HTML, чтобы создать
DOM-объекты для тегов, он распознаёт стандартные атрибуты и создаёт DOM-
свойства для них.
Например:
Пожалуйста, учтите, что стандартный атрибут для одного тега может быть
нестандартным для другого. Например, атрибут "type" является стандартным
для элемента <input> (HTMLInputElement ), но не является стандартным
для <body> (HTMLBodyElement ). Стандартные атрибуты описаны в
спецификации для соответствующего класса элемента.
51/319
Мы можем увидеть это на примере ниже:
<body something="non-standard">
<script>
alert(document.body.getAttribute('something')); // non-standard
</script>
</body>
<body>
<div id="elem" about="Elephant"></div>
<script>
alert( elem.getAttribute('About') ); // (1) 'Elephant', чтение
52/319
elem.setAttribute('Test', 123); // (2), запись
<input>
<script>
let input = document.querySelector('input');
53/319
<input>
<script>
let input = document.querySelector('input');
В примере выше:
● Изменение атрибута value обновило свойство.
● Но изменение свойства не повлияло на атрибут.
DOM-свойства типизированы
<script>
alert(input.getAttribute('checked')); // значение атрибута: пустая строка
alert(input.checked); // значение свойства: true
</script>
<script>
// строка
alert(div.getAttribute('style')); // color:red;font-size:120%
// объект
alert(div.style); // [object CSSStyleDeclaration]
54/319
alert(div.style.color); // red
</script>
Ниже пример:
// свойство
alert(a.href ); // полный URL в виде http://site.com/page#hello
</script>
Если же нужно значение href или любого другого атрибута в точности, как оно
записано в HTML, можно воспользоваться getAttribute .
Как тут:
<script>
// код находит элемент с пометкой и показывает запрошенную информацию
let user = {
name: "Pete",
age: 25
};
55/319
// вставить соответствующую информацию в поле
let field = div.getAttribute('show-info');
div.innerHTML = user[field]; // сначала Pete в name, потом 25 в age
}
</script>
<style>
/* стили зависят от пользовательского атрибута "order-state" */
.order[order-state="new"] {
color: green;
}
.order[order-state="pending"] {
color: blue;
}
.order[order-state="canceled"] {
color: red;
}
</style>
Это потому, что атрибутом удобнее управлять. Состояние может быть изменено
достаточно просто:
56/319
появляется больше атрибутов, чтобы удовлетворить потребности разработчиков.
В этом случае могут возникнуть неожиданные эффекты.
Как тут:
<body data-about="Elephants">
<script>
alert(document.body.dataset.about); // Elephants
</script>
<style>
.order[data-order-state="new"] {
color: green;
}
.order[data-order-state="pending"] {
color: blue;
}
.order[data-order-state="canceled"] {
color: red;
}
</style>
<script>
// чтение
alert(order.dataset.orderState); // new
// изменение
order.dataset.orderState = "pending"; // (*)
</script>
57/319
Использование data-* атрибутов – валидный, безопасный способ передачи
пользовательских данных.
Итого
●
Атрибуты – это то, что написано в HTML.
● Свойства – это то, что находится в DOM-объектах.
Небольшое сравнение:
Свойства Атрибуты
Имя
Имя Имя регистрозависимо
регистронезависимо
Задачи
Получите атрибут
важность: 5
58/319
Напишите код для выбора элемента с атрибутом data-widget-name из
документа и прочитайте его значение.
<!DOCTYPE html>
<html>
<body>
<script>
/* your code */
</script>
</body>
</html>
К решению
Пример:
<script>
// добавление стиля для одной ссылки
let link = document.querySelector('a');
link.style.color = 'orange';
</script>
59/319
The list:
https://google.com
/tutorial.html
local/path
ftp://ftp.com/my.zip
https://nodejs.org
http://internal.com/test
К решению
Изменение документа
Здесь мы увидим, как создавать новые элементы «на лету» и изменять уже
существующие.
Вот такое:
<style>
.alert {
padding: 15px;
border: 1px solid #d6e9c6;
border-radius: 4px;
color: #3c763d;
background-color: #dff0d8;
}
</style>
<div class="alert">
<strong>Всем привет!</strong> Вы прочитали важное сообщение.
</div>
Это был пример HTML. Теперь давайте создадим такой же div , используя
JavaScript (предполагаем, что стили в HTML или во внешнем CSS-файле).
60/319
Создание элемента
document.createElement(tag)
Создаёт новый элемент с заданным тегом:
document.createTextNode(text)
Создаёт новый текстовый узел с заданным текстом:
Большую часть времени нам нужно создавать узлы элементов, такие как div
для сообщения.
Создание сообщения
В нашем случае сообщение – это div с классом alert и HTML в нём:
Методы вставки
Чтобы наш div появился, нам нужно вставить его где-нибудь в document .
Например, в document.body .
<style>
.alert {
padding: 15px;
border: 1px solid #d6e9c6;
border-radius: 4px;
color: #3c763d;
61/319
background-color: #dff0d8;
}
</style>
<script>
let div = document.createElement('div');
div.className = "alert";
div.innerHTML = "<strong>Всем привет!</strong> Вы прочитали важное сообщение.";
document.body.append(div);
</script>
<ol id="ol">
<li>0</li>
<li>1</li>
<li>2</li>
</ol>
<script>
ol.before('before'); // вставить строку "before" перед <ol>
ol.after('after'); // вставить строку "after" после <ol>
62/319
before
1. prepend
2. 0
3. 1
4. 2
5. append
after
ol.before
ol.prepend
(…nodes or strings)
ol.append
ol.after
before
<ol id="ol">
<li>prepend</li>
<li>0</li>
<li>1</li>
<li>2</li>
<li>append</li>
</ol>
after
<div id="div"></div>
<script>
div.before('<p>Привет</p>', document.createElement('hr'));
</script>
63/319
<p>Привет</p>
<hr>
<div id="div"></div>
Поэтому эти методы могут использоваться только для вставки DOM-узлов или
текстовых фрагментов.
А что, если мы хотим вставить HTML именно «как html», со всеми тегами и
прочим, как делает это elem.innerHTML ?
insertAdjacentHTML/Text/Element
Например:
<div id="div"></div>
<script>
div.insertAdjacentHTML('beforebegin', '<p>Привет</p>');
div.insertAdjacentHTML('afterend', '<p>Пока</p>');
</script>
…Приведёт к:
<p>Привет</p>
<div id="div"></div>
<p>Пока</p>
64/319
beforebegin
afterbegin
ol.insertAdjacentHTML(*, html)
beforeend
afterend
<style>
.alert {
padding: 15px;
border: 1px solid #d6e9c6;
border-radius: 4px;
color: #3c763d;
background-color: #dff0d8;
}
</style>
<script>
document.body.insertAdjacentHTML("afterbegin", `<div class="alert">
<strong>Всем привет!</strong> Вы прочитали важное сообщение.
</div>`);
</script>
Удаление узлов
65/319
<style>
.alert {
padding: 15px;
border: 1px solid #d6e9c6;
border-radius: 4px;
color: #3c763d;
background-color: #dff0d8;
}
</style>
<script>
let div = document.createElement('div');
div.className = "alert";
div.innerHTML = "<strong>Всем привет!</strong> Вы прочитали важное сообщение.";
document.body.append(div);
setTimeout(() => div.remove(), 1000);
</script>
<div id="first">Первый</div>
<div id="second">Второй</div>
<script>
// нет необходимости вызывать метод remove
second.after(first); // берёт #second и после него вставляет #first
</script>
Иногда, когда у нас есть большой элемент, это может быть быстрее и проще.
● Вызов elem.cloneNode(true) создаёт «глубокий» клон элемента – со
всеми атрибутами и дочерними элементами. Если мы вызовем
elem.cloneNode(false) , тогда клон будет без дочерних элементов.
66/319
<style>
.alert {
padding: 15px;
border: 1px solid #d6e9c6;
border-radius: 4px;
color: #3c763d;
background-color: #dff0d8;
}
</style>
<script>
let div2 = div.cloneNode(true); // клонировать сообщение
div2.querySelector('strong').innerHTML = 'Всем пока!'; // изменить клонированный эле
DocumentFragment
<ul id="ul"></ul>
<script>
function getListContent() {
let fragment = new DocumentFragment();
return fragment;
}
ul.append(getListContent()); // (*)
</script>
67/319
Обратите внимание, что на последней строке с (*) мы добавляем
DocumentFragment , но он «исчезает», поэтому структура будет:
<ul>
<li>1</li>
<li>2</li>
<li>3</li>
</ul>
<ul id="ul"></ul>
<script>
function getListContent() {
let result = [];
return result;
}
Старая школа
Эта информация помогает понять старые скрипты, но не нужна для новой
разработки.
68/319
Сейчас уже нет причин их использовать, так как современные методы append ,
prepend , before , after , remove , replaceWith более гибкие и удобные.
parentElem.appendChild(node)
Добавляет node в конец дочерних элементов parentElem .
<ol id="list">
<li>0</li>
<li>1</li>
<li>2</li>
</ol>
<script>
let newLi = document.createElement('li');
newLi.innerHTML = 'Привет, мир!';
list.appendChild(newLi);
</script>
parentElem.insertBefore(node, nextSibling)
Вставляет node перед nextSibling в parentElem .
<ol id="list">
<li>0</li>
<li>1</li>
<li>2</li>
</ol>
<script>
let newLi = document.createElement('li');
newLi.innerHTML = 'Привет, мир!';
list.insertBefore(newLi, list.children[1]);
</script>
list.insertBefore(newLi, list.firstChild);
parentElem.replaceChild(node, oldChild)
69/319
Заменяет oldChild на node среди дочерних элементов parentElem .
parentElem.removeChild(node)
Удаляет node из parentElem (предполагается, что он родитель node ).
<ol id="list">
<li>0</li>
<li>1</li>
<li>2</li>
</ol>
<script>
let li = list.firstElementChild;
list.removeChild(li);
</script>
Синтаксис:
<p>Где-то на странице...</p>
<script>
document.write('<b>Привет из JS</b>');
</script>
<p>Конец</p>
70/319
Вызов document.write работает только во время загрузки страницы.
Например:
Так что после того, как страница загружена, он уже непригоден к использованию,
в отличие от других методов DOM, которые мы рассмотрели выше.
Поэтому он работает невероятно быстро, ведь при этом нет модификации DOM.
Метод пишет прямо в текст страницы, пока DOM ещё в процессе создания.
Так что, если нам нужно динамически добавить много текста в HTML, и мы
находимся на стадии загрузки, и для нас очень важна скорость, это может
помочь. Но на практике эти требования редко сочетаются. И обычно мы можем
увидеть этот метод в скриптах просто потому, что они старые.
Итого
●
Методы для создания узлов:
● document.createElement(tag) – создаёт элемент с заданным тегом,
● document.createTextNode(value) – создаёт текстовый узел (редко
используется),
●
elem.cloneNode(deep) – клонирует элемент, если deep==true , то со
всеми дочерними элементами.
● Вставка и удаление:
●
node.append(...nodes or strings) – вставляет в node в конец,
● node.prepend(...nodes or strings) – вставляет в node в начало,
●
node.before(...nodes or strings) – вставляет прямо перед node ,
●
node.after(...nodes or strings) – вставляет сразу после node ,
●
node.replaceWith(...nodes or strings) – заменяет node .
71/319
●
node.remove() – удаляет node .
●
Устаревшие методы:
●
parent.appendChild(node)
● parent.insertBefore(node, nextSibling)
● parent.removeChild(node)
●
parent.replaceChild(newElem, node)
Задачи
1. elem.append(document.createTextNode(text))
2. elem.innerHTML = text
3. elem.textContent = text
К решению
72/319
Очистите элемент
важность: 5
<ol id="elem">
<li>Привет</li>
<li>Мир</li>
</ol>
<script>
function clear(elem) { /* ваш код */ }
К решению
Но если вы запустите его, вы увидите, что текст "aaa" все еще виден.
<table id="table">
aaa
<tr>
<td>Тест</td>
</tr>
</table>
<script>
alert(table); // таблица, как и должно быть
table.remove();
// почему в документе остался текст "ааа"?
</script>
К решению
Создайте список
важность: 4
73/319
Напишите интерфейс для создания списка.
К решению
Например:
let data = {
"Рыбы": {
"форель": {},
"лосось": {}
},
"Деревья": {
"Огромные": {
"секвойя": {},
"дуб": {}
},
"Цветковые": {
"яблоня": {},
"магнолия": {}
}
}
};
Синтаксис:
74/319
Результат (дерево):
Рыбы
форель
лосось
Деревья
Огромные
секвойя
дуб
Цветковые
яблоня
магнолия
К решению
Результат:
75/319
Животные [9]
Млекопитающие [4]
Коровы
Ослы
Собаки
Тигры
Другие [3]
Змеи
Птицы
Ящерицы
Рыбы [5]
Аквариумные [2]
Гуппи
Скалярии
Морские [1]
Морская форель
К решению
Вызов функции должен создать календарь для заданного месяца month в году
year и вставить его в elem .
Календарь должен быть таблицей, где неделя – это <tr> , а день – это <td> . У
таблицы должен быть заголовок с названиями дней недели, каждый день –
<th> , первым днём недели должен быть понедельник.
пн вт ср чт пт сб вс
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
76/319
Открыть песочницу для задачи.
К решению
hh:mm:ss
Start Stop
К решению
<ul id="ul">
<li id="one">1</li>
<li id="two">4</li>
</ul>
К решению
Сортировка таблицы
важность: 5
Вот таблица:
<table>
<thead>
<tr>
<th>Name</th><th>Surname</th><th>Age</th>
</tr>
</thead>
<tbody>
77/319
<tr>
<td>John</td><td>Smith</td><td>10</td>
</tr>
<tr>
<td>Pete</td><td>Brown</td><td>15</td>
</tr>
<tr>
<td>Ann</td><td>Lee</td><td>5</td>
</tr>
<tr>
<td>...</td><td>...</td><td>...</td>
</tr>
</tbody>
</table>
К решению
Стили и классы
До того, как начнёте изучать способы работы со стилями и классами в JavaScript,
есть одно важное правило. Надеемся, это достаточно очевидно, но мы всё
равно должны об этом упомянуть.
Как правило, существует два способа задания стилей для элемента:
78/319
В других случаях, например, чтобы сделать текст красным, добавить значок
фона – описываем это в CSS и добавляем класс (JavaScript может это сделать).
Это более гибкое и лёгкое в поддержке решение.
className и classList
Например:
Например:
79/319
тот вариант, который нам удобнее.
Методы classList :
●
elem.classList.add/remove("class") – добавить/удалить класс.
●
elem.classList.toggle("class") – добавить класс, если его нет, иначе
удалить.
●
elem.classList.contains("class") – проверка наличия класса,
возвращает true/false .
Element style
Например:
80/319
Свойства с префиксом
Стили с браузерным префиксом, например, -moz-border-radius , -
webkit-border-radius преобразуются по тому же принципу: дефис
означает заглавную букву.
Например:
button.style.MozBorderRadius = '5px';
button.style.WebkitBorderRadius = '5px';
Сброс стилей
Иногда нам нужно добавить свойство стиля, а потом, позже, убрать его.
81/319
Полная перезапись style.cssText
<div id="div">Button</div>
<script>
// можем даже устанавливать специальные флаги для стилей, например, "important"
div.style.cssText=`color: red !important;
background-color: yellow;
width: 100px;
text-align: center;
`;
alert(div.style.cssText);
</script>
<body>
<script>
// не работает!
document.body.style.margin = 20;
alert(document.body.style.margin); // '' (пустая строка, присваивание игнорируется
82/319
alert(document.body.style.margin); // 20px
alert(document.body.style.marginTop); // 20px
alert(document.body.style.marginLeft); // 20px
</script>
</body>
Например, мы хотим знать размер, отступы, цвет элемента. Как это сделать?
Свойство style оперирует только значением атрибута "style" , без
учёта CSS-каскада.
<head>
<style> body { color: red; margin: 5px } </style>
</head>
<body>
Красный текст
<script>
alert(document.body.style.color); // пусто
alert(document.body.style.marginTop); // пусто
</script>
</body>
…Но что, если нам нужно, скажем, увеличить отступ на 20px ? Для начала
нужно его текущее значение получить.
Синтаксис:
getComputedStyle(element, [pseudo])
element
83/319
Элемент, значения для которого нужно получить
pseudo
Указывается, если нужен стиль псевдоэлемента, например ::before . Пустая
строка или отсутствие аргумента означают сам элемент.
<head>
<style> body { color: red; margin: 5px } </style>
</head>
<body>
<script>
let computedStyle = getComputedStyle(document.body);
</body>
84/319
getComputedStyle требует полное свойство!
Итого
85/319
Для чтения окончательных стилей (с учётом всех классов, после применения
CSS и вычисления окончательных значений) используется:
●
Метод getComputedStyle(elem, [pseudo]) возвращает объект, похожий
по формату на style . Только для чтения.
Задачи
Создать уведомление
важность: 5
К решению
Простой пример
86/319
В качестве простого примера демонстрации свойств мы будем использовать
следующий элемент:
<div id="example">
...Текст...
</div>
<style>
#example {
width: 300px;
height: 200px;
border: 25px solid #E8C48F;
padding: 20px;
overflow: auto;
}
</style>
padding
20px
padding scrollbar
20px 16px
border border
25px 25px
content width: 284px
padding: 20px
Introduction
This Ecma Standard is based on several
originating technologies, the most well
200px
87/319
Внимание, полоса прокрутки
В иллюстрации выше намеренно продемонстрирован самый сложный и
полный случай, когда у элемента есть ещё и полоса прокрутки. Некоторые
браузеры (не все) отбирают место для неё, забирая его у области,
отведённой для содержимого (помечена как «content width» выше).
Метрики
88/319
offsetTop
Introduction
This Ecma Standard is based on several
scrollTop
originating technologies, the most well
offsetLeft known being JavaScript (Netscape) and
clientTop
JScript (Microsoft). The language was
invented by Brendan Eich at Netscape and
first appeared in that company’s Navigator
2.0 browser. It has appeared in all
clientLeft
clientHeight
offsetHeight
scrollHeight
The development of this Standard started
in November 1996. The first edition of this
Ecma Standard was adopted by the Ecma
General Assembly of June 1997.
That Ecma Standard was submitted to ISO/
IEC JTC 1 for adoption under the fast-track
procedure, and approved as international
standard ISO/IEC 16262, in April 1998. The
Ecma General Assembly of June 1998
approved the second clientWidth
edition of ECMA-262
to keep it fully aligned with ISO/IEC 16262.
Changes between offsetWidth
the first and the second
edition are editorial in nature.
offsetParent, offsetLeft/Top
Эти свойства редко используются, но так как они являются «самыми внешними»
метриками, мы начнём с них.
В свойстве offsetParent находится предок элемента, который используется
внутри браузера для вычисления координат при рендеринге.
То есть, ближайший предок, который удовлетворяет следующим условиям:
89/319
Свойства offsetLeft/offsetTop содержат координаты x/y относительно
верхнего левого угла offsetParent .
offsetParent <MAIN>
180px
position: absolute;
left: 180px;
offsetTop:
top: 180px;
Introduction
This Ecma Standard is based on several
originating technologies, the most well
known being JavaScript (Netscape) and
JScript (Microsoft). The language was
invented by Brendan Eich at Netscape
and first appeared in that company’s
Navigator 2.0 browser. It has appeared
in all subsequent browsers from
Netscape and in all browsers from
Microsoft
90/319
3. Для элементов с position:fixed .
offsetWidth/Height
padding
20px
padding scrollbar
20px 16px
border border
25px 25px
content width: 284px
Introduction
This Ecma Standard is based on several
290px
originating technologies, the most well
200px
known being JavaScript (Netscape) and
JScript (Microsoft). The language was
invented by Brendan Eich at Netscape and offsetHeight:
height:
91/319
Метрики для не показываемых элементов равны нулю.
Координаты и размеры в JavaScript устанавливаются только для видимых
элементов.
function isHidden(elem) {
return !elem.offsetWidth && !elem.offsetHeight;
}
clientTop/Left
В нашем примере:
●
clientLeft = 25 – ширина левой рамки
●
clientTop = 25 – ширина верхней рамки
92/319
clientTop: 25px = border
25px
Introduction
clientLeft:
clientWidth/Height
93/319
Они включают в себя ширину области содержимого вместе с внутренними
отступами padding , но без прокрутки:
padding
20px
padding scrollbar
20px 16px
border border
25px 25px
content width: 284px
Introduction
This Ecma Standard is based on several
240px
originating technologies, the most well
200px
known being JavaScript (Netscape) and
JScript (Microsoft). The language was
clientHeight:
invented by Brendan Eich at Netscape and
height:
first appeared in that company’s Navigator
2.0 browser. It has appeared in all
subsequent browsers from Netscape and
in all browsers from Microsoft starting with
Горизонтальной прокрутки нет, так что это в точности то, что внутри рамок: CSS-
высота 200px плюс верхние и нижние внутренние отступы ( 2 * 20px ), итого
240px .
94/319
padding: 0;
width: 300px;
Introduction
This Ecma Standard is based on several
originating technologies, the most well
known being JavaScript (Netscape) and
JScript (Microsoft). The language was
invented by Brendan Eich at Netscape and
first appeared in that company’s Navigator
2.0 browser. It has appeared in all
subsequent browsers from Netscape and
in all browsers from Microsoft starting with
Поэтому в тех случаях, когда мы точно знаем, что отступов нет, можно
использовать clientWidth/clientHeight для получения размеров
внутренней области содержимого.
scrollWidth/Height
95/319
Introduction
This Ecma Standard is based on several
originating technologies,
scrollWidththe
= most well
324px
known being JavaScript (Netscape) and
JScript (Microsoft). The language was
invented by Brendan Eich at Netscape
and first appeared in that company’s
Navigator 2.0 browser. It has appeared
723px
in all subsequent browsers from
Netscape and in all browsers from
Microsoft starting with Internet Explorer
3.0.
scrollHeight:
The development of this Standard
started in November 1996. The first
edition of this Ecma Standard was
adopted by the Ecma General Assembly
of June 1997.
That Ecma Standard was submitted to
ISO/IEC JTC 1 for adoption under the
fast-track procedure, and approved as
international standard ISO/IEC 16262, in
April 1998. The Ecma General Assembly
of June 1998 approved the second
edition of ECMA-262 to keep it fully
aligned with ISO/IEC 16262. Changes
На рисунке выше:
●
scrollHeight = 723 – полная внутренняя высота, включая прокрученную
область.
●
scrollWidth = 324 – полная внутренняя ширина, в данном случае
прокрутки нет, поэтому она равна clientWidth .
scrollLeft/scrollTop
96/319
Introduction
This Ecma Standard is based on several
scrollTop
723px
subsequent browsers from Netscape and
in all browsers from Microsoft starting with
Internet Explorer 3.0.
The development of this Standard started
scrollHeight:
in November 1996. The first edition of this
Ecma Standard was adopted by the Ecma
General Assembly of June 1997.
That Ecma Standard was submitted to ISO/
IEC JTC 1 for adoption under the fast-track
procedure, and approved as international
standard ISO/IEC 16262, in April 1998. The
Ecma General Assembly of June 1998
approved the second edition of ECMA-262
to keep it fully aligned with ISO/IEC 16262.
Changes between the first and the second
edition are editorial in nature.
97/319
let elem = document.body;
<span id="elem">Привет!</span>
<script>
alert( getComputedStyle(elem).width ); // auto
</script>
Есть и ещё одна причина: полоса прокрутки. Бывает, без полосы прокрутки код
работает прекрасно, но стоит ей появиться, как начинают проявляться баги. Так
происходит потому, что полоса прокрутки «отъедает» место от области
внутреннего содержимого в некоторых браузерах. Таким образом, реальная
ширина содержимого меньше CSS-ширины. Как раз это и учитывают свойства
clientWidth/clientHeight .
Итого
98/319
У элементов есть следующие геометрические свойства (метрики):
●
offsetParent – ближайший CSS-позиционированный родитель или
ближайший td , th , table , body .
●
offsetLeft/offsetTop – позиция в пикселях верхнего левого угла
относительно offsetParent .
● offsetWidth/offsetHeight – «внешняя» ширина/высота элемента,
включая рамки.
●
clientLeft/clientTop – расстояние от верхнего левого внешнего угла до
внутренного. Для операционных систем с ориентацией слева-направо эти
свойства равны ширинам левой/верхней рамки. Если язык ОС таков, что
ориентация справа налево, так что вертикальная полоса прокрутки находится
не справа, а слева, то clientLeft включает в своё значение её ширину.
●
clientWidth/clientHeight – ширина/высота содержимого вместе с
внутренними отступами padding , но без полосы прокрутки.
● scrollWidth/scrollHeight – ширины/высота содержимого, аналогично
clientWidth/Height , но учитывают прокрученную, невидимую область
элемента.
● scrollLeft/scrollTop – ширина/высота прокрученной сверху части
элемента, считается от верхнего левого угла.
Задачи
P.S. Проверьте: если прокрутки нет вообще или элемент полностью прокручен –
оно должно давать 0 .
К решению
99/319
Напишите код, который возвращает ширину стандартной полосы прокрутки.
К решению
.........................
.........................
.........................
.........................
.........................
.........................
.........................
.........................
.........................
.........................
.........................
.........................
.........................
.........................
P.S. Да, центрирование можно сделать при помощи чистого CSS, но задача
именно на JavaScript. Далее будут другие темы и более сложные ситуации, когда
JavaScript будет уже точно необходим, это – своего рода «разминка».
100/319
К решению
К решению
Ширина/высота окна
documentElement.clientHeight
documentElement.clientWidth
101/319
Не window.innerWidth/Height
Браузеры также поддерживают свойства
window.innerWidth/innerHeight . Вроде бы, похоже на то, что нам
нужно. Почему же не использовать их?
Если полоса прокрутки занимает некоторое место, то эти две строки выведут
разные значения:
Ширина/высота документа
102/319
let scrollHeight = Math.max(
document.body.scrollHeight, document.documentElement.scrollHeight,
document.body.offsetHeight, document.documentElement.offsetHeight,
document.body.clientHeight, document.documentElement.clientHeight
);
Важно:
Для прокрутки страницы из JavaScript её DOM должен быть полностью
построен.
Например, если мы попытаемся прокрутить страницу из скрипта,
подключенного в <head> , это не сработает.
103/319
старом WebKit (Safari), где, как сказано выше,
document.body.scrollTop/Left ).
scrollIntoView
Запретить прокрутку
Недостатком этого способа является то, что сама полоса прокрутки исчезает.
Если она занимала некоторую ширину, то теперь эта ширина освободится, и
содержимое страницы расширится, текст «прыгнет», заняв освободившееся
место.
104/319
Это выглядит немного странно, но это можно обойти, если сравнить
clientWidth до и после остановки, и если clientWidth увеличится (значит
полоса прокрутки исчезла), то добавить padding в document.body вместо
полосы прокрутки, чтобы оставить ширину содержимого прежней.
Итого
Размеры:
●
Ширина/высота видимой части документа (ширина/высота области
содержимого): document.documentElement.clientWidth/Height
●
Ширина/высота всего документа со всей прокручиваемой областью страницы:
Прокрутка:
●
Прокрутку окна можно получить так: window.pageYOffset/pageXOffset .
●
Изменить текущую прокрутку:
●
window.scrollTo(pageX,pageY) – абсолютные координаты,
●
window.scrollBy(x,y) – прокрутка относительно текущего места,
●
elem.scrollIntoView(top) – прокрутить страницу так, чтобы сделать
elem видимым (выровнять относительно верхней/нижней части окна).
Координаты
105/319
Когда страница полностью прокручена в самое начало, то верхний левый угол
окна совпадает с левым верхним углом документа, при этом обе этих системы
координат тоже совпадают. Но если происходит прокрутка, то координаты
элементов в контексте окна меняются, так как они двигаются, но в то же время
их координаты относительно документа остаются такими же.
pageY
Introduction
This Ecma Standard is based on several
originating technologies, the most well
known being JavaScript (Netscape) and
JScript (Microsoft). The language was
invented by Brendan Eich at Netscape and
pageY first appeared in that company’s Navigator
Introduction clientY 2.0 browser. clientY
pageX
This Ecma Standard is based on several clientX 😍
It has appeared in all subsequent browsers
originating technologies, the most well
from Netscape and in all browsers from
known being JavaScript (Netscape) and
Microsoft starting with Internet Explorer
JScript (Microsoft). The language was
3.0. The development of this Standard
invented by Brendan Eich at Netscape and
started in November 1996.
first appeared in that company’s Navigator
The first edition of this Ecma Standard was
pageX
2.0 browser.
clientX 😍 adopted by the Ecma General Assembly of
June 1997.
It has appeared in all subsequent browsers
Th E S d d b i d ISO/
106/319
●
top/bottom – Y-координата верхней/нижней границы прямоугольника,
●
left/right – X-координата левой/правой границы прямоугольника.
y
top
x
left
Introduction
This Ecma Standard is based on
several originating technologies,
height
the most well known being
JavaScript (Netscape) and JScript
(Microsoft). The language was
invented by Brendan Eich at
bottom
width
right
Заметим:
●
Координаты могут считаться с десятичной частью, например 10.5 . Это
нормально, ведь браузер использует дроби в своих внутренних вычислениях.
Мы не обязаны округлять значения при установке style.left/top .
●
Координаты могут быть отрицательными. Например, если страница
прокручена так, что элемент elem ушёл вверх за пределы окна, то вызов
elem.getBoundingClientRect().top вернёт отрицательное значение.
107/319
Зачем вообще нужны зависимые свойства? Для чего существуют
top/left , если есть x/y ?
С математической точки зрения, прямоугольник однозначно задаётся
начальной точкой (x,y) и вектором направления (width,height) .
top
left
Introduction
(w
This Ecma Standard
idt is based on
h, h
several originating technologies,
eig bottom
ht )
the most well known being
JavaScript (Netscape) and JScript
(Microsoft). The language was
invented by Brendan Eich at
(x,y)
right
108/319
Internet Explorer и Edge: не поддерживают x/y
Internet Explorer и Edge не поддерживают свойства x/y по историческим
причинам.
Таким образом, мы можем либо сделать полифил (добавив соответствующие
геттеры в DomRect.prototype ), либо использовать top/left , так как это
всегда одно и то же при положительных width/height , в частности – в
результате вызова elem.getBoundingClientRect() .
Если взглянуть на картинку выше, то видно, что в JavaScript это не так. Все
координаты в контексте окна считаются от верхнего левого угла, включая
right/bottom .
elementFromPoint(x, y)
Синтаксис:
Например, код ниже выделяет с помощью стилей и выводит имя тега элемента,
который сейчас в центре окна браузера:
elem.style.background = "red";
alert(elem.tagName);
109/319
Для координат за пределами окна метод elementFromPoint возвращает
null
message.innerHTML = html;
return message;
}
110/319
// Использование:
// добавим сообщение на страницу на 5 секунд
let message = createMessageUnder(elem, 'Hello, world!');
document.body.append(message);
setTimeout(() => message.remove(), 5000);
111/319
function getCoords(elem) {
let box = elem.getBoundingClientRect();
return {
top: box.top + window.pageYOffset,
right: box.right + window.pageXOffset,
bottom: box.bottom + window.pageYOffset,
left: box.left + window.pageXOffset
};
}
message.innerHTML = html;
return message;
}
Итого
Задачи
112/319
Найдите координаты точек относительно окна браузера
важность: 5
Ваш код должен при помощи DOM получить четыре пары координат:
P.S. Код должен работать, если у элемента другие размеры или есть рамка, без
привязки к конкретным числам.
К решению
113/319
важность: 5
Демо заметки:
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Reprehenderit sint atque dolorum fuga ad
incidunt voluptatum error fugiat animi amet! Odio temporibus nulla id unde quaerat dignissimos enim nisi
rem provident molestias sit tempore omnis recusandae esse sequi officia sapiente.
note above
Teacher: That's nice. Were you helping him look for it?
Student: No. I was standing on it.
note below
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Reprehenderit sint atque dolorum fuga ad
incidunt voluptatum error fugiat animi amet! Odio temporibus nulla id unde quaerat dignissimos enim nisi
rem provident molestias sit tempore omnis recusandae esse sequi officia sapiente.
К решению
114/319
Это предотвратит расхождение элементов при прокрутке страницы.
К решению
Например:
Результат:
115/319
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Reprehenderit sint atque dolorum fuga ad
incidunt voluptatum error fugiat animi amet! Odio temporibus nulla id unde quaerat dignissimos enim nisi
rem provident molestias sit tempore omnis recusandae esse sequi officia sapiente.
note top-out
“
note top-in
Teacher: Why are you late?
Student: There was a man who lost a hundred dollar bill.
note right-out
Teacher: That's nice. Were you helping him look for it?
Student: No. I was standing on it.
note bottom-in
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Reprehenderit sint atque dolorum fuga ad
incidunt voluptatum error fugiat animi amet! Odio temporibus nulla id unde quaerat dignissimos enim nisi
rem provident molestias sit tempore omnis recusandae esse sequi officia sapiente.
К решению
Введение в события
Введение в браузерные события, их свойства и обработку.
Событие – это сигнал от браузера о том, что что-то произошло. Все DOM-узлы
подают такие сигналы (хотя события бывают и не только в DOM).
Вот список самых часто используемых DOM-событий, пока просто для
ознакомления:
События мыши:
●
click – происходит, когда кликнули на элемент левой кнопкой мыши (на
устройствах с сенсорными экранами оно происходит при касании).
●
contextmenu – происходит, когда кликнули на элемент правой кнопкой
мыши.
●
mouseover / mouseout – когда мышь наводится на / покидает элемент.
●
mousedown / mouseup – когда нажали / отжали кнопку мыши на элементе.
●
mousemove – при движении мыши.
116/319
●
focus – пользователь фокусируется на элементе, например нажимает на
<input> .
Клавиатурные события:
●
keydown и keyup – когда пользователь нажимает / отпускает клавишу.
События документа:
●
DOMContentLoaded – когда HTML загружен и обработан, DOM документа
полностью построен и доступен.
CSS events:
●
transitionend – когда CSS-анимация завершена.
Обработчики событий
117/319
вызвать её там.
<script>
function countRabbits() {
for(let i=1; i<=3; i++) {
alert("Кролик номер " + i);
}
}
</script>
Считать кроликов!
К примеру, elem.onclick :
Нажми меня!
1. Только HTML:
118/319
Кнопка
2. HTML + JS:
Кнопка
Так как у элемента DOM может быть только одно свойство с именем
onclick , то назначить более одного обработчика так нельзя.
Нажми меня
function sayThanks() {
alert('Спасибо!');
}
elem.onclick = sayThanks;
119/319
<button onclick="alert(this.innerHTML)">Нажми меня</button>
Нажми меня
Частые ошибки
// правильно
button.onclick = sayThanks;
// неправильно
button.onclick = sayThanks();
button.onclick = function() {
sayThanks(); // содержимое атрибута
};
120/319
// при нажатии на body будут ошибки,
// атрибуты всегда строки, и функция станет строкой
document.body.setAttribute('onclick', function() { alert(1) });
addEventListener
event
Имя события, например "click" .
handler
Ссылка на функцию-обработчик.
options
Дополнительный объект со свойствами:
121/319
● once : если true , тогда обработчик будет автоматически удалён после
выполнения.
●
capture : фаза, на которой должен сработать обработчик, подробнее об этом
будет рассказано в главе Всплытие и погружение. Так исторически сложилось,
что options может быть false/true , это то же самое, что {capture:
false/true} .
●
passive : если true , то указывает, что обработчик никогда не вызовет
preventDefault() , подробнее об этом будет рассказано в главе Действия
браузера по умолчанию.
function handler() {
alert( 'Спасибо!' );
}
input.addEventListener("click", handler);
// ....
input.removeEventListener("click", handler);
122/319
Метод addEventListener позволяет добавлять несколько обработчиков на
одно событие одного элемента, например:
<script>
function handler1() {
alert('Спасибо!');
};
function handler2() {
alert('Спасибо ещё раз!');
}
document.onDOMContentLoaded = function() {
alert("DOM построен"); // не будет работать
};
document.addEventListener("DOMContentLoaded", function() {
alert("DOM построен"); // а вот так сработает
});
Объект события
123/319
Чтобы хорошо обработать событие, могут понадобиться детали того, что
произошло. Не просто «клик» или «нажатие клавиши», а также – какие
координаты указателя мыши, какая клавиша нажата и так далее.
<script>
elem.onclick = function(event) {
// вывести тип события, элемент и координаты клика
alert(event.type + " на " + event.currentTarget);
alert("Координаты: " + event.clientX + ":" + event.clientY);
};
</script>
event.type
Тип события, в данном случае "click" .
event.currentTarget
Элемент, на котором сработал обработчик. Значение – обычно такое же, как и у
this , но если обработчик является функцией-стрелкой или при помощи bind
привязан другой объект в качестве this , то мы можем получить элемент из
event.currentTarget .
event.clientX / event.clientY
Координаты курсора в момент клика относительно окна, для событий мыши.
124/319
Объект события доступен и в HTML
При назначении обработчика в HTML, тоже можно использовать объект
event , вот так:
Тип события
Объект-обработчик: handleEvent
К примеру:
<script>
elem.addEventListener('click', {
handleEvent(event) {
alert(event.type + " на " + event.currentTarget);
}
});
</script>
<script>
class Menu {
handleEvent(event) {
switch(event.type) {
case 'mousedown':
125/319
elem.innerHTML = "Нажата кнопка мыши";
break;
case 'mouseup':
elem.innerHTML += "...и отжата.";
break;
}
}
}
<script>
class Menu {
handleEvent(event) {
// mousedown -> onMousedown
let method = 'on' + event.type[0].toUpperCase() + event.type.slice(1);
this[method](event);
}
onMousedown() {
elem.innerHTML = "Кнопка мыши нажата";
}
onMouseup() {
elem.innerHTML += "...и отжата.";
}
}
126/319
Итого
Задачи
Демо:
К решению
Спрятать себя
127/319
важность: 5
К решению
К решению
Пусть мяч перемещается при клике на поле, туда, куда был клик, вот так:
Требования:
● Центр мяча должен совпадать с местом нажатия мыши (если это возможно
без пересечения краёв поля);
● CSS-анимация желательна, но не обязательна;
● Мяч ни в коем случае не должен пересекать границы поля;
128/319
● При прокрутке страницы ничего не должно ломаться;
Заметки:
К решению
К решению
При помощи JavaScript для каждого сообщения добавьте в верхний правый угол
кнопку закрытия.
129/319
Лошадь [x]
Домашняя лошадь — животное семейства непарнокопытных,
одомашненный и единственный сохранившийся подвид
дикой лошади, вымершей в дикой природе, за исключением
небольшой популяции лошади Пржевальского.
Осёл [x]
Домашний осёл (лат. Equus asinus asinus), или ишак, —
одомашненный подвид дикого осла (Equus asinus), сыгравший
важную историческую роль в развитии хозяйства и культуры
человека и по-прежнему широко в хозяйстве многих
развивающихся стран.
[x]
Кошка
Кошка, или домашняя кошка (лат. Felis silvestris catus), —
домашнее животное, одно из наиболее популярных(наряду с
собакой) «животных-компаньонов». Являясь одиночным
охотником на грызунов и других мелких животных, кошка —
социальное животное, использующее для общения широкий
диапазон звуковых сигналов.
К решению
Карусель
важность: 4
⇦ ⇨
130/319
К решению
Всплытие и погружение
<div onclick="alert('Обработчик!')">
<em>Если вы кликните на <code>EM</code>, сработает обработчик на <code>DIV</code></e
</div>
Всплытие
<style>
body * {
margin: 10px;
border: 1px solid blue;
}
</style>
<form onclick="alert('form')">FORM
<div onclick="alert('div')">DIV
<p onclick="alert('p')">P</p>
</div>
</form>
131/319
FORM
DIV
P
3 Самый глубоко
вложенный элемент
event.target
132/319
● event.target – это «целевой» элемент, на котором произошло событие, в
процессе всплытия он неизменен.
●
this – это «текущий» элемент, до которого дошло всплытие, на нём сейчас
выполняется обработчик.
Попробуйте сами:
https://plnkr.co/edit/i3Q6mJoVe8HyXxdx?p=preview
Прекращение всплытия
Кликни меня
133/319
event.stopImmediatePropagation()
Если у элемента есть несколько обработчиков на одно событие, то даже при
прекращении всплытия все они будут выполнены.
То есть, event.stopPropagation() препятствует продвижению события
дальше, но на текущем элементе все обработчики будут вызваны.
Для того, чтобы полностью остановить обработку, существует метод
event.stopImmediatePropagation() . Он не только предотвращает
всплытие, но и останавливает обработку событий на текущем элементе.
Погружение
134/319
Стандарт DOM Events описывает 3 фазы прохода события:
Window
Document
<html>
Capture
Phase <body>
(1)
<table> Bubbling
Phase
(3)
<tbody>
<tr> <tr>
135/319
аргументами, ничего не знают о фазе погружения, а работают только на 2-ой и 3-
ей фазах.
Обратите внимание, что хоть и формально существует 3 фазы, 2-ую фазу («фазу
цели»: событие достигло элемента) нельзя обработать отдельно, при её
достижении вызываются все обработчики: и на всплытие, и на погружение.
Давайте посмотрим и всплытие и погружение в действии:
<style>
body * {
margin: 10px;
border: 1px solid blue;
}
</style>
<form>FORM
<div>DIV
<p>P</p>
</div>
</form>
<script>
for(let elem of document.querySelectorAll('*')) {
elem.addEventListener("click", e => alert(`Погружение: ${elem.tagName}`), true);
elem.addEventListener("click", e => alert(`Всплытие: ${elem.tagName}`));
}
</script>
FORM
DIV
P
136/319
Здесь обработчики навешиваются на каждый элемент в документе, чтобы
увидеть в каком порядке они вызываются по мере прохода события.
Если вы кликните по <p> , то последовательность следующая:
Итого
137/319
addEventListener без третьего аргумента или с третьим аргументом
равным false .
Делегирование событий
Всплытие и перехват событий позволяет реализовать один из самых важных
приёмов разработки – делегирование.
Идея в том, что если у нас есть много элементов, события на которых нужно
обрабатывать похожим образом, то вместо того, чтобы назначать обработчик
каждому, мы ставим один обработчик на их общего предка.
Из него можно получить целевой элемент event.target , понять на каком
именно потомке произошло событие и обработать его.
138/319
Рассмотрим пример – диаграмму Ба-Гуа . Это таблица, отражающая древнюю
китайскую философию.
Вот она:
Юго-Запад Юг Юго-Восток
Земля Огонь Дерево
Коричневый Оранжевый Зелёный
Спокойствие Слава Роман
Её HTML (схематично):
<table>
<tr>
<th colspan="3">Квадрат <em>Bagua</em>: Направление, Элемент, Цвет, Значение</th>
</tr>
<tr>
<td>...<strong>Северо-Запад</strong>...</td>
<td>...</td>
<td>...</td>
</tr>
<tr>...ещё 2 строки такого же вида...</tr>
<tr>...ещё 2 строки такого же вида...</tr>
</table>
В этой таблице всего 9 ячеек, но могло бы быть и 99, и даже 9999, не важно.
Вместо того, чтобы назначать обработчик onclick для каждой ячейки <td>
(их может быть очень много) – мы повесим «единый» обработчик на элемент
<table> .
let selectedTd;
139/319
table.onclick = function(event) {
let target = event.target; // где был клик?
highlight(target); // подсветить TD
};
function highlight(td) {
if (selectedTd) { // убрать существующую подсветку, если есть
selectedTd.classList.remove('highlight');
}
selectedTd = td;
selectedTd.classList.add('highlight'); // подсветить новый td
}
<td>
<strong>Северо-Запад</strong>
...
</td>
<table>
<td>
<strong> event.target
140/319
Вот улучшенный код:
table.onclick = function(event) {
let td = event.target.closest('td'); // (1)
highlight(td); // (4)
};
Разберём пример:
Первое, что может прийти в голову – это найти каждую кнопку и назначить ей
свой обработчик среди методов объекта. Но существует более элегантное
решение. Мы можем добавить один обработчик для всего меню и атрибуты
data-action для каждой кнопки в соответствии с методами, которые они
вызывают:
141/319
<div id="menu">
<button data-action="save">Сохранить</button>
<button data-action="load">Загрузить</button>
<button data-action="search">Поиск</button>
</div>
<script>
class Menu {
constructor(elem) {
this._elem = elem;
elem.onclick = this.onClick.bind(this); // (*)
}
save() {
alert('сохраняю');
}
load() {
alert('загружаю');
}
search() {
alert('ищу');
}
onClick(event) {
let action = event.target.dataset.action;
if (action) {
this[action]();
}
}
}
new Menu(menu);
</script>
●
Не нужно писать код, чтобы присвоить обработчик каждой кнопке.
Достаточно просто создать один метод и поместить его в разметку.
●
Структура HTML становится по-настоящему гибкой. Мы можем
добавлять/удалять кнопки в любое время.
142/319
Мы также можем использовать классы .action-save , .action-load , но
подход с использованием атрибутов data-action является более
семантичным. Их можно использовать и для стилизации в правилах CSS.
Поведение: «Счётчик»
Например, здесь HTML-атрибут data-counter добавляет кнопкам поведение:
«увеличить значение при клике»:
<script>
document.addEventListener('click', function(event) {
});
</script>
143/319
Всегда используйте метод addEventListener для обработчиков на уровне
документа
Когда мы устанавливаем обработчик событий на объект document , мы
всегда должны использовать метод addEventListener , а не
document.on<событие> , т.к. в случае последнего могут возникать
конфликты: новые обработчики будут перезаписывать уже существующие.
<button data-toggle-id="subscribe-mail">
Показать форму подписки
</button>
<script>
document.addEventListener('click', function(event) {
let id = event.target.dataset.toggleId;
if (!id) return;
elem.hidden = !elem.hidden;
});
</script>
Ещё раз подчеркнём, что мы сделали. Теперь для того, чтобы добавить скрытие-
раскрытие любому элементу, даже не надо знать JavaScript, можно просто
написать атрибут data-toggle-id .
144/319
Шаблон «поведение» может служить альтернативой для фрагментов JS-кода в
вёрстке.
Итого
Зачем использовать:
●
Упрощает процесс инициализации и экономит память: не нужно вешать
много обработчиков.
●
Меньше кода: при добавлении и удалении элементов не нужно ставить
или снимать обработчики.
●
Удобство изменений DOM: можно массово добавлять или удалять
элементы путём изменения innerHTML и ему подобных.
Задачи
145/319
Дан список сообщений с кнопками для удаления [x] . Заставьте кнопки
работать.
Лошадь [x]
Домашняя лошадь - животное семейства непарнокопытных,
одомашненный и единственный сохранившийся подвид
дикой лошади, вымершей в дикой природе, за исключением
небольшой популяции лошади Пржевальского.
Осёл [x]
Домашний осёл или ишак — одомашненный подвид дикого
осла, сыгравший важную историческую роль в развитии
хозяйства и культуры человека. Все одомашненные ослы
относятся к африканским ослам.
[x]
Кошка
Кошка, или домашняя кошка (лат. Félis silvéstris cátus), —
домашнее животное, одно из наиболее популярных (наряду с
собакой) "животных-компаньонов". С точки зрения научной
систематики, домашняя кошка — млекопитающее семейства
кошачьих отряда хищных. Ранее домашнюю кошку нередко
рассматривали как отдельный биологический вид.
К решению
Раскрывающееся дерево
важность: 5
146/319
Животные
Млекопитающие
Коровы
Ослы
Собаки
Тигры
Другие
Змеи
Птицы
Ящерицы
Рыбы
Аквариумные
Гуппи
Скалярии
Морские
Морская форель
Требования:
К решению
Сортируемая таблица
важность: 4
<table id="grid">
<thead>
<tr>
<th data-type="number">Возраст</th>
<th data-type="string">Имя</th>
</tr>
</thead>
<tbody>
<tr>
<td>5</td>
<td>Вася</td>
</tr>
<tr>
<td>10</td>
<td>Петя</td>
</tr>
...
147/319
</tbody>
</table>
Работающий пример:
Возраст Имя
5 Вася
2 Петя
12 Женя
9 Маша
1 Илья
К решению
Поведение "подсказка"
важность: 5
148/319
ЛяЛяЛя ЛяЛяЛя ЛяЛяЛя ЛяЛяЛя ЛяЛяЛя ЛяЛяЛя ЛяЛяЛя ЛяЛяЛя ЛяЛяЛя
Прокрутите страницу, чтобы кнопки оказались у верхнего края, а затем проверьте, правильно ли
выводятся подсказки.
Детали оформления:
К решению
149/319
Многие события автоматически влекут за собой действие браузера.
Например:
●
Клик по ссылке инициирует переход на новый URL.
●
Нажатие на кнопку «отправить» в форме – отсылку её на сервер.
●
Зажатие кнопки мыши над текстом и её движение в таком состоянии –
инициирует его выделение.
Пример: меню
Рассмотрим меню для сайта, например:
150/319
<li><a href="/css">CSS</a></li>
</ul>
menu.onclick = function(event) {
if (event.target.nodeName != 'A') return;
151/319
События, вытекающие из других
Некоторые события естественным образом вытекают друг из друга. Если мы
отменим первое событие, то последующие не возникнут.
152/319
Для некоторых браузеров (Firefox, Chrome) опция passive по умолчанию
включена в true для таких событий, как touchstart и touchmove .
event.defaultPrevented
Правый клик вызывает контекстное меню браузера Правый клик вызывает наше контекстное меню
<script>
elem.oncontextmenu = function(event) {
event.preventDefault();
alert("Контекстное меню кнопки");
};
document.oncontextmenu = function(event) {
event.preventDefault();
153/319
alert("Контекстное меню документа");
};
</script>
<script>
elem.oncontextmenu = function(event) {
event.preventDefault();
event.stopPropagation();
alert("Контекстное меню кнопки");
};
document.oncontextmenu = function(event) {
event.preventDefault();
alert("Контекстное меню документа");
};
</script>
Теперь контекстное меню для кнопки работает как задумано. Но цена слишком
высока. Мы навсегда запретили доступ к информации о правых кликах для
любого внешнего кода, включая счётчики, которые могли бы собирать
статистику, и т.п. Это слегка неразумно.
Альтернативным решением было бы проверить в обработчике document , было
ли отменено действие по умолчанию? Если да, тогда событие было обработано,
и нам не нужно на него реагировать.
<script>
154/319
elem.oncontextmenu = function(event) {
event.preventDefault();
alert("Контекстное меню кнопки");
};
document.oncontextmenu = function(event) {
if (event.defaultPrevented) return;
event.preventDefault();
alert("Контекстное меню документа");
};
</script>
Сейчас всё работает правильно. Если у нас есть вложенные элементы и каждый
из них имеет контекстное меню, то код также будет работать. Просто убедитесь,
что проверяете event.defaultPrevented в каждом обработчике
contextmenu .
event.stopPropagation() и event.preventDefault()
Как мы можем видеть, event.stopPropagation() и
event.preventDefault() (также известный как return false ) – это
две разные функции. Они никак не связаны друг с другом.
Итого
155/319
●
click на <input type="checkbox"> – ставит или убирает галочку в
input .
●
submit – при нажатии на <input type="submit"> или при нажатии
клавиши Enter в форме данные отправляются на сервер.
●
keydown – при нажатии клавиши в поле ввода появляется символ.
●
contextmenu – при правом клике показывается контекстное меню браузера.
●
…и многие другие…
156/319
Задачи
<script>
function handler() {
alert( "..." );
return false;
}
</script>
Как поправить?
К решению
#contents
Как насчёт того, чтобы прочитать Википедию или посетить W3.org и узнать о современных
стандартах?
Детали:
157/319
● Содержимое может иметь вложенные теги, в том числе внутри ссылок,
например, <a href=".."><i>...</i></a> .
К решению
Галерея изображений
важность: 5
Например:
К решению
158/319
Генерация пользовательских событий
Конструктор Event
Где:
●
type – тип события, строка, например "click" или же любой придуманный
нами – "my-event" .
●
options – объект с тремя необязательными свойствами:
●
bubbles: true/false – если true , тогда событие всплывает.
●
cancelable: true/false – если true , тогда можно отменить действие
по умолчанию. Позже мы разберём, что это значит для пользовательских
событий.
●
composed: true/false – если true , тогда событие будет всплывать
наружу за пределы Shadow DOM. Позже мы разберём это в разделе Веб-
компоненты.
Метод dispatchEvent
После того, как объект события создан, мы должны запустить его на элементе,
вызвав метод elem.dispatchEvent(event) .
159/319
Затем обработчики отреагируют на него, как будто это обычное браузерное
событие. Если при создании указан флаг bubbles , то оно будет всплывать.
<script>
let event = new Event("click");
elem.dispatchEvent(event);
</script>
event.isTrusted
Можно легко отличить «настоящее» событие от сгенерированного кодом.
Пример всплытия
<script>
// ловим на document...
document.addEventListener("hello", function(event) { // (1)
alert("Привет от " + event.target.tagName); // Привет от H1
});
</script>
Обратите внимание:
160/319
1. Мы должны использовать addEventListener для наших собственных
событий, т.к. on<event> -свойства существуют только для встроенных
событий, то есть document.onhello не сработает.
2. Мы обязаны передать флаг bubbles:true , иначе наше событие не будет
всплывать.
Механизм всплытия идентичен как для встроенного события ( click ), так и для
пользовательского события ( hello ). Также одинакова работа фаз всплытия и
погружения.
alert(event.clientX); // 100
Давайте проверим:
161/319
let event = new Event("click", {
bubbles: true, // только свойства bubbles и cancelable
cancelable: true, // работают в конструкторе Event
clientX: 100,
clientY: 100
});
Пользовательские события
Для генерации событий совершенно новых типов, таких как "hello" , следует
использовать конструктор new CustomEvent . Технически CustomEvent
абсолютно идентичен Event за исключением одной небольшой детали.
<script>
// дополнительная информация приходит в обработчик вместе с событием
elem.addEventListener("hello", function(event) {
alert(event.detail.name);
});
elem.dispatchEvent(new CustomEvent("hello", {
detail: { name: "Вася" }
}));
</script>
Свойство detail может содержать любые данные. Надо сказать, что никто не
мешает и в обычное new Event записать любые свойства. Но CustomEvent
предоставляет специальное поле detail во избежание конфликтов с другими
свойствами события.
162/319
Кроме того, класс события описывает, что это за событие, и если оно не
браузерное, а пользовательское, то лучше использовать CustomEvent , чтобы
явно об этом сказать.
event.preventDefault()
<pre id="rabbit">
|\ /|
\|_|/
/. .\
=\_Y_/=
{>o<}
</pre>
<button onclick="hide()">Hide()</button>
<script>
// hide() будет вызван при щелчке на кнопке
function hide() {
let event = new CustomEvent("hide", {
cancelable: true // без этого флага preventDefault не сработает
});
if (!rabbit.dispatchEvent(event)) {
alert('Действие отменено обработчиком');
} else {
163/319
rabbit.hidden = true;
}
}
rabbit.addEventListener('hide', function(event) {
if (confirm("Вызвать preventDefault?")) {
event.preventDefault();
}
});
</script>
|\ /|
\|_|/
/. .\
=\_Y_/=
{>o<}
Hide()
<script>
menu.onclick = function() {
alert(1);
// alert("вложенное событие")
menu.dispatchEvent(new CustomEvent("menu-open", {
bubbles: true
}));
alert(2);
};
164/319
document.addEventListener('menu-open', () => alert('вложенное событие'))
</script>
<script>
menu.onclick = function() {
alert(1);
// alert(2)
setTimeout(() => menu.dispatchEvent(new CustomEvent("menu-open", {
bubbles: true
})));
alert(2);
};
Итого
165/319
●
bubbles: true чтобы событие всплывало.
●
cancelable: true если мы хотим, чтобы event.preventDefault()
работал.
Интерфейсные события
Здесь мы изучим основные события пользовательского интерфейса и как с ними
работать.
166/319
mousedown/mouseup
Кнопка мыши нажата/отпущена над элементом.
mouseover/mouseout
Курсор мыши появляется над элементом и уходит с него.
mousemove
Каждое движение мыши над элементом генерирует это событие.
click
Вызывается при mousedown , а затем mouseup над одним и тем же
элементом, если использовалась левая кнопка мыши.
dblclick
Вызывается двойным кликом на элементе.
contextmenu
Вызывается при попытке открытия контекстного меню, как правило, нажатием
правой кнопки мыши. Но, заметим, это не совсем событие мыши, оно может
вызываться и специальной клавишей клавиатуры.
Порядок событий
Кнопки мыши
167/319
С другой стороны, обработчикам mousedown и mouseup может потребоваться
event.button , потому что эти события срабатывают на любую кнопку, таким
образом button позволяет различать «нажатие правой кнопки» и «нажатие
левой кнопки».
Кнопка X1 (назад) 3
Кнопка X2 (вперёд) 4
Свойства события:
●
shiftKey : Shift
168/319
●
altKey : Alt (или Opt для Mac)
●
ctrlKey : Ctrl
●
metaKey : Cmd для Mac
<script>
button.onclick = function(event) {
if (event.altKey && event.shiftKey) {
alert('Ура!');
}
};
</script>
169/319
Не забывайте про мобильные устройства
Комбинации клавиш хороши в качестве дополнения к рабочему процессу. Так
что, если посетитель использует клавиатуру – они работают.
Но если на их устройстве его нет – тогда должен быть способ жить без
клавиш-модификаторов.
Например, если у нас есть окно размером 500x500, и курсор мыши находится в
левом верхнем углу, то значения clientX и clientY равны 0 , независимо от
того, как прокручивается страница.
А если мышь находится в центре окна, то значения clientX и clientY равны
250 независимо от того, в каком месте документа она находится и до какого
места документ прокручен. В этом они похожи на position:fixed .
Отключаем выделение
Двойной клик мыши имеет побочный эффект, который может быть неудобен в
некоторых интерфейсах: он выделяет текст.
170/319
Если зажать левую кнопку мыши и, не отпуская кнопку, провести мышью, то
также будет выделение, которое в интерфейсах может быть «не кстати».
Есть несколько способов запретить выделение, о которых вы можете прочитать в
главе Selection и Range.
В данном случае самым разумным будет отменить действие браузера по
умолчанию при событии mousedown , это отменит оба этих выделения:
До...
<b ondblclick="alert('Клик!')" onmousedown="return false">
Сделайте двойной клик на мне
</b>
...После
Предотвращение копирования
Если мы хотим отключить выделение для защиты содержимого страницы от
копирования, то мы можем использовать другое событие: oncopy .
Итого
171/319
События мыши имеют следующие свойства:
●
Кнопка: button .
●
Клавиши-модификаторы ( true если нажаты): altKey , ctrlKey ,
shiftKey и metaKey (Mac).
● Если вы планируете обработать Ctrl , то не забудьте, что пользователи
Mac обычно используют Cmd , поэтому лучше проверить if (e.metaKey
|| e.ctrlKey) .
●
Координаты относительно окна: clientX/clientY .
● Координаты относительно документа: pageX/pageY .
Задачи
Выделяемый список
важность: 5
Демо:
Кристофер Робин
Винни Пух
Тигра
Кенга
Кролик. Просто Кролик.
P.S. В этом задании все элементы списка содержат только текст. Без вложенных
тегов.
172/319
P.P.S. Предотвратите стандартное для браузера выделение текста при кликах.
К решению
mouseover mouseout
<DIV>
173/319
Свойство relatedTarget может быть null
Пропуск элементов
mouseout mouseover
#FROM #TO
<DIV> <DIV> <DIV>
174/319
relatedTarget = null target
#TO
#parent
mouseout
mouseover
#child
175/319
#parent
mouseout
mouseover
#child
parent.onmouseout = function(event) {
/* event.target: внешний элемент */
};
parent.onmouseover = function(event) {
/* event.target: внутренний элемент (всплыло) */
};
176/319
Делегирование событий
table.onmouseout = function(event) {
let target = event.target;
target.style.background = '';
};
177/319
// ячейка <td> под курсором в данный момент (если есть)
let currentElem = null;
table.onmouseover = function(event) {
// перед тем, как войти на следующий элемент, курсор всегда покидает предыдущий
// если currentElem есть, то мы ещё не ушли с предыдущего <td>,
// это переход внутри - игнорируем такое событие
if (currentElem) return;
table.onmouseout = function(event) {
// если мы вне <td>, то игнорируем уход мыши
// это какой-то переход внутри таблицы, но вне <td>,
// например с <tr> на другой <tr>
if (!currentElem) return;
while (relatedTarget) {
// поднимаемся по дереву элементов и проверяем – внутри ли мы currentElem или нет
// если да, то это переход внутри элемента – игнорируем
if (relatedTarget == currentElem) return;
relatedTarget = relatedTarget.parentNode;
}
Итого
178/319
● При быстром движении мыши события не будут возникать на промежуточных
элементах.
●
События mouseover/out и mouseenter/leave имеют дополнительное
свойство: relatedTarget . Оно дополняет свойство target и содержит
ссылку на элемент, с/на который мы переходим.
Задачи
Улучшенная подсказка
важность: 5
Например:
Результат в iframe:
179/319
Жили-были на свете три поросенка. Три брата. Все
одинакового роста, кругленькие, розовые, с
одинаковыми веселыми хвостиками.
Даже имена у них были похожи. Звали поросят: Ниф-
Ниф, Нуф-Нуф и Наф-Наф. Все лето они кувыркались в
зеленой траве, грелись на солнышке, нежились в лужах.
Но вот наступила осень. Солнце уже не так сильно
припекало, серые облака тянулись над пожелтевшим
лесом.
- Пора нам подумать о зиме, - сказал как-то Наф-Наф.
Наведи курсор на меня
К решению
"Умная" подсказка
важность: 5
180/319
// пример подсказки
let tooltip = document.createElement('div');
tooltip.className = "tooltip";
tooltip.innerHTML = "Tooltip";
Демо:
12 : 30 : 00
passes: 5 failures: 0 duration: 4.49s
hoverIntent
✓ mouseover -> immediately no tooltip ‣
✓ mouseover -> pause shows tooltip ‣
✓ mouseover -> fast mouseout no tooltip ‣
К решению
181/319
dragend и так далее.
Они интересны тем, что позволяют легко решать простые задачи. Например,
можно перетащить файл в браузер, так что JS получит доступ к его
содержимому.
Алгоритм Drag’n’Drop
moveAt(event.pageX, event.pageY);
function onMouseMove(event) {
182/319
moveAt(event.pageX, event.pageY);
}
};
Если запустить этот код, то мы заметим нечто странное. При начале переноса
мяч «раздваивается» и переносится не сам мяч, а его «клон».
ball.ondragstart = function() {
return false;
};
Правильное позиционирование
В примерах выше мяч позиционируется так, что его центр оказывается под
указателем мыши:
183/319
Неплохо, но есть побочные эффекты. Мы, для начала переноса, можем нажать
мышью на любом месте мяча. Если мячик «взят» за самый край – то в начале
переноса он резко «прыгает», центрируясь под указателем мыши.
shiftY
shiftX
// onmousedown
let shiftX = event.clientX - ball.getBoundingClientRect().left;
let shiftY = event.clientY - ball.getBoundingClientRect().top;
// onmousemove
// ball has position:absoute
ball.style.left = event.pageX - shiftX + 'px';
ball.style.top = event.pageY - shiftY + 'px';
ball.onmousedown = function(event) {
ball.style.position = 'absolute';
ball.style.zIndex = 1000;
document.body.append(ball);
184/319
moveAt(event.pageX, event.pageY);
function onMouseMove(event) {
moveAt(event.pageX, event.pageY);
}
};
ball.ondragstart = function() {
return false;
};
В предыдущих примерах мяч можно было бросить просто где угодно в пределах
окна. В реальности мы обычно берём один элемент и перетаскиваем в другой.
Например, «файл» в «папку» или что-то ещё.
Абстрактно говоря, мы берём перетаскиваемый (draggable) элемент и помещаем
его в другой элемент «цель переноса» (droppable).
Нам нужно знать:
● куда пользователь положил элемент в конце переноса, чтобы обработать его
окончание
●
и, желательно, над какой потенциальной целью (элемент, куда можно
положить, например, изображение папки) он находится в процессе переноса,
чтобы подсветить её.
185/319
Решение довольно интересное и немного хитрое, давайте рассмотрим его.
Какой может быть первая мысль? Возможно, установить обработчики событий
mouseover/mouseup на элемент – потенциальную цель переноса?
Но это не работает.
<style>
div {
width: 50px;
height: 50px;
position: absolute;
top: 0;
}
</style>
<div style="background:blue" onmouseover="alert('никогда не сработает')"></div>
<div style="background:red" onmouseover="alert('над красным!')"></div>
186/319
ball.hidden = false;
Заметим, нам нужно спрятать мяч перед вызовом функции (*) . В противном
случае по этим координатам мы будем получать мяч, ведь это и есть элемент
непосредственно под указателем: elemBelow=ball . Так что мы прячем его и
тут же показываем обратно.
Мы можем использовать этот код для проверки того, над каким элементом мы
«летим», в любое время. И обработать окончание переноса, когда оно случится.
Расширенный код onMouseMove с поиском потенциальных целей переноса:
function onMouseMove(event) {
moveAt(event.pageX, event.pageY);
ball.hidden = true;
let elemBelow = document.elementFromPoint(event.clientX, event.clientY);
ball.hidden = false;
// потенциальные цели переноса помечены классом droppable (может быть и другая логи
let droppableBelow = elemBelow.closest('.droppable');
if (currentDroppable != droppableBelow) {
// мы либо залетаем на цель, либо улетаем из неё
// внимание: оба значения могут быть null
// currentDroppable=null,
// если мы были не над droppable до этого события (например, над пустым прост
// droppableBelow=null,
// если мы не над droppable именно сейчас, во время этого события
if (currentDroppable) {
// логика обработки процесса "вылета" из droppable (удаляем подсветку)
leaveDroppable(currentDroppable);
}
currentDroppable = droppableBelow;
if (currentDroppable) {
// логика обработки процесса, когда мы "влетаем" в элемент droppable
enterDroppable(currentDroppable);
}
}
}
187/319
В приведённом ниже примере, когда мяч перетаскивается через футбольные
ворота, ворота подсвечиваются.
https://plnkr.co/edit/Q1afzDb6iz4aJfWp?p=preview
Итого
Задачи
Слайдер
важность: 5
188/319
Создайте слайдер:
Важные детали:
● Слайдер должен нормально работать при резком движении мыши влево или
вправо за пределы полосы. При этом бегунок должен останавливаться чётко
в нужном конце полосы.
● При нажатом бегунке мышь может выходить за пределы полосы слайдера, но
слайдер пусть всё равно работает (это удобно для пользователя).
К решению
Требования к реализации:
189/319
К решению
Тестовый стенд
Для того, чтобы лучше понять, как работают события клавиатуры, можно
использовать тестовый стенд .
event.code и event.key
Свойство key объекта события позволяет получить символ, а свойство code –
«физический код клавиши».
190/319
Если пользователь работает с разными языками, то при переключении на другой
язык символ изменится с "Z" на совершенно другой. Получившееся станет
новым значением event.key , тогда как event.code останется тем же:
"KeyZ" .
Например:
●
Буквенные клавиши имеют коды по типу "Key<буква>" : "KeyA" ,
"KeyB" и т.д.
●
Коды числовых клавиш строятся по принципу: "Digit<число>" :
"Digit0" , "Digit1" и т.д.
● Код специальных клавиш – это их имя: "Enter" , "Backspace" , "Tab"
и т.д.
F1 F1 F1
191/319
Обратите внимание, что event.code точно указывает, какая именно клавиша
нажата. Так, большинство клавиатур имеют по две клавиши Shift : слева и
справа. event.code уточняет, какая именно из них была нажата, в то время как
event.key сообщает о «смысле» клавиши: что вообще было нажато ( Shift ).
document.addEventListener('keydown', function(event) {
if (event.code == 'KeyZ' && (event.ctrlKey || event.metaKey)) {
alert('Отменить!')
}
});
Caps Lock
Shift Shift
192/319
Strg Win Al Alt Gr Win Menu Strg
t
Автоповтор
193/319
Для событий, вызванных автоповтором, у объекта события свойство
event.repeat равно true .
Действия по умолчанию
Для примера:
●
Появление символа (самое очевидное).
●
Удаление символа (клавиша Delete ).
●
Прокрутка страницы (клавиша PageDown ).
●
Открытие диалогового окна браузера «Сохранить» ( Ctrl+S )
●
…и так далее.
<script>
function checkPhoneKey(key) {
return (key >= '0' && key <= '9') || key == '+' || key == '(' || key == ')' || key =
}
</script>
<input onkeydown="return checkPhoneKey(event.key)" placeholder="Введите телефон" type=
Введите телефон
<script>
function checkPhoneKey(key) {
return (key >= '0' && key <= '9') || key == '+' || key == '(' || key == ')' || key =
key == 'ArrowLeft' || key == 'ArrowRight' || key == 'Delete' || key == 'Backspace
}
194/319
</script>
<input onkeydown="return checkPhoneKey(event.key)" placeholder="Введите телефон" type=
Введите телефон
Итого
195/319
● key – символ ( "A" , "a" и так далее), для не буквенно-цифровых групп
клавиш (таких как Esc ) обычно имеет то же значение, что и code .
Задачи
Например, код ниже выведет alert при одновременном нажатии клавиш "Q"
и "W" (в любом регистре, в любой раскладке)
runOnKeys(
() => alert("Привет!"),
"KeyQ",
"KeyW"
);
К решению
События указателя
196/319
Краткая история
197/319
Событие указателя Аналогичное событие мыши
pointerdown mousedown
pointerup mouseup
pointermove mousemove
pointerover mouseover
pointerout mouseout
pointerenter mouseenter
pointerleave mouseleave
pointercancel -
gotpointercapture -
lostpointercapture -
198/319
● isPrimary – равно true для основного указателя (первый палец в мульти-
тач).
Мульти-тач
199/319
События, связанные с первым пальцем, всегда содержат свойство
isPrimary=true .
Событие: pointercancel
200/319
Предотвращайте действие браузера по умолчанию, чтобы избежать
pointercancel .
После того, как мы это сделаем, события будут работать как и ожидается,
браузер не будет перехватывать процесс и не будет вызывать событие
pointercancel .
Теперь мы можем добавить код для перемещения мяча и наш drag’n’drop будет
работать и для мыши и для устройств с сенсорным экраном.
Захват указателя
201/319
● при вызове elem.releasePointerCapture(pointerId) .
<div class="slider">
<div class="thumb"></div>
</div>
Однако это не самое правильное решение. Одна из проблем – это то, что
движения указателя по документу могут вызвать сторонние эффекты, заставить
работать другие обработчики (например, mouseover ), не имеющие отношения
к слайдеру.
Именно здесь вступает в игру setPointerCapture :
●
Мы можем вызывать thumb.setPointerCapture(event.pointerId) в
обработчике pointerdown ,
● Тогда дальнейшие события указателя до pointerup/cancel будут
привязаны к thumb .
●
Затем, когда произойдёт pointerup (передвижение завершено), привязка
будет автоматически удалена, нам об этом не нужно беспокоиться.
202/319
Так что, даже если пользователь будет двигать указателем по всему документу,
обработчики событий будут вызваны на thumb . Причём все свойства объекта
события, такие как clientX/clientY , будут корректны – захват указателя
влияет только на target/currentTarget .
thumb.onpointerdown = function(event) {
// перенацелить все события указателя (до pointerup) на thumb
thumb.setPointerCapture(event.pointerId);
// начать отслеживание перемещения указателя
thumb.onpointermove = function(event) {
// перемещение слайдера: отслеживание thumb, т.к все события указателя перенацеле
let newLeft = event.clientX - slider.getBoundingClientRect().left;
thumb.style.left = newLeft + 'px';
};
// если сработало событие pointerup, завершить отслеживание перемещения указателя
thumb.onpointerup = function(event) {
thumb.onpointermove = null;
thumb.onpointerup = null;
// ...при необходимости также обработайте "конец перемещения"
};
};
// примечание: нет необходимости вызывать thumb.releasePointerCapture,
// это происходит автоматически при pointerup
Итого
203/319
События указателя позволяют одновременно обрабатывать действия с
помощью мыши, касания и пера, в едином фрагменте кода.
Прокрутка
Событие прокрутки scroll позволяет реагировать на прокрутку страницы или
элемента. Есть много хороших вещей, которые при этом можно сделать.
Например:
●
Показать/скрыть дополнительные элементы управления или информацию,
основываясь на том, в какой части документа находится пользователь.
● Подгрузить данные, когда пользователь прокручивает страницу вниз до конца.
window.addEventListener('scroll', function() {
document.getElementById('showScroll').innerHTML = pageYOffset + 'px';
});
204/319
Предотвращение прокрутки
Задачи
Бесконечная страница
важность: 5
Как тут:
Прокрути меня
Date: Sun Apr 02 2023 05:50:02 GMT+0300 (Moscow Standard Time)
205/319
2. Прокрутка неточна. Если прокрутить страницу до конца, можно оказаться в
0-50px от реальной нижней границы документа.
К решению
Кнопка вверх/вниз
важность: 5
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63
64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83
84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102
103 104 105 106 107 108 109 110 111 112 113 114 115 116
117 118 119 120 121 122 123 124 125 126 127 128 129 130
131 132 133 134 135 136 137 138 139 140 141 142 143 144
145 146 147 148 149 150 151 152 153 154 155 156 157 158
159 160 161 162 163 164 165 166 167 168 169 170 171 172
173 174 175 176 177 178 179 180 181 182 183 184 185 186
К решению
206/319
Допустим, у нас есть клиент с низкой скоростью соединения, и мы хотим
сэкономить его трафик.
Солнечная система
Солнечная система — планетная система, включает в себя центральную звезду — Солнце — и
все естественные космические объекты, вращающиеся вокруг Солнца. Она сформировалась
путём гравитационного сжатия газопылевого облака примерно 4,57 млрд лет назад.
Большая часть массы объектов Солнечной системы приходится на Солнце; остальная часть
содержится в восьми относительно уединённых планетах, имеющих почти круговые орбиты и
располагающихся в пределах почти плоского диска — плоскости эклиптики. Общая масса
системы составляет около 1,0014. При таком распределении масс особенностью кинематики
Требования:
207/319
Открыть песочницу для задачи.
К решению
Например:
<form name="my">
<input name="one" value="1">
<input name="two" value="2">
</form>
<script>
// получаем форму
let form = document.forms.my; // <form name="my"> element
// получаем элемент
let elem = form.elements.one; // <input name="one"> element
alert(elem.value); // 1
</script>
208/319
Может быть несколько элементов с одним и тем же именем, это часто бывает с
кнопками-переключателями radio .
<form>
<input type="radio" name="age" value="10">
<input type="radio" name="age" value="20">
</form>
<script>
let form = document.forms[0];
<body>
<form id="form">
<fieldset name="userFields">
<legend>info</legend>
<input name="login" type="text">
</fieldset>
</form>
<script>
alert(form.elements.login); // <input name="login">
209/319
Сокращённая форма записи: form.name
Есть более короткая запись: мы можем получить доступ к элементу через
form[index/name] .
<form id="form">
<input name="login">
</form>
<script>
alert(form.elements.login == form.login); // true, ведь это одинаковые <input>
Для любого элемента форма доступна через element.form . Так что форма
ссылается на все элементы, а эти элементы ссылаются на форму.
Вот иллюстрация:
form
form form
210/319
Пример:
<form id="form">
<input type="text" name="login">
</form>
<script>
// form -> element
let login = form.login;
Элементы формы
input и textarea
К их значению можно получить доступ через свойство input.value (строка)
или input.checked (булево значение) для чекбоксов.
Вот так:
select и option
Элемент <select> имеет 3 важных свойства:
211/319
1. Найти соответствующий элемент <option> и установить в
option.selected значение true .
2. Установить в select.value значение нужного <option> .
3. Установить в select.selectedIndex номер нужного <option> .
<select id="select">
<option value="apple">Яблоко</option>
<option value="pear">Груша</option>
<option value="banana">Банан</option>
</select>
<script>
// все три строки делают одно и то же
select.options[2].selected = true;
select.selectedIndex = 2;
select.value = 'banana';
</script>
<script>
// получаем все выбранные значения из select с multiple
let selected = Array.from(select.options)
.filter(option => option.selected)
.map(option => option.value);
alert(selected); // blues,rock
</script>
212/319
Полное описание элемента <select> доступно в спецификации
https://html.spec.whatwg.org/multipage/forms.html#the-select-element .
new Option
Элемент <option> редко используется сам по себе, но и здесь есть кое-что
интересное.
В спецификации есть красивый короткий синтаксис для создания элемента
<option> :
Параметры:
●
text – текст внутри <option> ,
●
value – значение,
● defaultSelected – если true , то ставится HTML-атрибут selected ,
● selected – если true , то элемент <option> будет выбранным.
Пример:
option.selected
Выбрана ли опция.
option.index
Номер опции среди других в списке <select> .
option.value
213/319
Значение опции.
option.text
Содержимое опции (то, что видит посетитель).
Ссылки
● Спецификация: https://html.spec.whatwg.org/multipage/forms.html .
Итого
document.forms
Форма доступна через document.forms[name/index] .
form.elements
Элементы формы доступны через form.elements[name/index] , или можно
просто использовать form[name/index] . Свойство elements также
работает для <fieldset> .
element.form
Элементы хранят ссылку на свою форму в свойстве form .
Это были основы для начала работы с формами. Далее в учебнике мы встретим
ещё много примеров.
В следующей главе мы рассмотрим такие события, как focus и blur , которые
могут происходить на любом элементе, но чаще всего обрабатываются в
формах.
Задачи
Имеется <select> :
214/319
<select id="genres">
<option value="rock">Рок</option>
<option value="blues" selected>Блюз</option>
</select>
Используя JavaScript:
К решению
Фокусировка: focus/blur
Элемент получает фокус, когда пользователь кликает по нему или использует
клавишу Tab . Также существует HTML-атрибут autofocus , который
устанавливает фокус на элемент, когда страница загружается. Есть и другие
способы получения фокуса, о них – далее.
Момент потери фокуса («blur») может быть важнее. Это момент, когда
пользователь кликает куда-то ещё или нажимает Tab , чтобы переключиться на
следующее поле формы. Есть другие причины потери фокуса, о них – далее.
Потеря фокуса обычно означает «данные введены», и мы можем выполнить
проверку введённых данных или даже отправить эти данные на сервер и так
далее.
События focus/blur
215/319
● Обработчик focus скрывает это сообщение об ошибке (в момент потери
фокуса проверка повторится):
<style>
.invalid { border-color: red; }
#error { color: red }
</style>
<div id="error"></div>
<script>
input.onblur = function() {
if (!input.value.includes('@')) { // не email
input.classList.add('invalid');
error.innerHTML = 'Пожалуйста, введите правильный email.'
}
};
input.onfocus = function() {
if (this.classList.contains('invalid')) {
// удаляем индикатор ошибки, т.к. пользователь хочет ввести данные заново
this.classList.remove('invalid');
error.innerHTML = "";
}
};
</script>
Ваш email:
Методы focus/blur
<style>
.error {
background: red;
}
</style>
216/319
Ваш email: <input type="email" id="input">
<input type="text" style="width:280px" placeholder="введите неверный email и кликните
<script>
input.onblur = function() {
if (!this.value.includes('@')) { // не email
// показать ошибку
this.classList.add("error");
// ...и вернуть фокус обратно
input.focus();
} else {
this.classList.remove("error");
}
};
</script>
Если мы что-нибудь введём и нажмём Tab или кликнем в другое место, тогда
onblur вернёт фокус обратно.
217/319
Потеря фокуса, вызванная JavaScript
Потеря фокуса может произойти по множеству причин.
Одна из них – когда посетитель кликает куда-то ещё. Но и JavaScript может
быть причиной, например:
● alert переводит фокус на себя – элемент теряет фокус (событие blur ),
а когда alert закрывается – элемент получает фокус обратно (событие
focus ).
●
Если элемент удалить из DOM, фокус также будет потерян. Если элемент
добавить обратно, то фокус не вернётся.
218/319
Есть два специальных значения:
● tabindex="0" ставит элемент в один ряд с элементами без tabindex . То
есть, при переключении такие элементы будут после элементов с tabindex
≥ 1.
Кликните первый пункт в списке и нажмите Tab. Продолжайте следить за порядком. Обратит
<ul>
<li tabindex="1">Один</li>
<li tabindex="0">Ноль</li>
<li tabindex="2">Два</li>
<li tabindex="-1">Минус один</li>
</ul>
<style>
li { cursor: pointer; }
:focus { outline: 1px dashed green; }
</style>
Кликните первый пункт в списке и нажмите Tab. Продолжайте следить за порядком. Обратите
внимание, что много последовательных нажатий Tab могут вывести фокус из iframe с примером.
Один
Ноль
Два
Минус один
События focusin/focusout
219/319
События focus и blur не всплывают.
Имя Фамилия
<form id="form">
<input type="text" name="name" value="Имя">
<input type="text" name="surname" value="Фамилия">
</form>
<script>
// установить обработчик на фазе перехвата (последний аргумент true)
form.addEventListener("focus", () => form.classList.add('focused'), true);
form.addEventListener("blur", () => form.classList.remove('focused'), true);
</script>
Имя Фамилия
220/319
Заметьте, что эти события должны использоваться с
elem.addEventListener , но не с on<event> .
<form id="form">
<input type="text" name="name" value="Имя">
<input type="text" name="surname" value="Фамилия">
</form>
<script>
form.addEventListener("focusin", () => form.classList.add('focused'));
form.addEventListener("focusout", () => form.classList.remove('focused'));
</script>
Имя Фамилия
Итого
Задачи
Редактируемый div
важность: 5
221/319
Когда пользователь нажимает Enter или переводит фокус, <textarea>
превращается обратно в <div> , и его содержимое становится HTML-кодом в
<div> .
К решению
Редактирование TD по клику
важность: 5
Демо:
Кликните на ячейку таблицы, чтобы редактировать её. Нажмите ОК или ОТМЕНА, когда
закончите.
Юго-Запад Юг Юго-Восток
Земля Огонь Дерево
Коричневый Оранжевый Зеленый
Спокойствие Слава Роман
222/319
К решению
К решению
Событие: change
Для текстовых <input> это означает, что событие происходит при потере
фокуса.
Пока мы печатаем в текстовом поле в примере ниже, событие не происходит. Но
когда мы перемещаем фокус в другое место, например, нажимая на кнопку, то
произойдёт событие change :
Button
<select onchange="alert(this.value)">
223/319
<option value="">Выберите что-нибудь</option>
<option value="1">Вариант 1</option>
<option value="2">Вариант 2</option>
<option value="3">Вариант 3</option>
</select>
Выберите что-нибудь
Событие: input
oninput:
224/319
Они относятся к классу ClipboardEvent и обеспечивают доступ к копируемым/
вставляемым данным.
Мы также можем использовать event.preventDefault() для
предотвращения действия по умолчанию, и в итоге ничего не скопируется/не
вставится.
Например, код, приведённый ниже, предотвращает все подобные события и
показывает, что мы пытаемся вырезать/копировать/вставить:
Итого
Значение было
change Для текстовых полей срабатывает при потере фокуса.
изменено.
Срабатывает при
input каждом изменении Запускается немедленно, в отличие от change .
значения.
225/319
Событие Описание Особенности
Действия по
Действие можно предотвратить. Свойство
вырезанию/
cut/copy/paste event.clipboardData предоставляет доступ на чтение/
копированию/
запись в буфер обмена…
вставке.
Задачи
Депозитный калькулятор
Демо-версия:
Депозитный калькулятор.
Первоначальный депозит 10000
Срок вклада? 12 (год)
Годовая процентная ставка? 5
Было: Станет:
10000 10500
Формула:
К решению
226/319
Отправка формы: событие и метод submit
Событие: submit
В примере ниже:
1. Перейдите в текстовое поле и нажмите Enter .
2. Нажмите <input type="submit"> .
227/319
Взаимосвязь между submit и click
Метод: submit
form.submit();
Задачи
228/319
Создайте функцию showPrompt(html, callback) , которая выводит форму с
сообщением ( html ), полем ввода и кнопками OK/ОТМЕНА .
Требования:
Пример использования:
Демо во фрейме:
К решению
229/319
Загрузка документа и ресурсов
Страница: DOMContentLoaded, load, beforeunload, unload
DOMContentLoaded
document.addEventListener("DOMContentLoaded", ready);
// не "document.onDOMContentLoaded = ..."
Например:
<script>
function ready() {
alert('DOM готов');
// изображение ещё не загружено (если не было закешировано), так что размер будет
alert(`Размер изображения: ${img.offsetWidth}x${img.offsetHeight}`);
}
230/319
document.addEventListener("DOMContentLoaded", ready);
</script>
DOMContentLoaded и скрипты
Когда браузер обрабатывает HTML-документ и встречает тег <script> , он
должен выполнить его перед тем, как продолжить строить DOM. Это делается
на случай, если скрипт захочет изменить DOM или даже дописать в него
( document.write ), так что DOMContentLoaded должен подождать.
<script>
document.addEventListener("DOMContentLoaded", () => {
alert("DOM готов!");
});
</script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.3.0/lodash.js"></scri
<script>
alert("Библиотека загружена, встроенный скрипт выполнен");
</script>
231/319
DOMContentLoaded и стили
Внешние таблицы стилей не затрагивают DOM, поэтому DOMContentLoaded их
не ждёт.
Но здесь есть подводный камень. Если после стилей у нас есть скрипт, то этот
скрипт должен дождаться, пока загрузятся стили:
window.onload
<script>
window.onload = function() { // можно также использовать window.addEventListener('lo
alert('Страница загружена');
232/319
// к этому моменту картинка загружена
alert(`Image size: ${img.offsetWidth}x${img.offsetHeight}`);
};
</script>
window.onunload
window.addEventListener("unload", function() {
navigator.sendBeacon("/analytics", JSON.stringify(analyticsData));
});
● Отсылается POST-запрос.
●
Мы можем послать не только строку, но так же формы и другие форматы, как
описано в главе Fetch, но обычно это строковый объект.
● Размер данных ограничен 64 Кб.
233/319
Если мы хотим отменить переход на другую страницу, то здесь мы этого сделать
не сможем. Но сможем в другом месте – в событии onbeforeunload .
window.onbeforeunload
window.onbeforeunload = function() {
return false;
};
Вот пример:
window.onbeforeunload = function() {
return "Есть несохранённые изменения. Всё равно уходим?";
};
readyState
234/319
●
"loading" – документ загружается.
●
"interactive" – документ был полностью прочитан.
● "complete" – документ был полностью прочитан и все ресурсы (такие как
изображения) были тоже загружены.
if (document.readyState == 'loading') {
// ещё загружается, ждём события
document.addEventListener('DOMContentLoaded', work);
} else {
// DOM готов!
work();
}
// текущее состояние
console.log(document.readyState);
<script>
log('начальный readyState:' + document.readyState);
235/319
<iframe src="iframe.html" onload="log('iframe onload')"></iframe>
Типичный вывод:
1. [1] начальный readyState:loading
2. [2] readyState:interactive
3. [2] DOMContentLoaded
4. [3] iframe onload
5. [4] img onload
6. [4] readyState:complete
7. [4] window onload
Итого
236/319
● Событие load на window генерируется, когда страница и все ресурсы
загружены. Мы редко его используем, потому что обычно нет нужды ждать так
долго.
●
Событие beforeunload на window генерируется, когда пользователь
покидает страницу. Если мы отменим событие, браузер спросит, на самом ли
деле пользователь хочет уйти (например, у нас есть несохранённые
изменения).
● Событие unload на window генерируется, когда пользователь
окончательно уходит, в обработчике мы можем делать только простые вещи,
которые ни о чём не спрашивают пользователя и не заставляют его ждать.
Из-за этих ограничений оно редко используется. Мы можем послать сетевой
запрос с помощью navigator.sendBeacon .
● document.readyState – текущее состояние документа, изменения можно
отследить с помощью события readystatechange :
●
loading – документ грузится.
●
interactive – документ прочитан, происходит примерно в то же время,
что и DOMContentLoaded , но до него.
●
complete – документ и ресурсы загружены, происходит примерно в то же
время, что и window.onload , но до него.
В современных сайтах скрипты обычно «тяжелее», чем HTML: они весят больше,
дольше обрабатываются.
<script src="https://javascript.info/article/script-async-defer/long.js?speed=1"></sc
237/319
Конечно, есть пути, как это обойти. Например, мы можем поместить скрипт внизу
страницы. Тогда он сможет видеть элементы над ним и не будет препятствовать
отображению содержимого страницы:
<body>
...всё содержимое над скриптом...
<script src="https://javascript.info/article/script-async-defer/long.js?speed=1"></
</body>
defer
●
Скрипты с defer никогда не блокируют страницу.
●
Скрипты с defer всегда выполняются, когда дерево DOM готово, но до
события DOMContentLoaded .
238/319
<p>...содержимое до скрипта...</p>
<script>
document.addEventListener('DOMContentLoaded', () => alert("Дерево DOM готово после с
</script>
async
239/319
●
DOMContentLoaded может произойти как до асинхронного скрипта (если
асинхронный скрипт завершит загрузку после того, как страница будет
готова),
●
…так и после асинхронного скрипта (если он короткий или уже содержится
в HTTP-кеше)
● Остальные скрипты не ждут async , и скрипты c async не ждут другие
скрипты.
Так что если у нас есть несколько скриптов с async , они могут выполняться в
любом порядке. То, что первое загрузится – запустится в первую очередь:
<script>
document.addEventListener('DOMContentLoaded', () => alert("DOM готов!"));
</script>
240/319
Скрипт начнёт загружаться, как только он будет добавлен в документ (*) .
script.async = false;
document.body.append(script);
function loadScript(src) {
let script = document.createElement('script');
script.src = src;
script.async = false;
document.body.append(script);
}
Итого
Порядок DOMContentLoaded
241/319
Порядок DOMContentLoaded
Порядок загрузки
Не имеет значения. Может загрузиться и выполниться до того, как
(кто загрузится
async страница полностью загрузится. Такое случается, если скрипты
первым, тот и
маленькие или хранятся в кеше, а документ достаточно большой.
сработает).
Порядок документа
Выполняется после того, как документ загружен и обработан (ждёт),
defer (как расположены в
непосредственно перед DOMContentLoaded .
документе).
Загрузка скриптов
242/319
document.head.append(script);
…Но как нам вызвать функцию, которая объявлена внутри того скрипта? Нам
нужно подождать, пока скрипт загрузится, и только потом мы можем её вызвать.
На заметку:
Для наших собственных скриптов мы можем использовать JavaScript-модули,
но они не слишком широко распространены в сторонних библиотеках.
script.onload
Главный помощник – это событие load . Оно срабатывает после того, как
скрипт был загружен и выполнен.
Например:
script.onload = function() {
// в скрипте создаётся вспомогательная переменная с именем "_"
alert(_.VERSION); // отображает версию библиотеки
};
script.onerror
Ошибки, которые возникают во время загрузки скрипта, могут быть отслежены с
помощью события error .
script.onerror = function() {
alert("Ошибка загрузки " + this.src); // Ошибка загрузки https://example.com/404.js
};
243/319
Обратите внимание, что мы не можем получить описание HTTP-ошибки. Мы не
знаем, была ли это ошибка 404 или 500, или какая-то другая. Знаем только, что
во время загрузки произошла ошибка.
Важно:
Обработчики onload / onerror отслеживают только сам процесс загрузки.
Другие ресурсы
Например:
img.onload = function() {
alert(`Изображение загружено, размеры ${img.width}x${img.height}`);
};
img.onerror = function() {
alert("Ошибка во время загрузки изображения");
};
244/319
Или, если быть более точным, один источник (домен/порт/протокол) не может
получить доступ к содержимому с другого источника. Даже поддомен или просто
другой порт будут считаться разными источниками, не имеющими доступа друг к
другу.
Это правило также касается ресурсов с других доменов.
Если мы используем скрипт с другого домена, и в нем имеется ошибка, мы не
сможем узнать детали этой ошибки.
Для примера давайте возьмём мини-скрипт error.js , который состоит из
одного-единственного вызова функции, которой не существует:
// 📁 error.js
noSuchFunction();
<script>
window.onerror = function(message, url, line, col, errorObj) {
alert(`${message}\n${url}, ${line}:${col}`);
};
</script>
<script src="/article/onload-onerror/crossorigin/error.js"></script>
<script>
window.onerror = function(message, url, line, col, errorObj) {
alert(`${message}\n${url}, ${line}:${col}`);
};
</script>
<script src="https://cors.javascript.info/article/onload-onerror/crossorigin/error.js
Отчёт отличается:
Script error.
, 0:0
245/319
Детали отчёта могут варьироваться в зависимости от браузера, но основная
идея остаётся неизменной: любая информация о внутреннем устройстве
скрипта, включая стек ошибки, спрятана. Именно потому, что скрипт загружен с
другого домена.
Зачем нам могут быть нужны детали ошибки?
На заметку:
Почитать больше о кросс-доменных доступах вы можете в главе Fetch:
запросы на другие сайты. Там описан метод fetch для сетевых запросов,
но политика там точно такая же.
Такое понятие как «куки» (cookies) не рассматривается в текущей главе, но
вы можете почитать о них в главе Куки, document.cookie.
246/319
Если куки нас не волнуют, тогда смело выбираем "anonymous" :
<script>
window.onerror = function(message, url, line, col, errorObj) {
alert(`${message}\n${url}, ${line}:${col}`);
};
</script>
<script crossorigin="anonymous" src="https://cors.javascript.info/article/onload-oner
Итого
Задачи
Браузер начнёт загружать изображение и положит его в кеш. Позже, когда такое
же изображение появится в документе (не важно как), оно будет показано
мгновенно.
247/319
Создайте функцию preloadImages(sources, callback) , которая
загружает все изображения из массива sources и, когда все они будут
загружены, вызывает callback .
function loaded() {
alert("Изображения загружены")
}
К решению
Разное
MutationObserver: наблюдатель за изменениями
Синтаксис
248/319
Потом прикрепляем его к DOM-узлу:
observer.observe(node, config);
249/319
oldValue – предыдущее значение, только для изменений атрибута или
●
текста, если включена соответствующая опция
attributeOldValue / characterDataOldValue .
<script>
let observer = new MutationObserver(mutationRecords => {
console.log(mutationRecords); // console.log(изменения)
});
mutationRecords = [{
type: "characterData",
oldValue: "меня",
target: <text node>,
// другие свойства пусты
}];
mutationRecords = [{
type: "childList",
target: <div#elem>,
removedNodes: [<b>],
nextSibling: <text node>,
previousSibling: <text node>
// другие свойства пусты
}, {
type: "characterData"
target: <text node>
// ...детали изменений зависят от того, как браузер обрабатывает такое удаление
250/319
// он может соединить два соседних текстовых узла "Отредактируй " и ", пожалуйста"
// или может оставить их разными текстовыми узлами
}];
...
<pre class="language-javascript"><code>
// вот код
let hello = "world";
</code></pre>
...
251/319
Prism.highlightElem(pre) ищет такие элементы pre и добавляет в них
стили и теги, которые в итоге дают цветную подсветку синтаксиса, подобно той,
которую вы видите в примерах здесь, на этой странице.
Когда конкретно нам вызвать этот метод подсветки? Можно по событию
DOMContentLoaded или просто внизу страницы написать код, который будет
искать все pre[class*="language"] и вызывать Prism.highlightElem
для них:
Пока всё просто, правда? В HTML есть фрагменты кода в <pre> , и для них мы
включаем подсветку синтаксиса.
Идём дальше. Представим, что мы собираемся динамически подгружать
материалы с сервера. Позже в учебнике мы изучим способы для этого. На
данный момент имеет значение только то, что мы получаем HTML-статью с веб-
сервера и показываем её по запросу:
Мы можем добавить этот вызов к коду, который загружает статью, например, так:
…Но представьте, что у нас есть много мест в коде, где мы загружаем что-либо:
статьи, опросы, посты форума. Нужно ли нам в каждый такой вызов добавлять
Prism.highlightElem ? Получится не очень удобно, да и можно легко забыть
сделать это.
А что, если содержимое загружается вообще сторонним кодом? Например, у нас
есть форум, написанный другим человеком, загружающий содержимое
252/319
динамически, и нам захотелось добавить к нему выделение синтаксиса. Никто не
любит править чужие скрипты.
К счастью, есть другой вариант.
});
253/319
Демо-элемент с id="highlight-demo" , за которым следит код примера
выше.
Дополнительные методы
// мы отключаем наблюдатель
observer.disconnect();
Сборка мусора
Итого
254/319
MutationObserver может реагировать на изменения в DOM: атрибуты,
добавленные/удалённые элементы, текстовое содержимое.
Мы можем использовать его, чтобы отслеживать изменения, производимые
другими частями нашего собственного кода, а также интегрироваться со
сторонними библиотеками.
MutationObserver может отслеживать любые изменения. Разные опции
конфигурации «что наблюдать» предназначены для оптимизации, чтобы не
тратить ресурсы на лишние вызовы колбэка.
Selection и Range
Range
255/319
<p id="p">Example: <i>italic</i> and <b>bold</b></p>
▾P
#text Example:␣
▾I
#text italic
#text ␣and␣
▾B
#text bold
0 1 2 3
<script>
let range = new Range();
range.setStart(p, 0);
range.setEnd(p, 2);
256/319
Ниже представлен расширенный пример, в котором вы можете попробовать
другие варианты:
range.setStart(p, start.value);
range.setEnd(p, end.value);
0 1 2 3
0 1 2 3
Это также возможно, нужно просто установить начало и конец как относительное
смещение в текстовых узлах.
Нам нужно создать диапазон, который:
257/319
●
начинается со второй позиции первого дочернего узла тега <p>
(захватываем всё, кроме первых двух букв "Example: ")
● заканчивается на 3 позиции первого дочернего узла тега <b> (захватываем
первые три буквы «bold», но не более):
<script>
let range = new Range();
range.setStart(p.firstChild, 2);
range.setEnd(p.querySelector('b').firstChild, 3);
commonAncestorContainer
(<p>)
●
startContainer , startOffset – узел и начальное смещение,
● в примере выше: первый текстовый узел внутри тега <p> и 2 .
● endContainer , endOffset – узел и конечное смещение,
●
в примере выше: первый текстовый узел внутри тега <b> и 3 .
●
collapsed – boolean, true , если диапазон начинается и заканчивается на
одном и том же месте (следовательно, в диапазон ничего не входит),
● в примере выше: false
●
commonAncestorContainer – ближайший общий предок всех узлов в
пределах диапазона,
●
в примере выше: <p>
Методы Range
258/319
Существует множество удобных методов для манипулирования диапазонами.
Установить начало диапазона:
●
setStart(node, offset) установить начальную границу в позицию
offset в node
●
setStartBefore(node) установить начальную границу прямо перед node
● setStartAfter(node) установить начальную границу прямо после node
Как было показано, node может быть как текстовым узлом, так и
элементом: для текстовых узлов offset пропускает указанное количество
символов, в то время как для элементов – указанное количество дочерних
узлов.
Другие:
● selectNode(node) выделить node целиком
● selectNodeContents(node) выделить всё содержимое node
● collapse(toStart) если указано toStart=true , установить конечную
границу в начало, иначе установить начальную границу в конец, схлопывая
таким образом диапазон
● cloneRange() создать новый диапазон с идентичными границами
259/319
Нажмите на кнопку, чтобы соответствующий метод отработал на выделении, или на "resetEx
<p id="result"></p>
<script>
let range = new Range();
range.setStart(p.firstChild, 2);
range.setEnd(p.querySelector('b').firstChild, 3);
window.getSelection().removeAllRanges();
window.getSelection().addRange(range);
}
};
methods.resetExample();
</script>
260/319
Нажмите на кнопку, чтобы соответствующий метод отработал на выделении, или на
"resetExample", чтобы восстановить выделение как было.
deleteContents
extractContents
cloneContents
insertNode
surroundContents
resetExample
Selection
Выделение может включать ноль или более диапазонов. По крайней мере, так
утверждается в Спецификации Selection API . На практике же выделить
несколько диапазонов в документе можно только в Firefox, используя
Ctrl+click ( Cmd+click для Mac).
261/319
выделение
Свойства Selection
262/319
Конец выделения может быть в документе до его начала
Существует несколько методов выделить содержимое, в зависимости от
устройства пользователя: мышь, горячие клавиши, нажатия пальцем и
другие.
Некоторые из них, такие как мышь, позволяют создавать выделение в обоих
направлениях: слева направо и справа налево.
Если начало (якорь) выделения идёт в документе перед концом (фокус),
говорят, что такое выделение «направлено вперёд».
К примеру, если пользователь начинает выделение с помощью мыши в
направлении от «Example» до «italic»:
якорь фокус
фокус якорь
263/319
От <input id="from" disabled> – До <input id="to" disabled>
<script>
document.onselectionchange = function() {
let {anchorNode, anchorOffset, focusNode, focusOffset} = document.getSelection();
Ниже представлено демо получения выделения как в виде текста, так и в виде
DOM-узлов:
<script>
document.onselectionchange = function() {
let selection = document.getSelection();
Методы Selection
264/319
● getRangeAt(i) – взять i-ый диапазон, начиная с 0 . Во всех браузерах,
кроме Firefox, используется только 0 .
● addRange(range) – добавить range в выделение. Все браузеры, кроме
Firefox, проигнорируют этот вызов, если в выделении уже есть диапазон.
●
removeRange(range) – удалить range из выделения.
●
removeAllRanges() – удалить все диапазоны.
●
empty() – сокращение для removeAllRanges .
<script>
// выделить всё содержимое от нулевого потомка тега <p> до последнего
document.getSelection().setBaseAndExtent(p, 0, p, p.childNodes.length);
</script>
265/319
То же самое с помощью Range :
<script>
let range = new Range();
range.selectNodeContents(p); // или selectNode(p), чтобы выделить и тег <p>
Свойства:
●
input.selectionStart – позиция начала выделения (это свойство можно
изменять),
● input.selectionEnd – позиция конца выделения (это свойство можно
изменять),
● input.selectionDirection – направление выделения, одно из: «forward»
(вперёд), «backward» (назад) или «none» (без направления, если, к примеру,
выделено с помощью двойного клика мыши).
События:
● input.onselect – срабатывает, когда выделение завершено.
Методы:
●
input.select() – выделяет всё содержимое input (может быть
textarea вместо input ),
266/319
● input.setSelectionRange(start, end, [direction]) – изменить
выделение, чтобы начиналось с позиции start , и заканчивалось end , в
данном направлении direction (необязательный параметр).
● input.setRangeText(replacement, [start], [end],
[selectionMode]) – заменяет выделенный текст в диапазоне новым.
<script>
area.onselect = function() {
from.value = area.selectionStart;
to.value = area.selectionEnd;
};
</script>
От – До
Заметьте:
● onselect срабатывает при выделении чего-либо, но не при снятии
выделения.
267/319
событие document.onselectionchange не должно срабатывать при
●
выделении внутри элемента формы в соответствии со спецификацией , так
как оно не является выделением элементов в document . Хотя некоторые
браузеры генерируют это событие, полагаться на это не стоит.
<script>
area.onfocus = () => {
// нулевая задержка setTimeout нужна, чтобы это сработало после получения фокуса э
setTimeout(() => {
// мы можем задать любое выделение
// если начало и конец совпадают, курсор устанавливается на этом месте
area.selectionStart = area.selectionEnd = 10;
});
};
</script>
268/319
<input id="input" style="width:200px" value="Select here and click the button">
<button id="button">Обернуть выделение звёздочками *...*</button>
<script>
button.onclick = () => {
if (input.selectionStart == input.selectionEnd) {
return; // ничего не выделено
}
Select here and click the button Обернуть выделение звёздочками *...*
<script>
button.onclick = () => {
let pos = input.value.indexOf("ЭТО");
if (pos >= 0) {
input.setRangeText("*ЭТО*", pos, pos + 3, "select");
input.focus(); // ставим фокус, чтобы выделение было видно
}
};
</script>
269/319
<input id="input" style="width:200px" value="Текст Текст Текст Текст Текст">
<button id="button">Вставить "ПРИВЕТ" на месте курсора</button>
<script>
button.onclick = () => {
input.setRangeText("ПРИВЕТ", input.selectionStart, input.selectionEnd, "end");
input.focus();
};
</script>
<style>
#elem {
user-select: none;
}
</style>
<div>Можно выделить <div id="elem">Нельзя выделить</div> Можно выделить</div>
<script>
elem.onselectstart = () => false;
</script>
Это удобно, когда есть другой обработчик события на том действии, которое
запускает выделение (скажем, mousedown ). Так что мы отключаем
выделение, чтобы избежать конфликта.
270/319
А содержимое elem при этом может быть скопировано.
Ссылки
●
Спецификация DOM: Range
● Selection API
●
Спецификация HTML: API для выделения в элементах управления текстом
Итого
2. Установить выделение:
// напрямую:
selection.setBaseAndExtent(...from...to...);
271/319
И пару слов о курсоре. Позиция курсора в редактируемых элементах, таких как
<textarea> , всегда находится в начале или конце выделения.
Мы можем использовать это, как для того, чтобы получить позицию курсора, так
и чтобы переместить его, установив elem.selectionStart и
elem.selectionEnd .
P.S. Если вам нужна поддержка старого IE8-, посмотрите в архивную статью.
Событийный цикл
272/319
Задачи поступают на выполнение – движок выполняет их – затем ожидает
новые задачи (во время ожидания практически не нагружая процессор
компьютера)
Может так случиться, что задача поступает, когда движок занят чем-то другим,
тогда она ставится в очередь.
Очередь, которую формируют такие задачи, называют «очередью макрозадач»
(macrotask queue, термин v8).
скрипт
событийный mousemove очередь
цикл setTimeout макрозадач
...
Это была теория. Теперь давайте взглянем, как можно применить эти знания.
273/319
Пример 1: разбиение «тяжёлой» задачи.
let i = 0;
function count() {
count();
let i = 0;
274/319
let start = Date.now();
function count() {
if (i == 1e9) {
alert("Done in " + (Date.now() - start) + 'ms');
} else {
setTimeout(count); // планируем новый вызов (**)
}
count();
Чтобы сократить разницу ещё сильнее, давайте немного улучшим наш код.
Мы перенесём планирование очередного вызова в начало count() :
let i = 0;
function count() {
275/319
// перенесём планирование очередного вызова в начало
if (i < 1e9 - 1e6) {
setTimeout(count); // запланировать новый вызов
}
do {
i++;
} while (i % 1e6 != 0);
if (i == 1e9) {
alert("Done in " + (Date.now() - start) + 'ms');
}
count();
Почему?
Всё просто: как вы помните, в браузере есть минимальная задержка в 4
миллисекунды при множестве вложенных вызовов setTimeout . Даже если мы
указываем задержку 0 , на самом деле она будет равна 4 мс (или чуть
больше). Поэтому чем раньше мы запланируем выполнение – тем быстрее
выполнится код.
Итак, мы разбили ресурсоёмкую задачу на части – теперь она не блокирует
пользовательский интерфейс, причём почти без потерь в общем времени
выполнения.
С одной стороны, это хорошо, потому что наша функция может создавать много
элементов, добавлять их по одному в документ и изменять их стили –
пользователь не увидит «промежуточного», незаконченного состояния. Это
важно, верно?
276/319
В примере ниже изменения i не будут заметны, пока функция не завершится,
поэтому мы увидим только последнее значение i :
<div id="progress"></div>
<script>
function count() {
for (let i = 0; i < 1e6; i++) {
i++;
progress.innerHTML = i;
}
}
count();
</script>
<div id="progress"></div>
<script>
let i = 0;
function count() {
if (i < 1e7) {
setTimeout(count);
}
count();
</script>
277/319
Пример 3: делаем что-нибудь после события
menu.onclick = function() {
// ...
// создадим наше собственное событие с данными пункта меню, по которому щёлкнули мыш
let customEvent = new CustomEvent("menu-open", {
bubbles: true
});
Макрозадачи и Микрозадачи
Promise.resolve()
.then(() => alert("promise"));
alert("code");
278/319
Какой здесь будет порядок?
1. code появляется первым, т.к. это обычный синхронный вызов.
2. promise появляется вторым, потому что .then проходит через очередь
микрозадач и выполняется после текущего синхронного кода.
3. timeout появляется последним, потому что это макрозадача.
...
скрипт
микрозадачи
рендеринг
событийный
цикл mousemove
микрозадачи
рендеринг
setTimeout
Это важно, так как гарантирует, что общее окружение остаётся одним и тем же
между микрозадачами – не изменены координаты мыши, не получены новые
данные по сети и т.п.
<div id="progress"></div>
<script>
let i = 0;
279/319
function count() {
if (i < 1e6) {
queueMicrotask(count);
}
count();
</script>
Итого
280/319
События пользовательского интерфейса и сетевые события в промежутках
между микрозадачами не обрабатываются: микрозадачи исполняются
непрерывно одна за другой.
Поэтому queueMicrotask можно использовать для асинхронного выполнения
функции в том же состоянии окружения.
Web Workers
Для длительных тяжёлых вычислений, которые не должны блокировать
событийный цикл, мы можем использовать Web Workers .
Задачи
setTimeout(function timeout() {
console.log('Таймаут');
}, 0);
p.then(function(){
console.log('Обработка промиса');
});
console.log('Конец скрипта');
К решению
281/319
console.log(1);
console.log(7);
К решению
Решения
Навигация по DOM-элементам
document.body.firstElementChild
// или
document.body.children[0]
// или (первый узел пробел, поэтому выбираем второй)
document.body.childNodes[1]
document.body.lastElementChild
// или
document.body.children[1]
К условию
282/319
Вопрос о соседях
К условию
К условию
Поиск элементов
Вот некоторые:
// 1. Таблица с `id="age-table"`.
let table = document.getElementById('age-table')
283/319
table.getElementsByTagName('td')[0]
// или
table.querySelector('td')
// 4. Форма с name="search"
// предполагаем, что есть только один элемент с таким name в документе
let form = document.getElementsByName('search')[0]
// или, именно форма:
document.querySelector('form[name="search"]')
К условию
Считаем потомков
284/319
К условию
<html>
<body>
<script>
alert(document.body.lastChild.nodeType);
</script>
</body>
</html>
К условию
Тег в комментарии
Ответ: BODY .
<script>
let body = document.body;
Происходящее по шагам:
285/319
3. Значение свойства data для элемента-комментария – это его
содержимое (внутри <!--...--> ): "BODY" .
К условию
Или так:
alert(document.constructor.name); // HTMLDocument
alert(HTMLDocument.prototype.constructor.name); // HTMLDocument
alert(HTMLDocument.prototype.__proto__.constructor.name); // Document
alert(HTMLDocument.prototype.__proto__.__proto__.constructor.name); // Node
Вот и иерархия.
286/319
Мы также можем исследовать объект с помощью
console.dir(document) и увидеть имена функций-конструкторов,
открыв __proto__ . Браузерная консоль берёт их как раз из свойства
constructor .
К условию
Атрибуты и свойства
Получите атрибут
<!DOCTYPE html>
<html>
<body>
<script>
// получаем элемент
let elem = document.querySelector('[data-widget-name]');
// читаем значение
alert(elem.dataset.widgetName);
// или так
alert(elem.getAttribute('data-widget-name'));
</script>
</body>
</html>
К условию
287/319
let href = link.getAttribute('href');
if (!href) continue; // нет атрибута
link.style.color = 'orange';
}
К условию
Изменение документа
Ответ: 1 и 3.
Пример:
<div id="elem1"></div>
<div id="elem2"></div>
<div id="elem3"></div>
<script>
let text = '<b>текст</b>';
elem1.append(document.createTextNode(text));
elem2.innerHTML = text;
288/319
elem3.textContent = text;
</script>
К условию
Очистите элемент
function clear(elem) {
for (let i=0; i < elem.childNodes.length; i++) {
elem.childNodes[i].remove();
}
}
function clear(elem) {
while (elem.firstChild) {
elem.firstChild.remove();
}
}
function clear(elem) {
elem.innerHTML = '';
}
К условию
289/319
табличные теги. Поэтому браузер показывает "aaa" до <table> .
К условию
Создайте список
К условию
1. Решение с innerHTML .
2. Решение через DOM .
К условию
К условию
290/319
Для решения задачи сгенерируем таблицу в виде строки: "<table>...
</table>" , а затем присвоим в innerHTML .
Алгоритм:
К условию
<div id="clock">
<span class="hour">hh</span>:<span class="min">mm</span>:<span class="sec">s
</div>
function update() {
let clock = document.getElementById('clock');
let date = new Date(); // (*)
let hours = date.getHours();
291/319
if (hours < 10) hours = '0' + hours;
clock.children[0].innerHTML = hours;
let timerId;
function clockStop() {
clearInterval(timerId);
timerId = null;
}
К условию
Решение:
one.insertAdjacentHTML('afterend', '<li>2</li><li>3</li>');
292/319
К условию
Сортировка таблицы
table.tBodies[0].append(...sortedRows);
1.
2.
3.
4.
К условию
Стили и классы
293/319
Создать уведомление
К условию
Решение:
К условию
div.style.overflowY = 'scroll';
div.style.width = '50px';
div.style.height = '50px';
div.remove();
alert(scrollWidth);
294/319
К условию
(0,0)
clientWidth
.........................
.........................
.........................
.........................
.........................
.........................
.........................
Для того, чтобы центр мяча находился в центре поля, нам нужно
сместить мяч на половину его ширины влево и на половину его высоты
вверх:
295/319
ball.style.top = Math.round(field.clientHeight / 2 - ball.offsetHeight / 2) +
Код выше стабильно работать не будет, потому что <img> идёт без
ширины/высоты:
При первой загрузке браузер обычно кеширует изображения, так что при
последующей загрузке оно будет доступно тут же, вместе с размерами.
Но при первой загрузке значение ширины мяча ball.offsetWidth
равно 0 . Это приводит к вычислению неверных координат.
#ball {
width: 40px;
height: 40px;
}
К условию
Отличия:
296/319
3. clientWidth соответствует внутренней области элемента,
включая внутренние отступы padding , а CSS-ширина (при
стандартном значении box-sizing ) соответствует внутренней
области без внутренних отступов padding .
4. Если есть полоса прокрутки, и для неё зарезервировано место, то
некоторые браузеры вычитают его из CSS-ширины (т.к. оно больше
недоступно для содержимого), а некоторые – нет. Свойство
clientWidth всегда ведёт себя одинаково: оно всегда обозначает
размер за вычетом прокрутки, т.е. реально доступный для
содержимого.
К условию
Координаты
Внешние углы
Координаты внешних углов – это как раз то, что возвращает функция
elem.getBoundingClientRect() .
297/319
Это может быть сделано с помощью CSS:
let answer4 = [
coords.right - parseInt(getComputedStyle(field).borderRightWidth),
coords.bottom - parseInt(getComputedStyle(field).borderBottomWidth)
];
let answer4 = [
coords.left + elem.clientLeft + elem.clientWidth,
coords.top + elem.clientTop + elem.clientHeight
];
К условию
К условию
298/319
Открыть решение в песочнице.
К условию
К условию
К условию
Спрятать себя
К условию
Ответ: 1 и 2 .
299/319
Для того чтобы удалить функцию-обработчик, нужно где-то сохранить
ссылку на неё, например:
function handler() {
alert(1);
}
button.addEventListener("click", handler);
button.removeEventListener("click", handler);
К условию
#field {
width: 200px;
height: 150px;
position: relative;
}
#ball {
position: absolute;
left: 0; /* по отношению к ближайшему расположенному предку (поле) */
top: 0;
transition: 1s all; /* CSS-анимация для значений left/top делает передвижени
}
Картинка:
300/319
event.clientX
fieldCoords.left
?
ball.style.left
Нам нужно сдвинуть мяч на половину его высоты вверх и половину его
ширины влево, чтобы центр мяча точно совпадал с точкой нажатия
мышки.
Следует помнить, что ширина и высота мяча должна быть известна в тот
момент, когда мы получаем значение ball.offsetWidth . Это значение
может задаваться в HTML или CSS.
301/319
К условию
HTML/CSS
Пример HTML-структуры:
<div class="menu">
<span class="title">Сладости (нажми меня)!</span>
<ul>
<li>Пирожное</li>
<li>Пончик</li>
<li>Мёд</li>
</ul>
</div>
Например:
302/319
Сладости (нажми меня)!
Переключение меню
.menu ul {
margin: 0;
list-style: none;
padding-left: 20px;
display: none;
}
.menu .title::before {
content: '▶ ';
font-size: 80%;
color: green;
}
.menu.open .title::before {
content: '▼ ';
}
.menu.open ul {
display: block;
}
К условию
303/319
либо float:right . Преимущество варианта с float:right в том,
что кнопка закрытия никогда не перекроет текст, но вариант
position:absolute даёт больше свободы для действий. В общем,
выбор за вами.
К условию
Карусель
div (container)
ul (width: 9999px)
130x130 …
304/319
дополнительное место под «хвосты» символов. И чтобы его убрать, нам
нужно прописать display:block .
div (container)
ul (margin-left: -350px)
130x130 …
К условию
Делегирование событий
К условию
Раскрывающееся дерево
305/319
1. Оборачиваем текст каждого заголовка дерева в элемент <span> .
Затем мы можем добавить стили CSS на :hover и обрабатывать
клики только на тексте, т.к. ширина элемента <span> в точности
совпадает с шириной текста.
2. Устанавливаем обработчик на корневой узел дерева tree и ловим
клики на элементах <span> , содержащих заголовки.
К условию
Сортируемая таблица
К условию
Поведение "подсказка"
К условию
function(event) {
handler() // содержимое onclick
}
306/319
Исправить очень просто:
<script>
function handler() {
alert("...");
return false;
}
</script>
<script>
function handler(event) {
alert("...");
event.preventDefault();
}
</script>
К условию
К условию
Галерея изображений
307/319
Решение состоит в том, чтобы добавить обработчик на контейнер
#thumbs и отслеживать клики на ссылках. Если клик происходит по
ссылке <a> , тогда меняем атрибут src элемента #largeImg на href
уменьшенного изображения.
К условию
Выделяемый список
К условию
Улучшенная подсказка
К условию
"Умная" подсказка
308/319
Но как измерить скорость?
Первая идея может быть такой: запускать нашу функцию каждые 100ms
и находить разницу между прежними и текущими координатами курсора.
Если она мала, то значит и скорость низкая.
К условию
Слайдер
К условию
309/319
Чтобы перетащить элемент, мы можем использовать position:fixed ,
это делает управление координатами проще. В конце следует
переключиться обратно на position:absolute , чтобы положить
элемент в документ.
К условию
К условию
Прокрутка
Бесконечная страница
310/319
Мы можем вызвать её сразу же и добавить как обработчик для
window.onscroll .
311/319
Получить высоту окна можно как
document.documentElement.clientHeight .
function populate() {
while(true) {
// нижняя граница документа
let windowRelativeBottom = document.documentElement.getBoundingClientRect(
К условию
Кнопка вверх/вниз
К условию
312/319
// ...содержимое страницы выше...
function isVisible(elem) {
function showVisible() {
for (let img of document.querySelectorAll('img')) {
let realSrc = img.dataset.src;
if (!realSrc) continue;
if (isVisible(img)) {
img.src = realSrc;
img.dataset.src = '';
}
}
}
showVisible();
window.onscroll = showVisible;
К условию
313/319
Решение шаг за шагом:
<select id="genres">
<option value="rock">Рок</option>
<option value="blues" selected>Блюз</option>
</select>
<script>
// 1)
let selectedOption = genres.options[genres.selectedIndex];
alert( selectedOption.value );
alert( selectedOption.text );
// 2)
let newOption = new Option("Классика", "classic");
genres.append(newOption);
// 3)
newOption.selected = true;
</script>
К условию
Фокусировка: focus/blur
Редактируемый div
К условию
Редактирование TD по клику
314/319
К условию
К условию
Депозитный калькулятор
К условию
#cover-div {
position: fixed;
top: 0;
left: 0;
z-index: 9000;
width: 100%;
315/319
height: 100%;
background-color: gray;
opacity: 0.3;
}
Так как он перекрывает вообще всё, все клики будут именно по этому
<div> .
К условию
Алгоритм:
К условию
1. Создание промиса
2. Конец скрипта
316/319
3. Обработка промиса
4. Таймаут
К условию
Вывод в консоли: 1 7 3 5 2 6 4.
317/319
console.log(1);
// Первая строка выполняется сразу и выводит `1`.
// Очереди микрозадач и макрозадач на данный момент пусты.
console.log(7);
// Тут же выводит `7`.
Получается вывод 1 7 3 5 2 6 4 .
318/319
К условию
319/319