renovations
This commit is contained in:
parent
0eec1aaccb
commit
c018a2db03
56 changed files with 459 additions and 594 deletions
|
@ -112,7 +112,7 @@ var oranges = "3";
|
||||||
alert( +apples + +oranges); // 5, число, оба операнда предварительно преобразованы в числа
|
alert( +apples + +oranges); // 5, число, оба операнда предварительно преобразованы в числа
|
||||||
```
|
```
|
||||||
|
|
||||||
С точки зрения математики такое изобилие плюсов может показаться странном. С точки зрения программирования -- никаких разночтений: сначала выполнятся унарные плюсы, приведут строки к числам, а затем -- бинарный `'+'` их сложит.
|
С точки зрения математики такое изобилие плюсов может показаться странным. С точки зрения программирования -- никаких разночтений: сначала выполнятся унарные плюсы, приведут строки к числам, а затем -- бинарный `'+'` их сложит.
|
||||||
|
|
||||||
Почему унарные плюсы выполнились до бинарного сложения? Как мы сейчас увидим, дело в их приоритете.
|
Почему унарные плюсы выполнились до бинарного сложения? Как мы сейчас увидим, дело в их приоритете.
|
||||||
|
|
||||||
|
|
|
@ -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 |
|
@ -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): "В древности люди учились для того, чтобы совершенствовать себя. Нынче учатся для того, чтобы удивить других".
|
||||||
|
|
||||||
## Краткость -- сестра таланта!
|
## Краткость -- сестра таланта!
|
||||||
|
|
||||||
|
|
|
@ -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>
|
|
|
@ -1,15 +0,0 @@
|
||||||
# Функция и переменная
|
|
||||||
|
|
||||||
[importance 3]
|
|
||||||
|
|
||||||
Каков будет результат кода? Почему?
|
|
||||||
|
|
||||||
```js
|
|
||||||
var a = 5;
|
|
||||||
|
|
||||||
function a() { }
|
|
||||||
|
|
||||||
alert(a);
|
|
||||||
```
|
|
||||||
|
|
||||||
P.S. Это задание -- учебное, на понимание процесса инициализации и выполнения. В реальной жизни мы, конечно же, не будем называть переменную и функцию одинаково.
|
|
|
@ -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>
|
||||||
|
|
|
@ -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-
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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'ом`. Мы не помещаем форму в контейнер, чтобы она не унаследовала полупрозрачность.
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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-позиционированием - в песочнице.
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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]
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -4,9 +4,8 @@
|
||||||
Для надёжности необходима простота.
|
Для надёжности необходима простота.
|
||||||
[/quote]
|
[/quote]
|
||||||
|
|
||||||
В современной JavaScript-разработке используются фреймворки, которые дают готовые библиотеки и правила для написания кода. Эта глава не ставит своей целью их все заменить (зачем?), а равно как и научить какому-нибудь фреймворку (не факт, что он для вас лучший).
|
В современной JavaScript-разработке используются фреймворки, которые дают готовые библиотеки и правила для написания кода. Эта глава не ставит своей целью их все заменить, а равно как и научить какому-нибудь фреймворку.
|
||||||
|
|
||||||
Вместо этого мы разберём основные средства для построения архитектуры и при помощи чистого JavaScript построим компоненты интерфейса с их использованием.
|
Вместо этого мы разберём основные средства для построения архитектуры, и при помощи чистого JavaScript построим компоненты интерфейса с их использованием.
|
||||||
|
|
||||||
Это во-первых покажет, что и без фреймворков есть жизнь, а во-вторых даст фундамент для освоения любого фреймворка, какой бы вы ни выбрали.
|
Это во-первых покажет, что и без фреймворков есть жизнь, а во-вторых даст фундамент для освоения любого фреймворка, какой бы вы ни выбрали.
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
}
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -20,5 +20,5 @@ clock.stop(); // стоп
|
||||||
При нажатии на `alert` часы должны приостанавливаться, а затем продолжать идти с правильным временем.
|
При нажатии на `alert` часы должны приостанавливаться, а затем продолжать идти с правильным временем.
|
||||||
|
|
||||||
Пример результата:
|
Пример результата:
|
||||||
[iframe src="solution" border=1]
|
[iframe src="solution" height=80]
|
||||||
|
|
||||||
|
|
|
@ -1,2 +1 @@
|
||||||
Пример переписанного слайдера:
|
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -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();
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
```
|
```
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
[edit src="solution"]Открыть решение в песочнице[/edit]
|
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -15,7 +15,7 @@
|
||||||
|
|
||||||
По клику на `+` и `—` число должно увеличиваться или уменьшаться.
|
По клику на `+` и `—` число должно увеличиваться или уменьшаться.
|
||||||
|
|
||||||
**Публичный метод `voter.setVote(vote)` должен устанавливать текущее число -- значение голоса.**
|
Публичный метод `voter.setVote(vote)` должен устанавливать текущее число -- значение голоса.
|
||||||
|
|
||||||
Все остальные методы и свойства пусть будут приватными.
|
Все остальные методы и свойства пусть будут приватными.
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
});
|
});
|
||||||
|
|
|
@ -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;
|
||||||
|
};
|
|
@ -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;
|
||||||
|
};
|
||||||
|
|
|
@ -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`, а отнаследовать от него.
|
|
@ -1,2 +0,0 @@
|
||||||
Для показа голосов также добавлены семантические классы `.positive/.negative` в `style.css`.
|
|
||||||
|
|
|
@ -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');
|
|
||||||
}
|
|
||||||
};
|
|
|
@ -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>
|
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
|
|
@ -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();
|
|
||||||
};
|
|
|
@ -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).
|
|
|
@ -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);
|
|
||||||
};
|
|
|
@ -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);
|
|
||||||
};
|
|
|
@ -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>
|
||||||
|
|
||||||
Далее мы продолжим работать со разметкой виджета.
|
|
||||||
|
|
||||||
|
|
|
@ -1,11 +0,0 @@
|
||||||
function Menu(options) {
|
|
||||||
var elem = options.elem;
|
|
||||||
|
|
||||||
// отмена выделения при клике на меню
|
|
||||||
elem.on('mousedown selectstart', false);
|
|
||||||
|
|
||||||
elem.on('click', '.title', function() {
|
|
||||||
elem.toggleClass('open');
|
|
||||||
});
|
|
||||||
|
|
||||||
}
|
|
|
@ -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);
|
|
||||||
}
|
|
|
@ -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;
|
|
||||||
}
|
|
|
@ -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);
|
|
||||||
}
|
|
8
2-ui/5-widgets/2-widgets-structure/menu-3-elem.view/index.html → 2-ui/5-widgets/2-widgets-structure/menu-dom.view/index.html
Executable file → Normal file
8
2-ui/5-widgets/2-widgets-structure/menu-3-elem.view/index.html → 2-ui/5-widgets/2-widgets-structure/menu-dom.view/index.html
Executable file → Normal 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>
|
61
2-ui/5-widgets/2-widgets-structure/menu-dom.view/menu.js
Normal file
61
2-ui/5-widgets/2-widgets-structure/menu-dom.view/menu.js
Normal 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;
|
||||||
|
}
|
24
2-ui/5-widgets/2-widgets-structure/menu-dom.view/style.css
Normal file
24
2-ui/5-widgets/2-widgets-structure/menu-dom.view/style.css
Normal 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: '▼';
|
||||||
|
}
|
4
2-ui/5-widgets/2-widgets-structure/menu-1.view/index.html → 2-ui/5-widgets/2-widgets-structure/menu.view/index.html
Executable file → Normal file
4
2-ui/5-widgets/2-widgets-structure/menu-1.view/index.html → 2-ui/5-widgets/2-widgets-structure/menu.view/index.html
Executable file → Normal 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>
|
||||||
|
|
12
2-ui/5-widgets/2-widgets-structure/menu.view/menu.js
Normal file
12
2-ui/5-widgets/2-widgets-structure/menu.view/menu.js
Normal 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');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
24
2-ui/5-widgets/2-widgets-structure/menu.view/style.css
Normal file
24
2-ui/5-widgets/2-widgets-structure/menu.view/style.css
Normal 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: '▼';
|
||||||
|
}
|
BIN
figures.sketch
BIN
figures.sketch
Binary file not shown.
Loading…
Add table
Add a link
Reference in a new issue