This commit is contained in:
Ilya Kantor 2014-11-16 01:40:20 +03:00
parent 962caebbb7
commit 87bf53d076
1825 changed files with 94929 additions and 0 deletions

View file

@ -0,0 +1,45 @@
# Окружение: DOM, BOM и JS
Сам по себе язык JavaScript не предусматривает работы с браузером, он вообще не знает про HTML.
Но в браузере есть ряд специальных объектов, которые образуют Document Object Model (DOM) и дают доступ к документу, а также объекты Browser Object Model (BOM), которые позволяют использовать различные возможности браузера, такие как коммуникация с сервером, открытие новых окон и т.п.
[cut]
На рисунке ниже схематически отображена структура, которая получается если посмотреть на совокупность браузерных объектов с "высоты птичьего полёта".
<img src="windowObjects.png">
Как видно из рисунка, на вершине стоит `window`, который играет роль *глобального объекта*, но вместе с этим даёт доступ к функционалу по управлению окном браузера, у него есть методы `window.focus()`, `window.open()` и другие.
Все остальные объекты делятся на 3 группы.
## Объектная модель документа (DOM)
Глобальный объект `document` даёт возможность взаимодействовать с содержимым страницы.
Он и громадное количество его свойств, методов и связанных с ним интерфейсов описаны в [стандарте W3C DOM](http://www.w3.org/DOM/DOMTR).
По историческим причинам когда-то появилась первая версия стандарта DOM Level 1, затем придумали ещё свойства и методы, и появился DOM Level 2, на текущий момент поверх них добавили ещё DOM Level 3 и готовится DOM 4.
Современные браузеры также поддерживают некоторые возможности, которые не вошли в стандарты, но де-факто существуют давным-давно и отказываться от них никто не хочет. Их условно называют "DOM Level 0".
Также информацию по работе с элементами страницы можно найти в стандарте [HTML 5](http://www.w3.org/TR/html5/Overview.html).
## Объектная модель браузера (BOM)
BOM -- это объекты для работы с чем угодно, кроме документа.
Например:
<ul>
<li>Объект [navigator](https://developer.mozilla.org/en/DOM/window.navigator) содержит общую информацию о браузере и операционной системе. Особенно примечательны два свойства: `navigator.userAgent` -- содержит информацию о браузере и `navigator.platform` -- содержит информацию о платформе, позволяет различать Windows/Linux/Mac и т.п.</li>
<li>Объект [location](https://developer.mozilla.org/en-US/docs/Web/API/Window.location) содержит информацию о текущем URL страницы и позволяет перенаправить посетителя на новый URL.</li>
<li>Функции `alert/confirm/prompt` -- тоже входят в BOM.
</li>
</ul>
Большинство возможностей BOM стандартизированы в [HTML 5](http://www.w3.org/TR/html5/Overview.html), но браузеры любят изобрести что-нибудь своё, особенное.
## Объекты и функции JavaScript
JavaScript -- объекты и методы языка JavaScript, который даёт возможность управлять всем этим. Именно их описывает стандарт EcmaScript.
Далее в этом курсе мы будем, преимущественно, изучать DOM, поскольку именно документ занимает центральную роль в организации интерфейса, и работа с ним -- сложнее всего.

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

View file

@ -0,0 +1,209 @@
# Методы contains и compareDocumentPosition
Если есть два элемента, то иногда бывает нужно понять, находится ли один в другом, и произвести обработку в зависимости от результата.
Обычные поисковые методы здесь не дают ответа, но есть два специальных. Они используются редко, но когда подобная задача встаёт, то знание метода может сэкономить много строк кода.
[cut]
## Метод contains
Синтаксис:
```js
var result = parent.contains(child);
```
Возвращает `true`, если `parent` содержит `child` или `parent == child`.
## Метод compareDocumentPosition
Бывает, что у нас есть два элемента, к примеру, `<li>` в списке, и нужно понять, какой из них выше другого.
Это поможет сделать другой метод.
Синтаксис:
```js
var result = nodeA.compareDocumentPosition(nodeB);
```
Возвращаемое значение -- битовая маска (см. [](/bitwise-operators)), биты в которой означают следующее:
<table>
<tr>
<th>Биты</th>
<th>Число</th>
<th>Значение</th>
</tr>
<tr><td>000000</td><td>0</td><td>`nodeA` и `nodeB` -- один и тот же узел</td></tr>
<tr><td>000001</td><td>1</td><td>Узлы в разных документах (или один из них не в документе)</td></tr>
<tr><td>000010</td><td>2</td><td>`nodeB` предшествует `nodeA` (в порядке обхода документа)</td></tr>
<tr><td>000100</td><td>4</td><td>`nodeA` предшествует `nodeB`</td></tr>
<tr><td>001000</td><td>8</td><td>`nodeB` содержит `nodeA`</td></tr>
<tr><td>010000</td><td>16</td><td>`nodeA` содержит `nodeB`</td></tr>
<tr><td>100000</td><td>32</td><td>Зарезервировано для браузера</td></tr>
</table>
Понятие "предшествует" -- означает не только "предыдущий сосед при общем родителе", но и имеет более общий смысл: "раньше встречается в порядке [прямого обхода](http://algolist.manual.ru/ds/walk.php) дерева документа.
Могут быть и сочетания битов. Примеры реальных значений:
```html
<!--+ run -->
<p>...</p>
<ul>
<li>1.1</li>
</ul>
<p>...</p>
<script>
var ul = document.getElementsByTagName('ul')[0];
// 1. соседи
alert( ul.compareDocumentPosition( ul.previousSibling ) ); // 2 = 10
alert( ul.compareDocumentPosition( ul.nextSibling ) ); // 4 = 100
// 2. родитель/потомок
alert( ul.compareDocumentPosition( ul.firstChild ) ); // 20 = 10100
alert( ul.compareDocumentPosition( ul.parentNode ) ); // 10 = 1010
// 3. вообще разные узлы
var li = ul.children[0];
alert( li.compareDocumentPosition( document.body.lastChild ) ); // 4 = 100
</script>
```
Комментарии:
<ol>
<li>Узлы являются соседями, поэтому стоит только бит "предшествования": какой перед каким.</li>
<li>Здесь стоят сразу два бита: `10100` означает, что `ul` одновременно содержит `ul.firstChild` и является его предшественником, то есть при прямом обходе дерева документа сначала встречается `nodeA`, а потом `nodeB`.
Аналогично, `1010` означает, что `ul.parentNode` содержит `ul` и предшествует ему.</li>
<li>Так как ни один из узлов не является предком друг друга, то стоит только бит предшествования: `li` предшествует последнему узлу документа, никакого сюрприза здесь нет.</li>
</ol>
[smart header="Перевод в двоичную систему"]
Самый простой способ самостоятельно посмотреть, как число выглядит в 2-ной системе -- вызвать для него `toString(2)`, например:
```js
//+ run
var x = 20;
alert( x.toString(2) ); // "10100"
```
Или так:
```js
//+ run
alert( 20..toString(2) );
```
Здесь после `20` две точки, так как если одна, то JS подумает, что после неё десятичная часть -- будет ошибка.
[/smart]
Проверка условия "`nodeA` содержит `nodeB`" с использованием битовых операций: `nodeA.compareDocumentPosition(nodeB) & 16`, например:
```html
<!--+ run -->
<ul>
<li>1</li>
</ul>
<script>
var nodeA = document.body;
var nodeB = document.body.children[0].children[0];
*!*
if( nodeA.compareDocumentPosition(nodeB) & 16 ) {
alert( nodeA +' содержит ' + nodeB );
}
*/!*
</script>
```
Более подробно о битовых масках: [](/bitwise-operators).
## Поддержка в IE8-
В IE8- поддерживаются свои, нестандартные, метод и свойство:
<dl>
<dt>[nodeA.contains(nodeB)](http://msdn.microsoft.com/en-us/library/ms536377.aspx)</dt>
<dd>Результат: `true`, если `nodeA` содержит `nodeB`, а также в том случае, если `nodeA == nodeB`.</dd>
<dt>[node.sourceIndex](http://msdn.microsoft.com/en-us/library/ms534635.aspx)</dt>
<dd>Номер элемента `node` в порядке прямого обхода дерева. Только для узлов-элементов.</dd>
</dl>
На их основе можно написать кросс-браузерную реализацию `compareDocumentPosition`:
```js
// Адаптировано с http://ejohn.org/blog/comparing-document-position/
function compareDocumentPosition(a, b) {
return a.compareDocumentPosition ?
a.compareDocumentPosition(b) :
(a != b && a.contains(b) && 16) +
(a != b && b.contains(a) && 8) +
(a.sourceIndex >= 0 && b.sourceIndex >= 0 ?
(a.sourceIndex < b.sourceIndex && 4) +
(a.sourceIndex > b.sourceIndex && 2) :
1);
}
```
Эта функция будет работать для узлов-элементов во всех браузерах.
## Итого
Для проверки, лежит ли один узел внутри другого, достаточно метода `nodeA.contains(nodeB)`.
Для расширенной проверки на предшествование есть метод `compareDocumentPosition`, кросс-браузерный вариант которого приведён выше.
Пример использования:
```html
<!--+ run -->
<ul>
<li id="li1">1</li>
<li id="li2">2</li>
</ul>
<script>
var body = document.body;
*!*
if( compareDocumentPosition(body, li1) & 16 ) {
alert( 'BODY содержит LI-1' );
}
if( compareDocumentPosition(li1, li2) & 4 ) {
alert( 'LI-1 предшествует LI-2' );
}
*/!*
function compareDocumentPosition(a, b) {
return a.compareDocumentPosition ?
a.compareDocumentPosition(b) :
(a != b && a.contains(b) && 16) +
(a != b && b.contains(a) && 8) +
(a.sourceIndex >= 0 && b.sourceIndex >= 0 ?
(a.sourceIndex < b.sourceIndex && 4) +
(a.sourceIndex > b.sourceIndex && 2) :
1);
}
</script>
```
Список битовых масок для проверки:
<table class="bordered">
<tr>
<th>Биты</th>
<th>Число</th>
<th>Значение</th>
</tr>
<tr><td>000000</td><td>0</td><td>`nodeA` и `nodeB` -- один и тот же узел</td></tr>
<tr><td>000001</td><td>1</td><td>Узлы в разных документах (или один из них не в документе)</td></tr>
<tr><td>000010</td><td>2</td><td>`nodeB` предшествует `nodeA` (в порядке обхода документа)</td></tr>
<tr><td>000100</td><td>4</td><td>`nodeA` предшествует `nodeB`</td></tr>
<tr><td>001000</td><td>8</td><td>`nodeB` содержит `nodeA`</td></tr>
<tr><td>010000</td><td>16</td><td>`nodeA` содержит `nodeB`</td></tr>
</table>

View file

@ -0,0 +1,31 @@
Результат выполнения может быть разный: `innerHTML` вставит именно HTML, а `createTextNode` интерпретирует теги как текст.
Запустите следующие примеры, чтобы увидеть разницу:
<ul>
<li>`createTextNode` создает текст <code>'&lt;b&gt;текст&lt;/b&gt;'</code>:
```html
<!--+ run height=50 -->
<div id="elem"></div>
<script>
var text = '<b>текст</b>';
elem.appendChild(document.createTextNode(text));
</script>
```
</li>
<li>`innerHTML` присваивает HTML <code>&lt;b&gt;текст&lt;/b&gt;</code>:
```html
<!--+ run height=50 -->
<div id="elem"></div>
<script>
var text = '<b>текст</b>';
elem.innerHTML = text;
</script>
```
</li>
</ul>

View file

@ -0,0 +1,21 @@
# createTextNode vs innerHTML
[importance 5]
Есть *пустой* узел DOM `elem`.
**Одинаковый ли результат дадут эти скрипты?**
Первый:
```js
elem.appendChild(document.createTextNode(text));
```
Второй:
```js
elem.innerHTML = text;
```
Если нет -- дайте пример значения `text`, для которого результат разный.

View file

@ -0,0 +1,7 @@
Общее решение описано в [аналогичной задаче с setInterval](/task/clock-setinterval).
Способ через `setTimeout` -- по сути, такой же, только функция `update` каждый раз ставит себя в очередь заново.
Заметим, что в данном случае целесообразнее использовать `setInterval`, т.к. нужна не задержка между запусками, а просто запуск каждую секунду.
[edit src="solution"]Открыть решение в песочнице[/edit]

View file

@ -0,0 +1,58 @@
<!DOCTYPE HTML>
<html>
<head><meta charset="utf-8">
<style>
.hour { color: red }
.min { color: green }
.sec { color: blue }
</style>
</head>
<body>
<div id="clock">
<span class="hour">hh</span>:<span class="min">mm</span>:<span class="sec">ss</span>
</div>
<script>
var timerId;
function update() {
var clock = document.getElementById('clock');
var date = new Date();
var hours = date.getHours()
if (hours < 10) hours = '0'+hours;
clock.children[0].innerHTML = hours;
var minutes = date.getMinutes();
if (minutes < 10) minutes = '0'+minutes;
clock.children[1].innerHTML = minutes;
var seconds = date.getSeconds();
if (seconds < 10) seconds = '0'+seconds;
clock.children[2].innerHTML = seconds;
timerId = setTimeout(update, 1000);
}
function clockStart() {
if (timerId) return;
update();
}
function clockStop() {
clearTimeout(timerId);
timerId = null;
}
</script>
<input type="button" onclick="clockStart()" value="Старт">
<input type="button" onclick="clockStop()" value="Стоп">
</body>
</html>

View file

@ -0,0 +1,10 @@
<!DOCTYPE HTML>
<html>
<body>
<input type="button" onclick="clockStart()" value="Start">
<input type="button" onclick="clockStop()" value="Stop">
</body>
</html>

View file

@ -0,0 +1,10 @@
# Часики при помощи "setTimeout"
[importance 3]
Создайте цветные часы, **используя `setTimeout` вместо `setInterval`**:
[iframe src="solution"]
[edit src="source" task/]

View file

@ -0,0 +1,16 @@
Родителя `parentNode` можно получить из `elem`.
Нужно учесть два момента.
<ol>
<li>Родителя может не быть (элемент уже удален или еще не вставлен).</li>
<li>Для совместимости со стандартным методом нужно вернуть удаленный элемент.</li>
</ol>
Вот так выглядит решение:
```js
function remove(elem) {
return elem.parentNode ? elem.parentNode.removeChild(elem) : elem;
}
```

View file

@ -0,0 +1,23 @@
# Удаление элементов
[importance 5]
Напишите функцию, которая удаляет элемент из DOM.
Синтаксис должен быть таким: `remove(elem)`, то есть, в отличие от `parentNode.removeChild(elem)` -- без родительского элемента.
```html
<div>Это</div>
<div>Все</div>
<div>Элементы DOM</div>
<script>
var elem = document.body.children[0];
function remove(elem) { /* ваш код */ }
*!*
remove(elem); // <-- функция должна удалить элемент
*/!*
</script>
```

View file

@ -0,0 +1,29 @@
Для того, чтобы добавить элемент *после* `refElem`, мы можем вставить его *перед* `refElem.nextSibling`.
Но что если `nextSibling` нет? Это означает, что `refElem` является последним потомком своего родителя и можем использовать `appendChild`.
Код:
```js
function insertAfter(elem, refElem) {
var parent = refElem.parentNode;
var next = refElem.nextSibling;
if (next) {
return parent.insertBefore(elem, next);
} else {
return parent.appendChild(elem);
}
}
```
Но код может быть гораздо короче, если использовать фишку со вторым аргументом null метода `insertBefore`:
```js
function insertAfter(elem, refElem) {
return refElem.parentNode.insertBefore(elem, refElem.nextSibling);
}
```
Если нет `nextSibling`, то второй аргумент `insertBefore` становится `null` и тогда `insertBefore(elem,null)` работает как `appendChild`.
В решении нет проверки на существование `refElem.parentNode`, поскольку вставка после элемента без родителя -- уже ошибка, пусть она возникнет в функции, это нормально.

View file

@ -0,0 +1,27 @@
# insertAfter
[importance 5]
Напишите функцию `insertAfter(elem, refElem)`, которая добавит `elem` после узла `refElem`.
```html
<div>Это</div>
<div>Элементы</div>
<script>
var elem = document.createElement('div');
elem.innerHTML = '<b>Новый элемент</b>';
function insertAfter(elem, refElem) { /* ваш код */ }
var body = document.body;
// вставить elem после первого элемента
insertAfter(elem, body.firstChild); // <--- должно работать
// вставить elem за последним элементом
insertAfter(elem, body.lastChild); // <--- должно работать
</script>
```

View file

@ -0,0 +1,56 @@
# Неправильное решение
Для начала рассмотрим забавный пример того, как делать *не надо*:
```js
function removeChildren(elem) {
for(var k=0; k<elem.childNodes.length;k++) {
elem.removeChild(elem.childNodes[k]);
}
}
```
Если вы попробуете это на практике, то увидите, то это не сработает.
Не сработает потому, что `childNodes` всегда начинается 0 и автоматически смещается, когда первый потомок удален(т.е. тот, что был вторым, станет первым), поэтому такой цикл по `k` пропустит половину узлов.
# Решение через DOM
Правильное решение:
```js
function removeChildren(elem) {
while(elem.lastChild) {
elem.removeChild(elem.lastChild);
}
}
```
# Неправильное решение (innerHTML)
Прямая попытка использовать `innerHTML` была бы неправильной:
```js
function removeChildren(elem) {
elem.innerHTML = '';
}
```
Дело в том, что в IE8- свойство `innerHTML` на большинстве табличных элементов (кроме ячеек `TH/TD`) не работает. Будет ошибка.
# Верное решение (innerHTML)
Можно завернуть `innerHTML` в `try/catch`:
```js
function removeChildren(elem) {
try {
elem.innerHTML = '';
} catch(e) {
while(elem.firstChild) {
elem.removeChild(elem.firstChild);
}
}
}
```

View file

@ -0,0 +1,29 @@
# removeChildren
[importance 5]
Напишите функцию `removeChildren`, которая удаляет всех потомков элемента.
```html
<table id="table">
<tr>
<td>Это</td><td>Все</td><td>Элементы DOM</td>
</tr>
</table>
<ol id="ol">
<li>Вася</li>
<li>Петя</li>
<li>Маша</li>
<li>Даша</li>
</ol>
<script>
function removeChildren(elem) { /* ваш код */ }
removeChildren(table); // очищает таблицу
removeChildren(ol); // очищает список
</script>
```
P.S. Проверьте ваше решение в IE8.

View file

@ -0,0 +1,5 @@
HTML в задаче некорректен. В этом всё дело. И вопрос легко решится, если открыть отладчик.
В нём видно, что браузер поместил текст `aaa` *перед* таблицей. Поэтому он и остался в документе.
Вообще, в стандарте HTML5 описано, как браузеру обрабатывать некорректный HTML, так что такое действие браузера является правильным.

View file

@ -0,0 +1,25 @@
# Почему остаётся "ааа" ?
[importance 1]
Запустите этот пример. Почему вызов `removeChild` не удалил текст `"aaa"`?
```html
<!--+ height=100 run -->
<table>
aaa
<tr>
<td>Test</td>
</tr>
</table>
<script>
var table = document.body.children[0];
alert(table); // таблица, пока всё правильно
document.body.removeChild( table );
// почему в документе остался текст?
</script>
```

View file

@ -0,0 +1,6 @@
Делайте проверку на `null` в цикле. `prompt` возвращает это значение только если был нажат ESC.
Контент в `LI` добавляйте с помощью `document.createTextNode`, чтобы правильно работали &lt;, &gt; и т.д.
[edit src="solution"]Решение в песочнице[/edit]

View file

@ -0,0 +1,27 @@
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
</head>
<body>
<h1>Создание списка</h1>
<script>
var ul = document.createElement('ul');
document.body.appendChild(ul);
while (true) {
var data = prompt("Введите текст для пункта списка", "");
if (data === null) {
break;
}
var li = document.createElement('li');
li.appendChild(document.createTextNode(data));
ul.appendChild(li);
}
</script>
</body>
</html>

View file

@ -0,0 +1,20 @@
# Создать список
[importance 4]
Напишите интерфейс для создания списка.
Для каждого пункта:
<ol>
<li>Запрашивайте содержимое пункта у пользователя с помощью `prompt`.</li>
<li>Создавайте пункт и добавляйте его к `UL`.</li>
<li>Процесс прерывается, когда пользователь нажимает ESC.</li>
</ol>
**Все элементы должны создаваться динамически.**
Если посетитель вводит теги -- в списке они показываются как обычный текст.
[demo src="solution"]
P.S. `prompt` возвращает `null`, если пользователь нажал ESC.

View file

@ -0,0 +1,67 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
</head>
<body>
<div id="container"></div>
<script>
var data = {
"Рыбы":{
"Форель":{},
"Щука":{}
},
"Деревья":{
"Хвойные":{
"Лиственница":{},
"Ель":{}
},
"Цветковые":{
"Берёза":{},
"Тополь":{}
}
}
};
function createTree(container, obj) {
container.appendChild( createTreeDom(obj) );
}
function createTreeDom(obj) {
// если нет детей, то рекурсивный вызов ничего не возвращает
// так что вложенный UL не будет создан
if (isObjectEmpty(obj)) return;
var ul = document.createElement('ul');
for (var key in obj) {
var li = document.createElement('li');
li.innerHTML = key;
var childrenUl = createTreeDom(obj[key]);
if (childrenUl) li.appendChild(childrenUl);
ul.appendChild(li);
}
return ul;
}
function isObjectEmpty(obj) {
for (var key in obj) {
return false;
}
return true;
}
var container = document.getElementById('container');
createTree(container, data);
</script>
</body>
</html>

View file

@ -0,0 +1,6 @@
Решения через рекурсию.
<ol>
<li>[edit src="solution"]Через innerHTML[/edit].</li>
<li>[edit src="build-tree-dom"]Через DOM[/edit].</li>
</ol>

View file

@ -0,0 +1,49 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
</head>
<body>
<div id="container"></div>
<script>
var data = {
"Рыбы":{
"Форель":{},
"Щука":{}
},
"Деревья":{
"Хвойные":{
"Лиственница":{},
"Ель":{}
},
"Цветковые":{
"Берёза":{},
"Тополь":{}
}
}
};
function createTree(container, obj) {
container.innerHTML = createTreeText(obj);
}
function createTreeText(obj) { // отдельная рекурсивная функция
var li = '';
for (var key in obj) {
li += '<li>' + key + createTreeText(obj[key]) + '</li>';
}
if (li) {
var ul = '<ul>' + li + '</ul>'
}
return ul || '';
}
var container = document.getElementById('container');
createTree(container, data);
</script>
</body>
</html>

View file

@ -0,0 +1,64 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
</head>
<body>
<div id="tree"></div>
<!-- Результат должен быть таким:
<div id="tree">
<ul>
<li>Рыбы
<ul>
<li>Форель</li>
<li>Щука</li>
</ul>
</li>
<li>Деревья
<ul>
<li>Хвойные
<ul>
<li>Лиственница</li>
<li>Ель</li>
</ul>
</li>
<li>Цветковые
<ul>
<li>Берёза</li>
<li>Тополь</li>
</ul>
</li>
</ul>
</li>
</ul>
</div>
-->
<script>
var data = {
"Рыбы":{
"Форель":{},
"Щука":{}
},
"Деревья":{
"Хвойные":{
"Лиственница":{},
"Ель":{}
},
"Цветковые":{
"Берёза":{},
"Тополь":{}
}
}
};
/* ваш код */
createTree(document.getElementById('tree'), data);
</script>
</body>
</html>

View file

@ -0,0 +1,52 @@
# Создайте дерево из объекта
[importance 5]
Напишите функцию, которая создаёт вложенный список `UL/LI` (дерево) из объекта.
Например:
```js
var data = {
"Рыбы":{
"Форель":{},
"Щука":{}
},
"Деревья":{
"Хвойные":{
"Лиственница":{},
"Ель":{}
},
"Цветковые":{
"Берёза":{},
"Тополь":{}
}
}
};
```
Синтаксис:
```js
var container = document.getElementById('container');
*!*
createTree(container, data); // создаёт
*/!*
```
Результат (дерево):
[iframe border=1 src="solution"]
Выберите один из двух способов решения этой задачи:
<ol>
<li>Создать строку, а затем присвоить через `container.innerHTML`.</li>
<li>Создавать узлы через методы DOM.</li>
</ol>
Если получится -- сделайте оба.
[edit src="source" task/]
P.S. Желательно, чтобы в дереве не было лишних элементов, в частности -- пустых `<ul></ul>` на нижнем уровне.

View file

@ -0,0 +1,11 @@
Для решения задачи сгенерируем таблицу в виде строки: `"<table>...</table>"`, а затем присвоим в `innerHTML`.
Алгоритм:
<ol>
<li>Создать объект даты `d = new Date(year, month-1)`. Это первый день месяца `month` (с учетом того, что месяцы в JS начинаются от 0, а не от 1).</li>
<li>Ячейки первого ряда пустые от начала и до дня недели `d.getDay()`, с которого начинается месяц. Создадим их.</li>
<li>Увеличиваем день в `d` на единицу: `d.setDate(d.getDate()+1)`, и добавляем в календарь очередную ячейку, пока не достигли следующего месяца. При этом последний день недели означает вставку перевода строки <code>"&lt;/tr&gt;&lt;tr&gt;"</code>.</li>
<li>При необходимости, если календарь окончился не на воскресенье - добавить пустые `TD` в таблицу, чтобы было все ровно.</li>
</ol>
[edit src="solution"]Открыть полное решение[/edit]

View file

@ -0,0 +1,80 @@
<!DOCTYPE HTML>
<html>
<head>
<style>
table {
border-collapse: collapse;
}
td, th {
border: 1px solid black;
padding: 3px;
text-align: center;
}
th {
font-weight: bold;
background-color: #E6E6E6;
}
</style>
<meta charset="utf-8">
</head>
<body>
<div id="calendar"></div>
<script>
function createCalendar(id, year, month) {
var elem = document.getElementById(id);
var mon = month - 1; // месяцы в JS идут от 0 до 11, а не от 1 до 12
var d = new Date(year, mon);
var table = '<table><tr><th>пн</th><th>вт</th><th>ср</th><th>чт</th><th>пт</th><th>сб</th><th>вс</th></tr><tr>';
// заполнить первый ряд от понедельника
// и до дня, с которого начинается месяц
// * * * | 1 2 3 4
for (var i=0; i<getDay(d); i++) {
table += '<td></td>';
}
// ячейки календаря с датами
while(d.getMonth() == mon) {
table += '<td>'+d.getDate()+'</td>';
if (getDay(d) % 7 == 6) { // вс, последний день - перевод строки
table += '</tr><tr>';
}
d.setDate(d.getDate()+1);
}
// добить таблицу пустыми ячейками, если нужно
if (getDay(d) != 0) {
for (var i=getDay(d); i<7; i++) {
table += '<td></td>';
}
}
// закрыть таблицу
table += '</tr></table>';
// только одно присваивание innerHTML
elem.innerHTML = table;
}
function getDay(date) { // получить номер дня недели, от 0(пн) до 6(вс)
var day = date.getDay();
if (day == 0) day = 7;
return day - 1;
}
createCalendar("calendar", 2012, 9)
</script>
</body>
</html>

View file

@ -0,0 +1,39 @@
<!DOCTYPE HTML>
<html>
<head>
<style>
table {
border-collapse: collapse;
}
td, th {
border: 1px solid black;
padding: 3px;
text-align: center;
}
th {
font-weight: bold;
background-color: #E6E6E6;
}
</style>
<meta charset="utf-8">
</head>
<body>
<div id="calendar"></div>
<script>
function createCalendar(id, year, month) {
var elem = document.getElementById(id)
// ... ваш код, который генерирует в elem календарь
}
createCalendar('calendar', 2011, 1)
</script>
</body>
</html>

View file

@ -0,0 +1,19 @@
# Создать календарь в виде таблицы
[importance 4]
Напишите функцию, которая умеет генерировать календарь для заданной пары (месяц, год).
Календарь должен быть таблицей, где каждый день -- это `TD`. У таблицы должен быть заголовок с названиями дней недели, каждый день -- `TH`.
Синтаксис: `createCalendar(id, year, month)`.
Такой вызов должен генерировать текст для календаря месяца `month` в году `year`, а затем помещать его внутрь элемента с указанным `id`.
Например: `createCalendar("cal", 2012, 9)` сгенерирует в <code>&lt;div id='cal'&gt;&lt;/div&gt;</code> следующий календарь:
[iframe height=210 src="solution"]
P.S. Достаточно сгенерировать календарь, кликабельным его делать не нужно.
[edit src="source" task/]

View file

@ -0,0 +1,59 @@
Для начала, придумаем подходящую HTML/CSS-структуру.
Здесь каждый компонент времени удобно поместить в соответствующий `SPAN`:
```html
<div id="clock">
<span class="hour">hh</span>:<span class="min">mm</span>:<span class="sec">ss</span>
</div>
```
Каждый `SPAN` раскрашивается при помощи CSS.
Жизнь часам будет обеспечивать функция `update`, вызываемая каждую секунду: `setInterval(update, 1000)`.
```js
var timerId; // таймер, если часы запущены
function clockStart() { // запустить часы
if (timerId) return;
timerId = setInterval(update, 1000);
update(); // (*)
}
function clockStop() {
clearInterval(timerId);
timerId = null;
}
```
Обратите внимание, что вызов `update` не только запланирован, но и производится тут же в строке `(*)`. Иначе посетителю пришлось бы ждать до первого выполнения `setInterval`.
Функция обновления часов:
```js
function update() {
var clock = document.getElementById('clock');
*!*
var date = new Date(); // (*)
*/!*
var hours = date.getHours();
if (hours < 10) hours = '0'+hours;
clock.children[0].innerHTML = hours;
var minutes = date.getMinutes();
if (minutes < 10) minutes = '0'+minutes;
clock.children[1].innerHTML = minutes;
var seconds = date.getSeconds();
if (seconds < 10) seconds = '0'+seconds;
clock.children[2].innerHTML = seconds;
}
```
В строке `(*)` каждый раз мы получаем текущую дату. Мы должны это сделать, несмотря на то, что, казалось бы, могли бы просто увеличивать счетчик каждую секунду.
На самом деле мы не можем опираться на счетчик для вычисления даты, т.к. `setInterval` не гарантирует точную задержку. Если в другом участке кода будет вызван `alert`, то часы остановятся, как и любые счетчики.
[edit src="solution"]Полный код решения[/edit]

View file

@ -0,0 +1,59 @@
<!DOCTYPE HTML>
<html>
<head><meta charset="utf-8">
<style>
.hour { color: red }
.min { color: green }
.sec { color: blue }
</style>
</head>
<body>
<div id="clock">
<span class="hour">hh</span>:<span class="min">mm</span>:<span class="sec">ss</span>
</div>
<script>
var timerId;
function update() {
var clock = document.getElementById('clock');
var date = new Date();
var hours = date.getHours();
if (hours < 10) hours = '0'+hours;
clock.children[0].innerHTML = hours;
var minutes = date.getMinutes();
if (minutes < 10) minutes = '0'+minutes;
clock.children[1].innerHTML = minutes;
var seconds = date.getSeconds();
if (seconds < 10) seconds = '0'+seconds;
clock.children[2].innerHTML = seconds;
}
function clockStart() {
if (timerId) return;
timerId = setInterval(update, 1000);
update(); // <-- начать тут же, не ждать 1 секунду пока setInterval сработает
}
function clockStop() {
clearInterval(timerId);
timerId = null;
}
</script>
<input type="button" onclick="clockStart()" value="Старт">
<input type="button" onclick="clockStop()" value="Стоп">
</body>
</html>

View file

@ -0,0 +1,11 @@
<!DOCTYPE HTML>
<html>
<body>
<input type="button" onclick="clockStart()" value="Start">
<input type="button" onclick="clockStop()" value="Stop">
</body>
</html>

View file

@ -0,0 +1,9 @@
# Часики с использованием "setInterval"
[importance 4]
Создайте цветные часики как в примере ниже, **используя `setInterval`**:
[iframe src="solution"]
[edit src="source" task/]

View file

@ -0,0 +1,341 @@
# Добавление и удаление узлов
Изменение DOM -- ключ к созданию "живых" страниц.
В этой главе мы рассмотрим, как создавать новые элементы "на лету" и заполнять их данными.
[cut]
## Пример: показ сообщения
В качестве примера рассмотрим добавление сообщения на страницу, чтобы оно было оформленно красивее чем обычный `alert`.
HTML-код для сообщения (с подключённой библиотекой стилей [bootstrap](http://getbootstrap.com/)):
```html
<!--+ autorun height="100" -->
<link rel="stylesheet" href="http://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.1.1/css/bootstrap.css">
<div class="alert alert-success">
<strong>Ура!</strong> Вы прочитали это важное сообщение.
</div>
```
## Создание элемента
Для создания элементов используются следующие методы:
<dl>
<dt>`document.createElement(tag)`</dt>
<dd>Создает новый элемент с указанным тегом:
```js
var div = document.createElement('div');
```
</dd>
<dt>`document.createTextNode(text)`</dt>
<dd>Создает новый *текстовый* узел с данным текстом:
```js
var textElem = document.createTextNode('Тут был я');
```
</dl>
### Создание сообщения
В нашем случае мы хотим сделать DOM-элемент `div`, дать ему классы и заполнить текстом:
```js
var div = document.createElement('div');
div.className = "alert alert-success";
div.innerHTML = "<strong>Ура!</strong> Вы прочитали это важное сообщение.";
```
После этого кода у нас есть готовый DOM-элемент. Пока что он присвоен в переменную `div`, но не виден, так как никак не связан со страницей.
## Добавление элемента: appendChild, insertBefore
Чтобы DOM-узел был показан на странице, его необходимо вставить в `document`.
Для этого первым делом нужно решить, куда мы будем его вставлять. Предположим, что мы решили, что вставлять будем в некий элемент `parentElem`, например `var parentElem = document.body`.
Для вставки внутрь `parentElem` есть следующие методы:
<dl>
<dt>`parentElem.appendChild(elem)`</dt>
<dd>Добавляет `elem` в конец дочерних элементов `parentElem`.
Следующий пример добавляет новый элемент в конец `<ol>`:
```html
<!--+ run height=100 -->
<ol id="list">
<li>0</li>
<li>1</li>
<li>2</li>
</ol>
<script>
var newLi = document.createElement('li');
newLi.innerHTML = 'Привет, мир!';
list.appendChild(newLi);
</script>
```
</dd>
<dt>`parentElem.insertBefore(elem, nextSibling)`</dt>
<dd>Вставляет `elem` в список дочерних `parentElem` перед элементом `nextSibling`.
Следующий код вставляет новый элемент перед вторым `<li>`:
```html
<!--+ run height=100 -->
<ol id="list">
<li>0</li>
<li>1</li>
<li>2</li>
</ol>
<script>
var newLi = document.createElement('li');
newLi.innerHTML = 'Привет, мир!';
*!*
list.insertBefore(newLi, list.children[1]);
*/!*
</script>
```
**Этот же метод используется для вставки в начало элемента.**
Достаточно указать, что вставлять будем перед первым потомком:
```js
list.insertBefore(newLi, list.firstChild);
```
У человека, который посмотрит на этот код внимательно, наверняка возникнет вопрос: "А что, если `list` вообще пустой, в этом случае ведь `list.firstChild = null`, произойдёт ли вставка?"
Ответ -- да, произойдёт.
**Дело в том, что если в качестве `nextSibling` указать `null`, то `insertBefore` сработает как `appendChild`:**
```js
parentElem.insertBefore(elem, null);
// то же, что и:
parentElem.appendChild(elem)
```
</dd>
</dl>
Все методы вставки возвращают вставленный узел, например `parentElem.appendChild(elem)` возвращает `elem`.
### Добавление сообщения
Добавим сообщение в конец `<body>`:
```html
<!--+ height=150 run autorun -->
<link rel="stylesheet" href="http://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.1.1/css/bootstrap.css">
<body>
<h3>Моя страница</h3>
</body>
<script>
var div = document.createElement('div');
div.className = "alert alert-success";
div.innerHTML = "<strong>Ура!</strong> Вы прочитали это важное сообщение.";
*!*
document.body.appendChild(div);
*/!*
</script>
```
...А теперь -- в начало `<body>`:
```html
<!--+ height=150 run autorun -->
<link rel="stylesheet" href="http://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.1.1/css/bootstrap.css">
<body>
<h3>Моя страница</h3>
</body>
<script>
var div = document.createElement('div');
div.className = "alert alert-success";
div.innerHTML = "<strong>Ура!</strong> Вы прочитали это важное сообщение.";
*!*
document.body.insertBefore(div, document.body.firstChild);
*/!*
</script>
```
## Клонирование узлов: cloneNode
А как бы мне вставить второе такое же сообщение?
Конечно, можно сделать функцию для генерации сообщений и поместить туда этот код, но в ряде случаев гораздо эффективнее -- *клонировать* существующий `div`. В частности, если элемент большой, то клонировать его будет гораздо быстрее, чем пересоздавать.
Вызов `elem.cloneNode(true)` создаст "глубокую" копию элемента -- вместе с атрибутами, включая подэлементы. Если же вызвать с аргумнтом `false`, то он копия будет без подэлементов, но это нужно гораздо реже.
### Копия сообщения
Пример со вставкой копии сообщения:
```html
<!--+ height=200 run autorun -->
<link rel="stylesheet" href="http://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.1.1/css/bootstrap.css">
<body>
<h3>Моя страница</h3>
</body>
<script>
var div = document.createElement('div');
div.className = "alert alert-success";
div.innerHTML = "<strong>Ура!</strong> Вы прочитали это важное сообщение.";
document.body.insertBefore(div, document.body.firstChild);
*!*
// создать копию узла
var div2 = div.cloneNode(true);
// копию можно подправить
div2.querySelector('strong').innerHTML = 'Супер!';
// вставим её после текущего сообщения
div.parentNode.insertBefore( div2, div.nextSibling );
*/!*
</script>
```
Обратите внимание на последнюю строку, которая вставляет `div2` после `div`:
```js
div.parentNode.insertBefore( div2, div.nextSibling );
```
<ol>
<li>Для вставки нам нужен будущий родитель. Мы, возможно, не знаем, где точно находится `div` (или не хотим зависеть от того, где он), но если нужно вставить рядом с ним, то родителем определённо будет `div.parentNode`.</li>
<li>Вставляем в родителя. Мы хотели бы вставить после `div`, но метода `insertAfter` нет, есть только `insertBefore`, поэтому вставляем *перед* его правым соседом `div.nextSibling`.</li>
</ol>
## Удаление узлов: removeChild
Для удаления узла есть два метода:
<dl>
<dt>`parentElem.removeChild(elem)`</dt>
<dd>Удаляет `elem` из списка детей `parentElem`.</dd>
<dt>`parentElem.replaceChild(elem, currentElem)`</dt>
<dd>Среди детей `parentElem` заменяет `currentElem` на `elem`.</dd>
</dl>
Оба этих метода возвращают удаленный узел. Если нужно, его можно вставить в другое место DOM тут же или в будущем.
[smart]
Если вы хотите *переместить* элемент на новое место -- не нужно его удалять со старого.
**Все методы вставки автоматически удаляют вставляемый элемент со старого места.**
Конечно же, это очень удобно.
Например, поменяем элементы местами:
```html
<!--+ run height=150 -->
<div>Первый</div>
<div>Второй</div>
<script>
var first = document.body.children[0];
var last = document.body.children[1];
// нет необходимости в предварительном removeChild(last)
document.body.insertBefore(last, first); // поменять местами
</script>
```
[/smart]
### Удаление сообщения
Сделаем так, что через секунду сообщение пропадёт:
```html
<!--+ run -->
<link rel="stylesheet" href="http://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.1.1/css/bootstrap.css">
<body>
<h3>Сообщение пропадёт через секунду</h3>
</body>
<script>
var div = document.createElement('div');
div.className = "alert alert-success";
div.innerHTML = "<strong>Ура!</strong> Вы прочитали это важное сообщение.";
document.body.appendChild(div);
*!*
setTimeout(function() {
div.parentNode.removeChild(div);
}, 1000);
*/!*
</script>
```
## Пример использования текстовых узлов
При работе с сообщением мы использовали только узлы-элементы и `innerHTML`.
Но и текстовые узлы тоже имеют интересную область применения! Было бы несправедливо их обойти.
У них есть две особенности. Начнем с небольшого вопроса.
Ответили на вопрос выше? Даже если нет, то, поглядев в решение, вы легко увидите разницу.
Итак, отличий два:
<ol>
<li>При создании текстового узла `createTextNode('<b>...</b>')` любые специальные символы и теги в строке будут интерпретированы *как текст*. А `innerHTML` обработал бы их *как HTML*.</li>
<li>Во всех современных браузерах создание и вставка текстового узла работает гораздо быстрее, чем присвоение HTML.</li>
</ol>
## Итого
Методы для создания узлов:
<ul>
<li>`document.createElement(tag)` -- создает элемент</li>
<li>`document.createTextNode(value)` -- создает текстовый узел</li>
<li>`elem.cloneNode(deep)` -- клонирует элемент, если `deep == true`, то со всеми потомками, если `false` -- без потомков.</li>
</ul>
Вставка и удаление узлов:
<ul>
<li>`parent.appendChild(elem)`</li>
<li>`parent.insertBefore(elem, nextSibling)`</li>
<li>`parent.removeChild(elem)`</li>
<li>`parent.replaceChild(elem, currentElem)`</li>
</ul>
Все эти методы возвращают `elem`.
Запомнить порядок аргументов очень просто: **новый(вставляемый) элемент -- всегда первый.**
Методы для изменения DOM также описаны в спецификации <a href="http://www.w3.org/TR/REC-DOM-Level-1/level-one-core.html">DOM Level 1</a>.

View file

@ -0,0 +1,8 @@
Решение:
```js
var ul = document.body.children[0];
ul.insertAdjacentHTML("beforeEnd", "<li>3</li><li>4</li><li>5</li>");
```

View file

@ -0,0 +1,15 @@
# Вставьте элементы в конец списка
[importance 5]
Напишите код для вставки текста `html` в конец списка `ul` с использованием метода `insertAdjacentHTML`. Такая вставка, в отличие от присвоения `innerHTML+=`, не будет перезаписывать текущее содержимое.
Добавьте к списку ниже элементы `<li>3</li><li>4</li><li>5</li>`:
```html
<ul>
<li>1</li>
<li>2</li>
</ul>
```

View file

@ -0,0 +1,49 @@
# Подсказки
<ul>
<li>Проверить поддержку `insertAdjacentHTML` можно так:
```js
if (elem.insertAdjacentHTML) { ... }
```
</li>
<li>Если этот метод не поддерживается, то сделайте временный элемент, через `innerHTML` поставьте туда `html`, а затем переместите содержимое в `DocumentFragment`. Последнее действие -- вставка в документ.</li>
</ul>
# Решение
```html
<!--+ run -->
<ul>
<li>1</li>
<li>2</li>
<li>5</li>
</ul>
<script>
var ul = document.body.children[0];
var li5 = ul.children[2];
function insertBefore(elem, html) {
if (elem.insertAdjacentHTML) {
elem.insertAdjacentHTML("beforeBegin", html);
} else {
var fragment = document.createDocumentFragment();
var tmp = document.createElement('DIV');
tmp.innerHTML = html;
while(tmp.firstChild) {
// перенести все узлы во fragment
fragment.appendChild(tmp.firstChild);
}
elem.parentNode.insertBefore(fragment, elem);
}
}
insertBefore(li5, "<li>3</li><li>4</li>")
</script>
```

View file

@ -0,0 +1,34 @@
# Вставка insertAdjacentHTML/DocumentFragment
[importance 4]
Напишите кроссбраузерную функцию `insertBefore(elem, html)`, которая:
<ul>
<li>Вставляет HTML-строку `html` перед элементом `elem`, используя `insertAdjacentHTML`,</li>
<li>Если он не поддерживается (старый Firefox) -- то через `DocumentFragment`.</li>
</ul>
В обоих случаях должна быть лишь одна операция с DOM документа.
Следующий код должен вставить два пропущенных элемента списка `<li>3</li><li>4</li>`:
```html
<ul>
<li>1</li>
<li>2</li>
<li>5</li>
</ul>
<script>
var ul = document.body.children[0];
var li5 = ul.children[2];
function insertBefore(elem, html) {
/* ваш код */
}
insertBefore(li5, "<li>3</li><li>4</li>")
</script>
```

View file

@ -0,0 +1,11 @@
Для сортировки нам поможет функция `sort` массива.
Общая идея лежит на поверхности: сделать массив из строк и отсортировать его. Тонкости кроются в деталях.
В ифрейме ниже загружен документ, описывающий и реализующий разные алгоритмы. Обратите внимание: разница в производительности может достигать нескольких раз!
[iframe height=800 border=1 src="solution" link edit]
P.S. Создавать `DocumentFragment` здесь ни к чему. Можно вытащить из документа `TBODY` и иметь дело с ним в отрыве от DOM (алгоритм 4).
P.P.S. Если нужно сделать много узлов, то обычно `innerHTML` работает быстрее, чем генерация элементов через DOM-вызовы. Но в данном случае мы не создаём элементы, а сортируем и перевставляем готовые, так что результаты могут отличаться.

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,47 @@
# Отсортировать таблицу
[importance 5]
Есть таблица:
<table>
<tr>
<th>Имя</th>
<th>Фамилия</th>
<th>Отчество</th>
<th>Возраст</th>
</tr>
<tr>
<td>Вася</td>
<td>Петров</td>
<td>Александрович</td>
<td>10</td>
</tr>
<tr>
<td>Петя</td>
<td>Иванов</td>
<td>Петрович</td>
<td>15</td>
</tr>
<tr>
<td>Владимир</td>
<td>Ленин</td>
<td>Ильич</td>
<td>9</td>
</tr>
<tr>
<td>...</td>
<td>...</td>
<td>...</td>
<td>...</td>
</tr>
</table>
Строк в таблице много: может быть 20, 50, 100.. Есть и другие элементы в документе.
Как бы вы предложили отсортировать содержимое таблицы по полю `Возраст`? Обдумайте алгоритм, реализуйте его.
Как сделать, чтобы сортировка работала как можно быстрее? А если в таблице 10000 строк (бывает и такое)?
P.S. Может ли здесь помочь `DocumentFragment`?
P.P.S. Если предположить, что у нас заранее есть массив данных для таблицы в JavaScript -- что быстрее: отсортировать эту таблицу или сгенерировать новую?

View file

@ -0,0 +1,312 @@
# Мультивставка: insertAdjacentHTML и DocumentFragment
Обычные методы вставки работают с одним узлом. Но есть и способы вставлять множество узлов одновременно.
[cut]
## Оптимизация вставки в документ
Рассмотрим задачу: сгенерировать список `UL/LI`.
Есть две возможных последовательности:
<ol>
<li>Сначала вставить `UL` в документ, а потом добавить к нему `LI`:
```js
var ul = document.createElement('ul');
document.body.appendChild(ul); // сначала в документ
for(...) ul.appendChild(li); // потом узлы
```
</li>
<li>Полностью создать список "вне DOM", а потом -- вставить в документ:
```js
var ul = document.createElement('ul');
for(...) ul.appendChild(li); // сначала вставить узлы
document.body.appendChild(ul); // затем в документ
```
</li>
</ol>
Как ни странно, между этими последовательностями есть разница. В большинстве браузеров, второй вариант -- быстрее.
Почему же? Иногда говорят: "потому что браузер перерисовывает каждый раз при добавлении элемента". Это не так. **Дело вовсе не в перерисовке**.
Браузер достаточно "умён", чтобы ничего не перерисовывать понапрасну. В большинстве случаев процессы перерисовки и сопутствующие вычисления будут отложены до окончания работы скрипта, и на тот момент уже совершенно без разницы, в какой последовательности были изменены узлы.
**Тем не менее, при вставке узла происходят разные внутренние события и обновления внутренних структур данных, скрытые от наших глаз.**
Что именно происходит -- зависит от конкретной, внутренней браузерной реализации DOM, но это отнимает время. Конечно, браузеры развиваются и стараются свести лишние действия к минимуму.
### Бенчмарк [#insert-bench-tbody]
Чтобы легко проверить текущее состояние дел -- вот два бенчмарка.
Оба они создают таблицу 20x20, наполняя <code>TBODY</code> элементами <code>TR/TD</code>.
При этом первый вставляет все в документ тут же, второй -- задерживает вставку <code>TBODY</code> в документ до конца процесса.
Кликните, чтобы запустить.
<input type="button" onclick="alert(bench(appendFirst,50))" value="TBODY cразу в DOM"/> <input type="button" onclick="alert(bench(appendLast,50))" value="Отложенная вставка TBODY в DOM"/>
<table id="bench-table"></table>
```js
//+ hide="открыть код" src="insert-bench.js"
```
## Добавление множества узлов
Продолжим работать со вставкой узлов.
Рассмотрим случай, когда в документе *уже есть* большой список `UL`. И тут понадобилось срочно добавить еще 20 элементов `LI`.
Как это сделать?
Если новые элементы пришли в виде строки, то можно попробовать добавить их так:
```js
ul.innerHTML += "<li>1</li><li>2</li>...";
```
Но операция `+=` с `innerHTML` не работает с DOM. Она не прибавляет, а заменяет всё содержимое списка на дополненную строку. Это не только медленно, но все внешние ресурсы (картинки) будут загружены заново! Так лучше не делать.
А если нужно вставить в середину списка? Здесь `innerHTML` вообще не поможет.
Можно, конечно, вставить строку во временный DOM-элемент и перенести оттуда элементы, но есть и гораздо лучший вариант: метод `insertAdjacentHTML`!
## insertAdjacent*
Метод [insertAdjacentHTML](https://developer.mozilla.org/en/DOM/element.insertAdjacentHTML) позволяет вставлять произвольный HTML в любое место документа, в том числе *и между узлами*!
Он поддерживается всеми браузерами, кроме Firefox меньше версии 8, ну а там его можно эмулировать.
Синтаксис:
```js
elem.insertAdjacentHTML(where, html);
```
<dl>
<dt>`html`</dt>
<dd>Строка HTML, которую нужно вставить</dd>
<dt>`where`</dt>
<dd>Куда по отношению к `elem` вставлять строку. Всего четыре варианта:
<ol>
<li>`beforeBegin` -- перед `elem`.</li>
<li>`afterBegin` -- внутрь `elem`, в самое начало.</li>
<li>`beforeEnd` -- внутрь `elem`, в конец.</li>
<li>`afterEnd` -- после `elem`.</li>
</ol>
</dd>
<img src="insertAdjacentHTML.png">
Например, вставим пропущенные элементы списка *перед* `<li>5</li>`:
```html
<!--+ run -->
<ul>
<li>1</li>
<li>2</li>
<li>5</li>
</ul>
<script>
var ul = document.body.children[0];
var li5 = ul.children[2];
li5.insertAdjacentHTML("beforeBegin", "<li>3</li><li>4</li>");
</script>
```
Единственный недостаток этого метода -- он не работает в Firefox до версии 8. Но его можно легко добавить, используя следующий JavaScript: [insertAdjacentFF.js](/files/tutorial/browser/dom/insertAdjacentFF.js).
У этого метода есть "близнецы-братья", которые поддерживаются везде, кроме FF, но в него они добавляются этим же скриптом:
<ul>
<li>[elem.insertAdjacentElement(where, newElem)](http://help.dottoro.com/ljbreokf.php) -- вставляет в произвольное место не строку HTML, а элемент `newElem`.</li>
<li>[elem.insertAdjacentText(where, text)](http://help.dottoro.com/ljrsluxu.php) -- создаёт текстовый узел из строки `text` и вставляет его в указанное место относительно `elem`.</li>
</ul>
Синтаксис этих методов, за исключением последнего параметра, полностью совпадает с `insertAdjacentHTML`. Вместе они образуют "универсальный швейцарский нож" для вставки чего угодно куда угодно.
## DocumentFragment
[warn header="Важно для старых браузеров"]
Оптимизация, о которой здесь идёт речь, важна в первую очередь для старых браузеров, включая IE9-. В современных браузерах эффект от нее, как правило, не превышает 20%, а иногда может быть и отрицательным.
[/warn]
До этого мы говорили о вставке строки в DOM. А что делать в случае, когда надо в существующий `UL` вставить много *DOM-элементов*?
Можно вставлять их один за другим, вызовом `insertBefore/appendChild`, но при этом получится много операций с большим живым документом.
**Вставить пачку узлов единовременно поможет `DocumentFragment`. Это особенный *кросс-браузерный* DOM-объект, который похож на обычный DOM-узел, но им не является.**
Синтаксис для его создания:
```js
var fragment = document.createDocumentFragment();
```
В него можно добавлять другие узлы.
```js
fragment.appendChild(node);
```
Его можно клонировать:
```js
fragment.cloneNode(true); // клонирование с подэлементами
```
**У `DocumentFragment` нет обычных свойств DOM-узлов, таких как `innerHTML`, `tagName` и т.п. Это не узел.**
**"Фишка" заключается в том, что когда `DocumentFragment` вставляется в DOM -- то он исчезает, а вместо него вставляются его дети. Это свойство является уникальной особенностью `DocumentFragment`.**
Например, если добавить в него много `LI`, и потом `appendChild` к `UL`, то фрагмент растворится, и в DOM вставятся именно `LI`, причём в том же порядке, в котором были во фрагменте.
Псевдокод:
```js
// хотим вставить в список UL много LI
// делаем вспомогательный DocumentFragment
var fragment = document.createDocumentFragment();
for (цикл по li) {
fragment.appendChild(list[i]); // вставить каждый LI в DocumentFragment
}
ul.appendChild(fragment); // вместо фрагмента вставятся элементы списка
```
В современных браузерах эффект от такой оптимизации может быть различным. Чтобы понять текущее положение вещей, попробуйте в различных браузерах следующий небольшой бенчмарк.
При нажатии на кнопки ниже в список добавляются `100` элементов.
[pre]
<div>
<input type="button" onclick="alert(bench(DocumentFragmentTest.insertPlain,200))" value="Обычная вставка"/>
<input type="button" onclick="alert(bench(DocumentFragmentTest.insertDocumentFragment,200))" value="Вставка через DocumentFragment">
</div>
[/pre]
<ul id="bench-list"></ul>
<script src="/files/tutorial/browser/dom/documentfragment-bench.js"></script>
```js
//+ hide="открыть код" src="documentfragment-bench.js"
```
## Итого
<ul>
<li>**Манипуляции, меняющие структуру DOM (вставка, удаление элементов), как правило, быстрее с отдельным маленьким узлом, чем с большим DOM, который находится в документе.**
Конкретная разница зависит от внутренней реализации DOM в браузере.</li>
<li>**Семейство методов `elem.insertAdjacentHTML(where, html)`, `insertAdjacentElement`, `insertAdjacentText` позволяет вставлять HTML/элемент/текст в произвольное место документа.**
Метод `insertAdjacentHTML` не поддерживается в Firefox до версии 8, остальные два метода не поддерживаются в Firefox, на момент написания текста, вообще, но есть небольшой скрипт [insertAdjacentFF.js](/files/tutorial/browser/dom/insertAdjacentFF.js), который добавляет их. Конечно, он нужен только для Firefox.
</li>
<li>**`DocumentFragment` позволяет минимизировать количество вставок в большой живой DOM. Эта оптимизация особо эффективна в старых браузерах, в новых эффект от неё меньше.**
Элементы сначала вставляются в него, а потом -- он вставляется в DOM. При вставке `DocumentFragment` "растворяется", и вместо него вставляются содержащиеся в нём узлы.
`DocumentFragment`, в отличие от `insertAdjacent*`, работает с коллекцией DOM-узлов.
</li>
</ul>
[head]
<script>
function bench(test, times) {
var sum = 0;
for(var i=0; i<times; i++) {
if(test.setup) test.setup();
var t = new Date();
test.work();
sum += (new Date() - t);
if(test.tearDown) test.tearDown();
}
return sum;
}
/* 1. Вставляет TBODY в документ сразу. а затем элементы */
var appendFirst = new function() {
var benchTable;
this.setup = function() {
// очистить всё
benchTable = document.getElementById('bench-table')
while(benchTable.firstChild) {
benchTable.removeChild(benchTable.firstChild);
}
}
this.work = function() {
// встаить TBODY и элементы
var tbody = document.createElement('TBODY');
benchTable.appendChild(tbody);
for(var i=0; i<20; i++) {
var tr = document.createElement('TR');
tbody.appendChild(tr);
for(var j=0; j<20; j++) {
var td = document.createElement('td');
td.appendChild(document.createTextNode(''+i.toString(20)+j.toString(20)));
tr.appendChild(td);
}
}
}
}
/* 2. Полностью делает TBODY, а затем вставляет в документ */
var appendLast = new function() {
var benchTable;
this.setup = function() {
// очистить всё
benchTable = document.getElementById('bench-table');
while(benchTable.firstChild) {
benchTable.removeChild(benchTable.firstChild);
}
}
this.work = function() {
var tbody = document.createElement('TBODY');
for(var i=0; i<20; i++) {
var tr = document.createElement('TR');
tbody.appendChild(tr);
for(var j=0; j<20; j++) {
var td = document.createElement('td');
tr.appendChild(td);
td.appendChild(document.createTextNode(''+i.toString(20)+j.toString(20)));
}
}
benchTable.appendChild(tbody);
}
}
</script>
<style>
##bench-table td {
padding: 0;
}
##bench-list li {
display: inline-block;
margin: 0;
padding: 2px;
list-style-image: none;
list-style: none;
}
</style>
[/head]

View file

@ -0,0 +1,45 @@
var DocumentFragmentTest = new function() {
var benchList = document.getElementById('bench-list');
var items = [];
for(var i=0; i<100; i++) {
var li = document.createElement('li');
li.innerHTML = i;
items.push(li);
}
this.insertPlain = new function() {
this.setup = function() {
while(benchList.firstChild) {
benchList.removeChild(benchList.firstChild);
}
}
this.work = function() {
for(var i=0; i<items.length; i++) {
benchList.appendChild(items[i]);
}
}
};
this.insertDocumentFragment = new function() {
this.setup = function() {
// очистить всё
while(benchList.firstChild) {
benchList.removeChild(benchList.firstChild);
}
}
this.work = function() {
var docFrag = document.createDocumentFragment();
for(var i=0; i<items.length; i++) {
docFrag.appendChild(items[i]);
}
benchList.appendChild(docFrag);
}
};
}

View file

@ -0,0 +1,60 @@
/* 1. Вставляет TBODY в документ сразу. а затем элементы */
var appendFirst = new function() {
var benchTable;
this.setup = function() {
// очистить всё
benchTable = document.getElementById('bench-table')
while(benchTable.firstChild) {
benchTable.removeChild(benchTable.firstChild);
}
}
this.work = function() {
// встаить TBODY и элементы
var tbody = document.createElement('TBODY');
benchTable.appendChild(tbody);
for(var i=0; i<20; i++) {
var tr = document.createElement('TR');
tbody.appendChild(tr);
for(var j=0; j<20; j++) {
var td = document.createElement('td');
td.appendChild(document.createTextNode(''+i.toString(20)+j.toString(20)));
tr.appendChild(td);
}
}
}
}
/* 2. Полностью делает TBODY, а затем вставляет в документ */
var appendLast = new function() {
var benchTable;
this.setup = function() {
// очистить всё
benchTable = document.getElementById('bench-table');
while(benchTable.firstChild) {
benchTable.removeChild(benchTable.firstChild);
}
}
this.work = function() {
var tbody = document.createElement('TBODY');
for(var i=0; i<20; i++) {
var tr = document.createElement('TR');
tbody.appendChild(tr);
for(var j=0; j<20; j++) {
var td = document.createElement('td');
tr.appendChild(td);
td.appendChild(document.createTextNode(''+i.toString(20)+j.toString(20)));
}
}
benchTable.appendChild(tbody);
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

View file

@ -0,0 +1,143 @@
# Метод document.write
Метод `document.write` -- один из наиболее древних методов добавления текста к документу.
У него есть существенные ограничения, поэтому он используется редко, но по своей сути он совершенно уникален и иногда, хоть и редко, может быть полезен.
[cut]
## Как работает document.write
Метод `document.write(str)` работает только пока HTML-страница находится в процессе загрузки. Он дописывает текст в текущее место HTML ещё до того, как браузер построит из него DOM.
HTML-документ ниже будет содержать `1 2 3`.
```html
<!--+ run -->
<body>
1
<script>
document.write(2);
</script>
3
</body>
```
**Нет никаких ограничений на содержимое `document.write`**.
Строка просто пишется в HTML-документ без проверки структуры тегов, как будто она всегда там была.
Например:
```html
<!--+ run -->
<script>
document.write('<style> td { color: #F40 } </style>');
</script>
<table>
<tr>
<script> document.write('<td>') </script>
Текст внутри TD.
<script> document.write('</td>') </script>
</tr>
</table>
```
Также существует метод `document.writeln(str)` -- не менее древний, который добавляет после `str` символ перевода строки `"\n"`.
## Только до конца загрузки
Во время загрузки браузер читает документ и тут же строит из него DOM, по мере получения информации достраивая новые и новые узлы, и тут же отображая их. Этот процесс идет непрерывным потоком. Вы наверняка видели это, когда заходили на сайты в качестве посетителя -- браузер зачастую отображает неполный документ, добавляя его новыми узлами по мере их получения.
**Методы `document.write` и `document.writeln` пишут напрямую в текст документа, до того как браузер построит из него DOM, поэтому они могут записать в документ все, что угодно, любые стили и незакрытые теги.**
Браузер учтет их при построении DOM, точно так же, как учитывает очередную порцию HTML-текста.
Технически, вызвать `document.write` можно в любое время, однако, когда HTML загрузился, и браузер полностью построил DOM, документ становится *"закрытым"*. Попытка дописать что-то в закрытый документ открывает его заново. При этом все текущее содержимое удаляется.
Текущая страница, скорее всего, уже загрузилась, поэтому если вы нажмёте на эту кнопку -- её содержимое удалится:
<input type="button" onclick='document.write("Пустая страница!");' value="Запустить document.write('Пустая страница!')">
Из-за этой особенности `document.write` для загруженных документов не используют.
[warn header="XHTML и `document.write`"]
В некоторых современных браузерах при получении страницы с заголовком `Content-Type: text/xml` или `Content-Type: text/xhtml+xml` включается "XML-режим" чтения документа. Метод `document.write` при этом не работает.
Это одна из причин, по которой XML-режим обычно не используют.
[/warn]
## Преимущества перед innerHTML
Метод `document.write` -- динозавр, он существовал десятки <strike>миллионов</strike> лет назад. С тех пор, как появился и стал стандартным метод `innerHTML`, нужда в нём возникает редко, но некоторые преимущества, всё же, есть.
<ul>
<li>**Метод `document.write` работает быстрее, фактически это самый быстрый способ добавить на страницу текст, сгенерированный скриптом.**
Это естественно, ведь он не модифицирует существующий DOM, а пишет в текст страницы до его генерации.</li>
<li>**Метод `document.write` вставляет любой текст на страницу "как есть", в то время как `innerHTML` может вписать лишь валидный HTML.** (при попытке подсунуть невалидный -- браузер скорректирует его).</li>
</ul>
Эти преимущества являются скорее средством оптимизации, которое нужно использовать именно там, где подобная оптимизация нужна или уместна.
Однако, `document.write` по своей природе уникален: он добавляет текст "в текущее место документа", без всяких хитроумных DOM. Поэтому он бывает просто-напросто удобен, из-за чего его нередко используют не по назначению.
## Антипример: реклама
Например, `document.write` используют для вставки рекламных скриптов и различных счетчиков, когда URL скрипта необходимо генерировать динамически, добавляя в него параметры из JavaScript, например:
```html
<script>
// в url указано текущее разрешение экрана посетителя
var url = 'http://ads.com/buyme?screen=' + screen.width + "x" + screen.height;
// загрузить такой скрипт прямо сейчас
document.write('<script src="'+url+'"></scr'+'ipt>');
</script>
```
[smart]
Закрывающий тег <code>&lt;/script&gt;</code> в строке разделён, чтобы браузер не увидел `</script>` и не посчитал его концом скрипта.
Также используют запись:
```js
document.write('<script src="'+url+'"><\/script>');
```
Здесь `<\/script>` вместо `</script>`: обратный слеш `\` обычно используется для вставки спецсимволов типа `\n`, а если такго спецсимвола нет, в данном случае `\/` не является спецсимволом, то он просто исчезает. Так что получается такой альтернативный способ безопасно вставить строку `</script>`.
[/smart]
Сервер, получив запрос с такими параметрами, обрабатывает его и, исходя учитывая переданную информацию, генерирует текст скрипта, в котором обычно есть какой-то другой `document.write`, рисующий на этом месте баннер.
**Проблема здесь в том, что загрузка такого скрипта блокирует отрисовку всей страницы.**
То есть, дело даже не в самом `document.write`, а в том, что в страницу вставляется сторонний скрипт, а браузер устроен так, что пока он его не загрузит и не выполнит -- он не будет дальше строить DOM и показывать документ.
Представим на минуту, что сервер `ads.com`, с которого грузится скрипт, работает медленно или вообще завис -- зависнет и наша страница.
Что делать?
В современных браузерах у скриптов есть атрибуты `async` и `defer`, которые разрешают браузеру продолжать обработку страницы, но применить их здесь нельзя, так как рекламный скрипт захочет вызвать `document.write` именно на этом месте, и браузер не должен двигаться вперёд по документу.
Альтернатива -- использовать другие техники вставки рекламы и счётчиков. Примеры вы можете увидеть в коде Google Analytics, Яндекс.Метрики и других.
Если это невозможно -- применяют всякие хитрые оптимизации, например заменяют метод `document.write` своей функцией, и она уже разбирается со скриптами и баннерами.
## Итого
Метод `document.write` (или `writeln`) пишет текст прямо в HTML, как будто он там всегда был.
<ul>
<li>**Этот метод редко используется, так как работает только из скриптов, выполняемых в процессе загрузки страницы.**
Запуск после загрузки приведёт к очистке документа.</li>
<li>**Метод `document.write` очень быстр.**
В отличие от установки `innerHTML` и DOM-методов, он не изменяет существующий документ, а работает на стадии текста, до того как DOM-структура сформирована. </li>
<li>**Иногда `document.write` используют для добавления скриптов с динамическим URL.**
Рекомендуется избегать этого, так как браузер остановится на месте добавления скрипта и будет ждать его загрузки. Если скрипт будет тормозить, то и страница -- тоже.
Поэтому желательно подключать внешние скрипты, используя вставку скрипта через DOM.
</li>
</ul>

View file

@ -0,0 +1,43 @@
Есть два варианта.
<ol>
<li>Можно использовать свойство `elem.style.cssText` и присвоить стиль в текстовом виде. При этом все присвоенные ранее свойства `elem.style` будут удалены.</li>
<li>Можно назначить подсвойства `elem.style` одно за другим. Этот способ более безопасен, т.к. меняет только явно присваемые свойства.</li>
</ol>
Мы выберем второй путь.
[edit src="solution"]Открыть решение[/edit]
**Описание CSS-свойств:**
```css
.button {
-moz-border-radius: 8px;
-webkit-border-radius: 8px;
border-radius: 8px;
border: 2px groove green;
display: block;
height: 30px;
line-height: 30px;
width: 100px;
text-decoration: none;
text-align: center;
color: red;
font-weight: bold;
}
```
<dl>
<dt>`*-border-radius`</dt>
<dd>Добавляет скругленные углы. Свойство присваивается в вариантах для Firefox `-moz-...`, Chrome/Safari `-webkit-...` и стандартное CSS3-свойство для тех, кто его поддерживает (Opera).</dd>
<dt>`display`</dt>
<dd>По умолчанию, у `A` это свойство имеет значение `display: inline`.</dd>
<dt>`height`, `line-height`</dt>
<dd>Устанавливает высоту и делает текст вертикально центрированным путем установки `line-height` в значение, равное высоте. Такой способ центрирования текста работает, если он состоит из одной строки.</dd>
<dt>`text-align`</dt>
<dd>Центрирует текст горизонтально.</dd>
<dt>`color`, `font-weight`</dt>
<dd>Делает текст красным и жирным.</dd>
</dl>

View file

@ -0,0 +1,37 @@
<!DOCTYPE HTML>
<html>
<head><meta charset="utf-8"></head>
<body>
<div>
Кнопка:
<!-- создайте элемент и расположите его тут -->
</div>
<script>
var a = document.createElement('a');
a.className = 'button';
a.appendChild(document.createTextNode('Нажми меня'));
a.href = '/';
var s = a.style
s.MozBorderRadius = s.WebkitBorderRadius = s.borderRadius = '8px';
s.border = '2px groove green';
s.display = 'block';
s.height = '30px';
s.lineHeight = '30px';
s.width = '100px';
s.textDecoration = 'none';
s.textAlign = 'center';
s.color = 'red';
s.fontWeight = 'bold';
var div = document.body.children[0];
div.appendChild(a);
</script>
</body>
</html>

View file

@ -0,0 +1,17 @@
<!DOCTYPE HTML>
<html>
<head><meta charset="utf-8"></head>
<body>
<div>
Кнопка:
<!-- создайте элемент и расположите его тут -->
</div>
<script>
// .. ваш код
</script>
</body>
</html>

View file

@ -0,0 +1,33 @@
# Скругленая кнопка со стилями из JavaScript
[importance 3]
Создайте кнопку в виде элемента `<a>` с заданным стилем, используя JavaScript.
В примере ниже такая кнопка создана при помощи HTML/CSS. В вашем решении кнопка должна создаваться, настраиваться и добавляться в документ при помощи *только JavaScript*, без тегов `<style>` и `<a>`.
```html
<!--+ autorun height="50" -->
<style>
.button {
-moz-border-radius: 8px;
-webkit-border-radius: 8px;
border-radius: 8px;
border: 2px groove green;
display: block;
height: 30px;
line-height: 30px;
width: 100px;
text-decoration: none;
text-align: center;
color: red;
font-weight: bold;
}
</style>
<a class="button" href="/">Нажми меня</a>
```
**Проверьте себя: вспомните, что означает каждое свойство. В чём состоит эффект его появления здесь?**
[edit src="source" task/]

View file

@ -0,0 +1 @@
[edit src="solution"]Открыть в песочнице[/edit]

View file

@ -0,0 +1,15 @@
.notification {
position: fixed;
z-index: 1000;
padding: 5px;
border: 1px solid black;
font: normal 20px Georgia;
background: white;
text-align: center;
}
.welcome {
background: red;
color: yellow;
}

View file

@ -0,0 +1,61 @@
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
<link rel="stylesheet" href="index.css">
</head>
<body>
<h2>Уведомление</h2>
<p>
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Dolorum aspernatur quam ex eaque inventore quod voluptatem adipisci omnis nemo nulla fugit iste numquam ducimus cumque minima porro ea quidem maxime necessitatibus beatae labore soluta voluptatum magnam consequatur sit laboriosam velit excepturi laborum sequi eos placeat et quia deleniti? Corrupti velit impedit autem et obcaecati fuga debitis nemo ratione iste veniam amet dicta hic ipsam unde cupiditate incidunt aut iure ipsum officiis soluta temporibus. Tempore dicta ullam delectus numquam consectetur quisquam explicabo culpa excepturi placeat quo sequi molestias reprehenderit hic at nemo cumque voluptates quidem repellendus maiores unde earum molestiae ad.
</p>
<script>
/**
* Показывает уведомление, пропадающее через 1.5 сек
*
* @param options.top {number} вертикальный отступ, в px
* @param options.right {number} правый отступ, в px
* @param options.cssText {string} строка стиля
* @param options.className {string} CSS-класс
* @param options.html {string} HTML-текст для показа
*/
function showNotification(options) {
var notification = document.createElement('div');
notification.className = "notification";
if (options.cssText) {
notification.style.cssText = options.cssText;
}
notification.style.top = (options.top || 0) + 'px'; // can use cssText
notification.style.right = (options.right || 0) + 'px'; // can use cssText
if (options.className) {
notification.classList.add(options.className);
}
notification.innerHTML = options.html;
document.body.appendChild(notification); // over cover
setTimeout(function() {
document.body.removeChild(notification);
}, 1500);
}
// тест работоспособности
var i = 0;
setInterval(function() {
showNotification({
top: 10,
right: 10,
html: 'Привет ' + ++i,
className: "welcome"
});
}, 2000);
</script>
</body>
</html>

View file

@ -0,0 +1,15 @@
.notification {
position: fixed;
z-index: 1000;
padding: 5px;
border: 1px solid black;
font: normal 20px Georgia;
background: white;
text-align: center;
}
.welcome {
background: red;
color: yellow;
}

View file

@ -0,0 +1,46 @@
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
<link rel="stylesheet" href="index.css">
</head>
<body>
<h2>Уведомление</h2>
<p>
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Dolorum aspernatur quam ex eaque inventore quod voluptatem adipisci omnis nemo nulla fugit iste numquam ducimus cumque minima porro ea quidem maxime necessitatibus beatae labore soluta voluptatum magnam consequatur sit laboriosam velit excepturi laborum sequi eos placeat et quia deleniti? Corrupti velit impedit autem et obcaecati fuga debitis nemo ratione iste veniam amet dicta hic ipsam unde cupiditate incidunt aut iure ipsum officiis soluta temporibus. Tempore dicta ullam delectus numquam consectetur quisquam explicabo culpa excepturi placeat quo sequi molestias reprehenderit hic at nemo cumque voluptates quidem repellendus maiores unde earum molestiae ad.
</p>
<p>В CSS есть готовый класс notification, который можно ставить уведомлению.</p>
<script>
/**
* Показывает уведомление, пропадающее через 1.5 сек
*
* @param options.top {number} вертикальный отступ, в px
* @param options.right {number} правый отступ, в px
* @param options.cssText {string} строка стиля
* @param options.className {string} CSS-класс
* @param options.html {string} HTML-текст для показа
*/
function showNotification(options) {
// ваш код
}
// тест работоспособности
var i = 0;
setInterval(function() {
showNotification({
top: 10,
right: 10,
html: 'Привет ' + ++i,
className: "welcome"
});
}, 2000);
</script>
</body>
</html>

View file

@ -0,0 +1,41 @@
# Создать уведомление
[importance 5]
Напишите функцию `showNotification(options)`, которая показывает уведомление, пропадающее через 1.5 сек.
Описание функции:
```js
/**
* Показывает уведомление, пропадающее через 1.5 сек
*
* @param options.top {number} вертикальный отступ, в px
* @param options.right {number} правый отступ, в px
* @param options.cssText {string} строка стиля
* @param options.className {string} CSS-класс
* @param options.html {string} HTML-текст для показа
*/
function showNotification(options) {
// ваш код
}
```
Пример использования:
```js
// покажет элемент с текстом "Привет" и классом welcome справа-сверху окна
showNotification({
top: 10,
right: 10,
html: "Привет",
className: "welcome"
});
```
[demo src="solution"]
Элемент уведомления должен иметь CSS-класс `notification`, к которому добавляется класс из `options.className`, если есть. Исходный документ содержит готовые стили.
[edit src="source" task/]

View file

@ -0,0 +1,343 @@
# Стили, getComputedStyle
Эта глава -- о свойствах стиля, получении о них информации и изменении при помощи JavaScript.
Перед прочтением убедитесь, что хорошо знакомы с <a href="http://www.w3.org/TR/CSS2/box.html">блочной моделью CSS</a> и понимаете, что такое `padding`, `margin`, `border`.
[cut]
## Объект стилей style
Объект `element.style` дает доступ к стилю элемента на чтение и запись.
С его помощью можно изменять большинство CSS-свойств, например `element.style.width='100px'` работает так, как будто у элемента в атрибуте прописано `style="width:100px"`.
[warn header="Единицы измерения обязательны в `style`"]
Об этом иногда забывают, но в `style` так же, как и в CSS, нужно указывать единицы измерения, например `px`.
Ни в коем случае не просто `elem.style.width = 100` -- работать не будет.
[/warn]
**Для свойств, названия которых состоят из нескольких слов, используется вотТакаяЗапись:**
```js
background-color => elem.style.backgroundColor
z-index => elem.style.zIndex
border-left-width => elem.style.borderLeftWidth
```
Пример использования `style`:
```js
//+ run
document.body.style.backgroundColor = prompt('background color?', 'green');
```
[warn header="`style.cssFloat` вместо `style.float`"]
Исключением является свойство `float`. В старом стандарте JavaScript слово `"float"` было зарезервировано и недоступно для использования в качестве свойства объекта. Поэтому используется `element.style.cssFloat`.
[/warn]
[smart header="Свойства с префиксами"]
Специфические свойства браузеров, типа `-moz-border-radius`, `-webkit-border-radius`, записываются следующим способом:
```js
button.style.MozBorderRadius = '5px';
button.style.WebkitBorderRadius = '5px';
```
То есть, каждый дефис дает большую букву. В этом смысле преобразование -- такое же, как для обычных свойств.
[/smart]
**Чтобы сбросить поставленный стиль, присваивают в `style` пустую строку: `elem.style.width=""`.**
При сбросе свойства `style` стиль будет взят из CSS.
Например, для того, чтобы спрятать элемент, можно присвоить: `elem.style.display = "none"`.
А вот чтобы показать его обратно -- не обязательно явно указывать другой `elem.style.display = "block"`! Можно просто снять поставленный стиль: `elem.style.display = ""`.
```js
//+ run
// если запустить этот код, то <body> "мигнёт"
document.body.style.display = "none";
setTimeout(function() {
document.body.style.display = "";
}, 1000);
```
**Стиль в `style` находится в формате браузера, а не в том, в котором его присвоили.**
Например:
```html
<!--+ run height=100 -->
<body>
<script>
*!*
document.body.style.margin = '20px';
alert(document.body.style.marginTop); // 20px!
*/!*
*!*
document.body.style.color = '#abc';
alert(document.body.style.color); // rgb(170, 187, 204)
*/!*
</script>
</body>
```
Обратите внимание на то, как браузер "распаковал" свойство `style.margin`, предоставив для чтения `style.marginTop`. То же самое произойдет и для `border`, `background` и т.д.
[warn header="Свойство `style` мы используем лишь там, где не работают классы"]
В большинстве случаев внешний вид элементов задаётся классами. А JavaScript добавляет или удаляет их. Такой код красив и гибок, дизайн можно легко изменять.
Свойство `style` нужно использовать лишь там, где классы не подходят, например если точное значение цвета/отступа/высоты... Вычисляется в JavaScript.
[/warn]
## Строка стилей style.cssText
Свойство `style` является специальным объектом, ему нельзя присваивать строку.
Запись `div.style="color:blue"` работать не будет. Но как же, всё-таки, поставить свойство стиля, если хочется задать его строкой?
Можно попробовать использовать атрибут: `elem.setAttribute("style", ...)`, но самым правильным и, главное, кросс-браузерным (с учётом старых IE) решением такой задачи будет использование свойства `style.cssText`.
**Свойство `style.cssText` позволяет поставить стиль целиком в виде строки.**
Например:
```html
<!--+ run -->
<div>Button</div>
<script>
var div = document.body.children[0];
div.style.cssText="*!*color: red !important;*/!* \
background-color: yellow; \
width: 100px; \
text-align: center; \
*!*blabla: 5; \*/!*
";
alert(div.style.cssText);
</script>
```
Браузер разбирает строку `style.cssText` и применяет известные ему свойства. Нет никаких ограничений на запись несуществующих свойств, но если указать свойство `blabla` -- большинство браузеров его просто проигнорируют.
**При установке `style.cssText` все существующие свойства `style` перезаписываются.**
Поэтому, по возможности, во избежание конфликта, присваивают более конкретные подсвойства `style`: `style.color`, `style.width` и т.п, а `style.cssText` используют для более короткой записи, когда это заведомо безопасно.
## Чтение стиля из style
Записать в стиль очень просто. А как прочитать?
Например, мы хотим узнать размер, отступы элемента, его цвет... Как это сделать?
**Свойство `style` содержит лишь тот стиль, который указан в атрибуте элемента, без учёта каскада CSS.**
Вот так `style` уже ничего не увидит:
```html
<!--+ run height=100 -->
<head>
<style> body { color: red; margin: 5px } </style>
</head>
<body>
Красный текст
<script>
*!*
alert(document.body.style.color); //в большинстве браузеров
alert(document.body.style.marginTop); // ничего не выведет
*/!*
</script>
</body>
```
## Стиль из getComputedStyle
Итак, свойство `style` дает доступ только к той информации, которая хранится в `elem.style`.
Он не скажет ничего об отступе, если он появился в результате наложения CSS или встроенных стилей браузера:
А если мы хотим, например, сделать анимацию и плавно увеличивать `marginTop` от текущего значения? Как нам сделать это? Ведь для начала нам надо это текущее значение получить.
**Для того, чтобы получить текущее используемое значение свойства, используется метод `window.getComputedStyle`, описанный в стандарте <a href="http://www.w3.org/TR/2000/REC-DOM-Level-2-Style-20001113/css.html">DOM Level 2</a>.**
Его синтаксис таков:
```js
getComputedStyle(element, pseudo)
```
<dl>
<dt>element</dt>
<dd>Элемент, значения для которого нужно получить</dd>
<dt>pseudo</dt>
<dd>Указывается, если нужен стиль псевдо-элемента, например `"::before"`. Пустая строка означает сам элемент.</dd>
</dl>
Поддерживается всеми браузерами, кроме IE<9. Следующий код будет работать во всех не-IE браузерах и в IE9+:
```html
<!--+ run height=100 -->
<style>
body { margin: 10px }
</style>
<body>
<script>
var computedStyle = getComputedStyle(document.body, '');
alert(computedStyle.marginTop); // выведет отступ в пикселях
alert(computedStyle.color); // выведет цвет
</script>
</body>
```
[smart header="Вычисленное (computed) и окончательное (resolved) значения"]
В CSS есть две концепции:
<ol>
<li>*Вычисленное* (computed) значение -- это то, которое получено после применения всех правил CSS и CSS-наследования. Например, `width: auto` или `font-size: 125%`.</li>
<li>*Окончательное* ([resolved](http://dev.w3.org/csswg/cssom/#resolved-values)) значение -- непосредственно применяемое к элементу. При этом все размеры приводятся к пикселям, например `width: 212px` или `font-size: 16px`. В некоторых браузерах пиксели могут быть дробными.</li>
</ol>
Когда-то `getComputedStyle` задумывалось для возврата вычисленного значения, но со временем оказалось, что окончательное гораздо удобнее. Поэтому сейчас в целом все значения возвращаются именно такие, кроме некоторых небольших глюков в браузерах, которые постепенно вычищаются.
[/smart]
[warn header="`getComputedStyle` требует полное свойство!"]
Для правильного получения значения нужно указать точное свойство. Например: `paddingLeft`, `marginTop`, `borderLeftWidth`.
**При обращении к сокращенному: `padding`, `margin`, `border` -- правильный результат не гарантируется.**
Действительно, допустим свойства `paddingLeft/paddingTop` взяты из разных классов CSS. Браузер не обязан объединять их в одно свойство `padding`. Иногда, в простейших случаях, когда свойство задано сразу целиком, `getComputedStyle` сработает для сокращённого свойства, но не во всех браузерах.
Например, некоторые браузеры (Chrome) выведут `10px` в документе ниже, а некоторые (Firefox) -- нет:
```html
<!--+ run -->
<style>
body {
margin: 10px;
}
</style>
<script>
var style = getComputedStyle(document.body, '');
alert( style.margin ); // в Firefox пустая строка
</script>
```
[/warn]
[smart header="Стили посещенных ссылок -- тайна!"]
У посещенных ссылок может быть другой цвет, фон, чем у обычных. Это можно поставить в CSS с помощью псевдокласса `:visited`.
Но `getComputedStyle` не дает доступ к этой информации, чтобы произвольная страница не могла определить, посещал ли пользователь ту или иную ссылку.
Кроме того, большинство браузеров запрещают применять к `:visited` CSS-стили, которые могут изменить геометрию элемента, чтобы даже окольным путем нельзя было это понять. В целях безопасности.
[/smart]
## currentStyle для IE8-
В IE8- нет `getComputedStyle`, но у элементов есть свойство <a href="http://msdn.microsoft.com/en-us/library/ms536497.aspx">currentStyle</a>, которое возвращает вычисленное (computed) значение: уже с учётом CSS-каскада, но не всегда в окончательном формате.
Чтобы код работал и в старых и новых браузерах, обычно пишут кросс-браузерный код, наподобие такого:
```js
function getStyle(elem) {
return window.getComputedStyle ? getComputedStyle(elem, "") : elem.currentStyle;
}
```
Если вы откроете такой документ в IE8-, то размеры будут в процентах, а в современных браузерах -- в пикселях.
```html
<!--+ run height=100 -->
<style>
body { margin: 10% }
</style>
<body>
<script>
var elem = document.body;
function getStyle(elem) {
return window.getComputedStyle ? getComputedStyle(elem, "") : elem.currentStyle;
}
var marginTop = getStyle(elem).marginTop;
alert(marginTop); // IE8-: 10%, иначе пиксели
</script>
</body>
```
[smart header="IE8-: перевод `pt,em,%` из `currentStyle` в пиксели"]
Эта информация -- дополнительная, она не обязательна для освоения.
В IE для того, чтобы получить из процентов реальное значение в пикселях существует метод "runtimeStyle+pixel", [описанный Дином Эдвардсом](http://erik.eae.net/archives/2007/07/27/18.54.15/#comment-102291).
Он основан на свойствах `runtimeStyle` и `pixelLeft`, работающих только в IE.
В следующем примере функция `getIEComputedStyle(elem, prop)` получает значение в пикселях для свойства `prop`, используя `elem.currentStyle` и метод Дина Эдвардса.
Если вам интересно, как он работает, ознакомьтесь со свойствами с <a href="http://msdn.microsoft.com/en-us/library/ms535889(v=vs.85).aspx">runtimeStyle</a> и <a href="http://msdn.microsoft.com/en-us/library/ms531129%28VS.85%29.aspx">pixelLeft</a> в MSDN и раскройте код.
```js
//+ src="getIEComputedStyle.js" hide="Раскрыть код"
```
<script src="/files/tutorial/browser/dom/getIEComputedStyle.js"></script>
Рабочий пример (только IE):
```html
<style> #margin-test { margin: 1%; border: 1px solid black; } </style>
<div id="margin-test">Тестовый элемент с margin 1%</div>
<script>
var elem = document.getElementById('margin-test');
if (!elem.getComputedStyle) // старые IE
document.write(getIEComputedStyle(elem, 'marginTop'));
else
document.write('Пример работает только в IE<9');
</script>
```
[pre]
<style> #margin-test { margin: 1%; border: 1px solid black; } </style>
<div id="margin-test">Тестовый элемент с margin 1%</div>
<i>
<script>
var elem = document.getElementById('margin-test');
if (!window.getComputedStyle) // старые IE
document.write(getIEComputedStyle(elem, 'marginTop'));
else
document.write('Пример работает только в IE<9');
</script>
</i>
[/pre]
Современные Javascript-фреймворки используют этот прием для эмуляции `getComputedStyle` в старых IE.
[/smart]
## Итого
Все DOM-элементы предоставляют следующие свойства.
<ul>
<li>Свойство `style` -- это объект, в котором CSS-свойства пишутся `вотТакВот`. Чтение и изменение его свойств -- это, по сути, работа с компонентами атрибута `style`.</li>
<li>`style.cssText` -- строка стилей для чтения или записи. Аналог полного атрибута `style`.</li>
<li>Свойство `currentStyle`(IE8-) и метод `getComputedStyle` (IE9+, стандарт) позволяют получить реальное, применённое сейчас к элементу свойство стиля с учётом CSS-каскада и браузерных стилей по умолчанию.
При этом `currentStyle` возвращает значение из CSS, до окончательных вычислений, а `getComputedStyle` -- окончательное, непосредственно применённое к элементу (как правило).</li>
</ul>
Более полная информация о `style`, включающая другие, реже используемые методы работы с ним, доступна здесь: [CSSStyleDeclaration](https://developer.mozilla.org/en-US/docs/DOM/CSSStyleDeclaration).

View file

@ -0,0 +1,18 @@
function getIEComputedStyle(elem, prop) {
var value = elem.currentStyle[prop] || 0
// we use 'left' property as a place holder so backup values
var leftCopy = elem.style.left
var runtimeLeftCopy = elem.runtimeStyle.left
// assign to runtimeStyle and get pixel value
elem.runtimeStyle.left = elem.currentStyle.left
elem.style.left = (prop === "fontSize") ? "1em" : value
value = elem.style.pixelLeft + "px";
// restore values for left
elem.style.left = leftCopy
elem.runtimeStyle.left = runtimeLeftCopy
return value
}

View file

@ -0,0 +1 @@
Решение: `elem.scrollHeight - elem.scrollTop - elem.clientHeight`.

View file

@ -0,0 +1,9 @@
# Найти размер прокрутки снизу
[importance 5]
Свойство `elem.scrollTop` содержит размер прокрученной области при отсчете сверху. А как подсчитать его снизу?
Напишите соответствующее выражение для произвольного элемента `elem`.
Проверьте: если прокрутки нет или элемент полностью прокручен -- оно должно давать ноль.

View file

@ -0,0 +1,23 @@
Создадим элемент с прокруткой, но без `padding`. Тогда разница между его полной шириной `offsetWidth` и внутренней `clientWidth` будет равна как раз прокрутке:
```js
//+ run
// создадим элемент с прокруткой
var div = document.createElement('div');
div.style.overflowY = 'scroll';
div.style.width = '50px';
div.style.height = '50px';
// при display:none размеры нельзя узнать
// нужно, чтобы элемент был видим,
// visibility:hidden - можно, т.к. сохраняет геометрию
div.style.visibility = 'hidden';
document.body.appendChild(div);
var scrollWidth = div.offsetWidth - div.clientWidth;
document.body.removeChild(div);
alert( scrollWidth );
```

View file

@ -0,0 +1,7 @@
# Узнать ширину полосы прокрутки
[importance 3]
Напишите код, который возвращает ширину стандартной полосы прокрутки. Именно самой полосы, где ползунок. Обычно она равна `16px`, в редких и мобильных браузерах может колебаться от `14px` до `18px`, а кое-где даже равна `0px`.
P.S. Ваш код должен работать на любом HTML-документе, независимо от его содержимого.

View file

@ -0,0 +1,31 @@
Нам нужно создать `div` с такими же размерами и вставить его на место "переезжающего".
Один из вариантов -- это просто клонировать элемент.
Если делать это при помощи `div.cloneNode(true)`, то склонируется все содержимое, которого может быть много. Обычно нам это не нужно, поэтому можно использовать `div.cloneNode(false)` для клонирования элемента со стилями, и потом поправить его `width/height`.
Можно и просто создать новый `div` и поставить ему нужные размеры.
**Всё, кроме `margin`, можно получить из свойств DOM-элемента, а `margin` -- только через `getComputedStyle`.**
Причём `margin` мы обязаны поставить, так как иначе элемент не будет отодвинут от внешних.
Код:
```js
var div = document.getElementById('moving-div');
var placeHolder = document.createElement('div');
placeHolder.style.height = div.offsetHeight + 'px';
// можно и width, но в этом примере это не обязательно
// IE || другой браузер
var computedStyle = div.currentStyle || getComputedStyle(div, '');
placeHolder.style.marginTop = computedStyle.marginTop; // (1)
placeHolder.style.marginBottom = computedStyle.marginBottom;
```
В строке `(1)` использование полного название свойства `"marginTop"` гарантирует, что полученное значение будет корректным.
[edit src="solution"]Открыть решение в песочнице[/edit]

View file

@ -0,0 +1,47 @@
<!DOCTYPE HTML>
<html>
<head>
<style>
#moving-div {
border: 5px groove green;
padding: 5px;
margin: 10px;
background-color: yellow;
}
</style>
</head>
<body>
Before Before Before
<div id="moving-div">
Text Text Text<br>
Text Text Text<br>
</div>
After After After
<script>
var div = document.getElementById('moving-div')
var placeHolder = document.createElement('div')
placeHolder.style.height = div.offsetHeight + 'px'
var computedStyle = div.currentStyle || getComputedStyle(div, null)
placeHolder.style.marginTop = computedStyle.marginTop // full prop name
placeHolder.style.marginBottom = computedStyle.marginBottom
// highlight it for demo purposes
placeHolder.style.backgroundColor = '#C0C0C0'
document.body.insertBefore(placeHolder, div)
div.style.position = 'absolute'
div.style.right = div.style.top = 0
</script>
</body>
</html>

View file

@ -0,0 +1,33 @@
<!DOCTYPE HTML>
<html>
<head>
<style>
#moving-div {
border: 5px groove green;
background-color: yellow;
padding: 5px;
margin: 10px;
}
</style>
</head>
<body>
Before Before Before
<div id="moving-div">
Text Text Text<br>
Text Text Text<br>
</div>
After After After
<script>
// .. Add your code here
var div = document.getElementById('moving-div')
div.style.position = 'absolute'
div.style.right = div.style.top = 0
</script>
</body>
</html>

View file

@ -0,0 +1,50 @@
# Подменить div на другой с таким же размером
[importance 3]
Посмотрим следующий случай из жизни. Был текст, который, в частности, содержал `div` с зелеными границами:
```html
<!--+ run -->
<style>
#moving-div {
border: 5px groove green;
padding: 5px;
margin: 10px;
background-color: yellow;
}
</style>
Before Before Before
<div id="moving-div">
Text Text Text<br>
Text Text Text<br>
</div>
After After After
```
Программист Валера из вашей команды написал код, который позиционирует его абсолютно и смещает в правый верхний угол. Вот этот код:
```js
var div = document.getElementById('moving-div');
div.style.position = 'absolute';
div.style.right = div.style.top = 0;
```
Побочным результатом явилось смещение текста, который раньше шел после `DIV`. Теперь он поднялся вверх:
[iframe height=90 src="source"]
**Допишите код Валеры, сделав так, чтобы текст оставался на своем месте после того, как `DIV` будет смещен.**
Сделайте это путем создания вспомогательного `DIV` с теми же размерами (`width`, `height`, `border`, `margin`, `padding`), что и у желтого `DIV`. Используйте только JavaScript, без CSS.
Должно быть так (новому блоку задан фоновый цвет для демонстрации):
[iframe height=140 src="solution"]
[edit src="source" task/]

View file

@ -0,0 +1,39 @@
<!DOCTYPE HTML>
<html>
<head>
<style>
#field {
width: 200px;
border: 10px groove black;
background-color: #00FF00;
position: relative;
}
#ball {
position: absolute;
}
</style>
</head>
<body>
<div id="field">
<img src="http://js.cx/clipart/ball.gif" id="ball">
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
</div>
<script>
var ball = document.getElementById('ball')
var field = document.getElementById('field')
// ball.offsetWidth=0 before image loaded!
// to fix: set width
ball.style.left = Math.round(field.clientWidth / 2)+'px'
ball.style.top = Math.round(field.clientHeight / 2)+'px'
</script>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

View file

@ -0,0 +1,51 @@
При абсолютном позиционировании мяча внутри поля его координаты `left/top` отсчитываются от **внутреннего** угла поля, например верхнего-левого:
<img src="field.png">
Метрики для внутренней зоны поля -- это `clientWidth/Height`.
Центр - это `(clientWidth/2, clientHeight/2)`.
Но если мы установим мячу такие значения `ball.style.left/top`, то в центре будет не сам мяч, а его левый верхний угол:
```js
var ball = document.getElementById('ball');
var field = document.getElementById('field');
ball.style.left = Math.round(field.clientWidth / 2)+'px';
ball.style.top = Math.round(field.clientHeight / 2)+'px';
```
[iframe hide="Нажмите, чтобы посмотреть текущий результат" height=180 src="ball-half"]
Для того, чтобы центр мяча находился в центре поля, нам нужно сместить мяч на половину его ширины влево и на половину его высоты вверх.
```js
var ball = document.getElementById('ball');
var field = document.getElementById('field');
ball.style.left = Math.round(field.clientWidth/2 - ball.offsetWidth/2)+'px';
ball.style.top = Math.round(field.clientHeight/2 - ball.offsetHeight/2)+'px';
```
**Внимание, подводный камень!**
Код выше стабильно работать не будет, потому что `IMG` идет без ширины/высоты:
```html
<img src="ball.gif" id="ball">
```
**Высота и ширина изображения неизвестны браузеру до тех пор, пока оно не загрузится, если размер не указан явно.**
После первой загрузки изображение уже будет в кеше браузера, и его размеры будут известны. Но когда браузер впервые видит документ -- он ничего не знает о картинке, поэтому значение `ball.offsetWidth` равно `0`. Вычислить координаты невозможно.
Чтобы это исправить, добавим `width/height` к картинке:
```html
<img src="ball.gif" *!*width="40" height="40"*/!* id="ball">
```
Теперь браузер всегда знает ширину и высоту, так что все работает. Тот же эффект дало бы указание размеров в CSS.
[edit src="solution"]Полный код решения[/edit]

View file

@ -0,0 +1,39 @@
<!DOCTYPE HTML>
<html>
<head>
<style>
#field {
width: 200px;
border: 10px groove black;
background-color: #00FF00;
position: relative;
}
#ball {
position: absolute;
}
</style>
</head>
<body>
<div id="field">
<img src="http://js.cx/clipart/ball.gif" width="40" height="40" id="ball">
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
</div>
<script>
var ball = document.getElementById('ball')
var field = document.getElementById('field')
// ball.offsetWidth=0 before image loaded!
// to fix: set width
ball.style.left = Math.round(field.clientWidth / 2 - ball.offsetWidth / 2)+'px'
ball.style.top = Math.round(field.clientHeight / 2 - ball.offsetHeight / 2)+'px'
</script>
</body>
</html>

View file

@ -0,0 +1,27 @@
<!DOCTYPE HTML>
<html>
<head>
<style>
#field {
width: 200px;
border: 10px groove black;
background-color: #00FF00;
position: relative;
}
#ball {
position: absolute;
}
</style>
</head>
<body>
<div id="field">
<img src="http://js.cx/clipart/ball.gif" id="ball">
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
</div>
</body>
</html>

View file

@ -0,0 +1,21 @@
# Поместите мяч в центр поля
[importance 5]
Поместите мяч в центр поля.
Исходный документ выглядит так:
[iframe src="source" edit link]
**Используйте JavaScript, чтобы поместить мяч в центр:**
[iframe src="solution"]
<ul>
<li>Менять CSS нельзя, мяч должен переносить в центр ваш скрипт, через установку нужных стилей элемента.</li>
<li>Код не должен быть привязан к конкретному размеру мяча.</li>
<li>Обратите внимание: мяч должен быть строго по центру! Независимо от местоположения поля и ширины его рамки.</li>
</ul>
[edit src="source" task/]
P.S. Да, это можно сделать при помощи чистого CSS, но задача именно на JavaScript. Далее будет развитие темы и более сложные ситуации, когда JavaScript будет уже точно необходим.

View file

@ -0,0 +1,45 @@
**Вначале рассмотрим неверный вариант.**
Он выглядит так:
```js
elem.style.width = '100%';
```
Если вы его попробуете, то увидите, что элемент начинает вылезать за рамки родителя.
Так происходит потому, что ширина -- это то, что *внутри `padding`*. То есть, ставя ширину в `100%`, вы говорите: "внутренняя область должна занимать `100%` доступной ширины". А на `padding` остаётся `0%`. В результате поля вылезают наружу.
**Правильное решение через `clientWidth`.**
Доступную внутреннюю ширину родителя можно получить, вычитая `padding` из `clientWidth`, и присвоить элементу:
```js
var bodyClientWidth = document.body.clientWidth;
var style = window.getComputedStyle ? getComputedStyle(elem, '') : elem.currentStyle;
*!*
var bodyInnerWidth = bodyClientWidth - parseInt(style.paddingLeft) - parseInt(style.paddingRight);
*/!*
elem.style.width = bodyInnerWidth + 'px';
```
Этот вариант сломается, если в IE<9 значение `padding` указано не в пикселях. Получение пикселей из процентов и других единиц измерения рассмотрено в главе [](/styles-and-classes).
**Правильный вариант с CSS.**
**Самое лучшее решение получится, если вспомнить, что элемент и сам рад растянуться по всей доступной ширине, и делает это по умолчанию.**
Достаточно вернуть ему стандартный алгоритм вычисленя ширины, установив `width: auto`:
```js
elem.style.width = 'auto';
```
Но.. **Это не будет работать для элементов, которые сами по себе не растягиваются**, например в случае `position: absolute` или `float`.
Такой элемент можно расширить, используя предыдущее решение.
[edit src="solution"]Документ с обоими решениями[/edit]

View file

@ -0,0 +1,58 @@
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
<style>
#elem {
width: 200px;
height: 150px;
background-color: red;
padding: 20px;
overflow: auto;
/* position: absolute */
}
body {
border: 1px solid black;
}
</style>
</head>
<body>
<div id="elem">
текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст
текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст
текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст
текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст
текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст
текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст
текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст
текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст
</div>
<script>
var elem = document.getElementById("elem");
// неверно!
//elem.style.width = '100%';
// верно (решение с CSS)
//elem.style.width = 'auto';
// верно (решение с JS), т.к. padding указан px
// если паддинг в процентах, например 10%, то в IE<9 currentStyle вернет 10%,
// и нужно преобразовать % в px через runtimeStyle
var bodyWidth = document.body.clientWidth;
var style = window.getComputedStyle ? getComputedStyle(elem, '') : elem.currentStyle;
var bodyInnerWidth = bodyWidth - parseInt(style.paddingLeft) - parseInt(style.paddingRight);
elem.style.width = bodyInnerWidth + 'px';
</script>
</body>
</html>

View file

@ -0,0 +1,41 @@
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
<style>
#elem {
width: 200px;
height: 150px;
background-color: red;
padding: 20px;
overflow: auto;
}
body {
border: 1px solid black;
}
</style>
</head>
<body>
<div id="elem">
текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст
текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст
текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст
текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст
текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст
текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст
текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст
текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст
</div>
<script>
var elem = document.getElementById("elem");
// ... ваш код
</script>
</body>
</html>

View file

@ -0,0 +1,18 @@
# Расширить элемент
[importance 3]
В `BODY` есть элемент `DIV` с заданной шириной `width`.
Задача -- написать код, который "распахнет" `DIV` по ширине на всю страницу.
Исходный документ (`DIV` -- красный):
[iframe height=220 src="source"]
[edit src="source" task/]
Расширить нужно точно по ширине, чтобы красный `DIV` не вылез за границы `BODY`.
P.S. Пользоваться следует исключительно средствами JS, при этом не подглядывая в стили. То есть, код должен быть универсален и не ломаться, если цифры в CSS станут другими.
P.P.S. После того, как решите... Будет ли ваше решение работать, если у красного `DIV` стоит `position:absolute`? Если нет, то почему и как его поправить?

View file

@ -0,0 +1,11 @@
Отличия:
<ol>
<li>`getComputedStyle` не работает в IE8-.</li>
<li>`clientWidth` возвращает число, а `getComputedStyle(...).width` -- в `px`.</li>
<li>`getComputedStyle` не всегда даст ширину, он может вернуть, к примеру, `"auto"` для инлайнового элемента.</li>
<li>`clientWidth` соответствует внутренней видимой области элемента, *включая `padding`, а CSS-ширина `width`, при стандартном значении [box-sizing](/box-sizing), соответствует зоне *внутри `padding`*.</li>
<li>Если есть полоса прокрутки, то некоторые браузеры включают её ширину в `width`, а некоторые -- нет.
Свойство `clientWidth`, с другой стороны, полностью кросс-браузерно. Оно всегда обозначает размер *за вычетом прокрутки*, т.е. реально доступный для содержимого.</li>
</ol>

View file

@ -0,0 +1,7 @@
# В чём отличие "width" и "clientWidth" ?
[importance 5]
В чём отличия между `getComputedStyle(elem, "").width` и `elem.clientWidth`?
В решении приведены пять отличий. Постарайтесь найти столько же или больше :)

View file

@ -0,0 +1,276 @@
# Размеры и прокрутка элементов
Для того, чтобы показывать элементы правильно, подгонять на нужные места страницы, управлять ими при помощи мыши, необходимо во-первых, знать CSS-позиционирование, а во-вторых -- уметь работать с "геометрией элементов" из JavaScript.
В этой главе мы поговорим о размерах элементов DOM, способах их вычисления и *метриках* -- различных свойствах, которые содержат эту информацию.
[cut]
## Образец документа
Мы будем использовать для примера блок, у которого есть рамка (border), поля (padding), отступы (margin) и прокрутка:
```html
<div id="example">
...Текст...
</div>
<style>
##example {
width: 300px;
height: 200px;
border: 25px solid #F0E68C; /* рамка 25px */
padding: 20px; /* поля 20px */
margin: 20px; /* отступы 20px */
overflow: auto; /* прокрутка */
}
</style>
```
Результат выглядит так:
<img src="css.png">
Вы можете открыть документ [edit src="metric"]по этой ссылке[/edit].
## Получение width/height из CSS
Какой способ первый приходит на ум, когда есть задача определить `width/height`?
Если вы внимательно читали до этого момента, то уж точно знаете, что CSS-высоту и ширину `width/height` можно установить с помощью `elem.style` и извлечь, используя `getComputedStyle()/currentStyle`, которые в подробностях обсуждаются в главе [](/styles-and-classes).
Решение может быть таким:
```js
//+ run
var elem = document.body;
var style = window.getComputedStyle ? getComputedStyle(elem, "") : elem.currentStyle;
alert(style.width); // вывести CSS-ширину body
```
Всегда ли такой подход сработает? Увы, нет!
<ol>
<li>Во-первых, CSS-свойства `width/height` зависят от другого свойства -- `box-sizing`, которое определяет, что такое, собственно, эти ширина и высота. По умолчанию они относятся к размеру внутренней части элемента, которая лежит внутри `padding`, а если нужно узнать полную высоту/ширину?</li>
<li>В IE8- могут быть нестыковки с единицами измерения -- как мы помним, `currentStyle` не пересчитывает размеры в пиксели.</li>
<li>И, наконец, самое главное, свойства `width/height` могут быть равны `auto`!
Например, для инлайн-элемента:
```html
<!--+ run -->
<span id="elem">Привет!</span>
<script>
alert( getComputedStyle(elem, "").width ); // auto
</script>
```
Конечно, с точки зрения CSS размер `auto` -- совершенно нормально, но нам-то в JavaScript нужен конкретный размер в пикселях, который мы сможем использовать для вычислений.
</li>
</ol>
## Полоса прокрутки
Полоса прокрутки -- причина многих проблем и недопониманий. Как говорится, "дьявол кроется в деталях". Недопустимо, чтобы наш код работал на элементах без прокрутки и начинал "глючить" с ней. Поэтому мы с самого начала будем её учитывать.
**При наличии вертикальной полосы прокрутки -- она забирает себе часть ширины элемента.**
Ширина полосы прокрутки обычно составляет около `14-18px`, в зависимости от браузера и операционной системы. Бывает и `0` для полупрозрачной прокрутки, не отъедающей место. В примере подразумевается, что прокрутка место ест, поэтому внутренняя область будет уже не `300px`, а около `284px`.
**Несмотря на то, что на рисунке полоса прокрутки находится визуально в правом поле -- отнимает место она не у `padding`, а у внутренней области элемента.**
...Но при этом некоторые браузеры отражают это уменьшение ширины в результате `getComputedStyle(...).width`, а некоторые -- нет.
В примере ниже в стилях указано `width:300px`. А вот `getComputedStyle` возвращает `300px/284px`, в зависимости от браузера.
Если ваш браузер в принципе показывает полосу прокрутки (например, под Windows почти все браузеры так делают), то вы можете протестировать это сами, нажав на кнопку в ифрейме ниже:
[iframe src="cssWidthScroll" link border=1]
Описанные разночтения касаются только чтения свойства `getComputedStyle(...).width` из JavaScript, визуальное отображение корректно в обоих случаях -- ширина текста при наличии прокрутки в обоих случаях уменьшается.
**Здесь и далее, мы будем понимать под `width` именно реальную ширину внутренней области (около `284px`), а не результат чтения CSS-свойства `width`, который может быть разным в зависимости от браузера/OS.**
## JavaScript-метрики
В JavaScript существует ряд дополнительных свойств, содержащих размеры элементов. Мы будем называть их "метриками".
**Метрики JavaScript, в отличие от свойств CSS, содержат числа, всегда в пикселях и без единиц измерения на конце.**
### clientWidth/Height
Размер *клиентской зоны*, а именно: внутренняя область плюс `padding`.
<img src="clientWidth.png">
Общая ширина внутри рамки -- это `284 (width) + 20(padding left) + 20 (padding right) = 324`.
Получаем:
```js
clientWidth = 284(width) + 2*20(padding) = 324
clientHeight = 200(height) + 2*20(padding) = 240
```
Обратите внимание, в `clientHeight` входят и верхнее и нижнее поля, несмотря на то, что нижнее поле заполнено текстом.
**Если `padding` нет, то `clientWidth/Height` покажет реальный размер области данных, внутри рамок и полосы прокрутки.**
<img src="clientWidthNoPadding.png">
### scrollWidth/Height
Ширина и высота контента *с учетом прокручиваемой области*.
<ul>
<li>`scrollHeight = 723` -- полная высота, включая прокрученную область</li>
<li>`scrollWidth = 324` -- полная ширина, включая прокрученную область</li>
</ul>
**`scrollWidth/Height` то же самое, что и `clientWidth/Height`, но включает в себя прокручиваемую область.**
<img src="scrollWidth.png">
Эти свойства можно использовать, чтобы "распахнуть" элемент на всю ширину/высоту:
```js
element.style.height = element.scrollHeight + 'px';
```
Нажмите на кнопку, чтобы распахнуть элемент:
<div id="scrollOpen" style="width:300px;height:200px; padding: 0;overflow: auto; border:1px solid black;">текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст</div>
<button style="padding:0" onclick="document.getElementById('scrollOpen').style.height = document.getElementById('scrollOpen').scrollHeight + 'px'">element.style.height = element.scrollHeight + 'px'</button>
### scrollTop/scrollLeft
Размеры текущей прокрученной части элемента -- вертикальной и горизонтальной.
Следующее изображение иллюстрирует `scrollHeight` и `scrollTop` для блока с вертикальной прокруткой.
<img src="scrollTop.png">
[smart header="`scrollLeft/scrollTop` можно изменять"]
**В отличие от большинства свойств, которые доступны только для чтения, значения `scrollLeft/scrollTop` можно изменить, и браузер выполнит прокрутку элемента**.
При клике на следующий элемент будет выполняться код `elem.scrollTop += 10`. Поэтому он будет прокручиваться на `10px` вниз:
<div onclick="this.scrollTop+=10" style="cursor:pointer;border:1px solid black;width:100px;height:80px;overflow:auto">Кликни<br>Меня<br>1<br>2<br>3<br>4<br>5<br>6<br>7<br>8<br>9</div>
[/smart]
### offsetWidth/Height
Внешняя ширина/высота блока, полный размер, включая рамки, исключая внешние отступы `margin`.
<ul>
<li>`offsetWidth = 390` -- внешняя ширина блока</li>
<li>`offsetHeight = 290` -- внешняя высота блока</li>
</ul>
<img src="offsetWidth.png">
Эти свойства показывают *внешние* ширину и высоту блока, то как блок выглядит снаружи.
### clientTop/Left
Отступ *клиентской области* от внешнего угла блока.
Другими словами, это ширина верхней/левой рамки(border) в пикселях.
<ul>
<li>`clientLeft = 25` -- ширина левой рамки</li>
<li>`clientTop = 25` -- ширина верхней рамки</li>
</ul>
<img src="clientLeft.png">
Казалось бы, зачем еще какие-то свойства, если ширину рамки можно получить напрямую из CSS? Обычно они действительно не нужны.
Но есть две ситуации, когда эти свойства бывают полезны:
<ol>
<li>В случае, когда документ располагается *справа налево* (арабский язык, иврит), свойство `clientLeft` включает в себя еще и ширину *правой* полосы прокрутки.</li>
<li>В IE&lt;8 документ, а точнее -- элемент `document.documentElement` немного смещен относительно верхнего левого угла документа. Несмотря на то, что рамки там нет, сдвиг существует и хранится в `document.body.clientLeft/clientTop` (обычно это 2 пикселя).</li>
</ol>
### offsetParent, offsetLeft/Top
[warn header="Используются редко..."]
Ситуации, когда эти свойства нужны, можно перечислить по пальцам. Они возникают действительно редко. Как правило, эти свойства используют по ошибке, потому что не знают средств правильной работы с координатами, о которых мы поговорим позже.
[/warn]
**`offsetParent` -- это родительский элемент в смысле отображения на странице.**
Когда браузер рисует страницу, то он высчитывает дерево расположения элементов, иначе говоря "дерево геометрии" или "дерево рендеринга".
Обычно элементы вложены друг в друга и структура дерева рендеринга повторяет DOM.
Но, к примеру, если у элемента стоит `position: absolute`, то его расположение вычисляется уже не относительно непосредственного родителя `parentNode`, а относительно ближайшего <a href="http://www.w3.org/TR/CSS21/visuren.html#position-props">позиционированного элемента</a> (т.е. свойство `position` которого не равно `static`), или `BODY`, если таковой отсутствует.
Получается, что элемент имеет одного родителя в DOM и другого -- в плане позиционирования, относительно которого он рисуется. Этот элемент и будет в свойстве `offsetParent`.
**Свойства `offsetLeft/Top` задают смещение относительно `offsetParent`.**
```html
<div style="position: relative">
<div style="position: absolute; left: 180px; top: 180px">...</div>
</div>
```
<img src="offsetLeft.png">
</dd>
</dl>
[smart header="Метрики для невидимых элементов равны нулю."]
Координаты и размеры в JavaScript устанавливаются только для *видимых* элементов.
Для элементов с `display:none` или находящихся вне документа дерево рендеринга не строится. Для них метрики равны нулю. Кстати, и `offsetParent` для таких элементов тоже `null`.
Это дает нам **замечательный способ для проверки, виден ли элемент**:
```js
function isHidden(elem)
return !elem.offsetWidth && !elem.offsetHeight;
}
```
<ul>
<li>Работает, даже если родителю элемента установлено свойство `display:none`.</li>
<li>Работает для всех элементов, кроме `TR`, с которым возникают некоторые проблемы в разных браузерах. Обычно, проверяются не `TR`, поэтому всё ок :).</li>
<li>Считает элемент видимым, даже если позиционирован за пределами экрана или имеет свойство `visibility:hidden`.</li>
<li>"Схлопнутый" элемент, например пустой `div` без высоты и ширины, будет считаться невидимым.</li>
</ul>
[/smart]
## Итого
У элементов есть следующие метрики:
<ul>
<li>`clientWidth/clientHeight` -- ширина/высота видимой области, включая поля, но не полосы прокрутки.</li>
<li>`clientLeft/clientTop` -- ширина левой/верхней рамки или, точнее, сдвиг клиентской области, относительно верхнего левого угла блока.
Используется, преимущественно, в IE<8 для вычисления сдвига `document.body`.
</li>
<li>`scrollWidth/scrollHeight` -- ширина/высота прокручиваемой области. Включает в себя `padding` и не включает полосы прокрутки.</li>
<li>`scrollLeft/scrollTop` -- ширина/высота прокрученной части документа, считается от верхнего левого угла.</li>
<li>`offsetWidth/offsetHeight` -- "внешняя" ширина/высота блока, не считая отступов.</li>
<li>`offsetParent` -- "родитель по дереву рендеринга" -- ближайшая ячейка таблицы, body для статического позиционирования или ближайший позиционированный элемент для других типов позиционирования.</li>
<li>`offsetLeft/offsetTop` -- позиция в пикселях левого верхнего угла блока, относительно его `offsetParent`.</li>
</ul>
Все свойства, кроме `scrollLeft/scrollTop` доступны только для чтения. Изменение этих свойств заставляет браузер прокручивать элемент.
Краткая схема:
<img src="summary.png">
**Прокрутку *элемента* можно прочитать или изменить через свойства `scrollLeft/Top`.**
В этой главе мы считали, что страница находится в режиме соответствия стандартам. В режиме совместимости -- всё так же, но некоторые старые браузеры требуют `document.body` вместо `documentElement`.

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

View file

@ -0,0 +1,29 @@
<!DOCTYPE HTML>
<html>
<head><meta charset="utf-8"></head>
<body>
<div id="elem" style="overflow-y:scroll;width:300px;height:200px;border:1px solid black">
текст текст текст текст текст текст текст текст текст текст текст текст текст текст
текст текст текст текст текст текст текст текст текст текст текст текст текст текст
текст текст текст текст текст текст текст текст текст текст текст текст текст текст
текст текст текст текст текст текст текст текст текст текст текст текст текст текст
текст текст текст текст текст текст текст текст текст текст текст текст текст текст
текст текст текст текст текст текст текст текст текст текст текст текст текст текст
текст текст текст текст текст текст текст текст текст текст текст текст текст текст
</div>
<script>
function getWidth() {
var style = window.getComputedStyle ? getComputedStyle(elem, '') : elem.currentStyle;
return style.width;
}
// FF: 200, Ch/Sa: 184, Op: 200, IE9: 184, IE7,8:200
</script>
У элемента стоит <code>style="width:300px"</code><br>
<button onclick="alert(getWidth())">alert(width из getComputedStyle/currentStyle)</button>
</body>
</html>

View file

@ -0,0 +1,95 @@
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
<script src="http://code.jquery.com/jquery.min.js"></script>
<style type="text/css">
* {
margin: 0;
padding: 0;
}
#example {
width: 300px;
height: 200px;
overflow: auto;
border: 25px solid #F0E68C;
padding: 20px;
margin: 20px;
}
.key {
cursor: pointer;
text-decoration: underline;
}
</style>
</head>
<body>
<div id="example">
<h3>Introduction</h3>
<p>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 Internet Explorer 3.0.</p>
<p>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.</p>
<p>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.</p>
<p>The third edition of the Standard introduced powerful regular expressions, better string handling, new control statements, try/catch exception handling, tighter definition of errors, formatting for numeric output and minor changes in anticipation of forthcoming internationalisation facilities and future language growth. The third edition of the ECMAScript standard was adopted by the Ecma General Assembly of December 1999 and published as ISO/IEC 16262:2002 in June 2002.</p>
</div>
<div id="mouse-wrap">Координаты мыши: <span id="mouse">...</span></div>
<div id="info"></div>
<script>
var example = document.getElementById('example')
$(function() {
var info = document.getElementById('info')
var props = {
'размеры':
['clientLeft','clientTop', 'clientWidth','clientHeight','offsetWidth','offsetHeight','scrollWidth', 'scrollHeight'],
'прокрутка':
['scrollLeft','scrollTop'] ,
'позиционирование по рендерингу':
['offsetParent', 'offsetLeft','offsetTop']
}
info.innerHTML = '<h3>Нажмите для просмотра значения:</h3>';
for (var k in props) {
info.innerHTML += '<h4>' + k + '</h4>';
var prop = props[k];
for (var i = 0; i < prop.length; i++) {
info.innerHTML += "<span class='key'>"+prop[i]+'</span>: <span id="'+prop[i]+'">&nbsp;</span>' + " "
i++;
if (i<prop.length) {
info.innerHTML += "<span class='key'>"+prop[i]+'</span>: <span id="'+prop[i]+'">&nbsp;</span>';
}
info.innerHTML += "<br/>";
}
}
$(document).delegate('span.key','click', function() {
var prop = this.innerHTML;
var value = example[prop];
value = value.tagName || value;
$('#'+prop).html(value);
})
})
document.onmousemove = function(e) {
e = e || window.event;
document.getElementById('mouse').innerHTML = Math.round(e.clientX)+':'+Math.round(e.clientY);
}
</script>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

View file

@ -0,0 +1,47 @@
`top` -- можно кроссбраузерно получить, как указано в главе [](/metrics-window):
```js
function getDocumentScrollTop() {
var html = document.documentElement;
var body = document.body;
var scrollTop = html.scrollTop || body && body.scrollTop || 0;
scrollTop -= html.clientTop; // IE<8
return scrollTop;
}
```
`bottom` -- это `top` плюс высота видимой части:
```js
function getDocumentScrollBottom() {
return getDocumentScrollTop() + document.documentElement.clientHeight;
}
```
Полная высота -- максимум двух значений, детали см. в [](/metrics-window):
```js
function getDocumentScrollHeight() {
var scrollHeight = document.documentElement.scrollHeight;
var clientHeight = document.documentElement.clientHeight;
scrollHeight = Math.max(scrollHeight, clientHeight);
return scrollHeight;
}
```
Итого, ответ, использующий описанные выше функции:
```js
function getDocumentScroll() {
return {
top: getDocumentScrollTop(),
bottom: getDocumentScrollBottom(),
height: getDocumentScrollHeight()
};
}
```

View file

@ -0,0 +1,15 @@
# Получить прокрутки документа
[importance 5]
Напишите функцию `getDocumentScroll()`, которая возвращает объект с координатами области видимости относительно документа.
Свойства объекта результата:
<ul>
<li>`top` -- координата верхней границы видимой части (относительно документа).</li>
<li>`bottom` -- координата нижней границы видимой части (относительно документа).</li>
<li>`height` -- полная высота документа, включая прокрутку.</li>
</ul>
В задаче можно учитывать только вертикальную прокрутку (горизонтальную отдельно нет смысла разбирать, она делается аналогично, а нужна сильно реже).

View file

@ -0,0 +1,209 @@
# Размеры и прокрутка страницы
Многие метрики для страницы работают совсем не так, как для элементов. Поэтому рассмотрим решения типичных задач для страницы отдельно.
[cut]
## Ширина/высота видимой части окна
Свойства `clientWidth/Height` для элемента `document.documentElement` позволяют получить ширину/высоту видимой области окна.
Например, кнопка ниже выведет размер такой области для этой страницы:
<button onclick="alert(document.documentElement.clientHeight)">alert(document.documentElement.clientHeight)</button>
Этот способ -- кросс-браузерный.
## Ширина/высота всей страницы, с учётом прокрутки
Если прокрутка на странице присутствует, то полные размеры страницы можно взять в `document.documentElement.scrollWidth/scrollHeight`.
Проблемы с этими свойствами возникают, когда *прокрутка то есть, то нет*. В этом случае они работают некорректно.
В браузерах Chrome/Safari и Opera при отсутствии прокрутки значение `document.documentElement.scrollHeight` в этом случае может быть даже меньше, чем `document.documentElement.clientHeight` (нонсенс!). Эта проблема -- именно для `document.documentElement`, то есть для всей страницы. С обычными элементами здесь всё в порядке.
Надёжно определить размер с учетом прокрутки можно, взяв максимум из двух свойств:
```js
//+ run
var scrollHeight = document.documentElement.scrollHeight;
var clientHeight = document.documentElement.clientHeight;
*!*
scrollHeight = Math.max(scrollHeight, clientHeight);
*/!*
alert('Высота с учетом прокрутки: ' + scrollHeight);
```
## Прокрутка страницы [#page-scroll]
### Получение текущей прокрутки
Значение текущей прокрутки страницы хранится в свойствах `window.pageXOffset/pageYOffset`.
Но эти свойства:
<ul>
<li>Не поддерживаются IE<9</li>
<li>Их можно только читать, а менять нельзя.</li>
</ul>
Поэтому для кросс-браузерности рассмотрим другой способ -- свойство `document.documentElement.scrollLeft/Top`.
<ul>
<li>`document.documentElement` содержит значение прокрутки, если стоит правильный DOCTYPE. Это работает во всех браузерах, кроме Safari/Chrome.</li>
<li>Safari/Chrome используют вместо этого `document.body` (это баг в Webkit).</li>
<li>В режиме совместимости (если некорректный DOCTYPE) некоторые браузеры также используют `document.body`.</li>
</ul>
Таким образом, для IE8+ и других браузеров, работающих в режиме соответствия стандартам, получить значение прокрутки можно так:
```js
//+ run
var scrollTop = window.pageYOffset || document.documentElement.scrollTop;
alert("Текущая прокрутка: " + scrollTop);
```
### С учётом IE7- и Quirks Mode [#getPageScroll]
Если дополнительно нужна поддержка IE<8, то там тоже есть важная тонкость. Документ может быть смещен относительно начальной позиции (0,0). Это смещение хранится в `document.documentElement.clientLeft/clientTop`, и мы должны вычесть его.
Если дополнительно добавить возможность работы браузера в Quirks Mode, то надёжный способ будет таким:
```js
//+ run
var html = document.documentElement;
var body = document.body;
var scrollTop = html.scrollTop || body && body.scrollTop || 0;
scrollTop -= html.clientTop;
alert("Текущая прокрутка: " + scrollTop);
```
Итого, можно создать кросс-браузерную функцию, которая возвращает значения прокрутки и поддерживает в том числе IE8-:
```js
var getPageScroll = (window.pageXOffset != undefined) ?
function() {
return {
left: pageXOffset,
top: pageYOffset
};
} :
function() {
var html = document.documentElement;
var body = document.body;
var top = html.scrollTop || body && body.scrollTop || 0;
top -= html.clientTop;
var left = html.scrollLeft || body && body.scrollLeft || 0;
left -= html.clientLeft;
return { top: top, left: left };
}
```
### Изменение прокрутки: scrollTo, scrollBy, scrollIntoView [#window-scroll]
[smart]
Чтобы прокрутить страницу при помощи JavaScript, её DOM должен быть полностью загружен.
[/smart]
На обычных элементах свойства `scrollTop/scrollLeft` можно изменять, и при этом элемент будет прокручиваться.
Никто не мешает точно так же поступать и со страницей. Во всех браузерах, кроме Chrome/Safari можно осуществить прокрутку установкой `document.documentElement.scrollTop`, а в Chrome/Safari -- использовать для этого `document.body.scrollTop`. И будет работать.
Но есть и другое, полностью кросс-браузерное решение -- специальные методы прокрутки страницы [window.scrollBy(x,y)](https://developer.mozilla.org/en/Window.scrollBy) и [window.scrollTo(pageX,pageY)](https://developer.mozilla.org/en/Window.scrollTo).
<ul>
<li>**Метод `scrollBy(x,y)` прокручивает страницу относительно текущих координат.**
Например, кнопка ниже прокрутит страницу на `10px` вниз:
<button onclick="window.scrollBy(0,10)">window.scrollBy(0,10)</button>
</li>
<li>**Метод `scrollTo(pageX,pageY)` прокручивает страницу к указанным координатам относительно документа.** Он эквивалентен установке свойств `scrollLeft/scrollTop`.
Чтобы прокрутить в начало документа, достаточно указать координаты `(0,0)`:
<button onclick="window.scrollTo(0,0)">window.scrollTo(0,0)</button>
</li>
</ul>
Для полноты картины рассмотрим также метод [elem.scrollIntoView(top)](https://developer.mozilla.org/en/DOM/element.scrollIntoView).
Метод `elem.scrollIntoView(top)` вызывается на элементе и прокручивает страницу так, чтобы элемент оказался вверху, если параметр `top` равен `true`, и внизу, если `top` равен `false`. Причем, если параметр `top` не указан, то он считается равным `true`.
Кнопка ниже прокрутит страницу так, чтобы кнопка оказалась вверху:
<button onclick="this.scrollIntoView()">this.scrollIntoView()</button>
А следующая кнопка прокрутит страницу так, чтобы кнопка оказалась внизу:
<button onclick="this.scrollIntoView(false)">this.scrollIntoView(false)</button>
## Запрет прокрутки
Иногда бывает нужно временно сделать документ "непрокручиваемым". Например, при показе большого диалогового окна над документом -- чтобы посетитель мог прокручивать это окно, но не документ.
**Чтобы запретить прокрутку страницы, достаточно поставить `document.body.style.overflow = "hidden"`.**
При этом страница замрёт в текущем положении. Попробуйте сами:
<button onclick="document.body.style.overflow = 'hidden'">`document.body.style.overflow = 'hidden'`</button>
<button onclick="document.body.style.overflow = ''">`document.body.style.overflow = ''`</button>
При нажатии на верхнюю кнопку страница замрёт на текущем положении прокрутки. После нажатия на нижнюю -- прокрутка возобновится.
**Вместо `document.body` может быть любой элемент, прокрутку которого необходимо запретить.**
Недостатком этого способа является то, что сама полоса прокрутки исчезает. Если она занимала некоторую ширину, то теперь эта ширина освободится, и содержимое страницы расширится, заняв её место. Такая перерисовка иногда выглядит как "прыжок" страницы. Это может быть не очень красиво, но обходится, если вычислить размер прокрутки и добавить `padding-right`.
## Итого
Размеры:
<ul>
<li>Для получения размеров видимой части окна: `document.documentElement.clientWidth/Height`
</li>
<li>Для получения размеров страницы с учётом прокрутки:
```js
var scrollHeight = document.documentElement.scrollHeight;
var clientHeight = document.documentElement.clientHeight;
*!*
scrollHeight = Math.max(scrollHeight, clientHeight);
*/!*
```
</li>
</ul>
**Прокрутка окна:**
<ul>
<li>Прокрутку окна можно *получить* как `window.pageYOffset` (для горизонтальной -- `window.pageXOffset`) везде, кроме IE<9.
Для кросс-браузерности используется другой способ:
```js
//+ run
var html = document.documentElement;
var body = document.body;
var scrollTop = html.scrollTop || body && body.scrollTop || 0;
scrollTop -= html.clientTop; // IE<8
alert("Текущая прокрутка: " + scrollTop);
```
</li>
<li>Установить прокрутку можно при помощи специальных методов:
<ul>
<li>`window.scrollTo(pageX,pageY)` -- абсолютные координаты,</li>
<li>`window.scrollBy(x,y)` -- прокрутить относительно текущего места.</li><li>`elem.scrollIntoView(top)` -- прокрутить, чтобы элемент `elem` стал виден.</li>
</ul>
</li>
</ul>

View file

@ -0,0 +1,42 @@
# Координаты внешних углов
Координаты элемента возвращаются функцией [elem.getBoundingClientRect](https://developer.mozilla.org/en-US/docs/DOM/element.getBoundingClientRect). Она возвращает все координаты относительно окна в виде объекта со свойствами `left`, `top`, `right`, `bottom`. Некоторые браузеры также добавляют `width`, `height`.
Так что координаты верхнего-левого `coords1` и правого-нижнего `coords4` внешних углов:
```js
var coords = elem.getBoundingClientRect();
var coords1 = [coords.left, coords.top];
var coords2 = [coords.right, coords.bottom];
```
# Левый-верхний угол внутри
Этот угол отстоит от наружных границ на размер рамки, который доступен через `clientLeft/clientTop`:
```js
var coords3 = [coords.left + field.clientLeft, coords.top + field.clientTop];
```
# Правый-нижний угол внутри
Этот угол отстоит от правой-нижней наружной границы на размер рамки. Так как нужная рамка находится справа-внизу, то специальных свойств для нее нет, но мы можем получить этот размер из CSS:
```js
var coords4 = [
coords.right - parseInt(getComputedStyle(field).borderRightWidth) ,
coords.bottom - parseInt(getComputedStyle(field).borderBottomWidth)
]
```
Можно получить их альтернативным путем, прибавив `clientWidth/clientHeight` к координатам левого-верхнего внутреннего угла. Получится то же самое, пожалуй даже быстрее и изящнее.
```js
var coords4 = [
coords.left + elem.clientLeft + elem.clientWidth ,
coords.top + elem.clientTop + elem.clientHeight
]
```
[edit src="solution"]Полный код решения[/edit]

View file

@ -0,0 +1,27 @@
body {
padding: 20px 0 0 20px;
cursor: pointer;
}
#field {
overflow: hidden;
width: 200px;
height: 150px;
border-top: 10px solid black;
border-right: 10px solid gray;
border-bottom: 10px solid gray;
border-left: 10px solid black;
background-color: #00FF00;
font: 10px/1.2 monospace;
}
.triangle-right {
position: relative;
width: 0;
height: 0;
border-top: 6px solid transparent;
border-bottom: 6px solid transparent;
border-left: 20px solid red;
text-indent: -20px;
font: 12px/1 monospace;
}

View file

@ -0,0 +1,62 @@
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
<link rel="stylesheet" href="index.css">
<script>
document.onclick = function(e) { // выводит текущие координаты при клике
document.getElementById('coords').innerHTML = e.clientX + ':' + e.clientY;
};
</script>
</head>
<body>
Кликните на любое место, чтобы получить координаты относительно окна.<br>
Это для удобства тестирования, чтобы проверить результат, который вы получите из DOM.<br>
<div id="coords">(координаты появятся тут)</div>
<div id="field">
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
</div>
<div class="triangle-right" style="left:-20px;top:-176px">1</div>
<div class="triangle-right" style="left:-10px;top:-178px">3</div>
<div class="triangle-right" style="left:190px;top:-40px">4</div>
<div class="triangle-right" style="left:200px;top:-42px">2</div>
<script>
var field = document.getElementById('field');
// полное решение - в массиве result
var fieldCoords = field.getBoundingClientRect();
var result = [
[ // 1
fieldCoords.left,
fieldCoords.top
],
[ // 2
fieldCoords.right,
fieldCoords.bottom
],
[ // 3
fieldCoords.left + field.clientLeft,
fieldCoords.top + field.clientTop
],
[ // 4
fieldCoords.left + field.clientLeft + field.clientWidth,
fieldCoords.top + field.clientTop + field.clientHeight
]
];
alert(result.join(' '));
</script>
</body>
</html>

View file

@ -0,0 +1,27 @@
body {
padding: 20px 0 0 20px;
cursor: pointer;
}
#field {
overflow: hidden;
width: 200px;
height: 150px;
border-top: 10px solid black;
border-right: 10px solid gray;
border-bottom: 10px solid gray;
border-left: 10px solid black;
background-color: #00FF00;
font: 10px/1.2 monospace;
}
.triangle-right {
position: relative;
width: 0;
height: 0;
border-top: 6px solid transparent;
border-bottom: 6px solid transparent;
border-left: 20px solid red;
text-indent: -20px;
font: 12px/1 monospace;
}

View file

@ -0,0 +1,39 @@
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
<link rel="stylesheet" href="index.css">
<script>
document.onclick = function(e) { // выводит текущие координаты при клике
document.getElementById('coords').innerHTML = e.clientX + ':' + e.clientY;
};
</script>
</head>
<body>
Кликните на любое место, чтобы получить координаты относительно окна.<br>
Это для удобства тестирования, чтобы проверить результат, который вы получите из DOM.<br>
<div id="coords">(координаты появятся тут)</div>
<div id="field">
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
</div>
<div class="triangle-right" style="left:-20px;top:-176px">1</div>
<div class="triangle-right" style="left:-10px;top:-178px">3</div>
<div class="triangle-right" style="left:190px;top:-40px">4</div>
<div class="triangle-right" style="left:200px;top:-42px">2</div>
<script>
var field = document.getElementById('field');
// ваш код...
</script>
</body>
</html>

View file

@ -0,0 +1,25 @@
# Найдите координаты точки в документе
[importance 5]
В ифрейме ниже вы видите документ с зеленым "полем".
При помощи JavaScript найдите координаты указанных стрелками углов относительно окна браузера.
Для тестирования в документ добавлено удобство: клик в любом месте отображает координаты мыши относительно окна.
[iframe border=1 height=360 src="source" link edit]
Ваш код должен при помощи DOM получить четыре пары координат:
<ol>
<li>Левый-верхний угол снаружи, это просто.</li>
<li>Правый-нижний угол снаружи, это тоже просто.</li>
<li>Левый-верхний угол внутри, это чуть сложнее.</li>
<li>Правый-нижний угол внутри, это ещё сложнее, но можно сделать даже несколькими способами.</li>
</ol>
Они должны совпадать с координатами, которые вы получите кликом по полю.
P.S. Код не должен быть как-то привязан к конкретным размерам элемента, стилям, наличию или отсутствию рамки.
[edit src="source" task/]

View file

@ -0,0 +1 @@
[edit src="solution"]Открыть в песочнице[/edit]

Some files were not shown because too many files have changed in this diff Show more