This commit is contained in:
Ilya Kantor 2014-10-26 22:10:13 +03:00
parent 06f61d8ce8
commit f301cb744d
2271 changed files with 103162 additions and 0 deletions

View file

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

View file

@ -0,0 +1 @@
{"name":"clock","plunk":"APYZSjtojpw9wwlNc6xB"}

View file

@ -0,0 +1,31 @@
function Clock(options) {
var elem = options.elem;
var timer;
function render() {
var date = new Date();
var hours = date.getHours();
if (hours < 10) hours = '0' + hours;
$('.hour', elem).html(hours);
var min = date.getMinutes();
if (min < 10) min = '0' + min;
$('.min', elem).html(min);
var sec = date.getSeconds();
if (sec < 10) sec = '0' + sec;
$('.sec', elem).html(sec);
}
this.stop = function() {
clearInterval(timer);
};
this.start = function() {
render();
timer = setInterval(render, 1000);
}
}

View file

@ -0,0 +1 @@
{"name":"clock","plunk":"APYZSjtojpw9wwlNc6xB"}

View file

@ -0,0 +1,31 @@
function Clock(options) {
var elem = options.elem;
var timer;
function render() {
var date = new Date();
var hours = date.getHours();
if (hours < 10) hours = '0' + hours;
$('.hour', elem).html(hours);
var min = date.getMinutes();
if (min < 10) min = '0' + min;
$('.min', elem).html(min);
var sec = date.getSeconds();
if (sec < 10) sec = '0' + sec;
$('.sec', elem).html(sec);
}
this.stop = function() {
clearInterval(timer);
};
this.start = function() {
render();
timer = setInterval(render, 1000);
}
}

View file

@ -0,0 +1,37 @@
<!DOCTYPE HTML>
<html>
<head>
<title>Часики</title>
<script src="http://code.jquery.com/jquery.min.js"></script>
<script src="clock.js"></script>
<meta charset="utf-8">
<style>
.hour { color: green }
.min { color: blue }
.sec { color: red }
</style>
</head>
<body>
<div id="clock" class="clock">
<span class="hour">00</span>:<span class="min">00</span>:<span class="sec">00</span>
</div>
<script>
var pageClock = new Clock({
elem: $('#clock')
});
</script>
<input type="button" onclick="pageClock.start()" value="Старт">
<input type="button" onclick="pageClock.stop()" value="Стоп">
<input type="button"
onclick="alert('Часы должны останавливаться во время alert,\nи продолжать корректно работать после нажатия на ОК')"
value="alert для проверки корректного возобновления"
>
</body>
</html>

View file

@ -0,0 +1,37 @@
<!DOCTYPE HTML>
<html>
<head>
<title>Часики</title>
<script src="http://code.jquery.com/jquery.min.js"></script>
<script src="clock.js"></script>
<meta charset="utf-8">
<style>
.hour { color: green }
.min { color: blue }
.sec { color: red }
</style>
</head>
<body>
<div id="clock" class="clock">
<span class="hour">00</span>:<span class="min">00</span>:<span class="sec">00</span>
</div>
<script>
var pageClock = new Clock({
elem: $('#clock')
});
</script>
<input type="button" onclick="pageClock.start()" value="Старт">
<input type="button" onclick="pageClock.stop()" value="Стоп">
<input type="button"
onclick="alert('Часы должны останавливаться во время alert,\nи продолжать корректно работать после нажатия на ОК')"
value="alert для проверки корректного возобновления"
>
</body>
</html>

View file

@ -0,0 +1,25 @@
# Часики
[importance 5]
Создайте компонент "Часы" (Clock).
Интерфейс:
```js
var clock = new Clock({
elem: элемент
});
clock.start(); // старт
clock.stop(); // стоп
```
Остальные методы, если нужны, должны быть приватными.
При нажатии на `alert` часы должны приостанавливаться, а затем продолжать идти с правильным временем.
Пример результата:
[iframe src="solution" border=1]
[edit src="task" task/]

View file

@ -0,0 +1 @@
{"name":"clock-src","plunk":"p4DSlwiBguQ8iirU3RMu"}

View file

@ -0,0 +1,38 @@
<!DOCTYPE HTML>
<html>
<head>
<title>Часики</title>
<script src="http://code.jquery.com/jquery.min.js"></script>
<meta charset="utf-8">
<style>
.hour { color: green }
.min { color: blue }
.sec { color: red }
</style>
</head>
<body>
<div id="clock" class="clock">
<span class="hour">00</span>:<span class="min">00</span>:<span class="sec">00</span>
</div>
<script>
// .. ваш код Clock
var pageClock = new Clock({
elem: $('#clock')
});
</script>
<input type="button" onclick="pageClock.start()" value="Старт">
<input type="button" onclick="pageClock.stop()" value="Стоп">
<input type="button"
onclick="alert('Часы должны останавливаться во время alert,\nи продолжать корректно работать после нажатия на ОК')"
value="alert для проверки корректного возобновления"
>
</body>
</html>

View file

@ -0,0 +1,3 @@
Пример переписанного слайдера:
[edit src="solution"/]

View file

@ -0,0 +1 @@
{"name":"slider-simple","plunk":"Lby0d7r3u4irsI8Ml24K"}

View file

@ -0,0 +1,79 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<script src="lib.js"></script>
<style>
.slider {
border-radius: 5px;
background: #E0E0E0;
background: -moz-linear-gradient(left top , #E0E0E0, #EEEEEE) repeat scroll 0 0 transparent;
background: -webkit-gradient(linear, left top, right bottom, from(#E0E0E0), to(#EEEEEE));
background: linear-gradient(left top, #E0E0E0, #EEEEEE);
width: 310px;
height: 15px;
margin: 5px;
}
.thumb {
width: 10px;
height: 25px;
border-radius: 3px;
position: relative;
left: 10px;
top: -5px;
background: blue;
cursor: pointer;
}
</style>
</head>
<body>
<div id="slider" class="slider">
<div class="thumb"></div>
</div>
<script>
var sliderElem = document.getElementById('slider');
var thumbElem = sliderElem.children[0];
thumbElem.ondragstart = function() { return false; };
thumbElem.onmousedown = function(e) {
e = fixEvent(e);
var thumbCoords = getCoords(thumbElem);
var shiftX = e.pageX - thumbCoords.left;
// shiftY здесь не нужен, слайдер двигается только по горизонтали
var sliderCoords = getCoords(sliderElem);
document.onmousemove = function(e) {
e = fixEvent(e);
// вычесть координату родителя, т.к. position: relative
var newLeft = e.pageX - shiftX - sliderCoords.left;
// курсор ушёл вне слайдера
if (newLeft < 0) {
newLeft = 0;
}
var rightEdge = sliderElem.offsetWidth - thumbElem.offsetWidth;
if (newLeft > rightEdge) {
newLeft = rightEdge;
}
thumbElem.style.left = newLeft + 'px';
}
document.onmouseup = function() {
document.onmousemove = document.onmouseup = null;
};
return false; // disable selection start (cursor change)
};
</script>
</body>
</html>

View file

@ -0,0 +1,44 @@
function fixEvent(e) {
e = e || window.event;
if (!e.target) e.target = e.srcElement;
if (e.pageX == null && e.clientX != null ) { // если нет pageX..
var html = document.documentElement;
var body = document.body;
e.pageX = e.clientX + (html.scrollLeft || body && body.scrollLeft || 0);
e.pageX -= html.clientLeft || 0;
e.pageY = e.clientY + (html.scrollTop || body && body.scrollTop || 0);
e.pageY -= html.clientTop || 0;
}
if (!e.which && e.button) {
e.which = e.button & 1 ? 1 : ( e.button & 2 ? 3 : ( e.button & 4 ? 2 : 0 ) )
}
return e;
}
function getCoords(elem) {
var box = elem.getBoundingClientRect();
var body = document.body;
var docElem = document.documentElement;
var scrollTop = window.pageYOffset || docElem.scrollTop || body.scrollTop;
var scrollLeft = window.pageXOffset || docElem.scrollLeft || body.scrollLeft;
var clientTop = docElem.clientTop || body.clientTop || 0;
var clientLeft = docElem.clientLeft || body.clientLeft || 0;
var top = box.top + scrollTop - clientTop;
var left = box.left + scrollLeft - clientLeft;
return { top: Math.round(top), left: Math.round(left) };
}

View file

@ -0,0 +1 @@
{"name":"slider-component","plunk":"yOsWKXyShloaVwdeA5Of"}

View file

@ -0,0 +1,107 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<script src="http://code.jquery.com/jquery.min.js"></script>
<style>
.slider {
margin: 5px;
width: 310px;
height: 15px;
border-radius: 5px;
background: #E0E0E0;
background: -moz-linear-gradient(left top, #E0E0E0, #EEEEEE) repeat scroll 0 0 transparent;
background: -webkit-gradient(linear, left top, right bottom, from(#E0E0E0), to(#EEEEEE));
background: linear-gradient(left top, #E0E0E0, #EEEEEE);
}
.thumb {
position: relative;
top: -5px;
left: 10px;
width: 10px;
height: 25px;
border-radius: 3px;
background: blue;
cursor: pointer;
}
</style>
</head>
<body>
<div id="slider" class="slider">
<div class="thumb"></div>
</div>
<script>
var slider = new Slider({
elem: $('#slider')
});
function Slider(options) {
var elem = options.elem;
var thumbElem = elem.find('.thumb');
var sliderCoords, thumbCoords, shiftX, shiftY;
elem.on('dragstart', false)
.on('mousedown', '.thumb', onThumbMouseDown);
// ---------------
function onDocumentMouseMove(e) {
moveTo(e.pageX);
}
function onThumbMouseDown(e) {
startDrag(e.pageX, e.pageY);
return false; // disable selection start (cursor change)
}
function onDocumentMouseUp() {
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() {
$(document).off('.slider');
}
}
</script>
</body>
</html>

View file

@ -0,0 +1,8 @@
# Слайдер-компонент
[importance 5]
Перепишите слайдер в виде компонента:
[iframe src="solution" height=60 border=1]
Исходный документ возьмите из решения задачи [](/task/slider).

View file

@ -0,0 +1 @@
[edit src="solution"/]

View file

@ -0,0 +1 @@
{"name":"selectable-list-component","plunk":"qLlz9Z356jmTrqRYuIGU"}

View file

@ -0,0 +1,32 @@
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
<link rel="stylesheet" href="style.css">
<script src="http://code.jquery.com/jquery.min.js"></script>
<script src="listSelect.js"></script>
</head>
<body>
Клик на элементе выделяет только его.<br>
Ctrl(Cmd)+Клик добавляет/убирает элемент из выделенных.<br>
Shift+Клик добавляет промежуток от последнего кликнутого к выделению.<br>
<ul>
<li>Кристофер Робин</li>
<li>Винни-Пух</li>
<li>Ослик Иа</li>
<li>Мудрая Сова</li>
<li>Кролик. Просто кролик.</li>
</ul>
<button onclick="alert(listSelect.getSelected())">listSelect.getSelected()</button>
<script>
var listSelect = new ListSelect({
elem: $('ul')
});
</script>
</body>
</html>

View file

@ -0,0 +1,61 @@
function ListSelect(options) {
var elem = options.elem;
var lastClickedLi = null;
elem.on('click', 'li', onLiClick);
elem.on('selectstart mousedown', false);
function onLiClick(e) {
var li = $(this);
if(e.metaKey || e.ctrlKey) { // для Mac проверяем Cmd, т.к. Ctrl + click там контекстное меню
toggleSelect(li);
} else if (e.shiftKey) {
selectFromLast(li);
} else {
selectSingle(li);
}
lastClickedLi = li;
}
function deselectAll() {
elem.children().removeClass('selected');
}
function toggleSelect(li) {
li.toggleClass('selected');
}
function selectSingle(li) {
deselectAll();
li.addClass('selected');
}
function selectFromLast(target) {
var startElem = lastClickedLi || elem.children().first();
target.addClass('selected');
if (startElem[0] == target[0]) {
// клик на том же элементе, что и раньше
// это особый случай
return;
}
var isLastClickedBefore = startElem.index() < target.index();
if (isLastClickedBefore) {
startElem.nextUntil(target).add(startElem).addClass('selected');
} else {
startElem.prevUntil(target).add(startElem).addClass('selected');
}
}
this.getSelected = function() {
return elem.children('.selected').map(function() {
return this.innerHTML;
}).toArray();
};
}

View file

@ -0,0 +1 @@
{"name":"selectable-list-component","plunk":"qLlz9Z356jmTrqRYuIGU"}

View file

@ -0,0 +1,32 @@
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
<link rel="stylesheet" href="style.css">
<script src="http://code.jquery.com/jquery.min.js"></script>
<script src="listSelect.js"></script>
</head>
<body>
Клик на элементе выделяет только его.<br>
Ctrl(Cmd)+Клик добавляет/убирает элемент из выделенных.<br>
Shift+Клик добавляет промежуток от последнего кликнутого к выделению.<br>
<ul>
<li>Кристофер Робин</li>
<li>Винни-Пух</li>
<li>Ослик Иа</li>
<li>Мудрая Сова</li>
<li>Кролик. Просто кролик.</li>
</ul>
<button onclick="alert(listSelect.getSelected())">listSelect.getSelected()</button>
<script>
var listSelect = new ListSelect({
elem: $('ul')
});
</script>
</body>
</html>

View file

@ -0,0 +1,61 @@
function ListSelect(options) {
var elem = options.elem;
var lastClickedLi = null;
elem.on('click', 'li', onLiClick);
elem.on('selectstart mousedown', false);
function onLiClick(e) {
var li = $(this);
if(e.metaKey || e.ctrlKey) { // для Mac проверяем Cmd, т.к. Ctrl + click там контекстное меню
toggleSelect(li);
} else if (e.shiftKey) {
selectFromLast(li);
} else {
selectSingle(li);
}
lastClickedLi = li;
}
function deselectAll() {
elem.children().removeClass('selected');
}
function toggleSelect(li) {
li.toggleClass('selected');
}
function selectSingle(li) {
deselectAll();
li.addClass('selected');
}
function selectFromLast(target) {
var startElem = lastClickedLi || elem.children().first();
target.addClass('selected');
if (startElem[0] == target[0]) {
// клик на том же элементе, что и раньше
// это особый случай
return;
}
var isLastClickedBefore = startElem.index() < target.index();
if (isLastClickedBefore) {
startElem.nextUntil(target).add(startElem).addClass('selected');
} else {
startElem.prevUntil(target).add(startElem).addClass('selected');
}
}
this.getSelected = function() {
return elem.children('.selected').map(function() {
return this.innerHTML;
}).toArray();
};
}

View file

@ -0,0 +1,7 @@
.selected {
background: #0f0;
}
li {
cursor: pointer;
}

View file

@ -0,0 +1,7 @@
.selected {
background: #0f0;
}
li {
cursor: pointer;
}

View file

@ -0,0 +1,20 @@
# Компонент: список с выделением
[importance 5]
Перепишите решение задачи [](/task/selectable-list) (последний шаг) в виде компонента, с использованием jQuery.
У компонента должен быть единственный публичный метод `getSelected()`, который возвращает выбранные значения в виде массива.
Использование:
```js
var listSelect = new ListSelect({
elem: $('ul')
});
// listSelect.getSelected()
```
Демо:
[iframe border="1" src="solution"]

View file

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

View file

@ -0,0 +1 @@
{"name":"voter","plunk":"3zpTov5GyvhgEuJGHtUs"}

View file

@ -0,0 +1,69 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<style>
.voter {
font-family: Consolas, "Lucida Console", monospace;
font-size: 18px;
}
.up, .down {
cursor: pointer;
color: blue;
font-weight: bold;
}
</style>
<script src="http://code.jquery.com/jquery.min.js"></script>
</head>
<body>
<div id="voter" class="voter">
<span class="down"></span>
<span class="vote">0</span>
<span class="up">+</span>
</div>
<script>
function Voter(options) {
var elem = options.elem;
var voteElem = elem.find('.vote');
elem.on('click', '.down', onDownClick)
.on('click', '.up', onUpClick)
.on('mousedown selectstart', false);
// ----------- методы -------------
function onDownClick() {
voteDecrease(); // сам обработчик не меняет голос, он вызывает функцию
}
function onUpClick() {
voteIncrease();
}
function voteDecrease() {
voteElem.html( +voteElem.html()-1 );
}
function voteIncrease() {
voteElem.html( +voteElem.html()+1 );
}
this.setVote = function(vote) {
voteElem.html( +vote );
};
}
var voter = new Voter({
elem: $('#voter')
});
voter.setVote(1);
</script>
</body>
</html>

View file

@ -0,0 +1 @@
{"name":"voter","plunk":"3zpTov5GyvhgEuJGHtUs"}

View file

@ -0,0 +1,69 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<style>
.voter {
font-family: Consolas, "Lucida Console", monospace;
font-size: 18px;
}
.up, .down {
cursor: pointer;
color: blue;
font-weight: bold;
}
</style>
<script src="http://code.jquery.com/jquery.min.js"></script>
</head>
<body>
<div id="voter" class="voter">
<span class="down"></span>
<span class="vote">0</span>
<span class="up">+</span>
</div>
<script>
function Voter(options) {
var elem = options.elem;
var voteElem = elem.find('.vote');
elem.on('click', '.down', onDownClick)
.on('click', '.up', onUpClick)
.on('mousedown selectstart', false);
// ----------- методы -------------
function onDownClick() {
voteDecrease(); // сам обработчик не меняет голос, он вызывает функцию
}
function onUpClick() {
voteIncrease();
}
function voteDecrease() {
voteElem.html( +voteElem.html()-1 );
}
function voteIncrease() {
voteElem.html( +voteElem.html()+1 );
}
this.setVote = function(vote) {
voteElem.html( +vote );
};
}
var voter = new Voter({
elem: $('#voter')
});
voter.setVote(1);
</script>
</body>
</html>

View file

@ -0,0 +1,25 @@
# Голосовалка
[importance 5]
Напишите функцию-конструктор `new Voter(options)` для голосовалки.
Она должна получать элемент в `options.elem`, в следующей разметке:
```html
<div id="voter" class="voter">
<span class="down"></span>
<span class="vote">0</span>
<span class="up">+</span>
</div>
```
По клику на `+` и `—` число должно увеличиваться или уменьшаться.
**Публичный метод `voter.setVote(vote)` должен устанавливать текущее число -- значение голоса.**
Все остальные методы и свойства пусть будут приватными.
Результат:
[iframe src="solution" height=60 border=1]
[edit src="task" task/]

View file

@ -0,0 +1 @@
{"name":"voter-src","plunk":"GSEbTNTUewU3i46rcRkr"}

View file

@ -0,0 +1,41 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<style>
.voter {
font-family: Consolas, "Lucida Console", monospace;
font-size: 18px;
}
.up, .down {
cursor: pointer;
color: blue;
font-weight: bold;
}
</style>
<script src="http://code.jquery.com/jquery.min.js"></script>
</head>
<body>
<div id="voter" class="voter">
<span class="down"></span>
<span class="vote">0</span>
<span class="up">+</span>
</div>
<script>
function Voter(options) {
// ... ваш код
}
var voter = new Voter({
elem: $('#voter')
});
voter.setVote(1);
</script>
</body>
</html>

View file

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

View file

@ -0,0 +1 @@
{"name":"voter-proto","plunk":"Sm7J5wC8QbLt2rkIiCl9"}

View file

@ -0,0 +1,27 @@
<!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> <!-- отрефакторить -->
</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 Voter({
elem: $('#voter')
});
</script>
</body>
</html>

View file

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

View file

@ -0,0 +1,22 @@
function Voter(options) {
var elem = this._elem = options.elem;
this._voteElem = elem.find('.vote');
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._voteElem.html( +this._voteElem.html() - 1 );
};
Voter.prototype._onUpClick = function() {
this._voteElem.html( +this._voteElem.html() + 1 );
};
Voter.prototype.setVote = function(vote) {
this._voteElem.html(vote);
};

View file

@ -0,0 +1,9 @@
# Голосовалка в прототипном стиле ООП
[importance 5]
Поменяйте стиль ООП в голосовалке, созданной в задаче [](/task/voter) на прототипный.
Внешний код, использующий класс `Voter`, не должен измениться.
В качестве исходого кода возьмите решение задачи [](/task/voter).

View file

@ -0,0 +1,3 @@
Для показа голосов также добавлены семантические классы `.positive/.negative` в `style.css`.
[edit src="solution"]Открыть в песочнице[/edit]

View file

@ -0,0 +1 @@
{"name":"voter-colored","plunk":"Mjubul4i8Zo9jBsXnpwR"}

View file

@ -0,0 +1,15 @@
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

@ -0,0 +1,28 @@
<!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

@ -0,0 +1,18 @@
.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

@ -0,0 +1 @@
{"name":"voter-colored","plunk":"Mjubul4i8Zo9jBsXnpwR"}

View file

@ -0,0 +1,15 @@
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

@ -0,0 +1,28 @@
<!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

@ -0,0 +1,18 @@
.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

@ -0,0 +1,28 @@
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

@ -0,0 +1,28 @@
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

@ -0,0 +1,17 @@
# Добавить цвет в голосовалку
[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

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

View file

@ -0,0 +1 @@
{"name":"voter-step","plunk":"wWKiURmacREzSBJz8Aw2"}

View file

@ -0,0 +1,29 @@
<!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="step-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 StepVoter({
elem: $('#voter'),
step: 2
});
</script>
</body>
</html>

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._increase = function() {
this.setVote(this._vote + this._step);
};
StepVoter.prototype._decrease = function() {
this.setVote(this._vote - this._step);
};

View file

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

View file

@ -0,0 +1 @@
{"name":"voter-step","plunk":"wWKiURmacREzSBJz8Aw2"}

View file

@ -0,0 +1,29 @@
<!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="step-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 StepVoter({
elem: $('#voter'),
step: 2
});
</script>
</body>
</html>

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._increase = function() {
this.setVote(this._vote + this._step);
};
StepVoter.prototype._decrease = function() {
this.setVote(this._vote - this._step);
};

View file

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

View file

@ -0,0 +1,36 @@
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

@ -0,0 +1,36 @@
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

@ -0,0 +1,28 @@
# Добавить двойной голос в голосовалку
[importance 5]
Создайте функцию-конструктор `StepVoter`, которая наследует от голосовалки, созданной в задаче [](/task/voter-proto) и добавляет голосовалке опцию `options.step`, которая задаёт "шаг" голоса.
Пример:
```js
var voter = new StepVoter({
elem: $('#voter'),
step: 2 // увеличивать/уменьшать сразу на 2 пункта
});
```
Результат:
[iframe border=1 height=60 src="solution"]
В реальном проекте влияние клика на голосовалку может зависеть от полномочий или репутации посетителя.
Сделайте задачу в два этапа:
<ol>
<li>Поменять исходный класс `Voter`: вынести логику изменения значений в защищенные методы `_increase/_decrease`, так чтобы их можно было переопределить в наседнике.
На этом этапе использование кода не должно измениться, код в `index.html` будет тот же.</li>
<li>Сделать новый класс `StepVoter`, в котором обработать дополнительную опцию и переопределить `_increase/_decrease` соответственно. Затем использовать его в `index.html`.</li>
</ol>
В качестве исходного кода используйте решение задачи [](/task/voter-proto).

View file

@ -0,0 +1,287 @@
# Графические компоненты
Первый и главный шаг в наведении порядка -- это оформить код в объекты, каждый из которых будет решать свою задачу.
Здесь мы сосредоточимся на графических компонентах, которые также называют "виджетами".
В браузерах есть встроенные виджеты, например `<select>`, `<input>` и другие элементы, о которых мы даже и не думаем, как они работают. Просто работают: принимают значение, вызывают события...
Наша задача -- сделать то же самое на уровне выше. Мы будем создавать объекты, которые генерируют меню, диалог или другие компоненты интерфейса, и дают возможность удобно работать с ними.
## Виджет Menu
Мы начнём работу с виджета, который предусматривает уже готовую разметку.
То есть, в нужном месте HTML находится DOM-структура для меню -- заголовок и список опций:
```html
<div class="menu" id="sweets-menu">
<span class="title">Сладости</span>
<ul>
<li>Торт</li>
<li>Пончик</li>
<li>...</li>
</ul>
</div>
```
Далее она может дополняться, изменяться, но в начале -- она такая.
Обратим внимание на важные соглашения:
<dl>
<dt>Вся разметка заключена в корневой элемент `<div class="menu" id="sweeties-menu">`.</dt>
<dd>Это очень удобно: вынул этот элемент из DOM -- нет меню, вставил в другое место -- переместил меню. Кроме того, можно удобно искать подэлементы.</dd>
<dt>В разметке -- только классы.</dt>
<dd>Документ вполне может содержать много различных меню. Они не должны конфликтовать между собой, поэтому для разметки везде используются классы.
Исключение -- корневой элемент. В данном случае мы предполагаем, что данное конкретное "меню сладостей" в документе только одно, поэтому даём ему `id`.</dd>
</dl>
Для работы с разметкой будем создавать объект `new Menu` и передавать ему корневой элемент. В конструкторе он поставит необходимые обработчики.
```js
function Menu(options) {
var elem = options.elem;
elem.on('mousedown selectstart', false);
elem.on('click', '.title', function() {
elem.toggleClass('open');
});
}
// использование
var menu = new Menu({
elem: $('#sweets-menu')
});
```
Меню:
[example src="menu-1"]
Это, конечно, только первый шаг, но уже здесь видны некоторые важные соглашения в коде.
<dl>
<dt>У конструктора только один аргумент -- объект `options`.</dt>
<dd>Это удобно, так как у графических компонентов обычно много настроек, большинство из которых имеют разумные значения "по умолчанию". Если передавать аргументы через запятую -- их будет слишком много.</dd>
<dt>Обработчики назначаются через делегирование.</dt>
<dd>Вместо того, чтобы найти элемент и поставить обработчик на него:
```js
var titleElem = elem.find('.title');
titleElem.on('click', function() {
elem.toggleClass('open');
}
```
...Мы пишем так:
```js
elem.on('click', '.title', function() {
elem.toggleClass('open');
});
```
Это ускоряет инициализацию, так как не надо искать элементы, и даёт возможность в любой момент менять DOM внутри, в том числе через `innerHTML`, без необходимости переставлять обработчика.
</dd>
</dl>
## Публичные методы
Уважающий себя компонент обычно имеет публичные методы, которые позволяют управлять им снаружи.
Рассмотрим повнимательнее этот фрагмент:
```js
elem.on('click', '.title', function() {
elem.toggleClass('open');
});
```
Здесь в обработчике события сразу код работы с элементами. Пока одна строка -- всё понятно, но если их будет много, то при чтении понадобится долго и упорно вникать: "А что же, всё-таки, такое делается при клике?"
Для улучшения читаемости выделим обработчик в отдельную функцию `toggle`, которая к тому же станет полезным публичным методом:
```js
function Menu(options) {
var elem = options.elem;
elem.on('mousedown selectstart', false);
*!*
elem.on('click', '.title', onTitleClick);
function onTitleClick(e) {
toggle();
}
function toggle() {
elem.toggleClass('open');
};
*/!*
this.toggle = toggle;
}
```
Здесь и сам обработчик события тоже вынесен в отдельную функцию `onTitleClick`.
Наши бонусы:
<ol>
<li>Во-первых, стало проще найти и расширить обработчик события в коде -- имя `onTitleClick` найти и запомнить.</li>
<li>Во-вторых, код стал лучше читаться.</li>
<li>Во-третьих, `toggle` теперь -- отдельная функция, доступная извне.</li>
</ol>
Пример использования публичного метода:
```js
var menu = new Menu(...);
menu.toggle();
```
## Генерация DOM-дерева
До этого момента меню "оживляло" уже существующий HTML. Но в более сложном интерфейсе нужно уметь сгенерировать меню "на лету", по данным.
Для этого добавим меню три метода:
<ul>
<li>`render()` -- генерирует корневой DOM-элемент и заголовок меню, приватный.</li>
<li>`renderItems()` -- генерирует DOM для списка опций (`<li>`), приватный.</li>
<li>`getElem()` -- возвращает DOM-элемент меню, при необходимости запуская генерацию, публичный.</li>
</ul>
Функция генерации корневого элемента с заголовком `render` отделена от генерации списка `renderItems`. Почему -- будет видно чуть далее.
Новый способ использования меню:
```js
*!*
// создать объект меню с данным заголовком и опциями
*/!*
var menu = new Menu({
title: "Сладости",
items: [
"Торт",
"Пончик",
"Пирожное",
"Шоколадка",
"Мороженое"
]
});
*!*
// получить DOM-элемент меню
*/!*
var elem = menu.getElem();
*!*
// вставить меню в нужное место страницы
*/!*
$('#sweets-menu-holder').append( elem );
```
Код `Menu` с новыми методами:
```js
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);
}
// ...
}
```
**Важнейший принцип, который здесь использован -- ленивость.**
Мы стараемся откладывать работу до момента, когда она реально нужна. Например, когда `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() {
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;
}
```
Основные изменения -- теперь метод `toggle` не просто меняет класс. Этого недостаточно, ведь, чтобы открыть меню, нужно для начала отрендерить его опции. Поэтому добавлено два метода `open` и `close`, которые также полезны и для внешнего интерфейса.
В действии:
[example src="menu-3-elem" height="200"]
## Итого
Мы начали создавать компонент "с чистого листа", пока без дополнительных библиотек, но они скоро понадобятся.
Основные принципы:
<ul>
<li>В конструктор передаётся объект аргументов `options`, а не список аргументов -- для удобства дополнения и расширения виджета.</li>
<li>Обработчики назначаются через делегирование -- для производительности и упрощения виджета.</li>
<li>Не экономим буквы ценой понятности -- действие и/или обработчик заслуживают быть отдельными функциями.</li>
<li>Будем ленивыми -- если существенный участок работы можно отложить до реального задействования виджета -- откладываем его.</li>
</ul>
Далее мы продолжим работать со разметкой виджета.

View file

@ -0,0 +1 @@
{"name":"menu-1","plunk":"RdmJqSRbccWlmhJL4jkb"}

View file

@ -0,0 +1,29 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<link rel="stylesheet" href="style.css">
<script src="http://code.jquery.com/jquery.min.js"></script>
<script src="menu.js"></script>
</head>
<body>
<div id="sweets-menu" class="menu">
<span class="title">Сладости</span>
<ul>
<li>Торт</li>
<li>Пончик</li>
<li>Пирожное</li>
<li>Шоколадка</li>
<li>Мороженое</li>
</ul>
</div>
<script>
var menu = new Menu({
elem: $('#sweets-menu')
});
</script>
</body>
</html>

View file

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

View file

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

View file

@ -0,0 +1 @@
{"name":"menu-3-elem","plunk":"Hs6BitysvwUxDLWoVFo5"}

View file

@ -0,0 +1,34 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<link rel="stylesheet" href="style.css">
<script src="http://code.jquery.com/jquery.min.js"></script>
<script src="menu.js"></script>
</head>
<body>
<button onclick="menu.toggle()">menu.toggle()</button>
<button onclick="menu.open()">menu.open()</button>
<button onclick="menu.close()">menu.close()</button>
<div id="sweets-menu-holder"></div>
<script>
var menu = new Menu({
title: "Сладости",
items: [
"Торт",
"Пончик",
"Пирожное",
"Шоколадка",
"Мороженое"
]
});
$('#sweets-menu-holder').append(menu.getElem());
</script>
</body>
</html>

View file

@ -0,0 +1,51 @@
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

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