renovations
This commit is contained in:
parent
0eec1aaccb
commit
c018a2db03
56 changed files with 459 additions and 594 deletions
|
@ -8,15 +8,15 @@ function Clock(options) {
|
|||
|
||||
var hours = date.getHours();
|
||||
if (hours < 10) hours = '0' + hours;
|
||||
$('.hour', elem).html(hours);
|
||||
elem.querySelector('.hour').innerHTML = hours;
|
||||
|
||||
var min = date.getMinutes();
|
||||
if (min < 10) min = '0' + min;
|
||||
$('.min', elem).html(min);
|
||||
elem.querySelector('.min').innerHTML = min;
|
||||
|
||||
var sec = date.getSeconds();
|
||||
if (sec < 10) sec = '0' + sec;
|
||||
$('.sec', elem).html(sec);
|
||||
elem.querySelector('.sec').innerHTML = sec;
|
||||
}
|
||||
|
||||
this.stop = function() {
|
||||
|
@ -26,6 +26,6 @@ function Clock(options) {
|
|||
this.start = function() {
|
||||
render();
|
||||
timer = setInterval(render, 1000);
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
|
|
@ -2,7 +2,6 @@
|
|||
<html>
|
||||
<head>
|
||||
<title>Часики</title>
|
||||
<script src="http://code.jquery.com/jquery.min.js"></script>
|
||||
<script src="clock.js"></script>
|
||||
<meta charset="utf-8">
|
||||
<style>
|
||||
|
@ -20,7 +19,7 @@
|
|||
<script>
|
||||
|
||||
var pageClock = new Clock({
|
||||
elem: $('#clock')
|
||||
elem: document.getElementById('clock')
|
||||
});
|
||||
|
||||
</script>
|
||||
|
|
|
@ -2,7 +2,6 @@
|
|||
<html>
|
||||
<head>
|
||||
<title>Часики</title>
|
||||
<script src="http://code.jquery.com/jquery.min.js"></script>
|
||||
<meta charset="utf-8">
|
||||
<style>
|
||||
.hour { color: green }
|
||||
|
@ -21,7 +20,7 @@
|
|||
// .. ваш код Clock
|
||||
|
||||
var pageClock = new Clock({
|
||||
elem: $('#clock')
|
||||
elem: document.getElementById('clock')
|
||||
});
|
||||
|
||||
</script>
|
||||
|
|
|
@ -20,5 +20,5 @@ clock.stop(); // стоп
|
|||
При нажатии на `alert` часы должны приостанавливаться, а затем продолжать идти с правильным временем.
|
||||
|
||||
Пример результата:
|
||||
[iframe src="solution" border=1]
|
||||
[iframe src="solution" height=80]
|
||||
|
||||
|
|
|
@ -1,2 +1 @@
|
|||
Пример переписанного слайдера:
|
||||
|
||||
|
|
|
@ -3,9 +3,8 @@
|
|||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
|
||||
<script src="http://code.jquery.com/jquery.min.js"></script>
|
||||
|
||||
<script src="https://cdn.polyfill.io/v1/polyfill.js?features=Element.prototype.closest"></script>
|
||||
|
||||
<style>
|
||||
.slider {
|
||||
margin: 5px;
|
||||
|
@ -36,68 +35,68 @@
|
|||
</div>
|
||||
<script>
|
||||
var slider = new Slider({
|
||||
elem: $('#slider')
|
||||
elem: document.getElementById('slider')
|
||||
});
|
||||
|
||||
function Slider(options) {
|
||||
var elem = options.elem;
|
||||
var thumbElem = elem.find('.thumb');
|
||||
var thumbElem = elem.querySelector('.thumb');
|
||||
|
||||
var sliderCoords, thumbCoords, shiftX, shiftY;
|
||||
|
||||
elem.on('dragstart', false)
|
||||
.on('mousedown', '.thumb', onThumbMouseDown);
|
||||
elem.ondragstart = function() {
|
||||
return false;
|
||||
};
|
||||
|
||||
// ---------------
|
||||
|
||||
function onDocumentMouseMove(e) {
|
||||
moveTo(e.pageX);
|
||||
elem.onmousedown = function(event) {
|
||||
if (event.target.closest('.thumb')) {
|
||||
startDrag(event.clientX, event.clientY);
|
||||
return false; // disable selection start (cursor change)
|
||||
}
|
||||
}
|
||||
|
||||
function onThumbMouseDown(e) {
|
||||
startDrag(e.pageX, e.pageY);
|
||||
return false; // disable selection start (cursor change)
|
||||
function startDrag(startClientX, startClientY) {
|
||||
thumbCoords = thumbElem.getBoundingClientRect();
|
||||
shiftX = startClientX - thumbCoords.left;
|
||||
shiftY = startClientY - thumbCoords.top;
|
||||
|
||||
sliderCoords = elem.getBoundingClientRect();
|
||||
|
||||
document.addEventListener('mousemove', onDocumentMouseMove);
|
||||
document.addEventListener('mouseup', onDocumentMouseUp);
|
||||
}
|
||||
|
||||
|
||||
|
||||
function moveTo(clientX) {
|
||||
// вычесть координату родителя, т.к. position: relative
|
||||
var newLeft = clientX - shiftX - sliderCoords.left;
|
||||
|
||||
// курсор ушёл вне слайдера
|
||||
if(newLeft < 0) {
|
||||
newLeft = 0;
|
||||
}
|
||||
var rightEdge = elem.offsetWidth - thumbElem.offsetWidth;
|
||||
if(newLeft > rightEdge) {
|
||||
newLeft = rightEdge;
|
||||
}
|
||||
|
||||
thumbElem.style.left = newLeft + 'px';
|
||||
}
|
||||
|
||||
|
||||
function onDocumentMouseMove(e) {
|
||||
moveTo(e.clientX);
|
||||
}
|
||||
|
||||
function onDocumentMouseUp() {
|
||||
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');
|
||||
document.removeEventListener('mousemove', onDocumentMouseMove);
|
||||
document.removeEventListener('mouseup', onDocumentMouseUp);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
<head>
|
||||
<meta charset="utf-8">
|
||||
<link rel="stylesheet" href="style.css">
|
||||
<script src="http://code.jquery.com/jquery.min.js"></script>
|
||||
<script src="https://cdn.polyfill.io/v1/polyfill.js?features=Element.prototype.closest"></script>
|
||||
<script src="listSelect.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
|
@ -24,7 +24,7 @@ Shift+Клик добавляет промежуток от последнего
|
|||
|
||||
<script>
|
||||
var listSelect = new ListSelect({
|
||||
elem: $('ul')
|
||||
elem: document.querySelector('ul')
|
||||
});
|
||||
</script>
|
||||
|
||||
|
|
|
@ -4,11 +4,13 @@ function ListSelect(options) {
|
|||
|
||||
var lastClickedLi = null;
|
||||
|
||||
elem.on('click', 'li', onLiClick);
|
||||
elem.on('selectstart mousedown', false);
|
||||
elem.onmousedown = function() {
|
||||
return false;
|
||||
};
|
||||
|
||||
function onLiClick(e) {
|
||||
var li = $(this);
|
||||
elem.onclick = function(e) {
|
||||
var li = e.target.closest('li');
|
||||
if (!li) return;
|
||||
|
||||
if(e.metaKey || e.ctrlKey) { // для Mac проверяем Cmd, т.к. Ctrl + click там контекстное меню
|
||||
toggleSelect(li);
|
||||
|
@ -22,40 +24,46 @@ function ListSelect(options) {
|
|||
}
|
||||
|
||||
function deselectAll() {
|
||||
elem.children().removeClass('selected');
|
||||
}
|
||||
[].forEach.call(elem.children, function(child) {
|
||||
child.classList.remove('selected')
|
||||
});
|
||||
}
|
||||
|
||||
function toggleSelect(li) {
|
||||
li.toggleClass('selected');
|
||||
li.classList.toggle('selected');
|
||||
}
|
||||
|
||||
function selectSingle(li) {
|
||||
deselectAll();
|
||||
li.addClass('selected');
|
||||
li.classList.add('selected');
|
||||
}
|
||||
|
||||
function selectFromLast(target) {
|
||||
var startElem = lastClickedLi || elem.children().first();
|
||||
var startElem = lastClickedLi || elem.children[0];
|
||||
|
||||
target.addClass('selected');
|
||||
if (startElem[0] == target[0]) {
|
||||
target.classList.add('selected');
|
||||
if (startElem == target) {
|
||||
// клик на том же элементе, что и раньше
|
||||
// это особый случай
|
||||
return;
|
||||
}
|
||||
|
||||
var isLastClickedBefore = startElem.index() < target.index();
|
||||
var isLastClickedBefore = startElem.compareDocumentPosition(target) & 4;
|
||||
|
||||
if (isLastClickedBefore) {
|
||||
startElem.nextUntil(target).add(startElem).addClass('selected');
|
||||
for(var elem = startElem; elem != target; elem = elem.nextElementSibling) {
|
||||
elem.classList.add('selected');
|
||||
}
|
||||
} else {
|
||||
startElem.prevUntil(target).add(startElem).addClass('selected');
|
||||
for(var elem = startElem; elem != target; elem = elem.previousElementSibling) {
|
||||
elem.classList.add('selected');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.getSelected = function() {
|
||||
return elem.children('.selected').map(function() {
|
||||
return this.innerHTML;
|
||||
}).toArray();
|
||||
return [].map.call(elem.querySelectorAll('.selected'), function(li) {
|
||||
return li.innerHTML;
|
||||
});
|
||||
};
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
[importance 5]
|
||||
|
||||
Перепишите решение задачи [](/task/selectable-list) (последний шаг) в виде компонента, с использованием jQuery.
|
||||
Перепишите решение задачи [](/task/selectable-list) в виде компонента.
|
||||
|
||||
У компонента должен быть единственный публичный метод `getSelected()`, который возвращает выбранные значения в виде массива.
|
||||
|
||||
|
@ -10,7 +10,7 @@
|
|||
|
||||
```js
|
||||
var listSelect = new ListSelect({
|
||||
elem: $('ul')
|
||||
elem: document.querySelector('ul')
|
||||
});
|
||||
// listSelect.getSelected()
|
||||
```
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
[edit src="solution"]Открыть решение в песочнице[/edit]
|
|
@ -2,6 +2,7 @@
|
|||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<script src="https://cdn.polyfill.io/v1/polyfill.js?features=Element.prototype.closest"></script>
|
||||
<style>
|
||||
.voter {
|
||||
font-family: Consolas, "Lucida Console", monospace;
|
||||
|
@ -13,7 +14,6 @@
|
|||
font-weight: bold;
|
||||
}
|
||||
</style>
|
||||
<script src="http://code.jquery.com/jquery.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
|
@ -27,38 +27,39 @@
|
|||
function Voter(options) {
|
||||
var elem = options.elem;
|
||||
|
||||
var voteElem = elem.find('.vote');
|
||||
var voteElem = elem.querySelector('.vote');
|
||||
|
||||
elem.on('click', '.down', onDownClick)
|
||||
.on('click', '.up', onUpClick)
|
||||
.on('mousedown selectstart', false);
|
||||
elem.onclick = function(event) {
|
||||
// сам обработчик не меняет голос, он вызывает функцию
|
||||
if (event.target.closest('.down')) {
|
||||
voteDecrease();
|
||||
} else if (event.target.closest('.up')) {
|
||||
voteIncrease();
|
||||
}
|
||||
}
|
||||
|
||||
elem.onmousedown = function() {
|
||||
return false;
|
||||
};
|
||||
|
||||
// ----------- методы -------------
|
||||
|
||||
function onDownClick() {
|
||||
voteDecrease(); // сам обработчик не меняет голос, он вызывает функцию
|
||||
}
|
||||
|
||||
function onUpClick() {
|
||||
voteIncrease();
|
||||
}
|
||||
|
||||
function voteDecrease() {
|
||||
voteElem.html( +voteElem.html()-1 );
|
||||
voteElem.innerHTML = +voteElem.innerHTML - 1;
|
||||
}
|
||||
|
||||
function voteIncrease() {
|
||||
voteElem.html( +voteElem.html()+1 );
|
||||
voteElem.innerHTML = +voteElem.innerHTML + 1;
|
||||
}
|
||||
|
||||
this.setVote = function(vote) {
|
||||
voteElem.html( +vote );
|
||||
voteElem.innerHTML = +vote;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
var voter = new Voter({
|
||||
elem: $('#voter')
|
||||
elem: document.getElementById('voter')
|
||||
});
|
||||
voter.setVote(1);
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<script src="https://cdn.polyfill.io/v1/polyfill.js?features=Element.prototype.closest"></script>
|
||||
<style>
|
||||
.voter {
|
||||
font-family: Consolas, "Lucida Console", monospace;
|
||||
|
@ -13,7 +14,6 @@
|
|||
font-weight: bold;
|
||||
}
|
||||
</style>
|
||||
<script src="http://code.jquery.com/jquery.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
|
@ -29,7 +29,7 @@ function Voter(options) {
|
|||
}
|
||||
|
||||
var voter = new Voter({
|
||||
elem: $('#voter')
|
||||
elem: document.getElementById('voter')
|
||||
});
|
||||
|
||||
voter.setVote(1);
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
|
||||
По клику на `+` и `—` число должно увеличиваться или уменьшаться.
|
||||
|
||||
**Публичный метод `voter.setVote(vote)` должен устанавливать текущее число -- значение голоса.**
|
||||
Публичный метод `voter.setVote(vote)` должен устанавливать текущее число -- значение голоса.
|
||||
|
||||
Все остальные методы и свойства пусть будут приватными.
|
||||
|
||||
|
|
|
@ -3,8 +3,8 @@
|
|||
<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="https://cdn.polyfill.io/v1/polyfill.js?features=Element.prototype.closest"></script>
|
||||
<script src="voter.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
|
@ -17,7 +17,7 @@
|
|||
<script>
|
||||
|
||||
var voter = new Voter({
|
||||
elem: $('#voter')
|
||||
elem: document.getElementById('voter')
|
||||
});
|
||||
|
||||
</script>
|
||||
|
|
|
@ -1,22 +1,33 @@
|
|||
|
||||
function Voter(options) {
|
||||
var elem = this._elem = options.elem;
|
||||
this._voteElem = elem.find('.vote');
|
||||
this._voteElem = elem.querySelector('.vote');
|
||||
|
||||
elem.on('mousedown selectstart', false);
|
||||
elem.onmousedown = function() {
|
||||
return false;
|
||||
};
|
||||
|
||||
elem.on('click', '.down', this._onDownClick.bind(this));
|
||||
elem.on('click', '.up', this._onUpClick.bind(this));
|
||||
elem.onclick = this._onClick.bind(this);
|
||||
}
|
||||
|
||||
Voter.prototype._onDownClick = function() {
|
||||
this._voteElem.html( +this._voteElem.html() - 1 );
|
||||
Voter.prototype._onClick = function(event) {
|
||||
if (event.target.closest('.down')) {
|
||||
this._voteDecrease();
|
||||
} else if (event.target.closest('.up')) {
|
||||
this._voteIncrease();
|
||||
}
|
||||
};
|
||||
|
||||
Voter.prototype._onUpClick = function() {
|
||||
this._voteElem.html( +this._voteElem.html() + 1 );
|
||||
|
||||
Voter.prototype._voteIncrease = function() {
|
||||
this._voteElem.innerHTML = +this._voteElem.innerHTML + 1;
|
||||
};
|
||||
|
||||
Voter.prototype._voteDecrease = function() {
|
||||
this._voteElem.innerHTML = +this._voteElem.innerHTML - 1;
|
||||
};
|
||||
|
||||
Voter.prototype.setVote = function(vote) {
|
||||
this._voteElem.html(vote);
|
||||
this._voteElem.innerHTML = +vote;
|
||||
};
|
||||
|
||||
|
|
|
@ -3,8 +3,8 @@
|
|||
<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="https://cdn.polyfill.io/v1/polyfill.js?features=Element.prototype.closest"></script>
|
||||
<script src="voter.js"></script>
|
||||
<script src="step-voter.js"></script> <!-- отнаследовать и переопределить методы -->
|
||||
</head>
|
||||
<body>
|
||||
|
@ -18,7 +18,7 @@
|
|||
<script>
|
||||
|
||||
var voter = new StepVoter({
|
||||
elem: $('#voter'),
|
||||
elem: document.getElementById('voter'),
|
||||
step: 2
|
||||
});
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
function StepVoter(options) {
|
||||
Voter.apply(this, arguments);
|
||||
this._step = options.step || 1;
|
||||
}
|
||||
StepVoter.prototype = Object.create(Voter.prototype);
|
||||
|
||||
StepVoter.prototype._voteIncrease = function() {
|
||||
this._voteElem.innerHTML = +this._voteElem.innerHTML + this._step;
|
||||
};
|
||||
|
||||
StepVoter.prototype._voteDecrease = function() {
|
||||
this._voteElem.innerHTML = +this._voteElem.innerHTML - this._step;
|
||||
};
|
|
@ -0,0 +1,33 @@
|
|||
|
||||
function Voter(options) {
|
||||
var elem = this._elem = options.elem;
|
||||
this._voteElem = elem.querySelector('.vote');
|
||||
|
||||
elem.onmousedown = function() {
|
||||
return false;
|
||||
};
|
||||
|
||||
elem.onclick = this._onClick.bind(this);
|
||||
}
|
||||
|
||||
Voter.prototype._onClick = function(event) {
|
||||
if (event.target.closest('.down')) {
|
||||
this._voteDecrease();
|
||||
} else if (event.target.closest('.up')) {
|
||||
this._voteIncrease();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Voter.prototype._voteIncrease = function() {
|
||||
this._voteElem.innerHTML = +this._voteElem.innerHTML + 1;
|
||||
};
|
||||
|
||||
Voter.prototype._voteDecrease = function() {
|
||||
this._voteElem.innerHTML = +this._voteElem.innerHTML - 1;
|
||||
};
|
||||
|
||||
Voter.prototype.setVote = function(vote) {
|
||||
this._voteElem.innerHTML = +vote;
|
||||
};
|
||||
|
|
@ -8,7 +8,7 @@
|
|||
|
||||
```js
|
||||
var voter = new StepVoter({
|
||||
elem: $('#voter'),
|
||||
elem: document.getElementById('voter'),
|
||||
step: 2 // увеличивать/уменьшать сразу на 2 пункта
|
||||
});
|
||||
```
|
||||
|
@ -18,11 +18,6 @@ var voter = new StepVoter({
|
|||
|
||||
В реальном проекте влияние клика на голосовалку может зависеть от полномочий или репутации посетителя.
|
||||
|
||||
Сделайте задачу в два этапа:
|
||||
<ol>
|
||||
<li>Поменять исходный класс `Voter`: вынести логику изменения значений в защищенные методы `_increase/_decrease`, так чтобы их можно было переопределить в наседнике.
|
||||
На этом этапе использование кода не должно измениться, код в `index.html` будет тот же.</li>
|
||||
<li>Сделать новый класс `StepVoter`, в котором обработать дополнительную опцию и переопределить `_increase/_decrease` соответственно. Затем использовать его в `index.html`.</li>
|
||||
</ol>
|
||||
В качестве исходного кода используйте решение задачи [](/task/voter-proto).
|
||||
|
||||
В качестве исходного кода используйте решение задачи [](/task/voter-proto).
|
||||
P.S. Код `voter.js` изменять нельзя, нужно не переписать `Voter`, а отнаследовать от него.
|
|
@ -1,2 +0,0 @@
|
|||
Для показа голосов также добавлены семантические классы `.positive/.negative` в `style.css`.
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
function ColoredVoter(options) {
|
||||
Voter.apply(this, arguments);
|
||||
}
|
||||
ColoredVoter.prototype = Object.create(Voter.prototype);
|
||||
|
||||
ColoredVoter.prototype._renderVote = function() {
|
||||
Voter.prototype._renderVote.apply(this, arguments);
|
||||
this._voteElem.removeClass('positive negative');
|
||||
if (this._vote > 0) {
|
||||
this._voteElem.addClass('positive');
|
||||
}
|
||||
if (this._vote < 0) {
|
||||
this._voteElem.addClass('negative');
|
||||
}
|
||||
};
|
|
@ -1,28 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<link rel="stylesheet" href="style.css">
|
||||
<script src="http://code.jquery.com/jquery-latest.js"></script>
|
||||
<script src="voter.js"></script> <!-- отрефакторить -->
|
||||
<script src="colored-voter.js"></script> <!-- отнаследовать и переопределить методы -->
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div id="voter" class="voter">
|
||||
<span class="down">–</span>
|
||||
<span class="vote">0</span>
|
||||
<span class="up">+</span>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
|
||||
var voter = new ColoredVoter({
|
||||
elem: $('#voter')
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
|
||||
.voter {
|
||||
font-family: Consolas, "Lucida Console", monospace;
|
||||
font-size: 18px;
|
||||
}
|
||||
.up, .down {
|
||||
cursor: pointer;
|
||||
color: blue;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.positive {
|
||||
color: green;
|
||||
}
|
||||
|
||||
.negative {
|
||||
color: red;
|
||||
}
|
|
@ -1,28 +0,0 @@
|
|||
|
||||
function Voter(options) {
|
||||
var elem = this._elem = options.elem;
|
||||
this._voteElem = elem.find('.vote');
|
||||
this._vote = 0;
|
||||
|
||||
elem.on('mousedown selectstart', false);
|
||||
|
||||
elem.on('click', '.down', this._onDownClick.bind(this));
|
||||
elem.on('click', '.up', this._onUpClick.bind(this));
|
||||
}
|
||||
|
||||
Voter.prototype._onDownClick = function() {
|
||||
this.setVote(this._vote - 1);
|
||||
};
|
||||
|
||||
Voter.prototype._onUpClick = function() {
|
||||
this.setVote(this._vote + 1);
|
||||
};
|
||||
|
||||
Voter.prototype._renderVote = function() {
|
||||
this._voteElem.html(this._vote);
|
||||
};
|
||||
|
||||
Voter.prototype.setVote = function(vote) {
|
||||
this._vote = vote;
|
||||
this._renderVote();
|
||||
};
|
|
@ -1,17 +0,0 @@
|
|||
# Добавить цвет в голосовалку
|
||||
|
||||
[importance 5]
|
||||
|
||||
Создайте функцию-конструктор `ColoredVoter`, которая наследует от голосовалки, созданной в задаче [](/task/voter-proto) и отображает положительные значения зелёным, а отрицательные -- красным.
|
||||
|
||||
Результат работы `new ColoredVoter`: (проголосуйте, чтобы увидеть):
|
||||
[iframe border=1 src="solution"]
|
||||
|
||||
Решение задачи состоит из двух этапов:
|
||||
<ol>
|
||||
<li>Внести изменения в `Voter`, вынести логику отображения голоса в защищенный метод `_renderVote`, чтобы его можно было отнаследовать. При необходимости добавьте другие методы и свойства. Делайте такой код, который будет удобно расширять.</li>
|
||||
<li>Отнаследовать и переопределить `_renderVote` в `ColoredVoter`.</li>
|
||||
</ol>
|
||||
|
||||
|
||||
В качестве исходного кода используйте решение задачи [](/task/voter-proto).
|
|
@ -1,13 +0,0 @@
|
|||
function StepVoter(options) {
|
||||
Voter.apply(this, arguments);
|
||||
this._step = options.step || 1;
|
||||
}
|
||||
StepVoter.prototype = Object.create(Voter.prototype);
|
||||
|
||||
StepVoter.prototype._increase = function() {
|
||||
this.setVote(this._vote + this._step);
|
||||
};
|
||||
|
||||
StepVoter.prototype._decrease = function() {
|
||||
this.setVote(this._vote - this._step);
|
||||
};
|
|
@ -1,36 +0,0 @@
|
|||
|
||||
function Voter(options) {
|
||||
var elem = this._elem = options.elem;
|
||||
this._voteElem = elem.find('.vote');
|
||||
this._vote = 0;
|
||||
|
||||
elem.on('mousedown selectstart', false);
|
||||
|
||||
elem.on('click', '.down', this._onDownClick.bind(this));
|
||||
elem.on('click', '.up', this._onUpClick.bind(this));
|
||||
}
|
||||
|
||||
Voter.prototype._onDownClick = function() {
|
||||
this._decrease();
|
||||
};
|
||||
|
||||
Voter.prototype._onUpClick = function() {
|
||||
this._increase();
|
||||
};
|
||||
|
||||
Voter.prototype._renderVote = function() {
|
||||
this._voteElem.html(this._vote);
|
||||
};
|
||||
|
||||
Voter.prototype.setVote = function(vote) {
|
||||
this._vote = vote;
|
||||
this._renderVote();
|
||||
};
|
||||
|
||||
Voter.prototype._increase = function() {
|
||||
this.setVote(this._vote + 1);
|
||||
};
|
||||
|
||||
Voter.prototype._decrease = function() {
|
||||
this.setVote(this._vote - 1);
|
||||
};
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
Здесь мы сосредоточимся на графических компонентах, которые также называют "виджетами".
|
||||
|
||||
В браузерах есть встроенные виджеты, например `<select>`, `<input>` и другие элементы, о которых мы даже и не думаем, как они работают. Просто работают: принимают значение, вызывают события...
|
||||
В браузерах есть встроенные виджеты, например `<select>`, `<input>` и другие элементы, о которых мы даже и не думаем, "как они работают". Они "просто работают": показывают значение, вызывают события...
|
||||
|
||||
Наша задача -- сделать то же самое на уровне выше. Мы будем создавать объекты, которые генерируют меню, диалог или другие компоненты интерфейса, и дают возможность удобно работать с ними.
|
||||
|
||||
|
@ -27,38 +27,44 @@
|
|||
|
||||
Далее она может дополняться, изменяться, но в начале -- она такая.
|
||||
|
||||
Обратим внимание на важные соглашения:
|
||||
Обратим внимание на важные соглашения виджета:
|
||||
|
||||
<dl>
|
||||
<dt>Вся разметка заключена в корневой элемент `<div class="menu" id="sweeties-menu">`.</dt>
|
||||
<dd>Это очень удобно: вынул этот элемент из DOM -- нет меню, вставил в другое место -- переместил меню. Кроме того, можно удобно искать подэлементы.</dd>
|
||||
<dt>В разметке -- только классы.</dt>
|
||||
<dt>Внутри корневого элемента -- только классы, не `id`.</dt>
|
||||
<dd>Документ вполне может содержать много различных меню. Они не должны конфликтовать между собой, поэтому для разметки везде используются классы.
|
||||
|
||||
Исключение -- корневой элемент. В данном случае мы предполагаем, что данное конкретное "меню сладостей" в документе только одно, поэтому даём ему `id`.</dd>
|
||||
</dl>
|
||||
|
||||
Для работы с разметкой будем создавать объект `new Menu` и передавать ему корневой элемент. В конструкторе он поставит необходимые обработчики.
|
||||
|
||||
# Класс виджета
|
||||
|
||||
Для работы с разметкой будем создавать объект `new Menu` и передавать ему корневой элемент. В конструкторе он поставит необходимые обработчики:
|
||||
|
||||
```js
|
||||
function Menu(options) {
|
||||
var elem = options.elem;
|
||||
|
||||
elem.on('mousedown selectstart', false);
|
||||
elem.onmousedown = function() { return false; }
|
||||
|
||||
elem.onclick = function(event) {
|
||||
if (event.target.closest('.title')) {
|
||||
elem.classList.toggle('open');
|
||||
}
|
||||
};
|
||||
|
||||
elem.on('click', '.title', function() {
|
||||
elem.toggleClass('open');
|
||||
});
|
||||
}
|
||||
|
||||
// использование
|
||||
var menu = new Menu({
|
||||
elem: $('#sweets-menu')
|
||||
elem: document.getElementById('sweets-menu')
|
||||
});
|
||||
```
|
||||
|
||||
Меню:
|
||||
[codetabs src="menu-1"]
|
||||
[codetabs src="menu"]
|
||||
|
||||
Это, конечно, только первый шаг, но уже здесь видны некоторые важные соглашения в коде.
|
||||
|
||||
|
@ -69,25 +75,38 @@ var menu = new Menu({
|
|||
<dd>Вместо того, чтобы найти элемент и поставить обработчик на него:
|
||||
|
||||
```js
|
||||
var titleElem = elem.find('.title');
|
||||
var titleElem = elem.querySelector('.title');
|
||||
|
||||
titleElem.on('click', function() {
|
||||
elem.toggleClass('open');
|
||||
titleElem.onclick = function() {
|
||||
elem.classList.toggle('open');
|
||||
}
|
||||
```
|
||||
|
||||
...Мы пишем так:
|
||||
...Мы ставим обработчик на корневой `elem` и используем делегирование:
|
||||
|
||||
```js
|
||||
elem.on('click', '.title', function() {
|
||||
elem.toggleClass('open');
|
||||
});
|
||||
elem.onclick = function(event) {
|
||||
if (event.target.closest('.title')) {
|
||||
elem.classList.toggle('open');
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
Это ускоряет инициализацию, так как не надо искать элементы, и даёт возможность в любой момент менять DOM внутри, в том числе через `innerHTML`, без необходимости переставлять обработчика.
|
||||
Это ускоряет инициализацию, так как не надо искать элементы, и даёт возможность в любой момент менять DOM внутри, в том числе через `innerHTML`, без необходимости переставлять обработчик.
|
||||
</dd>
|
||||
</dl>
|
||||
|
||||
В этот код лучше добавить дополнительную проверку на то, что найденный `.title` находится внутри `elem`:
|
||||
|
||||
```js
|
||||
elem.onclick = function(event) {
|
||||
var closestTitle = event.target.closest('.title');
|
||||
if (closestTitle && elem.contains(closestTitle)) {
|
||||
elem.classList.toggle('open');
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
## Публичные методы
|
||||
|
||||
Уважающий себя компонент обычно имеет публичные методы, которые позволяют управлять им снаружи.
|
||||
|
@ -95,12 +114,12 @@ elem.on('click', '.title', function() {
|
|||
Рассмотрим повнимательнее этот фрагмент:
|
||||
|
||||
```js
|
||||
elem.on('click', '.title', function() {
|
||||
elem.toggleClass('open');
|
||||
});
|
||||
if (event.target.closest('.title')) {
|
||||
elem.classList.toggle('open');
|
||||
}
|
||||
```
|
||||
|
||||
Здесь в обработчике события сразу код работы с элементами. Пока одна строка -- всё понятно, но если их будет много, то при чтении понадобится долго и упорно вникать: "А что же, всё-таки, такое делается при клике?"
|
||||
Здесь в обработчике события сразу код работы с элементом. Пока одна строка -- всё понятно, но если их будет много, то при чтении понадобится долго и упорно вникать: "А что же, всё-таки, такое делается при клике?"
|
||||
|
||||
Для улучшения читаемости выделим обработчик в отдельную функцию `toggle`, которая к тому же станет полезным публичным методом:
|
||||
|
||||
|
@ -108,49 +127,42 @@ elem.on('click', '.title', function() {
|
|||
function Menu(options) {
|
||||
var elem = options.elem;
|
||||
|
||||
elem.on('mousedown selectstart', false);
|
||||
elem.onmousedown = function() { return false; }
|
||||
|
||||
*!*
|
||||
elem.on('click', '.title', onTitleClick);
|
||||
|
||||
function onTitleClick(e) {
|
||||
toggle();
|
||||
}
|
||||
elem.onclick = function(event) {
|
||||
if (event.target.closest('.title')) {
|
||||
toggle();
|
||||
}
|
||||
};
|
||||
|
||||
function toggle() {
|
||||
elem.toggleClass('open');
|
||||
};
|
||||
*/!*
|
||||
elem.classList.toggle('open');
|
||||
}
|
||||
|
||||
this.toggle = toggle;
|
||||
}
|
||||
```
|
||||
|
||||
Здесь и сам обработчик события тоже вынесен в отдельную функцию `onTitleClick`.
|
||||
|
||||
Наши бонусы:
|
||||
<ol>
|
||||
<li>Во-первых, стало проще найти и расширить обработчик события в коде -- имя `onTitleClick` найти и запомнить.</li>
|
||||
<li>Во-вторых, код стал лучше читаться.</li>
|
||||
<li>Во-третьих, `toggle` теперь -- отдельная функция, доступная извне.</li>
|
||||
</ol>
|
||||
|
||||
Пример использования публичного метода:
|
||||
Теперь метод `toggle` можно использовать и снаружи:
|
||||
|
||||
```js
|
||||
var menu = new Menu(...);
|
||||
menu.toggle();
|
||||
```
|
||||
|
||||
## Генерация DOM-дерева
|
||||
## Генерация DOM-элемента
|
||||
|
||||
До этого момента меню "оживляло" уже существующий HTML. Но в более сложном интерфейсе нужно уметь сгенерировать меню "на лету", по данным.
|
||||
До этого момента меню "оживляло" уже существующий HTML.
|
||||
|
||||
Для этого добавим меню три метода:
|
||||
Но далеко не всегда в HTML уже есть готовая разметка. В сложных интерфейсах намного чаще её нет, а есть данные, на основе которых компонент генерирует разметку.
|
||||
|
||||
В случае меню, данные -- это набор пунктов меню, которые передаются конструктору.
|
||||
|
||||
Для генерации DOM добавим меню три метода:
|
||||
<ul>
|
||||
<li>`render()` -- генерирует корневой DOM-элемент и заголовок меню, приватный.</li>
|
||||
<li>`renderItems()` -- генерирует DOM для списка опций (`<li>`), приватный.</li>
|
||||
<li>`getElem()` -- возвращает DOM-элемент меню, при необходимости запуская генерацию, публичный.</li>
|
||||
<li>`render()` -- генерирует корневой DOM-элемент и заголовок меню.</li>
|
||||
<li>`renderItems()` -- генерирует DOM для списка опций `ul/li`.</li>
|
||||
<li>`getElem()` -- возвращает DOM-элемент меню, при необходимости запуская генерацию, публичный метод.</li>
|
||||
</ul>
|
||||
|
||||
Функция генерации корневого элемента с заголовком `render` отделена от генерации списка `renderItems`. Почему -- будет видно чуть далее.
|
||||
|
@ -173,14 +185,14 @@ var menu = new Menu({
|
|||
});
|
||||
|
||||
*!*
|
||||
// получить DOM-элемент меню
|
||||
// получить сгенерированный DOM-элемент меню
|
||||
*/!*
|
||||
var elem = menu.getElem();
|
||||
|
||||
*!*
|
||||
// вставить меню в нужное место страницы
|
||||
*/!*
|
||||
$('#sweets-menu-holder').append( elem );
|
||||
document.body.appendChild(elem);
|
||||
```
|
||||
|
||||
Код `Menu` с новыми методами:
|
||||
|
@ -195,68 +207,52 @@ function Menu(options) {
|
|||
}
|
||||
|
||||
function render() {
|
||||
elem = $('<div class="menu"></div>');
|
||||
elem.append( $('<span/>', { class: "title", text: options.title }))
|
||||
elem = document.createElement('div');
|
||||
elem.className = "menu";
|
||||
|
||||
elem.on('mousedown selectstart', false);
|
||||
var titleElem = document.createElement('span');
|
||||
elem.appendChild(titleElem);
|
||||
titleElem.className = "title";
|
||||
titleElem.textContent = options.title;
|
||||
|
||||
elem.onmousedown = function() {
|
||||
return false;
|
||||
};
|
||||
|
||||
elem.onclick = function(event) {
|
||||
if (event.target.closest('.title')) {
|
||||
toggle();
|
||||
}
|
||||
}
|
||||
|
||||
elem.on('click', '.title', onTitleClick);
|
||||
}
|
||||
|
||||
function renderItems() {
|
||||
var items = options.items || [];
|
||||
var list = $('<ul/>');
|
||||
$.each(items, function(i, item) {
|
||||
list.append( $('<li>').text(item) );
|
||||
})
|
||||
list.appendTo(elem);
|
||||
var list = document.createElement('ul');
|
||||
items.forEach(function(item) {
|
||||
var li = document.createElement('li');
|
||||
li.textContent = item;
|
||||
list.appendChild(li);
|
||||
});
|
||||
elem.appendChild(list);
|
||||
}
|
||||
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
**Важнейший принцип, который здесь использован -- ленивость.**
|
||||
|
||||
Мы стараемся откладывать работу до момента, когда она реально нужна. Например, когда `new Menu` создаётся, то переменная `elem` лишь объявляется. DOM-дерево будет сгенерировано только при вызове `getElem()`.
|
||||
|
||||
Более того! Пока меню закрыто -- достаточно заголовка. Кроме того, возможно, посетитель вообще никогда не раскроет это меню, так зачем генерировать список раньше времени?
|
||||
|
||||
**Фаза инициализации очень чувствительна к производительности, так как при загрузке страницы со сложным интерфейсом создаётся много всего. А мы хотим, чтобы он начал работать как можно быстрее.**
|
||||
|
||||
Если изначально подходить к оптимизации на этой фазе "спустя рукава", то потом поправить может быть сложно. Всё-таки, инициализация -- это фундамент, начало работы виджета. Конечно, лучше без фанатизма. Бывают ситуации, когда по коду гораздо удобнее что-то сделать сразу, поэтому нужен взвешенный подход. Чем крупнее участок работы и чем больше шансов его вообще избежать -- тем больше доводов его отложить.
|
||||
|
||||
Ниже -- код меню с методами `open`, `close` и `toggle`, которые подразумевают ленивую генерацию DOM:
|
||||
|
||||
```js
|
||||
function Menu(options) {
|
||||
var elem;
|
||||
|
||||
function getElem() { /* см выше */ }
|
||||
|
||||
function render() { /* см выше */ }
|
||||
|
||||
function renderItems() { /* см выше */ }
|
||||
|
||||
function onTitleClick(e) { /* см выше */ }
|
||||
|
||||
*!*
|
||||
function open() {
|
||||
if (!elem.find('ul').length) {
|
||||
if (!elem.querySelector('ul')) {
|
||||
renderItems();
|
||||
}
|
||||
elem.addClass('open');
|
||||
elem.classList.add('open');
|
||||
};
|
||||
|
||||
function close() {
|
||||
elem.removeClass('open');
|
||||
elem.classList.remove('open');
|
||||
};
|
||||
|
||||
function toggle() {
|
||||
if (elem.hasClass('open')) close();
|
||||
if (elem.classList.contains('open')) close();
|
||||
else open();
|
||||
};
|
||||
*/!*
|
||||
|
||||
this.getElem = getElem;
|
||||
this.toggle = toggle;
|
||||
|
@ -265,23 +261,49 @@ function Menu(options) {
|
|||
}
|
||||
```
|
||||
|
||||
Основные изменения -- теперь метод `toggle` не просто меняет класс. Этого недостаточно, ведь, чтобы открыть меню, нужно для начала отрендерить его опции. Поэтому добавлено два метода `open` и `close`, которые также полезны и для внешнего интерфейса.
|
||||
Отметим некоторые особенности этого кода.
|
||||
|
||||
<dl>
|
||||
<dt>Обработчики отделяются от реальных действий.</dt>
|
||||
<dd>В обработчике `onclick` мы "ловим" событие и выясняем, что именно произошло. Возможно, нужно проверить `event.target`, координаты, клавиши-модификаторы, и т.п. Это всё можно делать здесь же.
|
||||
|
||||
Выяснив, что нужно сделать, обработчик `onclick` не делает это сам, а вызывает для этого соответствующий метод. Этот метод уже не знает ничего о событии, он просто делает что-то с виджетом. Его можно вызвать и отдельно, не из обработчика.
|
||||
|
||||
Здесь есть ряд важных плюсов:
|
||||
<ul>
|
||||
<li>Обработчик `onclick` не "распухает" чрезмерно.</li>
|
||||
<li>Код гораздо лучше читается.</li>
|
||||
<li>Метод можно повторно использовать, в том числе и сделать публичным, как в коде выше.</li>
|
||||
</ul>
|
||||
</dd>
|
||||
<dt>Генерация DOM, по возможности, должна быть "ленивой".</dt>
|
||||
<dd>Мы стараемся откладывать работу до момента, когда она реально нужна. Например, когда `new Menu` создаётся, то переменная `elem` лишь объявляется. DOM-дерево будет сгенерировано только при вызове `getElem()` функцией `render()`.
|
||||
|
||||
Более того! Пока меню закрыто -- достаточно заголовка. Кроме того, возможно, посетитель вообще никогда не раскроет это меню, так зачем генерировать список раньше времени? А при первом открытиии `open()` вызовет функцию `renderItems()`, которая специально для этого выделена отдельно от `render()`.
|
||||
|
||||
**Фаза инициализации очень чувствительна к производительности, так как обычно в сложном интерфейсе создаётся много всего.**
|
||||
|
||||
Если изначально подходить к оптимизации на этой фазе "спустя рукава", то потом поправить долгий старт может быть сложно. Тем более, что инициализация -- это фундамент, начало работы виджета, её оптимизация в будущем может потребовать сильных изменений кода.
|
||||
|
||||
Конечно, здесь, как и везде в оптимизации -- без фанатизма. Бывают ситуации, когда гораздо удобнее что-то сделать сразу. Если это один элемент, то оптимизация здесь ни к чему. А если большой фрагмент DOM, который, как в случае с меню, прямо сейчас не нужен -- то лучше отложить.
|
||||
</dd>
|
||||
</dl>
|
||||
|
||||
В действии:
|
||||
[codetabs src="menu-3-elem" height="200"]
|
||||
[codetabs src="menu-dom" height="200"]
|
||||
|
||||
|
||||
## Итого
|
||||
|
||||
Мы начали создавать компонент "с чистого листа", пока без дополнительных библиотек, но они скоро понадобятся.
|
||||
Мы начали создавать компонент "с чистого листа", пока без дополнительных библиотек.
|
||||
|
||||
Основные принципы:
|
||||
<ul>
|
||||
<li>В конструктор передаётся объект аргументов `options`, а не список аргументов -- для удобства дополнения и расширения виджета.</li>
|
||||
<li>Виджет -- это объект, который либо контролирует готовое дерево DOM, либо создаёт своё.</li>
|
||||
<li>В конструктор виджета передаётся объект аргументов `options`.</li>
|
||||
<li>Виджет при необходимости создаёт элемент или "оживляет" готовый. Внутре в разметке не используются `id`.</li>
|
||||
<li>Обработчики назначаются через делегирование -- для производительности и упрощения виджета.</li>
|
||||
<li>Не экономим буквы ценой понятности -- действие и/или обработчик заслуживают быть отдельными функциями.</li>
|
||||
<li>Будем ленивыми -- если существенный участок работы можно отложить до реального задействования виджета -- откладываем его.</li>
|
||||
<li>Обработчики событий вызывают соответствующий метод, не пытаются делать всё сами.</li>
|
||||
<li>При инициализации, если существенный участок работы можно отложить до реального задействования виджета -- откладываем его.</li>
|
||||
</ul>
|
||||
|
||||
Далее мы продолжим работать со разметкой виджета.
|
||||
|
||||
|
|
|
@ -1,11 +0,0 @@
|
|||
function Menu(options) {
|
||||
var elem = options.elem;
|
||||
|
||||
// отмена выделения при клике на меню
|
||||
elem.on('mousedown selectstart', false);
|
||||
|
||||
elem.on('click', '.title', function() {
|
||||
elem.toggleClass('open');
|
||||
});
|
||||
|
||||
}
|
|
@ -1,20 +0,0 @@
|
|||
|
||||
.menu ul {
|
||||
display: none;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.menu .title {
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
background: url(https://js.cx/clipart/arrow-right.png) left center no-repeat;
|
||||
padding-left: 18px;
|
||||
}
|
||||
|
||||
.menu.open ul {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.menu.open .title {
|
||||
background-image: url(https://js.cx/clipart/arrow-down.png);
|
||||
}
|
|
@ -1,51 +0,0 @@
|
|||
function Menu(options) {
|
||||
var elem;
|
||||
|
||||
function getElem() {
|
||||
if (!elem) render();
|
||||
return elem;
|
||||
}
|
||||
|
||||
function render() {
|
||||
elem = $('<div class="menu"></div>');
|
||||
elem.append( $('<span/>', { class: "title", text: options.title }))
|
||||
|
||||
elem.on('mousedown selectstart', false);
|
||||
|
||||
elem.on('click', '.title', onTitleClick);
|
||||
}
|
||||
|
||||
function renderItems() {
|
||||
var items = options.items || [];
|
||||
var list = $('<ul/>');
|
||||
$.each(items, function(i, item) {
|
||||
list.append( $('<li>').text(item) );
|
||||
})
|
||||
list.appendTo(elem);
|
||||
}
|
||||
|
||||
function onTitleClick(e) {
|
||||
toggle();
|
||||
}
|
||||
|
||||
function open() {
|
||||
if (!elem.find('ul').length) {
|
||||
renderItems();
|
||||
}
|
||||
elem.addClass('open');
|
||||
};
|
||||
|
||||
function close() {
|
||||
elem.removeClass('open');
|
||||
};
|
||||
|
||||
function toggle() {
|
||||
if (elem.hasClass('open')) close();
|
||||
else open();
|
||||
};
|
||||
|
||||
this.getElem = getElem;
|
||||
this.toggle = toggle;
|
||||
this.close = close;
|
||||
this.open = open;
|
||||
}
|
|
@ -1,20 +0,0 @@
|
|||
|
||||
.menu ul {
|
||||
display: none;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.menu .title {
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
background: url(https://js.cx/clipart/arrow-right.png) left center no-repeat;
|
||||
padding-left: 18px;
|
||||
}
|
||||
|
||||
.menu.open ul {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.menu.open .title {
|
||||
background-image: url(https://js.cx/clipart/arrow-down.png);
|
||||
}
|
8
2-ui/5-widgets/2-widgets-structure/menu-3-elem.view/index.html → 2-ui/5-widgets/2-widgets-structure/menu-dom.view/index.html
Executable file → Normal file
8
2-ui/5-widgets/2-widgets-structure/menu-3-elem.view/index.html → 2-ui/5-widgets/2-widgets-structure/menu-dom.view/index.html
Executable file → Normal file
|
@ -3,7 +3,8 @@
|
|||
<head>
|
||||
<meta charset="utf-8">
|
||||
<link rel="stylesheet" href="style.css">
|
||||
<script src="http://code.jquery.com/jquery.min.js"></script>
|
||||
<script src="https://cdn.polyfill.io/v1/polyfill.js?features=Element.prototype.closest"></script>
|
||||
|
||||
<script src="menu.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
|
@ -12,9 +13,6 @@
|
|||
<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: "Сладости",
|
||||
|
@ -27,7 +25,7 @@ var menu = new Menu({
|
|||
]
|
||||
});
|
||||
|
||||
$('#sweets-menu-holder').append(menu.getElem());
|
||||
document.body.appendChild( menu.getElem() );
|
||||
</script>
|
||||
|
||||
</body>
|
61
2-ui/5-widgets/2-widgets-structure/menu-dom.view/menu.js
Normal file
61
2-ui/5-widgets/2-widgets-structure/menu-dom.view/menu.js
Normal file
|
@ -0,0 +1,61 @@
|
|||
function Menu(options) {
|
||||
var elem;
|
||||
|
||||
function getElem() {
|
||||
if (!elem) render();
|
||||
return elem;
|
||||
}
|
||||
|
||||
function render() {
|
||||
elem = document.createElement('div');
|
||||
elem.className = "menu";
|
||||
|
||||
var titleElem = document.createElement('span');
|
||||
elem.appendChild(titleElem);
|
||||
titleElem.className = "title";
|
||||
titleElem.textContent = options.title;
|
||||
|
||||
elem.onmousedown = function() {
|
||||
return false;
|
||||
};
|
||||
|
||||
elem.onclick = function(event) {
|
||||
if (event.target.closest('.title')) {
|
||||
toggle();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function renderItems() {
|
||||
var items = options.items || [];
|
||||
var list = document.createElement('ul');
|
||||
items.forEach(function(item) {
|
||||
var li = document.createElement('li');
|
||||
li.textContent = item;
|
||||
list.appendChild(li);
|
||||
});
|
||||
elem.appendChild(list);
|
||||
}
|
||||
|
||||
function open() {
|
||||
if (!elem.querySelector('ul')) {
|
||||
renderItems();
|
||||
}
|
||||
elem.classList.add('open');
|
||||
};
|
||||
|
||||
function close() {
|
||||
elem.classList.remove('open');
|
||||
};
|
||||
|
||||
function toggle() {
|
||||
if (elem.classList.contains('open')) close();
|
||||
else open();
|
||||
};
|
||||
|
||||
this.getElem = getElem;
|
||||
this.toggle = toggle;
|
||||
this.close = close;
|
||||
this.open = open;
|
||||
}
|
24
2-ui/5-widgets/2-widgets-structure/menu-dom.view/style.css
Normal file
24
2-ui/5-widgets/2-widgets-structure/menu-dom.view/style.css
Normal file
|
@ -0,0 +1,24 @@
|
|||
|
||||
.menu ul {
|
||||
display: none;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.menu .title {
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.menu .title:before {
|
||||
content: '▶';
|
||||
padding-right: 6px;
|
||||
color: green;
|
||||
}
|
||||
|
||||
.menu.open ul {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.menu.open .title:before {
|
||||
content: '▼';
|
||||
}
|
6
2-ui/5-widgets/2-widgets-structure/menu-1.view/index.html → 2-ui/5-widgets/2-widgets-structure/menu.view/index.html
Executable file → Normal file
6
2-ui/5-widgets/2-widgets-structure/menu-1.view/index.html → 2-ui/5-widgets/2-widgets-structure/menu.view/index.html
Executable file → Normal file
|
@ -3,7 +3,7 @@
|
|||
<head>
|
||||
<meta charset="utf-8">
|
||||
<link rel="stylesheet" href="style.css">
|
||||
<script src="http://code.jquery.com/jquery.min.js"></script>
|
||||
<script src="https://cdn.polyfill.io/v1/polyfill.js?features=Element.prototype.closest"></script>
|
||||
<script src="menu.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
|
@ -20,8 +20,8 @@
|
|||
</div>
|
||||
|
||||
<script>
|
||||
var menu = new Menu({
|
||||
elem: $('#sweets-menu')
|
||||
var menu = new Menu({
|
||||
elem: document.getElementById('sweets-menu')
|
||||
});
|
||||
</script>
|
||||
|
12
2-ui/5-widgets/2-widgets-structure/menu.view/menu.js
Normal file
12
2-ui/5-widgets/2-widgets-structure/menu.view/menu.js
Normal file
|
@ -0,0 +1,12 @@
|
|||
function Menu(options) {
|
||||
var elem = options.elem;
|
||||
|
||||
elem.onmousedown = function() { return false; }
|
||||
|
||||
elem.onclick = function(event) {
|
||||
if (event.target.closest('.title')) {
|
||||
elem.classList.toggle('open');
|
||||
}
|
||||
};
|
||||
|
||||
}
|
24
2-ui/5-widgets/2-widgets-structure/menu.view/style.css
Normal file
24
2-ui/5-widgets/2-widgets-structure/menu.view/style.css
Normal file
|
@ -0,0 +1,24 @@
|
|||
|
||||
.menu ul {
|
||||
display: none;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.menu .title {
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.menu .title:before {
|
||||
content: '▶';
|
||||
padding-right: 6px;
|
||||
color: green;
|
||||
}
|
||||
|
||||
.menu.open ul {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.menu.open .title:before {
|
||||
content: '▼';
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue