init
This commit is contained in:
parent
06f61d8ce8
commit
f301cb744d
2271 changed files with 103162 additions and 0 deletions
|
@ -0,0 +1 @@
|
|||
[edit src="solution"]Открыть в песочнице[/edit]
|
1
02-ui/05-widgets/02-widgets-structure/01-clock/solution/.plnkr
Executable file
1
02-ui/05-widgets/02-widgets-structure/01-clock/solution/.plnkr
Executable file
|
@ -0,0 +1 @@
|
|||
{"name":"clock","plunk":"APYZSjtojpw9wwlNc6xB"}
|
31
02-ui/05-widgets/02-widgets-structure/01-clock/solution/clock.js
Executable file
31
02-ui/05-widgets/02-widgets-structure/01-clock/solution/clock.js
Executable 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);
|
||||
}
|
||||
|
||||
}
|
1
02-ui/05-widgets/02-widgets-structure/01-clock/solution/clock/.plnkr
Executable file
1
02-ui/05-widgets/02-widgets-structure/01-clock/solution/clock/.plnkr
Executable file
|
@ -0,0 +1 @@
|
|||
{"name":"clock","plunk":"APYZSjtojpw9wwlNc6xB"}
|
31
02-ui/05-widgets/02-widgets-structure/01-clock/solution/clock/clock.js
Executable file
31
02-ui/05-widgets/02-widgets-structure/01-clock/solution/clock/clock.js
Executable 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);
|
||||
}
|
||||
|
||||
}
|
37
02-ui/05-widgets/02-widgets-structure/01-clock/solution/clock/index.html
Executable file
37
02-ui/05-widgets/02-widgets-structure/01-clock/solution/clock/index.html
Executable 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>
|
37
02-ui/05-widgets/02-widgets-structure/01-clock/solution/index.html
Executable file
37
02-ui/05-widgets/02-widgets-structure/01-clock/solution/index.html
Executable 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>
|
25
02-ui/05-widgets/02-widgets-structure/01-clock/task.md
Normal file
25
02-ui/05-widgets/02-widgets-structure/01-clock/task.md
Normal 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/]
|
1
02-ui/05-widgets/02-widgets-structure/01-clock/task/.plnkr
Executable file
1
02-ui/05-widgets/02-widgets-structure/01-clock/task/.plnkr
Executable file
|
@ -0,0 +1 @@
|
|||
{"name":"clock-src","plunk":"p4DSlwiBguQ8iirU3RMu"}
|
38
02-ui/05-widgets/02-widgets-structure/01-clock/task/index.html
Executable file
38
02-ui/05-widgets/02-widgets-structure/01-clock/task/index.html
Executable 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>
|
|
@ -0,0 +1,3 @@
|
|||
Пример переписанного слайдера:
|
||||
|
||||
[edit src="solution"/]
|
1
02-ui/05-widgets/02-widgets-structure/02-slider-widget/solution/.plnkr
Executable file
1
02-ui/05-widgets/02-widgets-structure/02-slider-widget/solution/.plnkr
Executable file
|
@ -0,0 +1 @@
|
|||
{"name":"slider-simple","plunk":"Lby0d7r3u4irsI8Ml24K"}
|
79
02-ui/05-widgets/02-widgets-structure/02-slider-widget/solution/index.html
Executable file
79
02-ui/05-widgets/02-widgets-structure/02-slider-widget/solution/index.html
Executable 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>
|
||||
|
44
02-ui/05-widgets/02-widgets-structure/02-slider-widget/solution/lib.js
Executable file
44
02-ui/05-widgets/02-widgets-structure/02-slider-widget/solution/lib.js
Executable 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) };
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1 @@
|
|||
{"name":"slider-component","plunk":"yOsWKXyShloaVwdeA5Of"}
|
|
@ -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>
|
|
@ -0,0 +1,8 @@
|
|||
# Слайдер-компонент
|
||||
|
||||
[importance 5]
|
||||
|
||||
Перепишите слайдер в виде компонента:
|
||||
[iframe src="solution" height=60 border=1]
|
||||
|
||||
Исходный документ возьмите из решения задачи [](/task/slider).
|
|
@ -0,0 +1 @@
|
|||
[edit src="solution"/]
|
|
@ -0,0 +1 @@
|
|||
{"name":"selectable-list-component","plunk":"qLlz9Z356jmTrqRYuIGU"}
|
|
@ -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>
|
|
@ -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();
|
||||
};
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
{"name":"selectable-list-component","plunk":"qLlz9Z356jmTrqRYuIGU"}
|
|
@ -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>
|
|
@ -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();
|
||||
};
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
|
||||
.selected {
|
||||
background: #0f0;
|
||||
}
|
||||
li {
|
||||
cursor: pointer;
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
|
||||
.selected {
|
||||
background: #0f0;
|
||||
}
|
||||
li {
|
||||
cursor: pointer;
|
||||
}
|
|
@ -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"]
|
||||
|
|
@ -0,0 +1 @@
|
|||
[edit src="solution"]Открыть решение в песочнице[/edit]
|
1
02-ui/05-widgets/02-widgets-structure/04-voter/solution/.plnkr
Executable file
1
02-ui/05-widgets/02-widgets-structure/04-voter/solution/.plnkr
Executable file
|
@ -0,0 +1 @@
|
|||
{"name":"voter","plunk":"3zpTov5GyvhgEuJGHtUs"}
|
69
02-ui/05-widgets/02-widgets-structure/04-voter/solution/index.html
Executable file
69
02-ui/05-widgets/02-widgets-structure/04-voter/solution/index.html
Executable 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>
|
||||
|
1
02-ui/05-widgets/02-widgets-structure/04-voter/solution/voter/.plnkr
Executable file
1
02-ui/05-widgets/02-widgets-structure/04-voter/solution/voter/.plnkr
Executable file
|
@ -0,0 +1 @@
|
|||
{"name":"voter","plunk":"3zpTov5GyvhgEuJGHtUs"}
|
69
02-ui/05-widgets/02-widgets-structure/04-voter/solution/voter/index.html
Executable file
69
02-ui/05-widgets/02-widgets-structure/04-voter/solution/voter/index.html
Executable 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>
|
||||
|
25
02-ui/05-widgets/02-widgets-structure/04-voter/task.md
Normal file
25
02-ui/05-widgets/02-widgets-structure/04-voter/task.md
Normal 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/]
|
1
02-ui/05-widgets/02-widgets-structure/04-voter/task/.plnkr
Executable file
1
02-ui/05-widgets/02-widgets-structure/04-voter/task/.plnkr
Executable file
|
@ -0,0 +1 @@
|
|||
{"name":"voter-src","plunk":"GSEbTNTUewU3i46rcRkr"}
|
41
02-ui/05-widgets/02-widgets-structure/04-voter/task/index.html
Executable file
41
02-ui/05-widgets/02-widgets-structure/04-voter/task/index.html
Executable 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>
|
||||
|
|
@ -0,0 +1 @@
|
|||
[edit src="solution"]Открыть в песочнице[/edit]
|
1
02-ui/05-widgets/02-widgets-structure/05-voter-proto/solution/.plnkr
Executable file
1
02-ui/05-widgets/02-widgets-structure/05-voter-proto/solution/.plnkr
Executable file
|
@ -0,0 +1 @@
|
|||
{"name":"voter-proto","plunk":"Sm7J5wC8QbLt2rkIiCl9"}
|
27
02-ui/05-widgets/02-widgets-structure/05-voter-proto/solution/index.html
Executable file
27
02-ui/05-widgets/02-widgets-structure/05-voter-proto/solution/index.html
Executable 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>
|
||||
|
10
02-ui/05-widgets/02-widgets-structure/05-voter-proto/solution/style.css
Executable file
10
02-ui/05-widgets/02-widgets-structure/05-voter-proto/solution/style.css
Executable file
|
@ -0,0 +1,10 @@
|
|||
|
||||
.voter {
|
||||
font-family: Consolas, "Lucida Console", monospace;
|
||||
font-size: 18px;
|
||||
}
|
||||
.up, .down {
|
||||
cursor: pointer;
|
||||
color: blue;
|
||||
font-weight: bold;
|
||||
}
|
22
02-ui/05-widgets/02-widgets-structure/05-voter-proto/solution/voter.js
Executable file
22
02-ui/05-widgets/02-widgets-structure/05-voter-proto/solution/voter.js
Executable 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);
|
||||
};
|
|
@ -0,0 +1,9 @@
|
|||
# Голосовалка в прототипном стиле ООП
|
||||
|
||||
[importance 5]
|
||||
|
||||
Поменяйте стиль ООП в голосовалке, созданной в задаче [](/task/voter) на прототипный.
|
||||
|
||||
Внешний код, использующий класс `Voter`, не должен измениться.
|
||||
|
||||
В качестве исходого кода возьмите решение задачи [](/task/voter).
|
|
@ -0,0 +1,3 @@
|
|||
Для показа голосов также добавлены семантические классы `.positive/.negative` в `style.css`.
|
||||
|
||||
[edit src="solution"]Открыть в песочнице[/edit]
|
1
02-ui/05-widgets/02-widgets-structure/06-voter-colored/solution/.plnkr
Executable file
1
02-ui/05-widgets/02-widgets-structure/06-voter-colored/solution/.plnkr
Executable file
|
@ -0,0 +1 @@
|
|||
{"name":"voter-colored","plunk":"Mjubul4i8Zo9jBsXnpwR"}
|
|
@ -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');
|
||||
}
|
||||
};
|
28
02-ui/05-widgets/02-widgets-structure/06-voter-colored/solution/index.html
Executable file
28
02-ui/05-widgets/02-widgets-structure/06-voter-colored/solution/index.html
Executable 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>
|
||||
|
18
02-ui/05-widgets/02-widgets-structure/06-voter-colored/solution/style.css
Executable file
18
02-ui/05-widgets/02-widgets-structure/06-voter-colored/solution/style.css
Executable 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;
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
{"name":"voter-colored","plunk":"Mjubul4i8Zo9jBsXnpwR"}
|
|
@ -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');
|
||||
}
|
||||
};
|
|
@ -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>
|
||||
|
|
@ -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;
|
||||
}
|
|
@ -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();
|
||||
};
|
28
02-ui/05-widgets/02-widgets-structure/06-voter-colored/solution/voter.js
Executable file
28
02-ui/05-widgets/02-widgets-structure/06-voter-colored/solution/voter.js
Executable 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();
|
||||
};
|
|
@ -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).
|
|
@ -0,0 +1 @@
|
|||
[edit src="solution"]Открыть в песочнице[/edit]
|
|
@ -0,0 +1 @@
|
|||
{"name":"voter-step","plunk":"wWKiURmacREzSBJz8Aw2"}
|
|
@ -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>
|
||||
|
|
@ -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);
|
||||
};
|
|
@ -0,0 +1,10 @@
|
|||
|
||||
.voter {
|
||||
font-family: Consolas, "Lucida Console", monospace;
|
||||
font-size: 18px;
|
||||
}
|
||||
.up, .down {
|
||||
cursor: pointer;
|
||||
color: blue;
|
||||
font-weight: bold;
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
{"name":"voter-step","plunk":"wWKiURmacREzSBJz8Aw2"}
|
|
@ -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>
|
||||
|
|
@ -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);
|
||||
};
|
|
@ -0,0 +1,10 @@
|
|||
|
||||
.voter {
|
||||
font-family: Consolas, "Lucida Console", monospace;
|
||||
font-size: 18px;
|
||||
}
|
||||
.up, .down {
|
||||
cursor: pointer;
|
||||
color: blue;
|
||||
font-weight: bold;
|
||||
}
|
|
@ -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);
|
||||
};
|
|
@ -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);
|
||||
};
|
|
@ -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).
|
287
02-ui/05-widgets/02-widgets-structure/article.md
Normal file
287
02-ui/05-widgets/02-widgets-structure/article.md
Normal 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>
|
||||
|
||||
Далее мы продолжим работать со разметкой виджета.
|
||||
|
1
02-ui/05-widgets/02-widgets-structure/menu-1/.plnkr
Executable file
1
02-ui/05-widgets/02-widgets-structure/menu-1/.plnkr
Executable file
|
@ -0,0 +1 @@
|
|||
{"name":"menu-1","plunk":"RdmJqSRbccWlmhJL4jkb"}
|
29
02-ui/05-widgets/02-widgets-structure/menu-1/index.html
Executable file
29
02-ui/05-widgets/02-widgets-structure/menu-1/index.html
Executable 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>
|
11
02-ui/05-widgets/02-widgets-structure/menu-1/menu.js
Executable file
11
02-ui/05-widgets/02-widgets-structure/menu-1/menu.js
Executable 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');
|
||||
});
|
||||
|
||||
}
|
20
02-ui/05-widgets/02-widgets-structure/menu-1/style.css
Executable file
20
02-ui/05-widgets/02-widgets-structure/menu-1/style.css
Executable 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);
|
||||
}
|
1
02-ui/05-widgets/02-widgets-structure/menu-3-elem/.plnkr
Executable file
1
02-ui/05-widgets/02-widgets-structure/menu-3-elem/.plnkr
Executable file
|
@ -0,0 +1 @@
|
|||
{"name":"menu-3-elem","plunk":"Hs6BitysvwUxDLWoVFo5"}
|
34
02-ui/05-widgets/02-widgets-structure/menu-3-elem/index.html
Executable file
34
02-ui/05-widgets/02-widgets-structure/menu-3-elem/index.html
Executable 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>
|
51
02-ui/05-widgets/02-widgets-structure/menu-3-elem/menu.js
Executable file
51
02-ui/05-widgets/02-widgets-structure/menu-3-elem/menu.js
Executable 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;
|
||||
}
|
20
02-ui/05-widgets/02-widgets-structure/menu-3-elem/style.css
Executable file
20
02-ui/05-widgets/02-widgets-structure/menu-3-elem/style.css
Executable 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);
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue