renovations

This commit is contained in:
Ilya Kantor 2015-02-18 21:23:40 +03:00
parent 0eec1aaccb
commit c018a2db03
56 changed files with 459 additions and 594 deletions

View file

@ -112,7 +112,7 @@ var oranges = "3";
alert( +apples + +oranges); // 5, число, оба операнда предварительно преобразованы в числа alert( +apples + +oranges); // 5, число, оба операнда предварительно преобразованы в числа
``` ```
С точки зрения математики такое изобилие плюсов может показаться странном. С точки зрения программирования -- никаких разночтений: сначала выполнятся унарные плюсы, приведут строки к числам, а затем -- бинарный `'+'` их сложит. С точки зрения математики такое изобилие плюсов может показаться странным. С точки зрения программирования -- никаких разночтений: сначала выполнятся унарные плюсы, приведут строки к числам, а затем -- бинарный `'+'` их сложит.
Почему унарные плюсы выполнились до бинарного сложения? Как мы сейчас увидим, дело в их приоритете. Почему унарные плюсы выполнились до бинарного сложения? Как мы сейчас увидим, дело в их приоритете.

View file

@ -1,8 +1,8 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?> <?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="723px" height="562px" viewBox="0 0 723 562" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:sketch="http://www.bohemiancoding.com/sketch/ns"> <svg width="723px" height="562px" viewBox="0 0 723 562" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:sketch="http://www.bohemiancoding.com/sketch/ns">
<!-- Generator: bin/sketchtool 1.3 (252) - http://www.bohemiancoding.com/sketch --> <!-- Generator: Sketch 3.2.2 (9983) - http://www.bohemiancoding.com/sketch -->
<title>code-style.svg</title> <title>code-style.svg</title>
<desc>Created with bin/sketchtool.</desc> <desc>Created with Sketch.</desc>
<defs></defs> <defs></defs>
<g id="combined" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" sketch:type="MSPage"> <g id="combined" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" sketch:type="MSPage">
<g id="code-style.svg" sketch:type="MSArtboardGroup"> <g id="code-style.svg" sketch:type="MSArtboardGroup">

Before

Width:  |  Height:  |  Size: 126 KiB

After

Width:  |  Height:  |  Size: 126 KiB

Before After
Before After

View file

@ -78,12 +78,9 @@ div.append('<span/>');
В итоге, после вызова получается два независимых `div'а`: первый содержит `img` (этот неявный клон никуда не присвоен), а второй -- наш `span`. В итоге, после вызова получается два независимых `div'а`: первый содержит `img` (этот неявный клон никуда не присвоен), а второй -- наш `span`.
Злая магия? Плохой феншуй? Объяснения не очень понятны? Написано что-то странное? Это просто разум, привыкший, что соглашения уважаются, не допускает мысли, что вызов `wrap` -- неявно клонирует элемент. Ведь другие jQuery-методы, кроме `clone` этого не делают.
Ничего подобного, просто избирательное следование соглашениям. Вызов `wrap` -- неявно клонирует элемент.
Такой сюрприз, бесспорно, стоит многих часов отладки.
Как говорил [Учитель](https://ru.wikipedia.org/wiki/%D0%9A%D0%BE%D0%BD%D1%84%D1%83%D1%86%D0%B8%D0%B9): "В древности люди учились для того, чтобы совершенствовать себя. Нынче учатся для того, чтобы удивить других".
## Краткость -- сестра таланта! ## Краткость -- сестра таланта!

View file

@ -1,19 +0,0 @@
Ответ: `5`.
```js
//+ run untrusted
var a = 5;
function a() { }
alert(a);
```
Чтобы понять, почему -- разберём внимательно как работает этот код.
<ol>
<li>До начала выполнения создаётся переменная `a` и функция `a`. Стандарт написан так, что функция создаётся первой и переменная ее не перезаписывает. То есть, функция имеет приоритет. Но в данном случае это совершенно неважно, потому что...
</li>
<li>...После инициализации, когда код начинает выполняться -- срабатывает присваивание `a = 5`, перезаписывая `a`, и уже не важно, что там лежало.</li>
<li>Объявление `Function Declaration` на стадии выполнения игнорируется (уже обработано).</li>
<li>В результате `alert(a)` выводит 5.</li>
</ol>

View file

@ -1,15 +0,0 @@
# Функция и переменная
[importance 3]
Каков будет результат кода? Почему?
```js
var a = 5;
function a() { }
alert(a);
```
P.S. Это задание -- учебное, на понимание процесса инициализации и выполнения. В реальной жизни мы, конечно же, не будем называть переменную и функцию одинаково.

View file

@ -213,7 +213,7 @@ ul.appendChild(fragment); // вместо фрагмента вставятс
<html> <html>
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<script src="//polyfill.webservices.ft.com/v1/polyfill.js?features=Element.prototype.mutation"></script> <script src="https://cdn.polyfill.io/v1/polyfill.js?features=Element.prototype.mutation"></script>
</head> </head>
<body> <body>

View file

@ -69,9 +69,7 @@
<li>`event.target` будет содержать ссылку на конкретный элемент внутри формы, самый вложенный, на котором произошёл клик.</li> <li>`event.target` будет содержать ссылку на конкретный элемент внутри формы, самый вложенный, на котором произошёл клик.</li>
</ul> </ul>
[online]
[codetabs height=220 src="bubble-target"] [codetabs height=220 src="bubble-target"]
[/online]
Возможна и ситуация, когда `event.target` и `this` -- один и тот же элемент, например если в форме нет других тегов и клик был на самом элементе `<form>`. Возможна и ситуация, когда `event.target` и `this` -- один и тот же элемент, например если в форме нет других тегов и клик был на самом элементе `<form>`.
@ -148,11 +146,15 @@
Стадия цели, обозначенная на рисунке цифрой `(2)`, особо не обрабатывается, так как обработчики, назначаемые обоими этими способами, срабатывают также на целевом элементе. Стадия цели, обозначенная на рисунке цифрой `(2)`, особо не обрабатывается, так как обработчики, назначаемые обоими этими способами, срабатывают также на целевом элементе.
[smart header="Есть события, которые не всплывают, но которые можно перехватить"]
Бывают события, которые можно поймать только на стадии перехвата, а на стадии всплытия -- нельзя..
Например, таково событие фокусировки на элементе [onfocus](/focus-blur). Конечно, это большая редкость, такое исключение существует по историческим причинам.
[/smart]
## Примеры ## Примеры
В примере ниже на `form, div, p` стоят те же обработчики, что и раньше, но на этот раз -- на стадии погружения. В примере ниже на `<form>`, `<div>`, `<p>` стоят те же обработчики, что и раньше, но на этот раз -- на стадии погружения. Чтобы увидеть перехват в действии, кликните в нём на элементе `<p>`:
Чтобы увидеть перехват в действии, кликните на элементе `P`:
[codetabs height=220 src="capture"] [codetabs height=220 src="capture"]
@ -180,20 +182,14 @@ for(var i=0; i<elems.length; i++) {
} }
``` ```
Кликните по внутреннему элементу `P`, чтобы увидеть порядок прохода события: Кликните по внутреннему элементу `<p>`, чтобы увидеть порядок прохода события:
[codetabs height=220 src="both"] [codetabs height=220 src="both"]
Должно быть `FORM` -> `DIV` -> `P` -> `P` -> `DIV` -> `FORM`. Заметим, что элемент `P` участвует в обоих стадиях. Должно быть `FORM` -> `DIV` -> `P` -> `P` -> `DIV` -> `FORM`. Заметим, что элемент `<p>` участвует в обоих стадиях.
Как видно из примера, один и тот же обработчик можно назначить на разные стадии. При этом номер текущей стадии он, при необходимости, может получить из свойства `event.eventPhase` (=1, если погружение, =3, если всплытие). Как видно из примера, один и тот же обработчик можно назначить на разные стадии. При этом номер текущей стадии он, при необходимости, может получить из свойства `event.eventPhase` (=1, если погружение, =3, если всплытие).
[smart header="Есть события, которые не всплывают, но которые можно перехватить"]
Бывают события, которые можно поймать только на стадии перехвата, а на стадии всплытия -- нельзя..
Например, таково событие фокусировки на элементе [onfocus](/focus-blur). Конечно, это большая редкость, такое исключение существует по историческим причинам.
[/smart]
## Отличия IE8- ## Отличия IE8-

View file

@ -27,7 +27,7 @@ Shift+Клик добавляет промежуток от последнего
<script> <script>
var ul = document.getElementsByTagName('ul')[0]; var ul = document.querySelector('ul');
var lastClickedLi = null; var lastClickedLi = null;
@ -51,6 +51,10 @@ ul.onclick = function(event) {
lastClickedLi = target; lastClickedLi = target;
} }
ul.onmousedown = function() {
return false;
};
// --- функции для выделения --- // --- функции для выделения ---
function toggleSelect(li) { function toggleSelect(li) {
@ -60,7 +64,7 @@ function toggleSelect(li) {
function selectFromLast(target) { function selectFromLast(target) {
var startElem = lastClickedLi || ul.children[0]; var startElem = lastClickedLi || ul.children[0];
var isLastClickedBefore = compareDocumentPosition(startElem, target) & 4; var isLastClickedBefore = startElem.compareDocumentPosition(target) & 4;
if (isLastClickedBefore) { if (isLastClickedBefore) {
for(var elem = startElem; elem != target; elem = elem.nextElementSibling) { for(var elem = startElem; elem != target; elem = elem.nextElementSibling) {
@ -89,20 +93,6 @@ function selectSingle(li) {
} }
// --- вспомогательная функция ---
// http://learn.javascript.ru/compare-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);
}
</script> </script>
</body> </body>
</html> </html>

View file

@ -26,22 +26,8 @@ Shift+Клик добавляет промежуток от последнего
</ul> </ul>
<script> <script>
// ... ваш код // ... ваш код
</script>
// --- вспомогательная функция, может понадобиться для части 3 ---
// http://learn.javascript.ru/compare-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);
}
</body> </body>
</html> </html>

View file

@ -2,7 +2,7 @@
<html> <html>
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<script src="http://polyfill.webservices.ft.com/v1/polyfill.js?features=Element.prototype.closest"></script> <script src="https://cdn.polyfill.io/v1/polyfill.js?features=Element.prototype.closest"></script>
<script src="DragManager.js"></script> <script src="DragManager.js"></script>
<link rel="stylesheet" href="dragDemo.css"> <link rel="stylesheet" href="dragDemo.css">
<script> <script>

View file

@ -14,9 +14,8 @@
height: 100%; height: 100%;
background-color: gray; background-color: gray;
opacity: 0.3; opacity: 0.3;
filter: alpha(opacity=30);
} }
``` ```
Самой форме нужно, естественно, дать еще больший `z-index`, чтобы она была над `DIV'ом`. Мы не помещаем форму в контейнер, чтобы она не унаследовала полупрозрачность. Самой форме можно дать еще больший `z-index`, чтобы она была над `DIV'ом`. Мы не помещаем форму в контейнер, чтобы она не унаследовала полупрозрачность.

View file

@ -10,14 +10,12 @@ html, body {
height: 100%; height: 100%;
} }
#prompt-form { #prompt-form {
display: inline-block; display: inline-block;
padding: 5px 5px 5px 70px; padding: 5px 5px 5px 70px;
width: 200px; width: 200px;
border: 1px solid black; border: 1px solid black;
background: white url(prompt.png) no-repeat left 5px; background: white url(https://js.cx/clipart/prompt.png) no-repeat left 5px;
vertical-align: middle; vertical-align: middle;
} }
@ -48,7 +46,6 @@ html, body {
height: 100%; height: 100%;
background-color: gray; background-color: gray;
opacity: 0.3; opacity: 0.3;
filter: alpha(opacity=30);
} }
#prompt-form input[name="text"] { #prompt-form input[name="text"] {
@ -115,7 +112,6 @@ function showPrompt(text, callback) {
}; };
document.onkeydown = function(e) { document.onkeydown = function(e) {
e = e || event;
if (e.keyCode == 27) { // escape if (e.keyCode == 27) { // escape
complete(null); complete(null);
} }

View file

@ -29,6 +29,6 @@ showPrompt("Введите что-нибудь<br>... умное :)", function(v
Демо в ифрейме: Демо в ифрейме:
[iframe src="solution" height=160 border=1] [iframe src="solution" height=160 border=1]
Исходный HTML/CSS для формы с готовым fixed-позиционированием: Исходный HTML/CSS для формы с готовым fixed-позиционированием - в песочнице.

View file

@ -42,7 +42,7 @@
```html ```html
<!--+ autorun height=80 --> <!--+ autorun height=80 -->
<form onsubmit="alert('submit');return false"> <form onsubmit="alert('submit');return false">
<input type="text" size="30" value="Нажми здесь Enter"> <input type="text" size="30" value="При нажатии Enter будет click">
<input type="submit" value="Submit" *!*onclick="alert('click')"*/!*> <input type="submit" value="Submit" *!*onclick="alert('click')"*/!*>
</form> </form>
``` ```
@ -50,9 +50,7 @@
[/smart] [/smart]
[warn header="В IE8- событие `submit` не всплывает"] [warn header="В IE8- событие `submit` не всплывает"]
В IE8- событие `submit` не всплывает. Впрочем, если вешать обработчик `submit` на сам элемент формы, без использования делегирования, то это не создаёт проблем. В IE8- событие `submit` не всплывает. Нужно вешать обработчик `submit` на сам элемент формы, без использования делегирования.
</li>
</ul>
[/warn] [/warn]

View file

@ -4,9 +4,8 @@
Для надёжности необходима простота. Для надёжности необходима простота.
[/quote] [/quote]
В современной JavaScript-разработке используются фреймворки, которые дают готовые библиотеки и правила для написания кода. Эта глава не ставит своей целью их все заменить (зачем?), а равно как и научить какому-нибудь фреймворку (не факт, что он для вас лучший). В современной JavaScript-разработке используются фреймворки, которые дают готовые библиотеки и правила для написания кода. Эта глава не ставит своей целью их все заменить, а равно как и научить какому-нибудь фреймворку.
Вместо этого мы разберём основные средства для построения архитектуры и при помощи чистого JavaScript построим компоненты интерфейса с их использованием. Вместо этого мы разберём основные средства для построения архитектуры, и при помощи чистого JavaScript построим компоненты интерфейса с их использованием.
Это во-первых покажет, что и без фреймворков есть жизнь, а во-вторых даст фундамент для освоения любого фреймворка, какой бы вы ни выбрали. Это во-первых покажет, что и без фреймворков есть жизнь, а во-вторых даст фундамент для освоения любого фреймворка, какой бы вы ни выбрали.

View file

@ -8,15 +8,15 @@ function Clock(options) {
var hours = date.getHours(); var hours = date.getHours();
if (hours < 10) hours = '0' + hours; if (hours < 10) hours = '0' + hours;
$('.hour', elem).html(hours); elem.querySelector('.hour').innerHTML = hours;
var min = date.getMinutes(); var min = date.getMinutes();
if (min < 10) min = '0' + min; if (min < 10) min = '0' + min;
$('.min', elem).html(min); elem.querySelector('.min').innerHTML = min;
var sec = date.getSeconds(); var sec = date.getSeconds();
if (sec < 10) sec = '0' + sec; if (sec < 10) sec = '0' + sec;
$('.sec', elem).html(sec); elem.querySelector('.sec').innerHTML = sec;
} }
this.stop = function() { this.stop = function() {
@ -26,6 +26,6 @@ function Clock(options) {
this.start = function() { this.start = function() {
render(); render();
timer = setInterval(render, 1000); timer = setInterval(render, 1000);
} };
} }

View file

@ -2,7 +2,6 @@
<html> <html>
<head> <head>
<title>Часики</title> <title>Часики</title>
<script src="http://code.jquery.com/jquery.min.js"></script>
<script src="clock.js"></script> <script src="clock.js"></script>
<meta charset="utf-8"> <meta charset="utf-8">
<style> <style>
@ -20,7 +19,7 @@
<script> <script>
var pageClock = new Clock({ var pageClock = new Clock({
elem: $('#clock') elem: document.getElementById('clock')
}); });
</script> </script>

View file

@ -2,7 +2,6 @@
<html> <html>
<head> <head>
<title>Часики</title> <title>Часики</title>
<script src="http://code.jquery.com/jquery.min.js"></script>
<meta charset="utf-8"> <meta charset="utf-8">
<style> <style>
.hour { color: green } .hour { color: green }
@ -21,7 +20,7 @@
// .. ваш код Clock // .. ваш код Clock
var pageClock = new Clock({ var pageClock = new Clock({
elem: $('#clock') elem: document.getElementById('clock')
}); });
</script> </script>

View file

@ -20,5 +20,5 @@ clock.stop(); // стоп
При нажатии на `alert` часы должны приостанавливаться, а затем продолжать идти с правильным временем. При нажатии на `alert` часы должны приостанавливаться, а затем продолжать идти с правильным временем.
Пример результата: Пример результата:
[iframe src="solution" border=1] [iframe src="solution" height=80]

View file

@ -1,2 +1 @@
Пример переписанного слайдера:

View file

@ -3,8 +3,7 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<script src="https://cdn.polyfill.io/v1/polyfill.js?features=Element.prototype.closest"></script>
<script src="http://code.jquery.com/jquery.min.js"></script>
<style> <style>
.slider { .slider {
@ -36,68 +35,68 @@
</div> </div>
<script> <script>
var slider = new Slider({ var slider = new Slider({
elem: $('#slider') elem: document.getElementById('slider')
}); });
function Slider(options) { function Slider(options) {
var elem = options.elem; var elem = options.elem;
var thumbElem = elem.find('.thumb'); var thumbElem = elem.querySelector('.thumb');
var sliderCoords, thumbCoords, shiftX, shiftY; var sliderCoords, thumbCoords, shiftX, shiftY;
elem.on('dragstart', false) elem.ondragstart = function() {
.on('mousedown', '.thumb', onThumbMouseDown); return false;
};
// --------------- elem.onmousedown = function(event) {
if (event.target.closest('.thumb')) {
function onDocumentMouseMove(e) { startDrag(event.clientX, event.clientY);
moveTo(e.pageX); return false; // disable selection start (cursor change)
}
} }
function onThumbMouseDown(e) { function startDrag(startClientX, startClientY) {
startDrag(e.pageX, e.pageY); thumbCoords = thumbElem.getBoundingClientRect();
return false; // disable selection start (cursor change) shiftX = startClientX - thumbCoords.left;
shiftY = startClientY - thumbCoords.top;
sliderCoords = elem.getBoundingClientRect();
document.addEventListener('mousemove', onDocumentMouseMove);
document.addEventListener('mouseup', onDocumentMouseUp);
}
function moveTo(clientX) {
// вычесть координату родителя, т.к. position: relative
var newLeft = clientX - shiftX - sliderCoords.left;
// курсор ушёл вне слайдера
if(newLeft < 0) {
newLeft = 0;
}
var rightEdge = elem.offsetWidth - thumbElem.offsetWidth;
if(newLeft > rightEdge) {
newLeft = rightEdge;
}
thumbElem.style.left = newLeft + 'px';
}
function onDocumentMouseMove(e) {
moveTo(e.clientX);
} }
function onDocumentMouseUp() { function onDocumentMouseUp() {
endDrag(); endDrag();
} }
// -------------------
function moveTo(pageX) {
// вычесть координату родителя, т.к. position: relative
var newLeft = pageX - shiftX - sliderCoords.left;
// курсор ушёл вне слайдера
if(newLeft < 0) {
newLeft = 0;
}
var rightEdge = elem.width() - thumbElem.width();
if(newLeft > rightEdge) {
newLeft = rightEdge;
}
thumbElem.css('left', newLeft);
}
function startDrag(startPageX, startPageY) {
thumbCoords = thumbElem.offset();
shiftX = startPageX - thumbCoords.left;
shiftY = startPageY - thumbCoords.top;
sliderCoords = elem.offset();
$(document).on({
'mousemove.slider': onDocumentMouseMove,
'mouseup.slider': onDocumentMouseUp
});
}
function endDrag() { function endDrag() {
$(document).off('.slider'); document.removeEventListener('mousemove', onDocumentMouseMove);
document.removeEventListener('mouseup', onDocumentMouseUp);
} }
} }

View file

@ -3,7 +3,7 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<link rel="stylesheet" href="style.css"> <link rel="stylesheet" href="style.css">
<script src="http://code.jquery.com/jquery.min.js"></script> <script src="https://cdn.polyfill.io/v1/polyfill.js?features=Element.prototype.closest"></script>
<script src="listSelect.js"></script> <script src="listSelect.js"></script>
</head> </head>
<body> <body>
@ -24,7 +24,7 @@ Shift+Клик добавляет промежуток от последнего
<script> <script>
var listSelect = new ListSelect({ var listSelect = new ListSelect({
elem: $('ul') elem: document.querySelector('ul')
}); });
</script> </script>

View file

@ -4,11 +4,13 @@ function ListSelect(options) {
var lastClickedLi = null; var lastClickedLi = null;
elem.on('click', 'li', onLiClick); elem.onmousedown = function() {
elem.on('selectstart mousedown', false); return false;
};
function onLiClick(e) { elem.onclick = function(e) {
var li = $(this); var li = e.target.closest('li');
if (!li) return;
if(e.metaKey || e.ctrlKey) { // для Mac проверяем Cmd, т.к. Ctrl + click там контекстное меню if(e.metaKey || e.ctrlKey) { // для Mac проверяем Cmd, т.к. Ctrl + click там контекстное меню
toggleSelect(li); toggleSelect(li);
@ -22,40 +24,46 @@ function ListSelect(options) {
} }
function deselectAll() { function deselectAll() {
elem.children().removeClass('selected'); [].forEach.call(elem.children, function(child) {
child.classList.remove('selected')
});
} }
function toggleSelect(li) { function toggleSelect(li) {
li.toggleClass('selected'); li.classList.toggle('selected');
} }
function selectSingle(li) { function selectSingle(li) {
deselectAll(); deselectAll();
li.addClass('selected'); li.classList.add('selected');
} }
function selectFromLast(target) { function selectFromLast(target) {
var startElem = lastClickedLi || elem.children().first(); var startElem = lastClickedLi || elem.children[0];
target.addClass('selected'); target.classList.add('selected');
if (startElem[0] == target[0]) { if (startElem == target) {
// клик на том же элементе, что и раньше // клик на том же элементе, что и раньше
// это особый случай // это особый случай
return; return;
} }
var isLastClickedBefore = startElem.index() < target.index(); var isLastClickedBefore = startElem.compareDocumentPosition(target) & 4;
if (isLastClickedBefore) { if (isLastClickedBefore) {
startElem.nextUntil(target).add(startElem).addClass('selected'); for(var elem = startElem; elem != target; elem = elem.nextElementSibling) {
elem.classList.add('selected');
}
} else { } else {
startElem.prevUntil(target).add(startElem).addClass('selected'); for(var elem = startElem; elem != target; elem = elem.previousElementSibling) {
elem.classList.add('selected');
}
} }
} }
this.getSelected = function() { this.getSelected = function() {
return elem.children('.selected').map(function() { return [].map.call(elem.querySelectorAll('.selected'), function(li) {
return this.innerHTML; return li.innerHTML;
}).toArray(); });
}; };
} }

View file

@ -2,7 +2,7 @@
[importance 5] [importance 5]
Перепишите решение задачи [](/task/selectable-list) (последний шаг) в виде компонента, с использованием jQuery. Перепишите решение задачи [](/task/selectable-list) в виде компонента.
У компонента должен быть единственный публичный метод `getSelected()`, который возвращает выбранные значения в виде массива. У компонента должен быть единственный публичный метод `getSelected()`, который возвращает выбранные значения в виде массива.
@ -10,7 +10,7 @@
```js ```js
var listSelect = new ListSelect({ var listSelect = new ListSelect({
elem: $('ul') elem: document.querySelector('ul')
}); });
// listSelect.getSelected() // listSelect.getSelected()
``` ```

View file

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

View file

@ -2,6 +2,7 @@
<html> <html>
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<script src="https://cdn.polyfill.io/v1/polyfill.js?features=Element.prototype.closest"></script>
<style> <style>
.voter { .voter {
font-family: Consolas, "Lucida Console", monospace; font-family: Consolas, "Lucida Console", monospace;
@ -13,7 +14,6 @@
font-weight: bold; font-weight: bold;
} }
</style> </style>
<script src="http://code.jquery.com/jquery.min.js"></script>
</head> </head>
<body> <body>
@ -27,38 +27,39 @@
function Voter(options) { function Voter(options) {
var elem = options.elem; var elem = options.elem;
var voteElem = elem.find('.vote'); var voteElem = elem.querySelector('.vote');
elem.on('click', '.down', onDownClick) elem.onclick = function(event) {
.on('click', '.up', onUpClick) // сам обработчик не меняет голос, он вызывает функцию
.on('mousedown selectstart', false); if (event.target.closest('.down')) {
voteDecrease();
} else if (event.target.closest('.up')) {
voteIncrease();
}
}
elem.onmousedown = function() {
return false;
};
// ----------- методы ------------- // ----------- методы -------------
function onDownClick() {
voteDecrease(); // сам обработчик не меняет голос, он вызывает функцию
}
function onUpClick() {
voteIncrease();
}
function voteDecrease() { function voteDecrease() {
voteElem.html( +voteElem.html()-1 ); voteElem.innerHTML = +voteElem.innerHTML - 1;
} }
function voteIncrease() { function voteIncrease() {
voteElem.html( +voteElem.html()+1 ); voteElem.innerHTML = +voteElem.innerHTML + 1;
} }
this.setVote = function(vote) { this.setVote = function(vote) {
voteElem.html( +vote ); voteElem.innerHTML = +vote;
}; };
} }
var voter = new Voter({ var voter = new Voter({
elem: $('#voter') elem: document.getElementById('voter')
}); });
voter.setVote(1); voter.setVote(1);

View file

@ -2,6 +2,7 @@
<html> <html>
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<script src="https://cdn.polyfill.io/v1/polyfill.js?features=Element.prototype.closest"></script>
<style> <style>
.voter { .voter {
font-family: Consolas, "Lucida Console", monospace; font-family: Consolas, "Lucida Console", monospace;
@ -13,7 +14,6 @@
font-weight: bold; font-weight: bold;
} }
</style> </style>
<script src="http://code.jquery.com/jquery.min.js"></script>
</head> </head>
<body> <body>
@ -29,7 +29,7 @@ function Voter(options) {
} }
var voter = new Voter({ var voter = new Voter({
elem: $('#voter') elem: document.getElementById('voter')
}); });
voter.setVote(1); voter.setVote(1);

View file

@ -15,7 +15,7 @@
По клику на `+` и `—` число должно увеличиваться или уменьшаться. По клику на `+` и `—` число должно увеличиваться или уменьшаться.
**Публичный метод `voter.setVote(vote)` должен устанавливать текущее число -- значение голоса.** Публичный метод `voter.setVote(vote)` должен устанавливать текущее число -- значение голоса.
Все остальные методы и свойства пусть будут приватными. Все остальные методы и свойства пусть будут приватными.

View file

@ -3,8 +3,8 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<link rel="stylesheet" href="style.css"> <link rel="stylesheet" href="style.css">
<script src="http://code.jquery.com/jquery-latest.js"></script> <script src="https://cdn.polyfill.io/v1/polyfill.js?features=Element.prototype.closest"></script>
<script src="voter.js"></script> <!-- отрефакторить --> <script src="voter.js"></script>
</head> </head>
<body> <body>
@ -17,7 +17,7 @@
<script> <script>
var voter = new Voter({ var voter = new Voter({
elem: $('#voter') elem: document.getElementById('voter')
}); });
</script> </script>

View file

@ -1,22 +1,33 @@
function Voter(options) { function Voter(options) {
var elem = this._elem = options.elem; var elem = this._elem = options.elem;
this._voteElem = elem.find('.vote'); this._voteElem = elem.querySelector('.vote');
elem.on('mousedown selectstart', false); elem.onmousedown = function() {
return false;
};
elem.on('click', '.down', this._onDownClick.bind(this)); elem.onclick = this._onClick.bind(this);
elem.on('click', '.up', this._onUpClick.bind(this));
} }
Voter.prototype._onDownClick = function() { Voter.prototype._onClick = function(event) {
this._voteElem.html( +this._voteElem.html() - 1 ); if (event.target.closest('.down')) {
this._voteDecrease();
} else if (event.target.closest('.up')) {
this._voteIncrease();
}
}; };
Voter.prototype._onUpClick = function() {
this._voteElem.html( +this._voteElem.html() + 1 ); Voter.prototype._voteIncrease = function() {
this._voteElem.innerHTML = +this._voteElem.innerHTML + 1;
};
Voter.prototype._voteDecrease = function() {
this._voteElem.innerHTML = +this._voteElem.innerHTML - 1;
}; };
Voter.prototype.setVote = function(vote) { Voter.prototype.setVote = function(vote) {
this._voteElem.html(vote); this._voteElem.innerHTML = +vote;
}; };

View file

@ -3,8 +3,8 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<link rel="stylesheet" href="style.css"> <link rel="stylesheet" href="style.css">
<script src="http://code.jquery.com/jquery-latest.js"></script> <script src="https://cdn.polyfill.io/v1/polyfill.js?features=Element.prototype.closest"></script>
<script src="voter.js"></script> <!-- отрефакторить --> <script src="voter.js"></script>
<script src="step-voter.js"></script> <!-- отнаследовать и переопределить методы --> <script src="step-voter.js"></script> <!-- отнаследовать и переопределить методы -->
</head> </head>
<body> <body>
@ -18,7 +18,7 @@
<script> <script>
var voter = new StepVoter({ var voter = new StepVoter({
elem: $('#voter'), elem: document.getElementById('voter'),
step: 2 step: 2
}); });

View file

@ -0,0 +1,13 @@
function StepVoter(options) {
Voter.apply(this, arguments);
this._step = options.step || 1;
}
StepVoter.prototype = Object.create(Voter.prototype);
StepVoter.prototype._voteIncrease = function() {
this._voteElem.innerHTML = +this._voteElem.innerHTML + this._step;
};
StepVoter.prototype._voteDecrease = function() {
this._voteElem.innerHTML = +this._voteElem.innerHTML - this._step;
};

View file

@ -0,0 +1,33 @@
function Voter(options) {
var elem = this._elem = options.elem;
this._voteElem = elem.querySelector('.vote');
elem.onmousedown = function() {
return false;
};
elem.onclick = this._onClick.bind(this);
}
Voter.prototype._onClick = function(event) {
if (event.target.closest('.down')) {
this._voteDecrease();
} else if (event.target.closest('.up')) {
this._voteIncrease();
}
};
Voter.prototype._voteIncrease = function() {
this._voteElem.innerHTML = +this._voteElem.innerHTML + 1;
};
Voter.prototype._voteDecrease = function() {
this._voteElem.innerHTML = +this._voteElem.innerHTML - 1;
};
Voter.prototype.setVote = function(vote) {
this._voteElem.innerHTML = +vote;
};

View file

@ -8,7 +8,7 @@
```js ```js
var voter = new StepVoter({ var voter = new StepVoter({
elem: $('#voter'), elem: document.getElementById('voter'),
step: 2 // увеличивать/уменьшать сразу на 2 пункта step: 2 // увеличивать/уменьшать сразу на 2 пункта
}); });
``` ```
@ -18,11 +18,6 @@ var voter = new StepVoter({
В реальном проекте влияние клика на голосовалку может зависеть от полномочий или репутации посетителя. В реальном проекте влияние клика на голосовалку может зависеть от полномочий или репутации посетителя.
Сделайте задачу в два этапа:
<ol>
<li>Поменять исходный класс `Voter`: вынести логику изменения значений в защищенные методы `_increase/_decrease`, так чтобы их можно было переопределить в наседнике.
На этом этапе использование кода не должно измениться, код в `index.html` будет тот же.</li>
<li>Сделать новый класс `StepVoter`, в котором обработать дополнительную опцию и переопределить `_increase/_decrease` соответственно. Затем использовать его в `index.html`.</li>
</ol>
В качестве исходного кода используйте решение задачи [](/task/voter-proto). В качестве исходного кода используйте решение задачи [](/task/voter-proto).
P.S. Код `voter.js` изменять нельзя, нужно не переписать `Voter`, а отнаследовать от него.

View file

@ -1,2 +0,0 @@
Для показа голосов также добавлены семантические классы `.positive/.negative` в `style.css`.

View file

@ -1,15 +0,0 @@
function ColoredVoter(options) {
Voter.apply(this, arguments);
}
ColoredVoter.prototype = Object.create(Voter.prototype);
ColoredVoter.prototype._renderVote = function() {
Voter.prototype._renderVote.apply(this, arguments);
this._voteElem.removeClass('positive negative');
if (this._vote > 0) {
this._voteElem.addClass('positive');
}
if (this._vote < 0) {
this._voteElem.addClass('negative');
}
};

View file

@ -1,28 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<link rel="stylesheet" href="style.css">
<script src="http://code.jquery.com/jquery-latest.js"></script>
<script src="voter.js"></script> <!-- отрефакторить -->
<script src="colored-voter.js"></script> <!-- отнаследовать и переопределить методы -->
</head>
<body>
<div id="voter" class="voter">
<span class="down"></span>
<span class="vote">0</span>
<span class="up">+</span>
</div>
<script>
var voter = new ColoredVoter({
elem: $('#voter')
});
</script>
</body>
</html>

View file

@ -1,18 +0,0 @@
.voter {
font-family: Consolas, "Lucida Console", monospace;
font-size: 18px;
}
.up, .down {
cursor: pointer;
color: blue;
font-weight: bold;
}
.positive {
color: green;
}
.negative {
color: red;
}

View file

@ -1,28 +0,0 @@
function Voter(options) {
var elem = this._elem = options.elem;
this._voteElem = elem.find('.vote');
this._vote = 0;
elem.on('mousedown selectstart', false);
elem.on('click', '.down', this._onDownClick.bind(this));
elem.on('click', '.up', this._onUpClick.bind(this));
}
Voter.prototype._onDownClick = function() {
this.setVote(this._vote - 1);
};
Voter.prototype._onUpClick = function() {
this.setVote(this._vote + 1);
};
Voter.prototype._renderVote = function() {
this._voteElem.html(this._vote);
};
Voter.prototype.setVote = function(vote) {
this._vote = vote;
this._renderVote();
};

View file

@ -1,17 +0,0 @@
# Добавить цвет в голосовалку
[importance 5]
Создайте функцию-конструктор `ColoredVoter`, которая наследует от голосовалки, созданной в задаче [](/task/voter-proto) и отображает положительные значения зелёным, а отрицательные -- красным.
Результат работы `new ColoredVoter`: (проголосуйте, чтобы увидеть):
[iframe border=1 src="solution"]
Решение задачи состоит из двух этапов:
<ol>
<li>Внести изменения в `Voter`, вынести логику отображения голоса в защищенный метод `_renderVote`, чтобы его можно было отнаследовать. При необходимости добавьте другие методы и свойства. Делайте такой код, который будет удобно расширять.</li>
<li>Отнаследовать и переопределить `_renderVote` в `ColoredVoter`.</li>
</ol>
В качестве исходного кода используйте решение задачи [](/task/voter-proto).

View file

@ -1,13 +0,0 @@
function StepVoter(options) {
Voter.apply(this, arguments);
this._step = options.step || 1;
}
StepVoter.prototype = Object.create(Voter.prototype);
StepVoter.prototype._increase = function() {
this.setVote(this._vote + this._step);
};
StepVoter.prototype._decrease = function() {
this.setVote(this._vote - this._step);
};

View file

@ -1,36 +0,0 @@
function Voter(options) {
var elem = this._elem = options.elem;
this._voteElem = elem.find('.vote');
this._vote = 0;
elem.on('mousedown selectstart', false);
elem.on('click', '.down', this._onDownClick.bind(this));
elem.on('click', '.up', this._onUpClick.bind(this));
}
Voter.prototype._onDownClick = function() {
this._decrease();
};
Voter.prototype._onUpClick = function() {
this._increase();
};
Voter.prototype._renderVote = function() {
this._voteElem.html(this._vote);
};
Voter.prototype.setVote = function(vote) {
this._vote = vote;
this._renderVote();
};
Voter.prototype._increase = function() {
this.setVote(this._vote + 1);
};
Voter.prototype._decrease = function() {
this.setVote(this._vote - 1);
};

View file

@ -4,7 +4,7 @@
Здесь мы сосредоточимся на графических компонентах, которые также называют "виджетами". Здесь мы сосредоточимся на графических компонентах, которые также называют "виджетами".
В браузерах есть встроенные виджеты, например `<select>`, `<input>` и другие элементы, о которых мы даже и не думаем, как они работают. Просто работают: принимают значение, вызывают события... В браузерах есть встроенные виджеты, например `<select>`, `<input>` и другие элементы, о которых мы даже и не думаем, "как они работают". Они "просто работают": показывают значение, вызывают события...
Наша задача -- сделать то же самое на уровне выше. Мы будем создавать объекты, которые генерируют меню, диалог или другие компоненты интерфейса, и дают возможность удобно работать с ними. Наша задача -- сделать то же самое на уровне выше. Мы будем создавать объекты, которые генерируют меню, диалог или другие компоненты интерфейса, и дают возможность удобно работать с ними.
@ -27,38 +27,44 @@
Далее она может дополняться, изменяться, но в начале -- она такая. Далее она может дополняться, изменяться, но в начале -- она такая.
Обратим внимание на важные соглашения: Обратим внимание на важные соглашения виджета:
<dl> <dl>
<dt>Вся разметка заключена в корневой элемент `<div class="menu" id="sweeties-menu">`.</dt> <dt>Вся разметка заключена в корневой элемент `<div class="menu" id="sweeties-menu">`.</dt>
<dd>Это очень удобно: вынул этот элемент из DOM -- нет меню, вставил в другое место -- переместил меню. Кроме того, можно удобно искать подэлементы.</dd> <dd>Это очень удобно: вынул этот элемент из DOM -- нет меню, вставил в другое место -- переместил меню. Кроме того, можно удобно искать подэлементы.</dd>
<dt>В разметке -- только классы.</dt> <dt>Внутри корневого элемента -- только классы, не `id`.</dt>
<dd>Документ вполне может содержать много различных меню. Они не должны конфликтовать между собой, поэтому для разметки везде используются классы. <dd>Документ вполне может содержать много различных меню. Они не должны конфликтовать между собой, поэтому для разметки везде используются классы.
Исключение -- корневой элемент. В данном случае мы предполагаем, что данное конкретное "меню сладостей" в документе только одно, поэтому даём ему `id`.</dd> Исключение -- корневой элемент. В данном случае мы предполагаем, что данное конкретное "меню сладостей" в документе только одно, поэтому даём ему `id`.</dd>
</dl> </dl>
Для работы с разметкой будем создавать объект `new Menu` и передавать ему корневой элемент. В конструкторе он поставит необходимые обработчики.
# Класс виджета
Для работы с разметкой будем создавать объект `new Menu` и передавать ему корневой элемент. В конструкторе он поставит необходимые обработчики:
```js ```js
function Menu(options) { function Menu(options) {
var elem = options.elem; var elem = options.elem;
elem.on('mousedown selectstart', false); elem.onmousedown = function() { return false; }
elem.onclick = function(event) {
if (event.target.closest('.title')) {
elem.classList.toggle('open');
}
};
elem.on('click', '.title', function() {
elem.toggleClass('open');
});
} }
// использование // использование
var menu = new Menu({ var menu = new Menu({
elem: $('#sweets-menu') elem: document.getElementById('sweets-menu')
}); });
``` ```
Меню: Меню:
[codetabs src="menu-1"] [codetabs src="menu"]
Это, конечно, только первый шаг, но уже здесь видны некоторые важные соглашения в коде. Это, конечно, только первый шаг, но уже здесь видны некоторые важные соглашения в коде.
@ -69,25 +75,38 @@ var menu = new Menu({
<dd>Вместо того, чтобы найти элемент и поставить обработчик на него: <dd>Вместо того, чтобы найти элемент и поставить обработчик на него:
```js ```js
var titleElem = elem.find('.title'); var titleElem = elem.querySelector('.title');
titleElem.on('click', function() { titleElem.onclick = function() {
elem.toggleClass('open'); elem.classList.toggle('open');
} }
``` ```
...Мы пишем так: ...Мы ставим обработчик на корневой `elem` и используем делегирование:
```js ```js
elem.on('click', '.title', function() { elem.onclick = function(event) {
elem.toggleClass('open'); if (event.target.closest('.title')) {
}); elem.classList.toggle('open');
}
};
``` ```
Это ускоряет инициализацию, так как не надо искать элементы, и даёт возможность в любой момент менять DOM внутри, в том числе через `innerHTML`, без необходимости переставлять обработчика. Это ускоряет инициализацию, так как не надо искать элементы, и даёт возможность в любой момент менять DOM внутри, в том числе через `innerHTML`, без необходимости переставлять обработчик.
</dd> </dd>
</dl> </dl>
В этот код лучше добавить дополнительную проверку на то, что найденный `.title` находится внутри `elem`:
```js
elem.onclick = function(event) {
var closestTitle = event.target.closest('.title');
if (closestTitle && elem.contains(closestTitle)) {
elem.classList.toggle('open');
}
};
```
## Публичные методы ## Публичные методы
Уважающий себя компонент обычно имеет публичные методы, которые позволяют управлять им снаружи. Уважающий себя компонент обычно имеет публичные методы, которые позволяют управлять им снаружи.
@ -95,12 +114,12 @@ elem.on('click', '.title', function() {
Рассмотрим повнимательнее этот фрагмент: Рассмотрим повнимательнее этот фрагмент:
```js ```js
elem.on('click', '.title', function() { if (event.target.closest('.title')) {
elem.toggleClass('open'); elem.classList.toggle('open');
}); }
``` ```
Здесь в обработчике события сразу код работы с элементами. Пока одна строка -- всё понятно, но если их будет много, то при чтении понадобится долго и упорно вникать: "А что же, всё-таки, такое делается при клике?" Здесь в обработчике события сразу код работы с элементом. Пока одна строка -- всё понятно, но если их будет много, то при чтении понадобится долго и упорно вникать: "А что же, всё-таки, такое делается при клике?"
Для улучшения читаемости выделим обработчик в отдельную функцию `toggle`, которая к тому же станет полезным публичным методом: Для улучшения читаемости выделим обработчик в отдельную функцию `toggle`, которая к тому же станет полезным публичным методом:
@ -108,49 +127,42 @@ elem.on('click', '.title', function() {
function Menu(options) { function Menu(options) {
var elem = options.elem; var elem = options.elem;
elem.on('mousedown selectstart', false); elem.onmousedown = function() { return false; }
*!* elem.onclick = function(event) {
elem.on('click', '.title', onTitleClick); if (event.target.closest('.title')) {
function onTitleClick(e) {
toggle(); toggle();
} }
};
function toggle() { function toggle() {
elem.toggleClass('open'); elem.classList.toggle('open');
}; }
*/!*
this.toggle = toggle; this.toggle = toggle;
} }
``` ```
Здесь и сам обработчик события тоже вынесен в отдельную функцию `onTitleClick`. Теперь метод `toggle` можно использовать и снаружи:
Наши бонусы:
<ol>
<li>Во-первых, стало проще найти и расширить обработчик события в коде -- имя `onTitleClick` найти и запомнить.</li>
<li>Во-вторых, код стал лучше читаться.</li>
<li>Во-третьих, `toggle` теперь -- отдельная функция, доступная извне.</li>
</ol>
Пример использования публичного метода:
```js ```js
var menu = new Menu(...); var menu = new Menu(...);
menu.toggle(); menu.toggle();
``` ```
## Генерация DOM-дерева ## Генерация DOM-элемента
До этого момента меню "оживляло" уже существующий HTML. Но в более сложном интерфейсе нужно уметь сгенерировать меню "на лету", по данным. До этого момента меню "оживляло" уже существующий HTML.
Для этого добавим меню три метода: Но далеко не всегда в HTML уже есть готовая разметка. В сложных интерфейсах намного чаще её нет, а есть данные, на основе которых компонент генерирует разметку.
В случае меню, данные -- это набор пунктов меню, которые передаются конструктору.
Для генерации DOM добавим меню три метода:
<ul> <ul>
<li>`render()` -- генерирует корневой DOM-элемент и заголовок меню, приватный.</li> <li>`render()` -- генерирует корневой DOM-элемент и заголовок меню.</li>
<li>`renderItems()` -- генерирует DOM для списка опций (`<li>`), приватный.</li> <li>`renderItems()` -- генерирует DOM для списка опций `ul/li`.</li>
<li>`getElem()` -- возвращает DOM-элемент меню, при необходимости запуская генерацию, публичный.</li> <li>`getElem()` -- возвращает DOM-элемент меню, при необходимости запуская генерацию, публичный метод.</li>
</ul> </ul>
Функция генерации корневого элемента с заголовком `render` отделена от генерации списка `renderItems`. Почему -- будет видно чуть далее. Функция генерации корневого элемента с заголовком `render` отделена от генерации списка `renderItems`. Почему -- будет видно чуть далее.
@ -173,14 +185,14 @@ var menu = new Menu({
}); });
*!* *!*
// получить DOM-элемент меню // получить сгенерированный DOM-элемент меню
*/!* */!*
var elem = menu.getElem(); var elem = menu.getElem();
*!* *!*
// вставить меню в нужное место страницы // вставить меню в нужное место страницы
*/!* */!*
$('#sweets-menu-holder').append( elem ); document.body.appendChild(elem);
``` ```
Код `Menu` с новыми методами: Код `Menu` с новыми методами:
@ -195,68 +207,52 @@ function Menu(options) {
} }
function render() { function render() {
elem = $('<div class="menu"></div>'); elem = document.createElement('div');
elem.append( $('<span/>', { class: "title", text: options.title })) elem.className = "menu";
elem.on('mousedown selectstart', false); var titleElem = document.createElement('span');
elem.appendChild(titleElem);
titleElem.className = "title";
titleElem.textContent = options.title;
elem.onmousedown = function() {
return false;
};
elem.onclick = function(event) {
if (event.target.closest('.title')) {
toggle();
}
}
elem.on('click', '.title', onTitleClick);
} }
function renderItems() { function renderItems() {
var items = options.items || []; var items = options.items || [];
var list = $('<ul/>'); var list = document.createElement('ul');
$.each(items, function(i, item) { items.forEach(function(item) {
list.append( $('<li>').text(item) ); var li = document.createElement('li');
}) li.textContent = item;
list.appendTo(elem); list.appendChild(li);
});
elem.appendChild(list);
} }
// ...
}
```
**Важнейший принцип, который здесь использован -- ленивость.**
Мы стараемся откладывать работу до момента, когда она реально нужна. Например, когда `new Menu` создаётся, то переменная `elem` лишь объявляется. DOM-дерево будет сгенерировано только при вызове `getElem()`.
Более того! Пока меню закрыто -- достаточно заголовка. Кроме того, возможно, посетитель вообще никогда не раскроет это меню, так зачем генерировать список раньше времени?
**Фаза инициализации очень чувствительна к производительности, так как при загрузке страницы со сложным интерфейсом создаётся много всего. А мы хотим, чтобы он начал работать как можно быстрее.**
Если изначально подходить к оптимизации на этой фазе "спустя рукава", то потом поправить может быть сложно. Всё-таки, инициализация -- это фундамент, начало работы виджета. Конечно, лучше без фанатизма. Бывают ситуации, когда по коду гораздо удобнее что-то сделать сразу, поэтому нужен взвешенный подход. Чем крупнее участок работы и чем больше шансов его вообще избежать -- тем больше доводов его отложить.
Ниже -- код меню с методами `open`, `close` и `toggle`, которые подразумевают ленивую генерацию DOM:
```js
function Menu(options) {
var elem;
function getElem() { /* см выше */ }
function render() { /* см выше */ }
function renderItems() { /* см выше */ }
function onTitleClick(e) { /* см выше */ }
*!*
function open() { function open() {
if (!elem.find('ul').length) { if (!elem.querySelector('ul')) {
renderItems(); renderItems();
} }
elem.addClass('open'); elem.classList.add('open');
}; };
function close() { function close() {
elem.removeClass('open'); elem.classList.remove('open');
}; };
function toggle() { function toggle() {
if (elem.hasClass('open')) close(); if (elem.classList.contains('open')) close();
else open(); else open();
}; };
*/!*
this.getElem = getElem; this.getElem = getElem;
this.toggle = toggle; this.toggle = toggle;
@ -265,23 +261,49 @@ function Menu(options) {
} }
``` ```
Основные изменения -- теперь метод `toggle` не просто меняет класс. Этого недостаточно, ведь, чтобы открыть меню, нужно для начала отрендерить его опции. Поэтому добавлено два метода `open` и `close`, которые также полезны и для внешнего интерфейса. Отметим некоторые особенности этого кода.
<dl>
<dt>Обработчики отделяются от реальных действий.</dt>
<dd>В обработчике `onclick` мы "ловим" событие и выясняем, что именно произошло. Возможно, нужно проверить `event.target`, координаты, клавиши-модификаторы, и т.п. Это всё можно делать здесь же.
Выяснив, что нужно сделать, обработчик `onclick` не делает это сам, а вызывает для этого соответствующий метод. Этот метод уже не знает ничего о событии, он просто делает что-то с виджетом. Его можно вызвать и отдельно, не из обработчика.
Здесь есть ряд важных плюсов:
<ul>
<li>Обработчик `onclick` не "распухает" чрезмерно.</li>
<li>Код гораздо лучше читается.</li>
<li>Метод можно повторно использовать, в том числе и сделать публичным, как в коде выше.</li>
</ul>
</dd>
<dt>Генерация DOM, по возможности, должна быть "ленивой".</dt>
<dd>Мы стараемся откладывать работу до момента, когда она реально нужна. Например, когда `new Menu` создаётся, то переменная `elem` лишь объявляется. DOM-дерево будет сгенерировано только при вызове `getElem()` функцией `render()`.
Более того! Пока меню закрыто -- достаточно заголовка. Кроме того, возможно, посетитель вообще никогда не раскроет это меню, так зачем генерировать список раньше времени? А при первом открытиии `open()` вызовет функцию `renderItems()`, которая специально для этого выделена отдельно от `render()`.
**Фаза инициализации очень чувствительна к производительности, так как обычно в сложном интерфейсе создаётся много всего.**
Если изначально подходить к оптимизации на этой фазе "спустя рукава", то потом поправить долгий старт может быть сложно. Тем более, что инициализация -- это фундамент, начало работы виджета, её оптимизация в будущем может потребовать сильных изменений кода.
Конечно, здесь, как и везде в оптимизации -- без фанатизма. Бывают ситуации, когда гораздо удобнее что-то сделать сразу. Если это один элемент, то оптимизация здесь ни к чему. А если большой фрагмент DOM, который, как в случае с меню, прямо сейчас не нужен -- то лучше отложить.
</dd>
</dl>
В действии: В действии:
[codetabs src="menu-3-elem" height="200"] [codetabs src="menu-dom" height="200"]
## Итого ## Итого
Мы начали создавать компонент "с чистого листа", пока без дополнительных библиотек, но они скоро понадобятся. Мы начали создавать компонент "с чистого листа", пока без дополнительных библиотек.
Основные принципы: Основные принципы:
<ul> <ul>
<li>В конструктор передаётся объект аргументов `options`, а не список аргументов -- для удобства дополнения и расширения виджета.</li> <li>Виджет -- это объект, который либо контролирует готовое дерево DOM, либо создаёт своё.</li>
<li>В конструктор виджета передаётся объект аргументов `options`.</li>
<li>Виджет при необходимости создаёт элемент или "оживляет" готовый. Внутре в разметке не используются `id`.</li>
<li>Обработчики назначаются через делегирование -- для производительности и упрощения виджета.</li> <li>Обработчики назначаются через делегирование -- для производительности и упрощения виджета.</li>
<li>Не экономим буквы ценой понятности -- действие и/или обработчик заслуживают быть отдельными функциями.</li> <li>Обработчики событий вызывают соответствующий метод, не пытаются делать всё сами.</li>
<li>Будем ленивыми -- если существенный участок работы можно отложить до реального задействования виджета -- откладываем его.</li> <li>При инициализации, если существенный участок работы можно отложить до реального задействования виджета -- откладываем его.</li>
</ul> </ul>
Далее мы продолжим работать со разметкой виджета.

View file

@ -1,11 +0,0 @@
function Menu(options) {
var elem = options.elem;
// отмена выделения при клике на меню
elem.on('mousedown selectstart', false);
elem.on('click', '.title', function() {
elem.toggleClass('open');
});
}

View file

@ -1,20 +0,0 @@
.menu ul {
display: none;
margin: 0;
}
.menu .title {
font-weight: bold;
cursor: pointer;
background: url(https://js.cx/clipart/arrow-right.png) left center no-repeat;
padding-left: 18px;
}
.menu.open ul {
display: block;
}
.menu.open .title {
background-image: url(https://js.cx/clipart/arrow-down.png);
}

View file

@ -1,51 +0,0 @@
function Menu(options) {
var elem;
function getElem() {
if (!elem) render();
return elem;
}
function render() {
elem = $('<div class="menu"></div>');
elem.append( $('<span/>', { class: "title", text: options.title }))
elem.on('mousedown selectstart', false);
elem.on('click', '.title', onTitleClick);
}
function renderItems() {
var items = options.items || [];
var list = $('<ul/>');
$.each(items, function(i, item) {
list.append( $('<li>').text(item) );
})
list.appendTo(elem);
}
function onTitleClick(e) {
toggle();
}
function open() {
if (!elem.find('ul').length) {
renderItems();
}
elem.addClass('open');
};
function close() {
elem.removeClass('open');
};
function toggle() {
if (elem.hasClass('open')) close();
else open();
};
this.getElem = getElem;
this.toggle = toggle;
this.close = close;
this.open = open;
}

View file

@ -1,20 +0,0 @@
.menu ul {
display: none;
margin: 0;
}
.menu .title {
font-weight: bold;
cursor: pointer;
background: url(https://js.cx/clipart/arrow-right.png) left center no-repeat;
padding-left: 18px;
}
.menu.open ul {
display: block;
}
.menu.open .title {
background-image: url(https://js.cx/clipart/arrow-down.png);
}

View file

@ -3,7 +3,8 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<link rel="stylesheet" href="style.css"> <link rel="stylesheet" href="style.css">
<script src="http://code.jquery.com/jquery.min.js"></script> <script src="https://cdn.polyfill.io/v1/polyfill.js?features=Element.prototype.closest"></script>
<script src="menu.js"></script> <script src="menu.js"></script>
</head> </head>
<body> <body>
@ -12,9 +13,6 @@
<button onclick="menu.open()">menu.open()</button> <button onclick="menu.open()">menu.open()</button>
<button onclick="menu.close()">menu.close()</button> <button onclick="menu.close()">menu.close()</button>
<div id="sweets-menu-holder"></div>
<script> <script>
var menu = new Menu({ var menu = new Menu({
title: "Сладости", title: "Сладости",
@ -27,7 +25,7 @@ var menu = new Menu({
] ]
}); });
$('#sweets-menu-holder').append(menu.getElem()); document.body.appendChild( menu.getElem() );
</script> </script>
</body> </body>

View file

@ -0,0 +1,61 @@
function Menu(options) {
var elem;
function getElem() {
if (!elem) render();
return elem;
}
function render() {
elem = document.createElement('div');
elem.className = "menu";
var titleElem = document.createElement('span');
elem.appendChild(titleElem);
titleElem.className = "title";
titleElem.textContent = options.title;
elem.onmousedown = function() {
return false;
};
elem.onclick = function(event) {
if (event.target.closest('.title')) {
toggle();
}
}
}
function renderItems() {
var items = options.items || [];
var list = document.createElement('ul');
items.forEach(function(item) {
var li = document.createElement('li');
li.textContent = item;
list.appendChild(li);
});
elem.appendChild(list);
}
function open() {
if (!elem.querySelector('ul')) {
renderItems();
}
elem.classList.add('open');
};
function close() {
elem.classList.remove('open');
};
function toggle() {
if (elem.classList.contains('open')) close();
else open();
};
this.getElem = getElem;
this.toggle = toggle;
this.close = close;
this.open = open;
}

View file

@ -0,0 +1,24 @@
.menu ul {
display: none;
margin: 0;
}
.menu .title {
font-weight: bold;
cursor: pointer;
}
.menu .title:before {
content: '▶';
padding-right: 6px;
color: green;
}
.menu.open ul {
display: block;
}
.menu.open .title:before {
content: '▼';
}

View file

@ -3,7 +3,7 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<link rel="stylesheet" href="style.css"> <link rel="stylesheet" href="style.css">
<script src="http://code.jquery.com/jquery.min.js"></script> <script src="https://cdn.polyfill.io/v1/polyfill.js?features=Element.prototype.closest"></script>
<script src="menu.js"></script> <script src="menu.js"></script>
</head> </head>
<body> <body>
@ -21,7 +21,7 @@
<script> <script>
var menu = new Menu({ var menu = new Menu({
elem: $('#sweets-menu') elem: document.getElementById('sweets-menu')
}); });
</script> </script>

View file

@ -0,0 +1,12 @@
function Menu(options) {
var elem = options.elem;
elem.onmousedown = function() { return false; }
elem.onclick = function(event) {
if (event.target.closest('.title')) {
elem.classList.toggle('open');
}
};
}

View file

@ -0,0 +1,24 @@
.menu ul {
display: none;
margin: 0;
}
.menu .title {
font-weight: bold;
cursor: pointer;
}
.menu .title:before {
content: '▶';
padding-right: 6px;
color: green;
}
.menu.open ul {
display: block;
}
.menu.open .title:before {
content: '▼';
}

Binary file not shown.