renovations

This commit is contained in:
Ilya Kantor 2015-02-21 14:58:02 +03:00
parent a62682e188
commit 35081a779a
115 changed files with 439 additions and 325 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

View file

Before

Width:  |  Height:  |  Size: 5.3 KiB

After

Width:  |  Height:  |  Size: 5.3 KiB

Before After
Before After

View file

@ -1,4 +1,6 @@
Для решения этой задачи достаточно создать две функции: `valueToPosition` будет получать по значению положение бегунка, а `positionToValue` -- наоборот, транслировать текущую координату бегунка в значение.
Как сопоставить позицию слайдера и значение?
Для этого посмотрим крайние значения слайдера. Допустим, размер бегунка `10px`.
@ -17,10 +19,16 @@ pixelsPerValue = (sliderElem.clientWidth-thumbElem.clientWidth) / max;
Используя `pixelsPerValue` мы сможем переводить позицию бегунка в значение и обратно.
Крайнее левое значение `thumbElem.style.left` равно нулю, крайнее правой -- как раз ширине доступной области `sliderElem.clientWidth - thumbElem.clientWidth`. Поэтому можно получив значение, поделив его на `pixelsPerValue`:
Крайнее левое значение `thumbElem.style.left` равно нулю, крайнее правой -- как раз ширине доступной области `sliderElem.clientWidth - thumbElem.clientWidth`. Поэтому можно получить значение слайдера, поделив его на `pixelsPerValue`:
```js
value = Math.round( newLeft / pixelsPerValue);
function positionToValue(left) {
return Math.round( left / pixelsPerValue);
}
function valueToPosition(value) {
return pixelsPerValue * value;
}
```

View file

@ -0,0 +1,44 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<script src="https://cdn.polyfill.io/v1/polyfill.js?features=CustomEvents,Element.prototype.closest"></script>
<link rel="stylesheet" href="slider.css">
<script src="slider.js"></script>
</head>
<body>
<div id="slider" class="slider">
<div class="thumb"></div>
</div>
Slide:<span id="slide">&nbsp;</span>
Change:<span id="change">&nbsp;</span>
<button onclick="slider.setValue(50)">slider.setValue(50)</button>
<script>
var sliderElem = document.getElementById('slider');
var slider = new Slider({
elem: sliderElem,
max: 100
});
sliderElem.addEventListener('slide', function(event) {
document.getElementById('slide').innerHTML = event.detail;
});
sliderElem.addEventListener('change', function(event) {
document.getElementById('change').innerHTML = event.detail;
});
</script>
</body>
</html>

View file

@ -0,0 +1,19 @@
.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;
width: 10px;
height: 25px;
border-radius: 3px;
background: blue;
cursor: pointer;
}

View file

@ -0,0 +1,88 @@
function Slider(options) {
var elem = options.elem;
var thumbElem = elem.querySelector('.thumb');
var max = options.max || 100;
var sliderCoords, thumbCoords, shiftX, shiftY;
// [<*>----------------]
// |...............|
// first last
var pixelsPerValue = (elem.clientWidth - thumbElem.clientWidth) / max;
elem.ondragstart = function() {
return false;
};
elem.onmousedown = function(event) {
if (event.target.closest('.thumb')) {
startDrag(event.clientX, event.clientY);
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';
elem.dispatchEvent(new CustomEvent('slide', {
bubbles: true,
detail: positionToValue(newLeft)
}));
}
function valueToPosition(value) {
return pixelsPerValue * value;
}
function positionToValue(left) {
return Math.round( left / pixelsPerValue);
}
function onDocumentMouseMove(e) {
moveTo(e.clientX);
}
function onDocumentMouseUp() {
endDrag();
}
function endDrag() {
document.removeEventListener('mousemove', onDocumentMouseMove);
document.removeEventListener('mouseup', onDocumentMouseUp);
elem.dispatchEvent(new CustomEvent('change', {
bubbles: true,
detail: positionToValue(parseInt(thumbElem.style.left))
}));
}
function setValue(value) {
thumbElem.style.left = valueToPosition(value) + 'px';
}
this.setValue = setValue;
}

View file

@ -0,0 +1,46 @@
# Слайдер с событиями
[importance 5]
На основе слайдера из задачи [](/task/slider-widget) создайте графический компонент, который умеет возвращать/получать значение.
Синтаксис:
```js
var slider = new Slider({
elem: document.getElementById('slider'),
max: 100 // слайдер на самой правой позиции соответствует 100
});
```
Метод `setValue` устанавливает значение:
```js
slider.setValue(50);
```
У слайдера должно быть два события: `slide` при каждом передвижении и `change` при отпускании мыши (установке значения).
Пример использования:
```js
var sliderElem = document.getElementById('slider');
sliderElem.addEventListener('slide', function(event) {
document.getElementById('slide').innerHTML = event.detail;
});
sliderElem.addEventListener('change', function(event) {
document.getElementById('change').innerHTML = event.detail;
});
```
В действии:
[iframe src="solution" height="80"]
<ul>
<li>Ширина/высота слайдера может быть любой, JS-код это должен учитывать.</li>
<li>Центр бегунка должен располагаться в точности над выбранным значением. Например, он должен быть в центре для 50 при `max=100`.</li>
</ul>
Исходный документ -- возьмите решение задачи [](/task/slider-widget).

View file

@ -0,0 +1,32 @@
# Что изучать дальше
Если вы прочитали весь учебник и сделали задачи, то на текущий момент вы обладаете важнейшими фундаментальными знаниями и навыками JavaScript.
[cut]
В этом разделе мы изучали основы создания компонентов на JavaScript. Если проект большой и сложный, то понадобятся дополнительные инструменты для связывания компонент между собой, для привязки к ним данных и так далее.
Сейчас существует много фреймворков. Всё активно развивается, меняется, кипит и булькает, может быть из этого получится "общепринятая" архитектура, а может и нет. Сейчас явного победителя нет, выбор фреймворка зависит от проекта и личных предпочтений разработчиков.
Примеры удачных фреймворков, которые можно изучить:
<ul>
<li>[Angular.JS](http://angularjs.org)</li>
<li>[React.JS](http://facebook.github.io/react/) + [Flux](http://facebook.github.io/flux/)</li>
<li>[Backbone.JS](http://backbonejs.org/) + [Marionette](http://marionettejs.com/)</li>
</ul>
Также для работы с браузерами понадобятся различные [API](https://ru.wikipedia.org/wiki/%D0%98%D0%BD%D1%82%D0%B5%D1%80%D1%84%D0%B5%D0%B9%D1%81_%D0%BF%D1%80%D0%BE%D0%B3%D1%80%D0%B0%D0%BC%D0%BC%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D1%8F_%D0%BF%D1%80%D0%B8%D0%BB%D0%BE%D0%B6%D0%B5%D0%BD%D0%B8%D0%B9), в частности:
<ul>
<li>Работу с окнами и фреймами.</li>
<li>Регулярные выражения, класс `RegExp`.</li>
<li>Объекты `XMLHttpRequest` и `WebSocket` для работы с сервером.</li>
<li>Другие возможности современных браузеров.</li>
</ul>
В дополнительных разделах учебника мы обязательно разберём что-то из этого.
...И, конечно, понадобится система сборки проектов, например [WebPack](http://webpack.github.io/).
Успехов вам!

View file

@ -1,80 +0,0 @@
# Подсказки
Функция показа подсказки должна при первом показе сгенерировать элемент с подсказкой, а затем -- показать в нужном месте страницы.
Обсудим, как это сделать.
Для генерации подсказки добавим вспомогательную функцию `getTooltipElem()`. Она будет возвращать существующий элемент, если он есть, а если нет -- генерировать новый.
```js
function getTooltipElem() {
if (!tooltipElem) {
tooltipElem = $('<div/>', {
"class" : 'tooltip',
html: html
});
}
return tooltipElem;
}
```
**Основная настройка вида подсказки -- в CSS-классе `tooltip`.**
Например:
```css
.tooltip {
position:absolute;
z-index:100; /* подсказка должна перекрывать другие элементы */
padding: 10px 20px;
/* красивости... */
border: 1px solid #b3c9ce;
border-radius: 4px;
text-align: center;
font: italic 14px/1.3 arial, sans-serif;
color: #333;
background: #fff;
box-shadow: 3px 3px 3px rgba(0,0,0,.3);
}
```
**Как правильно отпозиционировать подсказку? Для начала, по горизонтали.**
Центр подсказки должен быть ровно над центром элемента, который имеет координату `elem.offset().left + elem.outerWidth()/2`.
Если поставить `tooltipElem.left` в это значение -- результат будет выглядеть так:
<img src="tooltip-fixed-center.png">
```js
tooltipElem.left = elem.offset().left + elem.outerWidth()/2
```
Дополнительно нужно сдвинуть подсказку на половину собственной ширины влево:
<img src="tooltip-fixed-center2.png">
```js
left -= tooltipElem.outerWidth()/2;
```
**Теперь отпозиционируем по вертикали.**
Это гораздо проще: нужно взять координату Y элемента и вычесть из неё высоту подсказки и дополнительный отступ.
```js
elemCoords.top - tooltipElem.outerHeight() - 10
```
# Проверка решения
Проверьте, пожалуйста, ваше решение на предмет возможных ошибок:
<ul>
<li>Подсказка корректно работает при прокрутке?</li>
<li>Не используете ли вы события `mouseout/mouseover`? Лучше -- `mouseenter/mouseleave` (или, более кратко в jQuery: `hover`).</li>
<li>Вторая подсказка с `offset:0` расположена сразу над элементом, а не как первая.</li>
<li>Элемент с подсказкой генерируется динамически, при наведении, а не когда подсказка только создаётся?</li>
<li>Элемент с подсказкой *позиционируется* при показе, а не при создании? Ведь элемент, на котором стоит подсказка, может менять своё положение.</li>
</ul>

View file

@ -1,109 +0,0 @@
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
<script src="http://code.jquery.com/jquery.min.js"></script>
<style>
body {
height: 2000px; /* подсказка должна работать независимо от прокрутки */
}
.tooltip {
position:absolute;
z-index:100; /* подсказка должна перекрывать другие элементы */
padding: 10px 20px;
/* красивости... */
border: 1px solid #b3c9ce;
border-radius: 4px;
text-align: center;
font: italic 14px/1.3 arial, sans-serif;
color: #333;
background: #fff;
box-shadow: 3px 3px 3px rgba(0,0,0,.3);
}
</style>
</head>
<body>
<p>ЛяЛяЛя ЛяЛяЛя ЛяЛяЛя ЛяЛяЛя ЛяЛяЛя ЛяЛяЛя ЛяЛяЛя ЛяЛяЛя ЛяЛяЛя</p>
<p>ЛяЛяЛя ЛяЛяЛя ЛяЛяЛя ЛяЛяЛя ЛяЛяЛя ЛяЛяЛя ЛяЛяЛя ЛяЛяЛя ЛяЛяЛя</p>
<p>ЛяЛяЛя ЛяЛяЛя ЛяЛяЛя ЛяЛяЛя ЛяЛяЛя ЛяЛяЛя ЛяЛяЛя ЛяЛяЛя ЛяЛяЛя</p>
<a href="#" id="link">Ссылка с <i>подсказкой</i></a>
<a href="#" id="link2">Еще ссылка</a>
<script>
// должно работать даже если страница имеет прокрутку
// подсказка должна перекрывать текст под ней
// подсказка не должна вылезать за экран
function Tooltip(options) {
var offsetFromElement = (options.offset === undefined) ? 10 : options.offset;
var tooltipElem;
var isEnabled = true;
var elem = options.elem;
var html = options.html;
// обычно обработчики - отдельные функции, но здесь все слишком просто, так что без них
elem.hover(show, hide);
function hide() {
getTooltipElem().remove();
}
function show() {
var elemCoords = elem.offset();
var tooltipElem = getTooltipElem();
var winLeft = $(window).scrollLeft();
var winTop = $(window).scrollTop();
tooltipElem.appendTo('body'); // если не показать элемент, то не получится получить ширину
var left = elemCoords.left + (elem.outerWidth() - tooltipElem.outerWidth())/2^0;
if (left < winLeft) left = winLeft; // вылезли за левую границу
var top = elemCoords.top - tooltipElem.outerHeight() - offsetFromElement;
if (top < winTop) { // вылезли за верхнюю границу
top = elemCoords.top + elem.outerHeight() + offsetFromElement;
}
tooltipElem.css({
left: left,
top: top
});
}
function getTooltipElem() {
if (!tooltipElem) {
tooltipElem = $('<div/>', {
"class" : 'tooltip',
html: html
});
}
return tooltipElem;
}
}
new Tooltip({
elem: $('#link'),
html: "подсказка длиннее, чем элемент"
});
new Tooltip({
elem: $('#link2'),
html: "HTML<br>подсказка",
offset: 0
});
</script>
</body>
</html>

View file

@ -1,50 +0,0 @@
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
<script src="http://code.jquery.com/jquery.min.js"></script>
<style>
body {
height: 2000px; /* подсказка должна работать независимо от прокрутки */
}
.tooltip {
/* ваш стиль */
}
</style>
</head>
<body>
<p>ЛяЛяЛя ЛяЛяЛя ЛяЛяЛя ЛяЛяЛя ЛяЛяЛя ЛяЛяЛя ЛяЛяЛя ЛяЛяЛя ЛяЛяЛя</p>
<p>ЛяЛяЛя ЛяЛяЛя ЛяЛяЛя ЛяЛяЛя ЛяЛяЛя ЛяЛяЛя ЛяЛяЛя ЛяЛяЛя ЛяЛяЛя</p>
<p>ЛяЛяЛя ЛяЛяЛя ЛяЛяЛя ЛяЛяЛя ЛяЛяЛя ЛяЛяЛя ЛяЛяЛя ЛяЛяЛя ЛяЛяЛя</p>
<a href="#" id="link">Ссылка с <i>подсказкой</i></a>
<a href="#" id="link2">Еще ссылка</a>
<script>
// должно работать даже если страница имеет прокрутку
// подсказка должна перекрывать текст под ней
// подсказка не должна вылезать за экран
function Tooltip(options) {
/* ваш код */
}
new Tooltip({
elem: $('#link'),
html: "подсказка длиннее, чем элемент"
});
new Tooltip({
elem: $('#link2'),
html: "HTML<br>подсказка",
offset: 0
});
</script>
</body>
</html>

View file

@ -1,32 +0,0 @@
# Подсказка над элементом
[importance 5]
Создайте всплывающую подсказку над элементом.
Подсказка должна появляться при наведении на элемент, по центру и на небольшом расстоянии сверху. При уходе курсора с элемента -- исчезать.
Вы можете посмотреть поведение подсказки в ифрейме ниже, наведя курсор на ссылку.
[iframe src="solution" height=200 link border=1]
Способ добавления подсказки к элементу:
```js
new Tooltip({
elem: $('#elem'),
html: "Вот <b>такая</b> подсказка!",
// вертикальное расстояние от подсказки до элемента
offset: 20 // необязательный параметр, по умолчанию 10
});
```
<ul>
<li>В подсказке и элементе, на который она поставлена, может быть произвольный HTML. Оформление подсказки должно задаваться CSS.</li>
<li>Подсказка не должна вылезать за пределы экрана, если нельзя показать сверху -- показывать снизу элемента.</li>
<li>Объект подсказки не должен иметь публичных методов, только приватные.</li>
</ul>
P.S. Подсказки, если их мало, можно реализовать и при помощи CSS. Но JS-подход более универсален и не зависит от вёрстки, в частности, он может проверять, не вылезает ли подсказка за экран.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2 KiB

View file

@ -1,30 +0,0 @@
# События
Три события имеют отношение к подсказке:
<ol>
<li>При `mouseover` -- показать.</li>
<li>При `mousemove` -- показать на новом месте.</li>
<li>При `mouseout` -- спрятать.</li>
</ol>
...Но при заходе на элемент происходят сразу оба события: `mouseover` и `mousemove` на нём же. Зачем вызывать код для показа два раза? Можно оставить его только в `mousemove`.
Стиль элемента подсказки:
```css
.tooltip {
position:absolute;
z-index:100; /* подсказка должна перекрывать другие элементы */
...
}
```
# Решение
[edit src="solution"]Решение в песочнице[/edit]
В нём есть две тонкости.
<ol>
<li>Для того, чтобы можно было получить высоту и ширину, подсказку надо сначала показать.</li>
<li>При показе -- важно, чтобы подсказка не оказалась *под* курсором. Если такое произойдет, то дальнейшие события `mousemove` станут происходить *уже на подсказке*, а не на элементе, и их обработка сломается.</li>
</ol>

View file

@ -1,138 +0,0 @@
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
<script src="http://code.jquery.com/jquery.min.js"></script>
<style>
body {
height: 2000px; /* подсказка должна работать независимо от прокрутки */
}
.tooltip {
position:absolute;
z-index:100; /* подсказка должна перекрывать другие элементы */
padding: 10px 20px;
/* красивости... */
border: 1px solid #b3c9ce;
border-radius: 4px;
text-align: center;
font: italic 14px/1.3 arial, sans-serif;
color: #333;
background: #fff;
box-shadow: 3px 3px 3px rgba(0,0,0,.3);
}
</style>
</head>
<body>
<a href="#" id="link">Ссылка с подсказкой</a>
<a href="#" id="link2">Еще ссылка</a>
<div id="elem" style="border:1px solid black; position:absolute;width:300px;height:150px;right:10px;bottom:10px">
<em>Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст
Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст</em>
</div>
<script>
// должно работать даже если страница имеет прокрутку
// подсказка должна перекрывать текст под ней
// у нижнего и правого края окна подсказка должна идти налево/вверх от курсора
function Tooltip(options) {
var offsetFromCursor = (options.offset === undefined) ? 10 : options.offset;
var tooltipElem;
var isEnabled = true;
var elem = options.elem;
var html = options.html;
elem.on({
mouseenter: onMouseEnter,
mouseleave: onMouseLeave,
mousemove: onMouseMove
});
function onMouseEnter(e) {
show(e.pageX, e.pageY);
}
function onMouseLeave() {
hide();
}
function onMouseMove(e) {
show(e.pageX, e.pageY);
}
function hide() {
getTooltipElem().remove();
};
function getTooltipElem() {
if (!tooltipElem) {
tooltipElem = $('<div/>', {
"class" : 'tooltip',
html: html
});
}
return tooltipElem;
}
function show(pageX, pageY) {
var tooltipElem = getTooltipElem();
if (!tooltipElem.is(':visible')) {
// первым делом - отобразить подсказку, чтобы можно было получить её размеры
tooltipElem.appendTo('body');
}
var scrollY = $(window).scrollTop();
var winBottom = scrollY + $(window).height();
var scrollX = $(window).scrollLeft();
var winRight = scrollX + $(window).width();
var newLeft = pageX + offsetFromCursor;
var newTop = pageY + offsetFromCursor;
if (newLeft + tooltipElem.outerWidth() > winRight) { // если за правой границей окна
newLeft -= tooltipElem.outerWidth();
newLeft -= offsetFromCursor + 2; // немного левее, чтобы курсор был не над подсказкой
}
if (newTop + tooltipElem.outerHeight() > winBottom) { // если за нижней границей окна
newTop -= tooltipElem.outerHeight();
newTop -= offsetFromCursor + 2; // немного выше
}
tooltipElem.css({
left: newLeft,
top: newTop
});
};
}
new Tooltip({
elem: $('#elem'),
html: "Вот <b>такая</b> подсказка!"
});
new Tooltip({
elem: $('#link'),
html: $('#link').html()
});
new Tooltip({
elem: $('#link2'),
html: $('#link2').html()
});
</script>
</body>
</html>

View file

@ -1,54 +0,0 @@
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
<script src="http://code.jquery.com/jquery.min.js"></script>
<style>
body {
height: 2000px; /* подсказка должна работать независимо от прокрутки */
}
.tooltip {
/* ваш стиль */
}
</style>
</head>
<body>
<a href="#" id="link">Ссылка с подсказкой</a>
<a href="#" id="link2">Еще ссылка</a>
<div id="elem" style="border:1px solid black; position:absolute;width:300px;height:150px;right:10px;bottom:10px">
<em>Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст
Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст</em>
</div>
<script>
// должно работать даже если страница имеет прокрутку
// подсказка должна перекрывать текст под ней
// у нижнего и правого края окна подсказка должна идти налево/вверх от курсора
function Tooltip(options) {
/* ваш код */
}
new Tooltip({
elem: $('#elem'),
html: "Вот <b>такая</b> подсказка!"
});
new Tooltip({
elem: $('#link'),
html: $('#link').html()
});
new Tooltip({
elem: $('#link2'),
html: $('#link2').html()
});
</script>
</body>
</html>

View file

@ -1,34 +0,0 @@
# Подсказка, следующая за курсором
[importance 4]
Создайте всплывающую подсказку, следующую за курсором.
<ul>
<li>Подсказка должна появляться при наведении на элемент, на небольшом расстоянии справа-снизу от курсора.</li>
<li>При передвижении курсора подсказка следует за ним.</li>
<li>Если курсор слишком низко/справа, то чтобы подсказка не вылезла за нижнюю/правую границу экрана -- пусть отображается сверху/слева от курсора.</li>
</ul>
Вы можете посмотреть поведение подсказки в ифрейме ниже, наведя курсор на правый-нижний угол.
[iframe src="solution" height=200 link border=1]
Способ добавления подсказки к элементу:
```js
new Tooltip({
elem: $('#elem'),
html: "Вот <b>такая</b> подсказка!")
});
```
Естественные пожелания:
<ul>
<li>Подсказка не должна "моргать" при движении мыши.</li>
<li>Подсказка должна правильно работать, если у страницы есть прокрутка.</li>
<li>В подсказке и элементе, на который она поставлена, может быть произвольный HTML. Оформление подсказки должно задаваться CSS.</li>
<li>Объект подсказки не должен иметь публичных методов, только приватные.</li>
</ul>

View file

@ -1,75 +0,0 @@
# CSS
Для захвата проще всего создать дополнительные `DIV'ы` и отпозиционировать их сбоку и справа-снизу `IMG`. При нажатии на них начинать смену размера.
CSS-структура:
```html
<div class="resize-wrapper" style="width: 503px; height: 285px;">
<img src="heroes.jpg" id="heroes" style="width:500px;height:282px">
<div class="resize-handle-s"></div>
<div class="resize-handle-e"></div>
<div class="resize-handle-se"></div>
</div>
```
Внешний `DIV` подстраивается под размер картинки и имеет `position:relative`. Внутри него расположены абсолютно позиционированные "ручки" для захвата.
Стиль:
```css
.resize-wrapper {
position: relative;
}
.resize-wrapper img {
/* img при таком DOCTYPE в некоторых браузерах имеет display:inline, в некоторых display:block
если оставить inline, то под img браузер оставит пустое место для "хвостов" букв
*/
display: block;
}
.resize-handle-se { /* правый-нижний угол */
position: absolute;
bottom: 0;
right: 0;
width: 16px;
height: 16px;
background: url(handle-se.png) no-repeat;
cursor: se-resize;
}
.resize-handle-s { /* нижняя "рамка", за которую можно потащить */
position: absolute;
bottom: 0;
height: 3px;
width:100%;
background: gray;
cursor: s-resize;
}
.resize-handle-e { /* правая "рамка", за которую можно потащить */
position: absolute;
right: 0;
top: 0;
width: 3px;
height:100%;
background: gray;
cursor: e-resize;
}
```
Для обозначения низа используется буква "s" (south, "юг" по-английски), обозначения правой стороны -- буква "e" (east, "восток" по-английски).
Использование сторон света для обозначения направления можно часто встретить в скриптах.
# Алгоритм
<ol>
<li>При инициализации IMG оборачивается во внешнюю обертку и обкладывается `DIV'ами`, на которых ловим `mousedown`. Обертка нужна, чтобы абсолютно позиционировать `DIV'ы` внутри неё под/сбоку изображения.</li>
<li>При наступлении `mousedown`, начинаем ловить `document.mousemove` и подгонять картинку под размер, а обертку -- под картинку.
Желательно заодно отменить браузерное выделение и Drag'n'Drop, возвратив `false` из собработчиков событий `mousedown` и `dragstart`.
</li>
<li>При наступлении `mouseup` -- конец.</li>
[edit src="solution"]Открыть полное решение[/edit]

View file

@ -1,149 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<script src="http://code.jquery.com/jquery.min.js"></script>
<style>
.resize-wrapper {
position: relative;
}
.resize-wrapper img {
/* img при таком DOCTYPE в некоторых браузерах имеет display:inline, в некоторых display:block
если оставить inline, то под img браузер оставит пустое место для "хвостов" букв
*/
display: block;
}
.resize-handle-se { /* правый-нижний угол */
position: absolute;
bottom: 0;
right: 0;
width: 16px;
height: 16px;
background: url(https://js.cx/clipart/handle-se.png) no-repeat;
cursor: se-resize;
}
.resize-handle-s { /* нижняя "рамка", за которую можно потащить */
position: absolute;
bottom: 0;
height: 3px;
width:100%;
background: gray;
cursor: s-resize;
}
.resize-handle-e { /* правая "рамка", за которую можно потащить */
position: absolute;
right: 0;
top: 0;
width: 3px;
height:100%;
background: gray;
cursor: e-resize;
}
</style>
</head>
<body>
<img style="width:500px;height:282px" id="heroes" src="https://js.cx/clipart/heroes.jpg">
<div id="info"></div>
<script>
function Resizeable(options) {
var self = this;
var elem = options.elem;
var handleSize = options.handleSize || 3;
var newWidth, newHeight, resizeType; // размеры в процессе ресайза
// внешняя обертка
var wrapper = $('<div class="resize-wrapper"/>')
wrapper.prependTo(elem.parent())
wrapper.append(elem);
// добавляем "ручки" в обертку
wrapper.append(
'<div class="resize-handle-s"/><div class="resize-handle-e"/><div class="resize-handle-se"/>'
);
// отменить перенос и выделение браузера
// в дополнение к return false из mousedown
wrapper.on('selectstart dragstart', false);
adjustWrapperSize();
wrapper.on('mousedown', onMouseDown);
function onMouseDown(e) {
var className = e.target.className;
if (className.indexOf("resize-handle-") == 0) {
// поймали клик на "ручке" - вызываем начало ресайза
resizeType = className.slice("resize-handle-".length);
startResize();
}
return false;
}
function adjustWrapperSize() {
// подгоняет размер обертки под картинку
wrapper.css({
width: elem.width() + handleSize,
height: elem.height() + handleSize
});
}
function startResize() {
$(document).on('mousemove.resizeable', onDocumentMouseMove);
$(document).on('mouseup.resizeable', onDocumentMouseUp);
}
function onDocumentMouseMove(e) {
var offset = wrapper.offset();
if (~resizeType.indexOf("s")) {
// в ручке есть буква "s" - значит меняем высоту картинки
newHeight = Math.max(e.pageY - offset.top, handleSize);
elem.css('height', newHeight);
}
if (~resizeType.indexOf("e")) {
// в ручке есть буква "e" - значит меняем ширину картинки
newWidth = Math.max(e.pageX - offset.left, handleSize);
elem.css('width', newWidth);
}
// подогнать обертку
adjustWrapperSize();
}
function onDocumentMouseUp() {
endResize();
}
function endResize() {
$(document).off('.resizeable');
$(self).triggerHandler({
type: "resize",
newWidth: newWidth,
newHeight: newHeight
});
}
}
var resizeMe = new Resizeable({
elem: $('#heroes')
});
$(resizeMe).on("resize", function(e) {
// вывести результат
$('#info').html("ширина:" + e.newWidth + ", высота:" + e.newHeight);
});
</script>
</body>
</html>

View file

@ -1,32 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<script src="http://code.jquery.com/jquery.min.js"></script>
</head>
<body>
<img style="width:500px;height:282px" id="heroes" src="https://js.cx/clipart/heroes.jpg">
Картинка для декорации правого-нижнего угла: <img src="https://js.cx/clipart/handle-se.png">
<div id="info"></div>
<script>
function Resizeable(options) {
/* ваш код */
}
var resizeMe = new Resizeable({
elem: $('#heroes')
});
$(resizeMe).on("resize", function(e) {
// вывести результат
$('#info').html("ширина:" + e.newWidth + ", высота:" + e.newHeight);
});
</script>
</body>
</html>

View file

@ -1,28 +0,0 @@
# Менять размер картинки при помощи мыши
[importance 5]
Реализуйте интерфейс для смены размера изображения мышью.
Пусть изображение "тянется" при захвате его снизу, справа и в правом-нижнем углу, вот так:
[iframe height=350 src="solution" link]
По окончанию смены размеров должно быть событие **`resize`** c новыми размерами.
Синтаксис:
```js
var resizeMe = new Resizeable({
elem: $('#heroes')
});
$(resizeMe).on("resize", function(e) {
// вывести результат
$('#info').html("ширина:" + e.newWidth + ", высота:" + e.newHeight);
});
```
Минимальный размер изображения 3x3, меньше ресайзить нельзя.
В исходном документе есть ссылка на <a href="https://js.cx/clipart/handle-se.png">картинку handle-se.png</a> для правого-нижнего угла.

View file

@ -1,21 +0,0 @@
# HTML/CSS
Область выделения можно оформить как `DIV`, серого цвета, полупрозрачный, с рамкой:
```css
.crop-area {
position: absolute;
border: 1px dashed black;
background: #F5DEB3;
opacity: 0.3;
filter:alpha(opacity=30); /* IE opacity */
}
```
# Решение
Обратите внимание: обработчики `mousemove/mouseup` ставятся на `document`, не на элемент.
Это для того, чтобы посетитель мог начать выделение на элементе, а продолжить и завершить его, двигая зажатой мышкой снаружи, вне его границ.

View file

@ -1,130 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<script src="http://code.jquery.com/jquery.min.js"></script>
<style>
.crop-area {
position: absolute;
border: 1px dashed black;
background: #F5DEB3;
opacity: 0.3;
filter:progid:DXImageTransform.Microsoft.Alpha(opacity=30); /* IE opacity */
}
</style>
</head>
<body>
<img width="500" height="282" id="heroes" src="https://js.cx/clipart/heroes.jpg">
<div id="info"></div>
<script>
function Croppable(options) {
var self = this;
var elem = options.elem;
var cropArea;
var cropStartX, cropStartY;
elem.on('selectstart dragstart', false)
.on('mousedown', onImgMouseDown);
function initCropArea(pageX, pageY) {
cropArea = $('<div class="crop-area"/>')
.appendTo('body');
cropStartX = pageX;
cropStartY = pageY;
}
function onDocumentMouseMove(e) {
drawCropArea(e.pageX, e.pageY);
}
function onDocumentMouseUp(e) {
endCrop(e.pageX, e.pageY);
cropArea.remove();
$(document).off('.croppable');
}
function onImgMouseDown(e) {
initCropArea(e.pageX, e.pageY);
$(document).on({
'mousemove.croppable': onDocumentMouseMove,
'mouseup.croppable': onDocumentMouseUp
});
return false;
};
function drawCropArea(pageX, pageY) {
var dims = getCropDimensions(pageX, pageY);
cropArea.css(dims);
// вычитаем 2, т.к. ширина будет дополнена рамкой
// если не вычесть, то рамка может вылезти за изображение
cropArea.css({
height: Math.max(dims.height-2, 0),
width: Math.max(dims.width-2,0)
});
// здесь мы просто рисуем полупрозрачный квадрат
// альтернативный подход - накладывать на каждую часть изображения div'ы с opacity и черным цветом, чтобы только кроп был ярким
}
function endCrop(pageX, pageY) {
var dims = getCropDimensions(pageX, pageY);
var coords = elem.offset();
// получить координаты относительно изображения
dims.left -= coords.left;
dims.top -= coords.top;
$(self).triggerHandler($.extend({ type: "crop" }, dims));
}
function getCropDimensions(pageX, pageY) {
// 1. Делаем квадрат из координат нажатия и текущих
// 2. Ограничиваем размерами img, если мышь за его пределами
var left = Math.min(cropStartX, pageX);
var right = Math.max(cropStartX, pageX);
var top = Math.min(cropStartY, pageY);
var bottom = Math.max(cropStartY, pageY);
var coords = elem.offset();
left = Math.max(left, coords.left);
top = Math.max(top, coords.top);
right = Math.min(right, coords.left + elem.outerWidth());
bottom = Math.min(bottom, coords.top + elem.outerHeight());
return { left: left, top: top, width: right-left, height: bottom-top };
}
}
var croppable = new Croppable({
elem: $('#heroes')
});
$(croppable).on("crop", function(event) {
// вывести координаты и размеры crop-квадрата относительно изображения
var str = "";
$(['left','top','width','height']).each(function() {
str += this+":"+event[this]+" ";
});
$('#info').html("Crop: " + str);
});
</script>
</body>
</html>

View file

@ -1,34 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<script src="http://code.jquery.com/jquery.min.js"></script>
</head>
<body>
<img width="500" height="282" id="heroes" src="https://js.cx/clipart/heroes.jpg">
<div id="info"></div>
<script>
function Croppable(options) {
/* ваш код */
}
var croppable = new Croppable({
elem: $('#heroes')
});
$(croppable).on("crop", function(event) {
// вывести координаты и размеры crop-квадрата относительно изображения
var str = "";
$(['left','top','width','height']).each(function() {
str += this+":"+event[this]+" ";
});
$('#info').html("Crop: " + str);
});
</script>
</body>
</html>

View file

@ -1,31 +0,0 @@
# Выбор фрагмента картинки мышью
[importance 3]
Реализуйте интерфейс для выбора фрагмента изображения мышью.
Нажмите на изображении и проведите мышью для выбора:
[iframe height=350 src="solution" link]
По окончании смены размеров должно быть событие `crop` c `left/top` координатами и `width/height` размерами выбранной области.
Синтаксис:
```js
var croppable = new Croppable({
elem: $('#heroes')
});
$(croppable).on("crop", function(e) {
// вывести crop-квадрат, left/top - относительно изображения
// e.left, e.top, e.width, e.height
});
```
<ul>
<li>Область можно выбирать, двигая курсором в любую сторону от исходной точки.</li>
<li>Область не может вылезать за пределы изображения.</li>
</ul>

View file

@ -1,117 +0,0 @@
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
<script src="http://code.jquery.com/jquery.min.js"></script>
<style>
.menu {
padding: 5px;
width: 250px;
border: 1px solid black;
}
.menu ul {
margin: 0;
padding-left: 20px;
margin-left: 10px;
height: 0;
list-style-position: outside;
overflow: hidden;
}
.menu .menu-title {
padding-left: 16px;
background: url(https://js.cx/clipart/arrow-right.png) left center no-repeat;
font-size: 18px;
cursor: pointer;
}
.menu-open .menu-title {
background: url(https://js.cx/clipart/arrow-down.png) left center no-repeat;
}
.menu-open ul {
display: block;
}
</style>
</head>
<body>
<div id="selected"></div>
<div id="sweeties" class="menu">
<span class="menu-title">Сладости (наведи курсор)!</span>
<ul>
<li><a href="#cake">Торт</a></li>
<li><a href="#donut">Пончик</a></li>
<li><a href="#cake-small">Пирожное</a></li>
<li><a href="#chokolate">Шоколадка</a></li>
<li><a href="#icecream">Мороженое</a></li>
<li><a href="#souflet">Суфле</a></li>
<li><a href="#rahatlukum">Рахат-Лукум</a></li>
</ul>
</div>
<script>
var menu = new SlidingMenu({
elem: $('#sweeties'),
duration: 1000
});
$(menu).on("select", function(e) {
$("#selected").html(e.value);
});
function SlidingMenu(options) {
var self = this;
var elem = options.elem;
var duration = options.duration || 100;
var titleElem = elem.find('.menu-title');
var listElem = elem.find('ul');
elem.hover(slideListShow, slideListHide);
elem.on('click', 'a', onItemClick);
function onItemClick(e) {
$(self).triggerHandler({
type: "select",
value: e.target.getAttribute('href').slice(1)
});
return false;
}
function slideListShow() {
// анимируем высоту от нуля до scrollHeight
// чтобы scrollHeight был верный - ставим overflow: hidden
// скрываем при помощи height: 0 вместо display: none, чтобы анимация могла определить высоту
listElem.stop().animate({
height: listElem.prop('scrollHeight')
}, {
// время на часть высоты, которую еще надо показать
duration: duration * (listElem.prop('scrollHeight') - listElem.height()) / listElem.prop('scrollHeight')
});
}
function slideListHide() {
listElem.stop().animate({
height: 0
}, {
duration: duration * (listElem.height() / listElem.prop('scrollHeight'))
});
}
}
</script>
</body>
</html>

View file

@ -1,78 +0,0 @@
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
<script src="http://code.jquery.com/jquery.min.js"></script>
<style>
.menu {
padding: 5px;
width: 250px;
border: 1px solid black;
}
.menu ul {
margin: 0;
padding-left: 20px;
margin-left: 10px;
height: 0;
list-style-position: outside;
overflow: hidden;
}
.menu .menu-title {
padding-left: 16px;
background: url(https://js.cx/clipart/arrow-right.png) left center no-repeat;
font-size: 18px;
cursor: pointer;
}
.menu-open .menu-title {
background: url(https://js.cx/clipart/arrow-down.png) left center no-repeat;
}
.menu-open ul {
display: block;
}
</style>
</head>
<body>
<div id="selected"></div>
<div id="sweeties" class="menu">
<span class="menu-title">Сладости (наведи курсор)!</span>
<ul>
<li><a href="#cake">Торт</a></li>
<li><a href="#donut">Пончик</a></li>
<li><a href="#cake-small">Пирожное</a></li>
<li><a href="#chokolate">Шоколадка</a></li>
<li><a href="#icecream">Мороженое</a></li>
<li><a href="#souflet">Суфле</a></li>
<li><a href="#rahatlukum">Рахат-Лукум</a></li>
</ul>
</div>
<script>
var menu = new SlidingMenu({
elem: $('#sweeties'),
duration: 1000
});
$(menu).on("select", function(e) {
$("#selected").html(e.value);
});
function SlidingMenu(options) {
/* ваш код, оживляющий меню */
}
</script>
</body>
</html>

View file

@ -1,25 +0,0 @@
# Меню с анимированным раскрытием
[importance 3]
Создайте раскрывающееся при наведении на заголовок меню:
[iframe height=210 src="solution"]
Меню должно плавно скрываться-раскрываться, когда курсор переводят в него, а затем обратно, затем опять в него...
При клике меню должно генерировать событие `"select"`.
```js
var menu = new SlidingMenu({
elem: $('#sweeties'),
duration: 1000 // длительность анимации
});
$(menu).on("select", function(e) {
$("#selected").html(e.value);
});
```
В исходном документе находится DOM-структура и стили для меню.

View file

@ -1,183 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<script src="http://code.jquery.com/jquery.min.js"></script>
<style>
.slider {
width: 310px;
height: 15px;
margin: 5px;
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);
}
.slider-thumb {
position: relative;
width: 10px;
height: 25px;
left: 10px;
top: -5px;
border-radius: 3px;
background: blue;
cursor: pointer;
}
.sliding .slider-thumb {
cursor: move;
}
</style>
</head>
<body>
<div id="slider" class="slider">
<div class="slider-thumb"></div>
</div>
Slide:<span id="slide">&nbsp;</span>
Change:<span id="change">&nbsp;</span>
<script>
function Slider(options) {
var self = this;
var elem = options.elem;
var thumbElem = elem.find('.slider-thumb');
// [<*>----------------]
// |...............|
// first last
var pixelsPerValue = (elem.width() - thumbElem.width()) / options.max;
var value = options.value || 0;
// при начале переноса фиксируются
var thumbCursorShift; // сдвиг курсора относительно бегунка
var sliderCoords; // и координаты слайдера
thumbElem.on('mousedown', onThumbMouseDown)
.on('selectstart dragstart', false);
setSlidingValue(value, true);
function onThumbMouseDown(e) {
startSlide(e.pageX, e.pageY);
return false;
}
function onDocumentMouseMove(e) {
// вычесть координату родителя, т.к. position: relative
var newLeft = e.pageX - thumbCursorShift.x - sliderCoords.left;
// курсор ушёл вне слайдера
if (newLeft < 0) {
newLeft = 0;
}
var rightEdge = elem.outerWidth() - thumbElem.outerWidth();
if (newLeft > rightEdge) {
newLeft = rightEdge;
}
setSlidingValue( Math.round( newLeft / pixelsPerValue) );
}
function onDocumentMouseUp() {
endSlide();
}
function endSlide() {
$(document).off('.slider');
$(self).triggerHandler({
type: "change",
value: value
});
elem.removeClass('sliding');
}
function startSlide(downPageX, downPageY) {
var thumbCoords = thumbElem.offset();
thumbCursorShift = {
x: downPageX - thumbCoords.left,
y: downPageY - thumbCoords.top
};
sliderCoords = elem.offset();
$(document).on({
'mousemove.slider': onDocumentMouseMove,
'mouseup.slider': onDocumentMouseUp
});
elem.addClass('sliding');
}
/**
* Установить промежуточное значение
* quiet -- означает "не генерировать событие"
*/
function setSlidingValue(newValue, quiet) {
value = newValue;
thumbElem.css('left', value * pixelsPerValue ^ 0);
if (!quiet) {
$(self).triggerHandler({
type: "slide",
value: value
});
}
}
/**
* Установить окончательное значение
* @param {number} newValue новое значение
* @param {boolean} quiet если установлен, то без события
*/
this.setValue = function(newValue, quiet) {
// установить значение БЕЗ генерации события slide
// т.е. slide в любом случае нет
setSlidingValue(newValue, true);
// ..а change будет, если не указан quiet
if (!quiet) {
$(self).triggerHandler({
type: "change",
value: value
});
}
}
}
var slider = new Slider({
elem: $('#slider'),
max: 100,
value: 10
});
$(slider).on({
slide: function(e) {
$('#slide').html(e.value);
},
change: function(e) {
$('#change').html(e.value);
}
});
slider.setValue(50);
</script>
</body>
</html>

View file

@ -1,45 +0,0 @@
# Слайдер с событиями
[importance 5]
На основе слайдера из задачи [](/task/slider) создайте графический компонент, который умеет возвращать/получать значение.
Синтаксис:
```js
var slider = new Slider({
elem: $('#slider'),
max: 100 // слайдер на самой правой позиции соответствует 100
});
```
Метод `setValue` устанавливает значение:
```js
slider.setValue(50);
```
У слайдера должно быть два события: `slide` при каждом передвижении и `change` при отпускании мыши (установке значения).
Пример использования:
```js
$(slider).on({
slide: function(value) {
$('#slide').html(value);
},
change: function(value) {
$('#change').html(value);
}
});
```
В действии:
[iframe src="solution" height="60"]
<ul>
<li>Дизайн слайдера, ширина/высота элементов должна быть изменяемой через CSS, без необходимости трогать JS-код.</li>
<li>Центр бегунка должен располагаться над значением. Например, он должен быть в центре для 50 при максимуме 100.</li>
</ul>
Исходный документ -- возьмите решение из задачи [](/task/slider).

View file

@ -1,28 +0,0 @@
.calendar-table {
border-collapse: collapse;
}
.calendar-table td, .calendar-table th {
border: 1px solid black;
padding: 3px;
text-align: center;
}
.calendar-table th {
font-weight: bold;
background-color: #E6E6E6;
}
.date-cell:hover {
background: #eee;
cursor: pointer;
}
.date-cell.selected {
background: #0F0;
}
.calendar-table caption {
text-align: center;
}

View file

@ -1,153 +0,0 @@
/**
* options:
* value Date или объект {year,month,day}} -- дата, для которой показывать календарь
* если в объекте указаны только {year,month}, то день не выбран
*/
function Calendar(options) {
var self = this;
var monthNames = 'Январь Февраль Март Апрель Май Июнь Июль Август Сентябрь Октябрь Ноябрь Декабрь'.split(' ');
var elem;
var year, month, day;
var showYear, showMonth;
parseValue(options.value);
// -----------------------
this.getElement = function() {
if (!elem) {
render();
}
return elem;
};
this.getValue = function() {
return day ? new Date(year, month, day) : null;
};
function parseValue(value) {
if (value instanceof Date) {
year = value.getFullYear();
month = value.getMonth();
day = value.getDate();
} else {
year = value.year;
month = value.month;
day = value.day;
}
}
/**
* Установить значение календаря
* @param newValue {Date или объект {year,month[,day]} Новое значение
* @param quiet Если true, то событие не генерируется
*/
this.setValue = function(newValue, quiet) {
parseValue(newValue);
if (elem) {
clearSelected();
render();
}
if (!quiet) {
$(self).triggerHandler({
type: "select",
value: new Date(year, month, day)
});
}
};
function render() {
if (!elem) {
elem = $('<div class="calendar"/>')
.on('click', '.date-cell', onDateCellClick);
}
if (showYear != year || showMonth != month) {
elem.html(renderCalendarTable(year, month));
elem.find('caption').html(monthNames[month]+' '+year);
showYear = year;
showMonth = month;
}
if (day) {
var num = getCellByDate(new Date(year, month, day));
elem.find('td').eq(num).addClass("selected");
}
}
function clearSelected() {
elem.find('.selected').removeClass('selected');
}
function onDateCellClick(e) {
day = $(e.target).html();
self.setValue({year: year, month: month, day: day});
}
/**
* Возвращает по дате номер TD в таблице
* Использование:
* var td = table.getElementsByTagName('td')[getCellByDate(date)]
*/
function getCellByDate(date) {
var dateDayOne = new Date(date.getFullYear(), date.getMonth(), 1);
return getDay(dateDayOne) + date.getDate() - 1;
}
/**
* получить номер дня недели для date, от 0(пн) до 6(вс)
* @param date
*/
function getDay(date) { //
var day = date.getDay();
if (day == 0) day = 7;
return day - 1;
}
/**
* Генерирует таблицу для календаря заданного месяца/года
*/
function renderCalendarTable(year, month) {
var d = new Date(year, month);
var table = ['<table class="calendar-table"><caption></caption><tr><th>пн</th><th>вт</th><th>ср</th><th>чт</th><th>пт</th><th>сб</th><th>вс</th></tr><tr>'];
for (var i=0; i<getDay(d); i++) {
table.push('<td></td>');
}
// ячейки календаря с датами
while(d.getMonth() == month) {
table.push('<td class="date-cell">'+d.getDate()+'</td>');
if (getDay(d) % 7 == 6) { // вс, последний день - перевод строки
table.push('</tr><tr>');
}
d.setDate(d.getDate()+1);
}
// добить таблицу пустыми ячейками, если нужно
if (getDay(d) != 0) {
for (var i=getDay(d); i<7; i++) {
table.push('<td></td>');
}
}
table.push('</tr></table>');
return table.join('\n')
}
}

View file

@ -1,31 +0,0 @@
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
<link type="text/css" rel="stylesheet" href="calendar.css">
<script src="http://code.jquery.com/jquery.min.js"></script>
<script src="calendar.js"></script>
</head>
<body>
<button onclick="calendar.setValue(new Date())">setValue(сегодня)</button>
<div id="calendar-holder"></div>
<div id="value">тут будет выбранное значение (дата)</div>
<script>
var calendar = new Calendar({
value: {year: 2012, month: 2 }
});
$('#calendar-holder').append(calendar.getElement());
$(calendar).on("select", function(e) {
$('#value').html(e.value+'');
});
</script>
</body>
</html>

View file

@ -1,58 +0,0 @@
/**
* Возвращает по дате номер TD в таблице
* Использование:
* var td = table.find('td').eq(getCellByDate(date))
*/
function getCellByDate(date) {
var date1 = new Date(date.getFullYear(), date.getMonth(), 1);
return getDay(date1) + date.getDate() - 1;
}
/**
* получить номер дня недели для date, от 0(пн) до 6(вс)
* @param date
*/
function getDay(date) { //
var day = date.getDay();
if (day == 0) day = 7;
return day - 1;
}
/**
* Генерирует таблицу для календаря заданного месяца/года
* @param year
* @param month
*/
function renderCalendarTable(year, month) {
var d = new Date(year, month);
var table = ['<table class="calendar-table"><tr><th>пн</th><th>вт</th><th>ср</th><th>чт</th><th>пт</th><th>сб</th><th>вс</th></tr><tr>'];
for (var i=0; i<getDay(d); i++) {
table.push('<td></td>');
}
// ячейки календаря с датами
while(d.getMonth() == month) {
table.push('<td class="date-cell">'+d.getDate()+'</td>');
if (getDay(d) % 7 == 6) { // вс, последний день - перевод строки
table.push('</tr><tr>');
}
d.setDate(d.getDate()+1);
}
// добить таблицу пустыми ячейками, если нужно
if (getDay(d) != 0) {
for (var i=getDay(d); i<7; i++) {
table.push('<td></td>');
}
}
table.push('</tr></table>');
return table.join('\n')
}

View file

@ -1,28 +0,0 @@
.calendar-table {
border-collapse: collapse;
}
.calendar-table td, .calendar-table th {
border: 1px solid black;
padding: 3px;
text-align: center;
}
.calendar-table th {
font-weight: bold;
background-color: #E6E6E6;
}
.date-cell:hover {
background: #eee;
cursor: pointer;
}
.date-cell.selected {
background: #0F0;
}
.calendar-table caption {
text-align: center;
}

View file

@ -1,10 +0,0 @@
/**
* options:
* year/month {number} год/месяц для календаря
* value {Date} текущая выбранная дата
*/
function Calendar(options) {
var monthNames = 'Январь Февраль Март Апрель Май Июнь Июль Август Сентябрь Октябрь Ноябрь Декабрь'.split(' ');
/* ваш код */
}

View file

@ -1,31 +0,0 @@
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
<link type="text/css" rel="stylesheet" href="calendar.css">
<script src="http://code.jquery.com/jquery.min.js"></script>
<script src="calendar.js"></script>
</head>
<body>
<button onclick="calendar.setValue(new Date())">setValue(сегодня)</button>
<div id="calendar-holder"></div>
<div id="value">тут будет выбранное значение (дата)</div>
<script>
var calendar = new Calendar({
value: {year: 2012, month: 2 }
});
$('#calendar-holder').append(calendar.getElement());
$(calendar).on("select", function(e) {
$('#value').html(e.value+'');
});
</script>
</body>
</html>

View file

@ -1,46 +0,0 @@
# Календарь
[importance 5]
Создайте календарь.
Конструктор:
```js
var calendar = new Calendar({
year: 2012, // календарь для года 2012
month: 2 // месяц - март (нумерация с нуля!)
});
```
События:
<ul>
<li>`select` -- при изменении даты.</li>
</ul>
Публичные методы:
<ul>
<li>`setValue(date, quiet)` -- устанавливает дату `date`. Если второй аргумент `true`, то событие не генерируется.</li>
<li>`getElement()` -- возвращает DOM-элемент для компонента для вставки в документ. При первом вызове создаёт DOM.</li>
</ul>
Использование -- добавление в документ:
```js
var calendar = new Calendar({... });
calendar.getElement().appendTo('body');
```
Использование -- подписка на изменение и вывод значения:
```js
calendar.on("select", function(e) {
$('#value').html( e.value+'' );
})
```
Пример в действии:
[iframe border=1 src="solution"]
В исходный документ входит файл `calendar-table.js` со вспомогательными функциями, в частности, `renderCalendarTable(year, month)` генерирует таблицу.
[edit task src="source"/].

View file

@ -1,62 +0,0 @@
function AutocompleteList(provider) {
var elem;
var filteredResults;
var currentIndex = 0;
this.render = function() {
elem = $('<ol/>');
return elem;
};
this.update = function(value) {
filteredResults = provider.filterByStart(value);
if (filteredResults.length) {
elem.html( '<li>' + filteredResults.join('</li><li>') + '</li>' );
} else {
elem.empty();
}
currentIndex = 0;
renderCurrent();
// это событие, как и всё обновление,
// может быть асинхронным (при получении списка с сервера)
$(this).triggerHandler({
type: 'update',
values: filteredResults
});
};
function renderCurrent() {
elem.children().eq(currentIndex).addClass('selected');
}
function clearCurrent() {
elem.children().eq(currentIndex).removeClass('selected');
}
this.get = function() {
return filteredResults[currentIndex];
};
this.down = function() {
if (currentIndex == filteredResults.length - 1) return;
clearCurrent();
currentIndex++;
renderCurrent();
};
this.up = function() {
if (currentIndex == 0) return;
clearCurrent();
currentIndex--;
renderCurrent();
};
this.clear = function() {
this.update('');
};
}

View file

@ -1,51 +0,0 @@
.autocomplete ol {
display: none;
margin: 0;
padding: 3px;
border: 1px solid gray;
list-style: none;
}
.autocomplete input {
-moz-box-sizing: border-box;
box-sizing: border-box;
margin: 0;
padding: 1px 0 1px 3px;
width: 100%;
border: 1px solid blue;
border-radius: 3px;
-webkit-appearance: none;
font-size: 16px;
}
.autocomplete input:focus {
outline: none;
}
.autocomplete.open input {
border-bottom: none;
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
}
.autocomplete.open ol {
display: block;
background: white;
z-index: 10000;
position: relative;
}
.autocomplete {
width: 400px;
height: 20px;
}
.autocomplete ol .selected {
background: blue;
}

View file

@ -1,103 +0,0 @@
function Autocomplete(options) {
var self = this;
var elem = options.elem;
var input = $('input', elem);
var list;
input.on({
focus: onInputFocus,
blur: onInputBlur,
keydown: onInputKeyDown
});
var inputCheckTimer;
// ------------------
function onInputKeyDown(e) {
var KEY_ARROW_UP = 38;
var KEY_ARROW_RIGHT = 39;
var KEY_ARROW_DOWN = 40;
var KEY_ENTER = 13;
var KEY_ESC = 27;
switch(e.keyCode) {
case KEY_ARROW_UP:
list.up();
return false;
break;
case KEY_ARROW_RIGHT:
if (list.get()) {
self.setValue( list.get(), true );
}
break;
case KEY_ENTER:
self.setValue( list.get() || input.val() );
input.blur();
break;
case KEY_ESC:
list.clear();
break;
case KEY_ARROW_DOWN:
list.down();
return false;
break;
}
}
function initList() {
list = new AutocompleteList(options.provider);
list.render().appendTo(elem);
$(list).on('update', onListUpdate);
}
function onListUpdate(e) {
if (e.values.length) {
elem.addClass('open');
} else {
elem.removeClass('open');
}
}
function onInputFocus() {
var inputValue = input.val();
function checkInput() {
if (inputValue != input.val()) {
if (!list) {
initList();
}
list.update(input.val());
inputValue = input.val();
}
}
inputCheckTimer = setInterval(checkInput, 30);
}
function onInputBlur() {
clearInterval(inputCheckTimer);
if (list) {
list.clear();
}
}
this.setValue = function(value, quiet) {
input.val(value);
if (!quiet) {
$(self).triggerHandler({
type: 'change',
value: value
});
}
}
}

View file

@ -1,20 +0,0 @@
function FilteringListProvider(strings) {
this.get = function(index) {
return strings[index];
};
this.filterByStart = function(stringStart) {
if (stringStart.length < 2) return [];
var stringStartLC = stringStart.toLowerCase();
return strings.filter(function(str) {
var strLC = str.toLowerCase();
return strLC.slice(0, stringStartLC.length) == stringStartLC && strLC != stringStartLC;
});
}
}

View file

@ -1,43 +0,0 @@
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
<link rel="stylesheet" href="autocomplete.css">
<script src="http://code.jquery.com/jquery-latest.js"></script>
<script src="autocomplete.js"></script>
<script src="filtering-list-provider.js"></script>
<script src="autocomplete-list.js"></script>
</head>
<body>
<div class="autocomplete" id="search">
<input type="text" autocomplete="off">
</div>
<div>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Quisquam explicabo nostrum eum placeat aliquid voluptatem nihil modi libero nulla tempore dolore itaque accusamus distinctio veniam quae voluptatibus suscipit provident quas quaerat tempora sequi magnam.</div>
<script>
var dataProvider = new FilteringListProvider([
'Человек',
'Чело',
'Че Гевара',
'Яблоко',
'Ноутбук',
'Но Пасаран!'
]);
var autocomplete = new Autocomplete({
elem: $('#search'),
provider: dataProvider
});
$(autocomplete).on('change', function(e) {
$('#search').next().html(e.value);
});
</script>
</body>
</html>

View file

@ -1,36 +0,0 @@
# Автокомплит
[importance 4]
Создайте `input` с автодополнением из списка.
Список задаётся массивом, например:
```js
var list = [
'Человек',
'Че Гевара',
'Яблоко',
'Ноутбук',
'Но Пасаран!'
];
var autocomplete = new Autocomplete({
elem: $('#search'),
data: list
});
```
Результат:
[iframe src="solution" border=1]
Требования:
<ul>
<li>Автодополнение начинается со 2го символа.</li>
<li>Выпадающий список перекрывает документ, навигация по нему клавишами ↑ и ↓, выбор: → или Enter, скрытие списка Escape.</li>
<li>Событие `change` при нажатии Enter.</li>
</ul>
Поддержка кликов мыши, а также получение списка с сервера не требуются, но могут быть добавлены в будущем.
Исходный документ -- пустой HTML, ну или [поиск Google с автокомплитом](http://google.com) ;)

View file

@ -1,62 +0,0 @@
/**
* Taken from jQuery UI accordion and fixed, special hoverintent event
* http://benalman.com/news/2010/03/jquery-special-events/
*/
!function($) {
var cfg = {
sensitivity: 9,
interval: 50
};
$.event.special.hoverintent = {
setup: function() {
$(this).on("mouseover", handler);
},
teardown: function() {
$(this).on("mouseover", handler);
},
cfg: cfg
};
function handler(event) {
var self = this,
args = arguments,
target = $(event.target),
cX, cY, pX, pY;
function track(event) {
cX = event.pageX;
cY = event.pageY;
};
pX = event.pageX;
pY = event.pageY;
function clear() {
target.off("mousemove", track).off("mouseout", clear);
clearTimeout(timeout);
}
function handler() {
if((Math.abs(pX - cX) + Math.abs(pY - cY)) < cfg.sensitivity) {
clear();
event.type = "hoverintent";
jQuery.event.simulate("hoverintent", event.target, $.event.fix(event), true);
} else {
pX = cX;
pY = cY;
timeout = setTimeout(handler, cfg.interval);
}
}
var timeout = setTimeout(handler, cfg.interval);
target.mousemove(track).mouseout(clear);
return true;
}
}(jQuery);

View file

@ -1,61 +0,0 @@
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
<link rel="stylesheet" href="menu.css">
<script src="http://code.jquery.com/jquery-latest.js"></script>
<script src="hoverintent.js"></script>
<script src="menu.js"></script>
</head>
<body>
<!-- Разрешить вложенные меню -->
<div class="menu" id="menu">
<a href="#" class="title">Моё меню</a>
<ol>
<li class="has-children">
<a href="#a1">Элемент 1</a>
<ol>
<li><a href="#a1.1">Элемент 1.1</a></li>
<li class="has-children">
<a href="#a1.2">Элемент 1.2</a>
<ol>
<li><a href="#a1.2.1">Элемент 1.2.1</a></li>
<li><a href="#a1.2.2">Элемент 1.2.2</a></li>
</ol>
</li>
<li><a href="#a1.3">Элемент 1.3</a></li>
</ol>
</li>
<li><a href="#a2">Элемент 2</a></li>
<li class="has-children">
<a href="#a3">Элемент 3</a>
<ol>
<li><a href="#a3.1">Элемент 3.1</a></li>
<li><a href="#a3.2">Элемент 3.2</a></li>
<li><a href="#a3.3">Элемент 3.3</a></li>
</ol>
</li>
<li><a href="#a4">Элемент 4</a></li>
<li><a href="#a5">Элемент 5</a></li>
</ol>
</div>
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Dolor eos.
<script>
var menu = new Menu({
elem: $('#menu')
});
$(menu).on("select", function(e) {
if (!e.value) return;
alert(e.value);
});
</script>
</body>
</html>

View file

@ -1,54 +0,0 @@
.menu::before {
content: '☞';
font-size: 16px;
float: left;
}
.menu.open::before {
content: '☟';
}
.menu a {
text-decoration: none;
color: black;
display: block;
}
.menu > a {
display: inline;
}
.menu a {
padding: 3px 5px;
white-space: nowrap;
}
li.active > a {
background: blue;
color: white;
}
.menu ol {
margin: 0;
padding: 3px 0;
list-style: none;
display: none;
background: #eee;
border: 1px solid black;
position: absolute;
}
.menu.open > ol {
display: block;
margin-left: 16px;
}
.menu li.has-children > a::after {
content: ' >';
}
.menu li.active > ol {
display: block;
}

View file

@ -1,131 +0,0 @@
function Menu(options) {
var elem = options.elem;
var self = this;
var activeLi;
elem.on('click', 'a.title', onTitleClick);
// item clicks are all inside ol
elem.on('click', 'ol a', onLiClick);
// I use "a" here instead of LI, because hover on LI also works if going over it's child container OL
// child container may have paddings etc, so the click may happen on that OL, outside of this item
elem.on('hoverintent', 'ol a', onLiHoverIntent);
// ----------------------
function onLiClick(e) {
close();
var li = $(e.currentTarget).closest('li');
$(self).triggerHandler({
type: 'select',
value: getLiValue(li)
});
return false;
}
function onTitleClick() {
if (elem.hasClass('open')) {
close();
} else {
open();
}
}
function onLiHoverIntent(e) {
var li = $(this).closest('li');
if (isActiveLi(li)) return;
activateLi(li);
openChildren();
}
// ----------------------
function isActiveLi(li) {
if (activeLi && activeLi[0] == li[0]) return true;
return false;
}
function close(argument) {
elem.removeClass('open');
if (activeLi) {
activeLi.parentsUntil(elem).andSelf().removeClass('active');
activeLi = null;
}
$(document).off('.menu-nested');
}
function getLiValue(li) {
if (!li.length) return null;
return li.children('a').attr('href').slice(2);
}
function open() {
elem.addClass('open');
// TODO: close menu on document click outside of it
$(document).on('click.menu-nested', function(e) {
if ( $(e.target).closest(elem).length ) return;
close();
});
}
function getParentLi(li) {
return li.parent().parent();
}
function activateLi(li) {
if (activeLi && getParentLi(li)[0] != activeLi[0]) { // if (new li is not child of activeLi)
// not a child item, then need to close currently active ones
// collapse parents of last active until container of new element
collapseActiveUntil(li);
}
activeLi = li;
activeLi.addClass("active");
}
function collapseActiveUntil(li) {
var el = activeLi;
for(;;) {
el.removeClass('active');
if (el[0].parentNode == li[0].parentNode) break;
el = getParentLi(el);
}
}
function openChildren() {
var subcontainer = activeLi.children('ol');
if (!subcontainer.length) return;
// show children
var left = activeLi.width(); // to the right of the parent
var top = activeLi.position().top; // at same height as current parent
// lift it up, so that first subchild will be aligned with the parent
top -= subcontainer.children('li').position().top;
top -= subcontainer.prop('clientTop')
subcontainer.css({
left: left,
top: top
})
}
}

View file

@ -1,12 +0,0 @@
# Вложенное меню с выпаданием по клику
[importance 4]
Создайте меню с выпаданием по клику и раскрытием внутренних пунктов при наведении.
Демо:
[iframe src="solution" height=200 border=1]
При выборе -- событие `select` с выбранным значением.
HTML/CSS -- на ваш вкус.

View file

@ -1,2 +0,0 @@
# Практика, практика, практика!

View file

@ -1,21 +0,0 @@
# Что изучать дальше
Если вы прочитали весь учебник и сделали задачи, то на текущий момент вы обладаете важнейшими фундаментальными знаниями JavaScript и квалификацией, чтобы создавать графические компоненты, достойные современного сайта.
Ещё предстоит изучить:
<ul>
<li>Работу с окнами и фреймами.</li>
<li>Регулярные выражения.</li>
<li>JavaScript-фреймворк (jQuery?)</li>
<li>Оптимизацию.</li>
<li>AJAX и COMET.</li>
<li>HTML5, возможности современных браузеров.</li>
</ul>
Кое-что из этого вы можете узнать из дополнительных глав учебника. Многое -- из Open Source.
Обратите внимание на раздел [](/books).
Присоединитесь к какому-нибудь интересному проекту и сделайте что-то хорошее. Разберитесь в том, как оно работает. Пофиксите пару багов.
Успехов вам!

View file

@ -1,104 +0,0 @@
/**
* options:
* yearFrom {number} начальный год в селекторе
* yearTo {number} конечный год в селекторе
* value {Date} текущая выбранная дата
*/
function DateSelector(options) {
var self = this;
var monthNames = 'января февраля марта апреля мая июня июля августа сентября октября ноября декабря'.split(' ');
var value = options.value;
var elem;
var yearSelect, monthSelect, daySelect;
function render() {
var tmpl = _.template( options.template );
elem = $('<div class="date-selector"/>').html(tmpl({
yearFrom: options.yearFrom,
yearTo: options.yearTo,
dayTo: getLastDayOfMonth(value.getFullYear(), value.getMonth()),
monthNames: monthNames
}));
yearSelect = elem.find('.year');
monthSelect = elem.find('.month');
daySelect = elem.find('.day');
elem.on('change', onChange); // обычно оно не всплывает, но jQuery эмулирует всплытие
self.setValue(value, true);
}
this.getValue = function() {
return value;
};
this.setValue = function(newValue, quiet) {
value = newValue;
yearSelect.val(value.getFullYear());
monthSelect.val(value.getMonth());
daySelect.val(value.getDate());
if (!quiet) {
$(self).triggerHandler({
type: "select",
value: value
});
}
};
this.getElement = function() {
if (!elem) render();
return elem;
};
function onChange(e) {
var selectType = e.target.className;
if (selectType == "month" || selectType == "year") {
// поправить день с учетом месяца и, возможно, високосного года
adjustDayOptions(+yearSelect.val(), +monthSelect.val());
}
readValue(); // для простоты -- получим значения из всех селектов
$(self).triggerHandler({
type: "select",
value: value
});
}
function readValue() {
// если я сделаю сначала value.setMonth(),
// то может получится некорректная дата типа 31 марта -> 31 февраля,
// которая автоскорректируется в 2 марта, т.е месяц не поставится.
// поэтому сначала именно setDate, и так далее.
value.setDate(daySelect.val());
value.setMonth(monthSelect.val());
value.setFullYear(yearSelect.val());
}
function getLastDayOfMonth(year, month) {
var date = new Date(year, month+1, 0);
return date.getDate();
}
function adjustDayOptions(year, month) {
var maxDay = getLastDayOfMonth(year, month);
// укоротить селект, если дней стало меньше
daySelect.children().filter(function() {
return this.value > maxDay;
}).remove();
// добавить дни, если новый месяц дольше
for(var i = +daySelect.last().val(); i <= maxDay; i++) {
daySelect.append(new Option(i, i));
}
}
}

View file

@ -1,51 +0,0 @@
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
<script src="http://code.jquery.com/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/3.2.0/lodash.js"></script>
<script src="dateselector.js"></script>
</head>
<body>
<script type="text/template" id="date-selector-template">
<select class="year">
<% for(var year = yearFrom; year <= yearTo; year++) { %>
<option value="<%=year%>"><%=year%></option>
<% } %>
</select>
<select class="month">
<% for(var month = 0; month <= 11; month++) { %>
<option value="<%=month%>"><%=monthNames[month]%></option>
<% } %>
</select>
<select class="day">
<% for(var day = 1; day <= dayTo; day++) { %>
<option value="<%=day%>"><%=day%></option>
<% } %>
</select>
</script>
<div id="selector-holder"></div>
<div id="value">тут будет значение даты при выборе</div>
<script>
var dateSelector = new DateSelector({
yearFrom: 2010,
yearTo: 2020,
value: new Date(2012, 2, 31), // 31 марта 2012
template: $('#date-selector-template').html().trim()
});
$('#selector-holder').append(dateSelector.getElement());
$(dateSelector).on("select", function(e) {
$('#value').html( e.value + '' );
});
</script>
<button onclick="dateSelector.setValue(new Date)">setValue(сегодня)</button>
</body>
</html>

View file

@ -1,12 +0,0 @@
/**
* options:
* yearFrom {number} начальный год в селекторе
* yearTo {number} конечный год в селекторе
* value {Date} текущая выбранная дата
*/
function DateSelector(options) {
var self = this;
/* ваш код */
}

View file

@ -1,37 +0,0 @@
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
<script src="http://code.jquery.com/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/3.2.0/lodash.js"></script>
<script src="dateselector.js"></script>
</head>
<body>
<script type="text/template" id="date-selector-template">
/* шаблон для виджета */
</script>
<div id="selector-holder"></div>
<div id="value">тут будет значение даты при выборе</div>
<script>
var dateSelector = new DateSelector({
yearFrom: 2010,
yearTo: 2020,
value: new Date(2012, 2, 31), // 31 марта 2012
template: $('#date-selector-template').html()
});
$('#selector-holder').append(dateSelector.getElement());
$(dateSelector).on("select", function(e) {
$('#value').html( e.value + '' );
});
</script>
<button onclick="dateSelector.setValue(new Date)">setValue(сегодня)</button>
</body>
</html>

View file

@ -1,46 +0,0 @@
# Селектор даты
[importance 5]
Создайте тройной селектор даты, который выступает как единый компонент. То есть, можно подписываться на `"select"` сразу у этого компонента.
Конструктор:
```js
var dateSelector = new DateSelector({
yearFrom: 2010, // начальный год в селекторе
yearTo: 2020, // конечный год в селекторе
value: new Date(2012,2,31) // текущая выбранная дата
});
```
События:
<ul>
<li>`select` -- при изменении даты.</li>
</ul>
Методы:
<ul>
<li>`setValue(date, quiet)` -- устанавливает дату `date`. Если второй аргумент `true`, то событие не генерируется.</li>
<li>`getElement()` -- возвращает DOM-элемент для компоненты для вставки в документ.</li>
</ul>
Использование - добавление в документ:
```js
dateSelector.getElement().appendTo('body');
```
Использование - подписка на изменение и вывод значения:
```js
$(dateSelector).on("select", function(e) {
$('#value').html(e.value);
});
```
Пример в действии:
[iframe border=1 src="solution"]
[edit task src="source"/]
P.S. При выборе месяца дни должны подстраиваться под него. Чтобы не было доступно 31 февраля.

View file

@ -1,45 +0,0 @@
<!DOCTYPE HTML>
<html>
<head>
<meta charset="UTF-8">
<script src="http://code.jquery.com/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/3.2.0/lodash.js"></script>
<link type="text/css" rel="stylesheet" href="window.css" />
<style>
html, body {
padding: 0;
margin: 0;
}
</style>
<script src="draggableWindow.js"></script>
</head>
<body style="height:2000px">
Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст
Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст
Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст
Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст
Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст
Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст
<script type="text/template" id="draggable-window-template">
<div class="window-title"><%=title%></div>
<div class="window-content"></div>
<form class="window-message-form">
<input type="text" name="message" class="window-message-text"><input type="submit" name="submit" class="window-message-submit" value="Послать">
</form>
</script>
<script>
var win = new DraggableWindow({
title: "Чат с Петей",
template: $('#draggable-window-template').html()
});
win.getElement().appendTo('body');
</script>
</body>
</html>

View file

@ -1,10 +0,0 @@
# Подсказки
<ul>
<li>Так как высота и ширина окна известны, вёрстка внутри может содержать точные пиксельные размеры.</li>
<li>При обработке события `document.onmousemove`, мы вычисляем новые координаты `left/top` и смотрим, вылезает ли окно за границы. Если да -- меняем `left/top` на максимально возможные, чтобы не вылезало.</li>
<li>На форме вешаем обработчик `onsubmit`, т.к. иначе Enter в поле отправить её на сервер.</li>
</ul>
# Решение

View file

@ -1,126 +0,0 @@
// если начать перенос и передвинуть мышь максимально вверх -- верхний край окна не должен вылетать за край страницы.
// в другие стороны
// чтобы даже при резких движениях мыши -- окно прилипало к краю но не вылезало за край
// исключение -- максимальная нижняя позиция окна -- это когда только заголовок виден
// позиция мыши смещается
function DraggableWindow(options) {
var self = this;
var title = options.title;
var template = typeof options.template == 'function' ? // компиляция, если строка
options.template : _.template(options.template);
var elem, contentElem;
var mouseDownShift;
function render() {
elem = $('<div/>', {
"class": "window",
html: template({
title: title
})
});
$('form', elem).on('submit', onSubmit);
titleElem = elem.find('.window-title');
titleElem.on('selectstart dragstart', false);
titleElem.on('mousedown', onTitleMouseDown);
contentElem = elem.find('.window-content'); // = children[1]
}
this.getElement = function() {
if (!elem) render();
return elem;
}
function onTitleMouseDown(e) {
startDrag(e.pageX, e.pageY);
return false;
};
function startDrag(mouseDownX, mouseDownY) {
// запомнить координаты нажатия
var coords = elem.offset();
mouseDownShift = {
x: mouseDownX - coords.left,
y: mouseDownY - coords.top
};
// двигать
$(document).on({
'mousemove.draggablewindow': onDocumentMouseMove,
'mouseup.draggablewindow': onDocumentMouseUp
});
elem.addClass('window-moving');
}
function onDocumentMouseUp() {
$(document).off('.draggablewindow');
elem.removeClass('window-moving');
}
function onDocumentMouseMove(e) {
moveWindowAtCursor(e.pageX, e.pageY);
}
function moveWindowAtCursor(pageX, pageY) {
var newLeft = pageX - mouseDownShift.x;
var newTop = pageY - mouseDownShift.y;
// проверим, не вылезем ли мы за границы экрана
var windowLeft = window.pageXOffset || document.documentElement.scrollLeft;
var windowTop = window.pageYOffset || document.documentElement.scrollTop;
var windowRight = windowLeft + document.documentElement.clientWidth;
var windowBottom = windowTop + document.documentElement.clientHeight;
// тут не if..else, а if т.к., возможно, обе координаты надо поправить
if (newLeft < windowLeft) {
newLeft = windowLeft;
}
if (newLeft > windowRight - elem.outerWidth()) {
newLeft = windowRight - elem.outerWidth(); // внешняя ширина с учетом рамки внешней
}
if (newTop < windowTop) {
newTop = windowTop;
}
if (newTop > windowBottom - titleElem.outerHeight()) {
newTop = windowBottom - titleElem.outerHeight();
// внизу заголовок будет немного залезать за край, т.к. нужно мерять вместе с рамкой верхней на самом деле
// но здесь это не страшно
}
elem.css({
left: newLeft,
top: newTop
});
};
function onSubmit(e) {
// принять события, совершить обработку события, связанную с формой
var form = e.currentTarget;
var value = form.elements.message.value;
form.elements.message.value = '';
if (value) {
submit(value); // логика по отправке сообщения
}
return false;
}
function submit(message) {
// добавить
var newMessageElem = $('<div>', {text: message }).appendTo(contentElem);
// прокрутить к новому сообщению
contentElem.prop('scrollTop', 999999999);
}
}

View file

@ -1,45 +0,0 @@
<!DOCTYPE HTML>
<html>
<head>
<meta charset="UTF-8">
<script src="http://code.jquery.com/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/3.2.0/lodash.js"></script>
<link type="text/css" rel="stylesheet" href="window.css" />
<style>
html, body {
padding: 0;
margin: 0;
}
</style>
<script src="draggableWindow.js"></script>
</head>
<body style="height:2000px">
Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст
Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст
Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст
Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст
Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст
Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст
<script type="text/template" id="draggable-window-template">
<div class="window-title"><%=title%></div>
<div class="window-content"></div>
<form class="window-message-form">
<input type="text" name="message" class="window-message-text"><input type="submit" name="submit" class="window-message-submit" value="Послать">
</form>
</script>
<script>
var win = new DraggableWindow({
title: "Чат с Петей",
template: $('#draggable-window-template').html()
});
win.getElement().appendTo('body');
</script>
</body>
</html>

View file

@ -1,56 +0,0 @@
.window {
position: absolute;
left: 0;
top: 0;
z-index: 10;
width: 200px;
height: 300px;
border: 2px #ADD8E6 groove;
/* раскомментировать для проверки, вылезает ли внешняя рамка за экран
border: 2px red solid;
*/
background: white;
}
.window-title {
width: 100%;
height: 30px;
line-height: 30px;
background-color: #4169E1;
color: white;
text-align:center;
cursor: pointer;
}
.window-moving .window-title {
cursor: move;
}
.window-message-form {
position: absolute;
bottom: 0;
height: 30px;
line-height: 30px;
width: 100%;
background: #ADD8E6;
}
.window-content {
height: 240px;
padding: 3px;
overflow-y: auto;
}
.window-message-text {
width: 120px;
}
.window-message-submit {
width: 70px;
margin-left: 2px;
}

View file

@ -1,55 +0,0 @@
<!DOCTYPE HTML>
<html>
<head>
<meta charset="UTF-8">
<script src="http://code.jquery.com/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/3.2.0/lodash.js"></script>
<style>
html, body {
padding: 0;
margin: 0;
}
/* ваши стили */
</style>
</head>
<body>
Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст
Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст
Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст
Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст
Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст
Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст
<script type="text/template" id="draggable-window-template">
/* шаблон для окна */
</script>
<script>
// если начать перенос и передвинуть мышь максимально вверх -- верхний край окна не должен вылетать за край страницы.
// в другие стороны
// чтобы даже при резких движениях мыши -- окно прилипало к краю но не вылезало за край
// исключение -- максимальная нижняя позиция окна -- это когда только заголовок виден
// позиция мыши смещается
function DraggableWindow(options) {
var self = this;
/* ваш код */
}
var win = new DraggableWindow({
title: "Чат с Петей",
template: $('#draggable-window-template').html()
});
win.getElement().appendTo('body');
</script>
</body>
</html>

View file

@ -1,25 +0,0 @@
# Переносимые окна
[importance 5]
Создайте виджет окна для чата.
Окно -- это `DIV`, который можно переносить, взявшись за заголовок. По нажатию на "Послать" данные передаются в содержание окна.
[demo src="index.html"/]
Синтаксис:
```js
new DraggableWindow({
title: "Чат с Петей",
template: HTML-шаблон окна
});
```
<ul>
<li>Возможно появление прокрутки внутри окна, если сообщений много. Сообщения не должны вылезать вовне окна.</li>
<li>Окно нельзя вытащить за пределы экрана, даже резкими движениями мыши. Влево-вправо-вверх оно вообще не должно вылезать за границу, а вниз -- только до заголовка. Попробуйте перемещать его в демо, чтобы увидеть.</li>
<li>Для задания DOM-структуры окна используйте шаблон. Может быть создано несколько окон.</li>
</ul>

View file

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

View file

@ -1,142 +0,0 @@
// если начать перенос и передвинуть мышь максимально вверх -- верхний край окна не должен вылетать за край страницы.
// в другие стороны
// чтобы даже при резких движениях мыши -- окно прилипало к краю но не вылезало за край
// исключение -- максимальная нижняя позиция окна -- это когда только заголовок виден
// позиция мыши смещается
function DraggableWindow(options) {
var self = this;
var title = options.title;
var template = typeof options.template == 'function' ? // компиляция, если строка
options.template : _.template(options.template);
var elem, contentElem;
var mouseDownShift;
function render() {
elem = $('<div tabindex="0" class="window"/>').html(template({
title: title
}));
$('form', elem).on('submit', onSubmit);
titleElem = elem.find('.window-title');
titleElem.on('selectstart dragstart', false);
titleElem.on('mousedown', onTitleMouseDown);
contentElem = elem.find('.window-content'); // = children[1]
elem.on('focusin', onFocus);
}
this.getElement = function() {
if (!elem) render();
return elem;
}
function onFocus() {
$(self).triggerHandler({ type: 'focus' });
}
function onTitleMouseDown(e) {
startDrag(e.pageX, e.pageY);
setTimeout(onFocus, 0);
return false; // returning false prevents onfocus
};
function startDrag(mouseDownX, mouseDownY) {
// запомнить координаты нажатия
var coords = elem.offset();
mouseDownShift = {
x: mouseDownX - coords.left,
y: mouseDownY - coords.top
};
// двигать
$(document).on({
'mousemove.draggablewindow': onDocumentMouseMove,
'mouseup.draggablewindow': onDocumentMouseUp
});
elem.addClass('window-moving');
}
function onDocumentMouseUp() {
$(document).off('.draggablewindow');
elem.removeClass('window-moving');
}
function onDocumentMouseMove(e) {
moveWindowAtCursor(e.pageX, e.pageY);
}
function moveWindowAtCursor(pageX, pageY) {
var newLeft = pageX - mouseDownShift.x;
var newTop = pageY - mouseDownShift.y;
// проверим, не вылезем ли мы за границы экрана
var windowLeft = window.pageXOffset || document.documentElement.scrollLeft;
var windowTop = window.pageYOffset || document.documentElement.scrollTop;
var windowRight = windowLeft + document.documentElement.clientWidth;
var windowBottom = windowTop + document.documentElement.clientHeight;
// тут не if..else, а if т.к., возможно, обе координаты надо поправить
if (newLeft < windowLeft) {
newLeft = windowLeft;
}
if (newLeft > windowRight - elem.outerWidth()) {
newLeft = windowRight - elem.outerWidth(); // внешняя ширина с учетом рамки внешней
}
if (newTop < windowTop) {
newTop = windowTop;
}
if (newTop > windowBottom - titleElem.outerHeight()) {
newTop = windowBottom - titleElem.outerHeight();
// внизу заголовок будет немного залезать за край, т.к. нужно мерять вместе с рамкой верхней на самом деле
// но здесь это не страшно
}
elem.css({
left: newLeft,
top: newTop
});
};
function onSubmit(e) {
// принять события, совершить обработку события, связанную с формой
var form = e.currentTarget;
var value = form.elements.message.value;
form.elements.message.value = '';
if (value) {
submit(value); // логика по отправке сообщения
}
return false;
}
function submit(message) {
// добавить
var newMessageElem = $('<div>', {text: message }).appendTo(contentElem);
// прокрутить к новому сообщению
contentElem.prop('scrollTop', 999999999);
}
this.setZIndex = function(zIndex) {
elem.css('z-index', zIndex);
};
this.getZIndex = function() {
return +elem.css('z-index') || 0;
};
this.toString = function() {
return "[Window " + options.title + " " + elem.css('z-index') + "]";
}
}

View file

@ -1,50 +0,0 @@
<!DOCTYPE HTML>
<html>
<head>
<meta charset="UTF-8">
<script src="http://code.jquery.com/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/3.2.0/lodash.js"></script>
<link type="text/css" rel="stylesheet" href="window.css" />
<style>
html, body {
padding: 0;
margin: 0;
}
</style>
<script src="draggableWindow.js"></script>
<script src="windowManager.js"></script>
</head>
<body>
Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст
Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст
Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст
Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст
Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст
Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст Текст
<script type="text/template" id="draggable-window-template">
<div class="window-title"><%=title%></div>
<div class="window-content"></div>
<form class="window-message-form">
<input type="text" name="message" class="window-message-text"><input type="submit" name="submit" class="window-message-submit" value="Послать">
</form>
</script>
<script>
WindowManager.setTemplate( $('#draggable-window-template').html().trim() );
WindowManager.addWindow({
title: "Чат с Петей"
});
WindowManager.addWindow({
title: "Чат с Машей"
});
</script>
</body>
</html>

View file

@ -1,56 +0,0 @@
.window {
position: absolute;
left: 0;
top: 0;
z-index: 10;
width: 200px;
height: 300px;
border: 2px #ADD8E6 groove;
/* раскомментировать для проверки, вылезает ли внешняя рамка за экран
border: 2px red solid;
*/
background: white;
}
.window-title {
width: 100%;
height: 30px;
line-height: 30px;
background-color: #4169E1;
color: white;
text-align:center;
cursor: pointer;
}
.window-moving .window-title {
cursor: move;
}
.window-message-form {
position: absolute;
bottom: 0;
height: 30px;
line-height: 30px;
width: 100%;
background: #ADD8E6;
}
.window-content {
height: 240px;
padding: 3px;
overflow-y: auto;
}
.window-message-text {
width: 120px;
}
.window-message-submit {
width: 70px;
margin-left: 2px;
}

View file

@ -1,52 +0,0 @@
var WindowManager = new function() {
var windows = [];
var activeWindow;
var template;
this.setTemplate = function(newTemplate) {
template = _.template(newTemplate);
};
this.addWindow = function(options) {
var win = new DraggableWindow({
title: options.title,
template: template
});
$(win).on("focus", function() {
activateWindow(this);
});
win.getElement().appendTo('body');
windows.push(win);
activateWindow(win);
return win;
}
function activateWindow(win) {
if (activeWindow == win) return;
activeWindow = win;
sortWindows();
}
/**
* пересортировать окна
*/
function sortWindows() {
windows.sort(function(a, b) {
if (activeWindow == a) return 1; // активное окно больше всех
if (activeWindow == b) return -1; // активное окно больше всех
return a.getZIndex() - b.getZIndex(); // порядок среди остальных - оставляем "как есть"
});
for(var i=0; i<windows.length; i++) {
windows[i].setZIndex(i+1);
}
}
}

View file

@ -1,35 +0,0 @@
# Менеджер окон
[importance 5]
Эта задача является продолжением задачи [](/task/draggable-windows).
Предусмотрите возможность создавать несколько окон. При этом то, которое получает фокус, становится выше всех.
Общее управление окнами должен осуществлять объект `WindowManager`.
Синтаксис:
```js
WindowManager.setTemplate(html-шаблон для окон);
WindowManager.addWindow({ // создаёт окно по шаблону с заголовком
title: "Чат с Петей" // и показывает его
});
WindowManager.addWindow({ // ещё одно окно
title: "Чат с Машей"
});
```
Демо в ифрейме:
[iframe src="solution" height=300 border=1]
<ul>
<li>Окна перекрывают друг-друга по `z-index`. Максимальный `z-index` должен быть ограничен: от 1 до количества окон.</li>
<li>Информация от окна к менеджеру передаётся через события.</li>
<li>Инкапсуляция: `WindowManager` использует объект `DraggableWindow`, но не лезет в DOM окна. Обработчики -- только на объект.</li>
</ul>
В качестве исходного документа можно взять решение [предыдущей задачи](/task/draggable-windows). Скорее всего, класс `DraggableWindow` понадобиться немного изменить, добавить события.

View file

@ -1,3 +0,0 @@
В решении присутствуют файлы из [задачи про календарь](/task/calendar), т.к. он используется в качестве компонента.

View file

@ -1,28 +0,0 @@
.calendar-table {
border-collapse: collapse;
}
.calendar-table td, .calendar-table th {
border: 1px solid black;
padding: 3px;
text-align: center;
}
.calendar-table th {
font-weight: bold;
background-color: #E6E6E6;
}
.date-cell:hover {
background: #eee;
cursor: pointer;
}
.date-cell.selected {
background: #0F0;
}
.calendar-table caption {
text-align: center;
}

View file

@ -1,158 +0,0 @@
/**
* options:
* value Date или объект {year,month,day}} -- дата, для которой показывать календарь
* если в объекте указаны только {year,month}, то день не выбран
*/
function Calendar(options) {
var self = this;
var monthNames = 'Январь Февраль Март Апрель Май Июнь Июль Август Сентябрь Октябрь Ноябрь Декабрь'.split(' ');
var elem;
var year, month, day;
var showYear, showMonth;
parseValue(options.value);
// -----------------------
this.getElement = function() {
if (!elem) {
render();
}
return elem;
};
this.getValue = function() {
return day ? new Date(year, month, day) : null;
};
function parseValue(value) {
if (value instanceof Date) {
year = value.getFullYear();
month = value.getMonth();
day = value.getDate();
} else {
year = value.year;
month = value.month;
day = value.day;
}
}
/**
* Установить значение календаря
* @param newValue {Date или объект {year,month[,day]} Новое значение
* @param quiet Если true, то событие не генерируется
*/
this.setValue = function(newValue, quiet) {
parseValue(newValue);
if (elem) {
clearSelected();
render();
}
if (!quiet) {
$(self).triggerHandler({
type: "select",
value: new Date(year, month, day)
});
}
};
this.clearValue = function() {
day = null;
clearSelected();
};
function render() {
if (!elem) {
elem = $('<div class="calendar"/>')
.on('click', '.date-cell', onDateCellClick);
}
if (showYear != year || showMonth != month) {
elem.html(renderCalendarTable(year, month));
elem.find('caption').html(monthNames[month]+' '+year);
showYear = year;
showMonth = month;
}
if (day) {
var num = getCellByDate(new Date(year, month, day));
elem.find('td').eq(num).addClass("selected");
}
}
function clearSelected() {
elem.find('.selected').removeClass('selected');
}
function onDateCellClick(e) {
day = $(e.target).html();
self.setValue({year: year, month: month, day: day});
}
/**
* Возвращает по дате номер TD в таблице
* Использование:
* var td = table.getElementsByTagName('td')[getCellByDate(date)]
*/
function getCellByDate(date) {
var dateDayOne = new Date(date.getFullYear(), date.getMonth(), 1);
return getDay(dateDayOne) + date.getDate() - 1;
}
/**
* получить номер дня недели для date, от 0(пн) до 6(вс)
* @param date
*/
function getDay(date) { //
var day = date.getDay();
if (day == 0) day = 7;
return day - 1;
}
/**
* Генерирует таблицу для календаря заданного месяца/года
*/
function renderCalendarTable(year, month) {
var d = new Date(year, month);
var table = ['<table class="calendar-table"><caption></caption><tr><th>пн</th><th>вт</th><th>ср</th><th>чт</th><th>пт</th><th>сб</th><th>вс</th></tr><tr>'];
for (var i=0; i<getDay(d); i++) {
table.push('<td></td>');
}
// ячейки календаря с датами
while(d.getMonth() == month) {
table.push('<td class="date-cell">'+d.getDate()+'</td>');
if (getDay(d) % 7 == 6) { // вс, последний день - перевод строки
table.push('</tr><tr>');
}
d.setDate(d.getDate()+1);
}
// добить таблицу пустыми ячейками, если нужно
if (getDay(d) != 0) {
for (var i=getDay(d); i<7; i++) {
table.push('<td></td>');
}
}
table.push('</tr></table>');
return table.join('\n')
}
}

View file

@ -1,42 +0,0 @@
.datepicker {
border: 1px solid black;
width: 350px;
}
.datepicker .body {
clear: both;
background: red;
}
.datepicker .prev-link {
float: left;
cursor: pointer;
}
.datepicker .next-link {
float: right;
cursor: pointer;
}
.datepicker .prev-link:hover, .datepicker .next-link:hover {
color: red;
}
.datepicker .header {
padding: 5px;
font-size: 120%;
}
.datepicker .calendar-left-holder {
float: left;
}
.datepicker .calendar-right-holder {
float: right;
}
.datepicker .title {
text-align: center;
}

View file

@ -1,130 +0,0 @@
/**
* options:
* value Date или объект {year,month,day}} -- дата, для которой показывать календарь
* если в объекте указаны только {year,month}, то день не выбран
*/
function DatePicker(options) {
var elem;
var calendarLeft;
var calendarRight;
var calendarCache = {};
var showYear, showMonth;
var selectedYear, selectedMonth, selectedDay;
var self = this;
parseValue(options.value);
showYear = selectedYear;
showMonth = selectedMonth;
// -------------
this.getElement = function() {
if (!elem) render();
return elem;
};
this.getValue = function() {
if (!selectedDay) return null;
return new Date(selectedYear, selectedMonth, selectedDay);
};
function parseValue(value) {
if (value instanceof Date) {
selectedYear = value.getFullYear();
selectedMonth = value.getMonth();
selectedDay = value.getDate();
} else {
selectedYear = value.year;
selectedMonth = value.month;
selectedDay = value.day;
}
}
function getCalendar(year, month) {
var key = '' + year + month;
var calendar;
if (!calendarCache[key]) {
calendar = calendarCache[key] = new Calendar({
value: {year: year, month: month}
});
$(calendar).on("select", onCalendarSelect);
} else {
calendar = calendarCache[key];
}
var value = {
year: year,
month: month,
// если календарь отображает текущий выбранный месяц, то передать и день для выбора
day: (year == selectedYear && month == selectedMonth) ? selectedDay : null
};
calendar.setValue(value, true);
return calendar;
}
function onCalendarSelect(e) {
parseValue(e.value);
if (e.target == calendarLeft) {
calendarRight.clearValue();
} else {
calendarLeft.clearValue();
}
$(self).triggerHandler({
type: "select",
value: e.target.getValue()
});
}
function render() {
elem = $('<div class="datepicker"/>').html(
_.template(options.template)()
);
elem.on('mousedown', '.next-link', onNextLinkMouseDown);
elem.on('mousedown', '.prev-link', onPrevLinkMouseDown);
renderCalendars();
}
function renderCalendars() {
calendarLeft = getCalendar(showYear, showMonth);
// использую children().detach() вместо empty(),
// т.к. empty() убивает обработчики на календаре
elem.find('.calendar-left-holder').children().detach().end()
.append(calendarLeft.getElement());
var dateNextMonth = new Date(showYear, showMonth + 1);
calendarRight = getCalendar(dateNextMonth.getFullYear(), dateNextMonth.getMonth());
elem.find('.calendar-right-holder').children().detach().end()
.append(calendarRight.getElement());
}
function onNextLinkMouseDown() {
showMonth++;
if (showMonth > 11) {
showMonth = 0;
showYear++;
}
renderCalendars();
return false;
}
function onPrevLinkMouseDown() {
showMonth--;
if (showMonth < 0) {
showMonth = 11;
showYear--;
}
renderCalendars();
return false;
}
}

View file

@ -1,49 +0,0 @@
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
<link type="text/css" rel="stylesheet" href="calendar.css">
<link type="text/css" rel="stylesheet" href="datepicker.css">
<script src="http://code.jquery.com/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/3.2.0/lodash.js"></script>
<script src="calendar.js"></script>
<script src="datepicker.js"></script>
</head>
<body>
<div id="value">тут будет выбранное значение (дата)</div>
<script type="text/template" id="template-datepicker">
<div class="header">
<div class="prev-link">&laquo;</div>
<div class="next-link">&raquo;</div>
<div class="title">Выберите дату</div>
</div>
<div class="body">
<div class="calendar-left-holder prev-month">
<!-- left calendar -->
</div>
<div class="calendar-right-holder next-month">
<!-- right calendar -->
</div>
</div>
</script>
<script>
var datePicker = new DatePicker({
template: $('#template-datepicker').html(),
value: {year: 2012, month: 2 }
});
datePicker.getElement().appendTo('body');
$(datePicker).on("select", function(e) {
$("#value").html( e.value + '' );
});
</script>
</body>
</html>

View file

@ -1,28 +0,0 @@
.calendar-table {
border-collapse: collapse;
}
.calendar-table td, .calendar-table th {
border: 1px solid black;
padding: 3px;
text-align: center;
}
.calendar-table th {
font-weight: bold;
background-color: #E6E6E6;
}
.date-cell:hover {
background: #eee;
cursor: pointer;
}
.date-cell.selected {
background: #0F0;
}
.calendar-table caption {
text-align: center;
}

View file

@ -1,158 +0,0 @@
/**
* options:
* value Date или объект {year,month,day}} -- дата, для которой показывать календарь
* если в объекте указаны только {year,month}, то день не выбран
*/
function Calendar(options) {
var self = this;
var monthNames = 'Январь Февраль Март Апрель Май Июнь Июль Август Сентябрь Октябрь Ноябрь Декабрь'.split(' ');
var elem;
var year, month, day;
var showYear, showMonth;
parseValue(options.value);
// -----------------------
this.getElement = function() {
if (!elem) {
render();
}
return elem;
};
this.getValue = function() {
return day ? new Date(year, month, day) : null;
};
function parseValue(value) {
if (value instanceof Date) {
year = value.getFullYear();
month = value.getMonth();
day = value.getDate();
} else {
year = value.year;
month = value.month;
day = value.day;
}
}
/**
* Установить значение календаря
* @param newValue {Date или объект {year,month[,day]} Новое значение
* @param quiet Если true, то событие не генерируется
*/
this.setValue = function(newValue, quiet) {
parseValue(newValue);
if (elem) {
clearSelected();
render();
}
if (!quiet) {
$(self).triggerHandler({
type: "select",
value: new Date(year, month, day)
});
}
};
this.clearValue = function() {
day = null;
clearSelected();
};
function render() {
if (!elem) {
elem = $('<div class="calendar"/>')
.on('click', '.date-cell', onDateCellClick);
}
if (showYear != year || showMonth != month) {
elem.html(renderCalendarTable(year, month));
elem.find('caption').html(monthNames[month]+' '+year);
showYear = year;
showMonth = month;
}
if (day) {
var num = getCellByDate(new Date(year, month, day));
elem.find('td').eq(num).addClass("selected");
}
}
function clearSelected() {
elem.find('.selected').removeClass('selected');
}
function onDateCellClick(e) {
day = $(e.target).html();
self.setValue({year: year, month: month, day: day});
}
/**
* Возвращает по дате номер TD в таблице
* Использование:
* var td = table.getElementsByTagName('td')[getCellByDate(date)]
*/
function getCellByDate(date) {
var dateDayOne = new Date(date.getFullYear(), date.getMonth(), 1);
return getDay(dateDayOne) + date.getDate() - 1;
}
/**
* получить номер дня недели для date, от 0(пн) до 6(вс)
* @param date
*/
function getDay(date) { //
var day = date.getDay();
if (day == 0) day = 7;
return day - 1;
}
/**
* Генерирует таблицу для календаря заданного месяца/года
*/
function renderCalendarTable(year, month) {
var d = new Date(year, month);
var table = ['<table class="calendar-table"><caption></caption><tr><th>пн</th><th>вт</th><th>ср</th><th>чт</th><th>пт</th><th>сб</th><th>вс</th></tr><tr>'];
for (var i=0; i<getDay(d); i++) {
table.push('<td></td>');
}
// ячейки календаря с датами
while(d.getMonth() == month) {
table.push('<td class="date-cell">'+d.getDate()+'</td>');
if (getDay(d) % 7 == 6) { // вс, последний день - перевод строки
table.push('</tr><tr>');
}
d.setDate(d.getDate()+1);
}
// добить таблицу пустыми ячейками, если нужно
if (getDay(d) != 0) {
for (var i=getDay(d); i<7; i++) {
table.push('<td></td>');
}
}
table.push('</tr></table>');
return table.join('\n')
}
}

View file

@ -1,8 +0,0 @@
/**
* options:
* value Date или объект {year,month,day}} -- дата, для которой показывать календарь
* если в объекте указаны только {year,month}, то день не выбран
*/
function DatePicker(options) {
/* ваш код */
}

View file

@ -1,37 +0,0 @@
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
<link type="text/css" rel="stylesheet" href="calendar.css">
<link type="text/css" rel="stylesheet" href="datepicker.css">
<script src="http://code.jquery.com/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/3.2.0/lodash.js"></script>
<script src="calendar.js"></script>
<script src="datepicker.js"></script>
</head>
<body>
<div id="value">тут будет выбранное значение (дата)</div>
<script type="text/template" id="template-datepicker">
/* ваш шаблон */
</script>
<script>
var datePicker = new DatePicker({
template: $('#template-datepicker').html(),
value: {year: 2012, month: 2 }
});
datePicker.getElement().appendTo('body');
$(datePicker).on("select", function(e) {
$("#value").html( e.value + '' );
});
</script>
</body>
</html>

View file

@ -1,47 +0,0 @@
# Двойной календарь со стрелками
[importance 5]
Создайте календарь, который показывает два месяца сразу. Стрелки позволяют менять текущий месяц.
Конструктор:
```js
var datePicker = new DatePicker({
template: HTML-шаблон,
value: Date или объект {year, month, day}
});
```
Если значение `value` передано в виде объекта `{year, month}`, т.е. без `day`, то дата не выбрана.
События:
<ul>
<li>`select` -- при изменении даты.</li>
</ul>
Методы:
<ul>
<li>`setValue(date, quiet)` -- устанавливает значение даты, формат -- как в конструкторе. Если второй аргумент `true`, то событие не генерируется.</li>
<li>`getElement()` -- возвращает DOM-элемент для компоненты для вставки в документ. При первом вызове создаёт DOM.</li>
</ul>
Использование -- добавление в документ:
```js
$('body').append(datePicker.getElement());
```
Использование - подписка на изменение и вывод значения:
```js
$(datePicker).on("select", function(e) {
$('#value').html( e.value + '' );
});
```
Пример в действии:
[iframe border=1 src="solution"]
**В решении используйте готовый компонент -- календарь из задачи [](/task/calendar).**

View file

@ -1,75 +0,0 @@
# Получение данных
Данные регионов можно хранить в разном виде.
Наиболее естественное представление дерева -- в виде вложенного объекта: свойство `children` содержит поддеревья.
Выглядит это так:
```js
var regions = [
{
title: 'Россия',
id: 1,
children: [
{
title: 'Центр',
id: 2,
children: [ ... поддеревья ... ]
},
...
}
}
...
]
```
У такого вложенного объекта есть важный недостаток: сложно перейти напрямую к узлу по ID. Нужно "прыгать" по дереву.
Поэтому может быть более удобен другой вариант:
```js
var regions = [
{
title: 'Россия',
id: 1,
children: [ 2 ]
},
{
title: 'Центр',
id: 2,
children: [ ... ]
},
...
]
```
..То есть, массив содержит все узлы дерева, и каждый узел хранит в `children` список `id` детей.
Но и это не совсем удобно. Ведь хочется по ID получить данные. Значит, нужно хранить не массив, а объект вида `id => { title: .., id: .., children: [... ] }`.
<ul>
<li>Свойство `children` может отсутствовать, если детей нет.</li>
<li>`id` есть и в ключе объекта и в данных узла. Так удобнее.</li>
<li>Список внешних узлов дерева содержится в корневом узле без имени, с `id = 0`.</li>
</ul>
**Выберите наиболее симпатичную структуру и получите её из исходного документа.**
# Данные
Скрипт для получения данных в последнем формате, описанном выше: [fetch.js](/files/tutorial/widgets/checkbox-tree/fetch.js).
Результат в файле (после `JSON.stringify`): [regions.js](/files/tutorial/widgets/checkbox-tree/regions.js).
# Исправления
Желательно сделать следующие исправления:
<ul>
<li>Переделать вёрстку. В частности, заменить текстовые + - на оформление при помощи CSS.</li>
<li>Оформить всё в виде виджета с шаблоном.</li>
<li>Использовать ленивый рендеринг! Не рисовать всё дерево сразу (зачем рисовать то, чего не видно?), а дорисовывать при открытии.</li>
# Решение

View file

@ -1,23 +0,0 @@
var result = {
0: { children: [] }
};
$('div[id^="region"]').each(function() {
el = $(this);
var id = el.attr('id').slice(6);
result[id] = {
title: el.children('label').html(),
children: [],
id: id
};
var parent = el.parent().closest('div[id^="region"]');
if (parent.length) {
var pid = parent.attr('id').slice(6);
} else {
pid = 0;
}
result[pid].children.push(+id);
});

View file

@ -1,75 +0,0 @@
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
<link href="tree.css" rel="stylesheet">
<script src="http://code.jquery.com/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/3.2.0/lodash.js"></script>
<script src="regions.js"></script>
<script src="tree.js"></script>
</head>
<body>
<script id="tree-template" type="text/template">
<ul>
<% $(children).each(function() {
var nodeData = data[this];
var hasChildren = nodeData.children && nodeData.children.length;
%>
<li data-id="<%=nodeData.id%>" class="<%=hasChildren ? 'tree-closed' : ''%>">
<% if (hasChildren) { %> <span class="tree-toggler"></span> <% } %>
<input type="checkbox" value="<%=nodeData.id%>">
<%=nodeData.title%>
</li>
<% }); %>
</ul>
</script>
<script>
// требование - как можно меньше DOM-элементов
// работа с данными из 1000 узлов
/* пример структуры данных
var data = [
{
children: [1,2,3]
},
{
title: 'Россия',
id: 1,
children: [4,5]
},
{
title: 'Украина',
id: 2
},
{
title: 'Беларусь',
id: 3
},
{
title: 'Север',
id: 4
},
{
title: 'Юг',
id: 5
}
];
*/
var tree = new Tree({
template: _.template($('#tree-template').html().trim()),
data: regions
});
tree.getElement().appendTo('body');
</script>
</body>
</html>

File diff suppressed because one or more lines are too long

View file

@ -1,37 +0,0 @@
.tree, .tree ul {
list-style: none;
margin: 0;
padding: 2px;
}
.tree .tree-open ul {
display: block;
}
.tree .tree-closed ul {
display: none;
}
.tree-toggler {
color: blue;
cursor: pointer;
float: left;
margin-left: -16px;
width: 1em;
height: 1em;
}
.tree-open .tree-toggler {
background: url(https://js.cx/tree/minus.gif) no-repeat center;
}
.tree-closed .tree-toggler {
background: url(https://js.cx/tree/plus.gif) no-repeat center;
}
.tree li {
line-height: 1em;
padding-left: 16px;
}

View file

@ -1,74 +0,0 @@
function Tree(options) {
var self = this;
var template = options.template;
var data = options.data;
this.getElement = function() {
if (!this.elem) render();
return this.elem;
}
function onTogglerClick(e) {
self.toggle( $(e.currentTarget.parentNode) );
}
function onCheckboxChange(e) {
var checked = e.target.checked;
var id = e.target.value;
var node = $(e.currentTarget.parentNode);
if (checked) {
self.open(node);
}
node.find('input').prop('checked', checked);
}
this.toggle = function(node) {
ensureChildrenRendered(node);
node.toggleClass("tree-open tree-closed");
};
this.open = function(node) {
ensureChildrenRendered(node);
node.addClass("tree-open").removeClass("tree-closed");
}
this.close = function(node) {
ensureChildrenRendered(node);
node.addClass("tree-closed").removeClass("tree-open");
}
function ensureChildrenRendered(node) {
var ul = node.children('ul');
if (!ul.length) {
renderChildren( node.data('id') ).appendTo(node);
if (node.children('input').is(':checked')) {
node.find('input').prop('checked', true);
}
}
}
function renderChildren(id) {
return $(template({
children: data[id].children,
data: data
}));
}
function render() {
self.elem = renderChildren(0)
.addClass('tree');
self.elem.on('click', '.tree-toggler', onTogglerClick);
self.elem.on('change', 'input', onCheckboxChange);
}
}

File diff suppressed because one or more lines are too long

View file

@ -1,33 +0,0 @@
# Дерево с чекбоксами
[importance 5]
Вы получили проект, в котором есть дерево с чекбоксами.
К сожалению, оно "кривое" -- некрасивое, плохо закодированное и с ошибками. Ваша задача -- сделать "как надо".
Образец:
[iframe src="source" height=400 link edit border=1]
Особенности работы:
<ul>
<li>При изначальном рисовании все элементы закрыты и не отмечены.</li>
<li>Установка отметки в чекбоксе раскрывает узел и отмечает все элементы под ним.</li>
<li>Снятие отметки из чекбокса -- снимает отметки из всех элементов под ним.</li>
</ul>
Требования к реализации:
<ul>
<li>Все данные для дерева (регионы) нужно хранить в JS. Как именно -- определяете вы сами. Можно в одном объекте, можно -- ещё как-то.</li>
<li>Программист, который делал этот код, сейчас недоступен. Ваша задача -- восстановить данные регионов из дерева примера.</li>
<li>Можно считать, что название региона всегда занимает только 1 строку.</li>
<li>Дерево должно легко работать с 1000 узлов (в примере их около 450).</li>
<li>Используйте минимум DOM-элементов.</li>
</ul>
Исходный документ содержит "образец" дерева.
[edit task src="source"/]
P.S. При получении данных из дерева-образца вам поможет `JSON.stringify(объект)`. Он превратит объект в строку, которую можно положить в файл.

View file

@ -1,2 +0,0 @@
# Еще практика!