renovations
Before Width: | Height: | Size: 4.9 KiB |
Before Width: | Height: | Size: 5.7 KiB |
Before Width: | Height: | Size: 9.1 KiB |
Before Width: | Height: | Size: 3.7 KiB |
Before Width: | Height: | Size: 5.7 KiB |
Before Width: | Height: | Size: 5.7 KiB |
Before Width: | Height: | Size: 10 KiB |
Before Width: | Height: | Size: 8.6 KiB |
Before Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 7.9 KiB |
Before Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 14 KiB |
0
2-ui/5-widgets/6-widget-tasks/6-slider-events/slider.png → 2-ui/5-widgets/5-custom-events/4-slider-events/slider.png
Executable file → Normal file
Before Width: | Height: | Size: 5.3 KiB After Width: | Height: | Size: 5.3 KiB |
|
@ -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;
|
||||
}
|
||||
```
|
||||
|
||||
|
|
@ -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"> </span>
|
||||
Change:<span id="change"> </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>
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
46
2-ui/5-widgets/5-custom-events/4-slider-events/task.md
Normal 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).
|
32
2-ui/5-widgets/6-what-next/article.md
Normal 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/).
|
||||
|
||||
Успехов вам!
|
|
@ -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"> </span>
|
||||
Change:<span id="change"> </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>
|
||||
|
|
@ -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).
|
|
@ -1,2 +0,0 @@
|
|||
# Практика, практика, практика!
|
||||
|
|
@ -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).
|
||||
|
||||
Присоединитесь к какому-нибудь интересному проекту и сделайте что-то хорошее. Разберитесь в том, как оно работает. Пофиксите пару багов.
|
||||
|
||||
Успехов вам!
|
|
@ -58,7 +58,7 @@
|
|||
<li>"Твиты"</li>
|
||||
</ol>
|
||||
|
||||
Как мы решаем, что именно выделять в компонент? Конечно, нам подсказывает опыт, но
|
||||
Как мы решаем, что именно выделять в компонент? Это нам подсказывает опыт и здравый смысл.
|
||||
|
||||
В случае с твиттером разбиение на компоненты особенно очевидно. Странца "сама распадается" на чётко очерченные блоки, каждый из которых выполняет свою роль.
|
||||
|
||||
|
@ -87,5 +87,7 @@
|
|||
|
||||
Веб-компоненты (Web Components) -- это не один стандарт, а целая платформа, комплекс стандартов, которые вместе добавляют в браузер технологии для удобной реализации компонент.
|
||||
|
||||
Если глядеть "сверху", то веб-компоненты -- это возможность добавлять свои элементы в браузер, например `document.createElement("tweets-list")`. Но "под капотом" кроются расширенные возможности по инкапсуляции поддерева DOM и стилей, по генерации событий и многое другое.
|
||||
Если глядеть "сверху", то веб-компоненты -- это возможность добавлять свои элементы в браузер, например `document.createElement("tweets-list")`, которые описываются с помощью классов JavaScript, могут иметь свои методы и свойства.
|
||||
|
||||
Также "под капотом" кроются расширенные возможности по инкапсуляции поддерева DOM и стилей, по генерации событий и многое другое, что мы рассмотрим далее.
|
||||
|
||||
|
|
|
@ -1,19 +1,21 @@
|
|||
# Веб-компоненты
|
||||
# Свои элементы: Custom Elements
|
||||
|
||||
Сердцем платформы "веб-компоненты" является стандарт [Web Components](http://w3c.github.io/webcomponents/explainer/), который находится в разработке и позволяет описывать свои элементы.
|
||||
Платформа "веб-компоненты" включает в себя несколько стандартов [Web Components](http://www.w3.org/standards/techs/components#w3c_all), которые находятся в разработке.
|
||||
|
||||
Начнём мы со стандарта [Custom Elements](http://www.w3.org/TR/custom-elements/), который позволяет создавать свои типы элементов.
|
||||
[cut]
|
||||
## Зачем Web Components?
|
||||
## Зачем Custom Elements?
|
||||
|
||||
Критично настроенный читатель скажет: "Зачем ещё стандарт для создания своих элементов? Я могу создать любой элемент и прямо сейчас! В любом из современных браузеров можно писать любой HTML, используя свои теги. В чём же разница?"
|
||||
Критично настроенный читатель скажет: "Зачем ещё стандарт для создания своих элементов? Я могу создать любой элемент и прямо сейчас! В любом из современных браузеров можно писать любой HTML, используя свои теги: `<mytag>`. Или создавать элементы из JavaScript при помощи `document.createElement('mytag')`. В чём же разница?"
|
||||
|
||||
Она в том, что обычно элемент с нестандартным названием (например `<message>`) воспринимается браузером, как нечто неопределённо-непонятное. Ему соответствует класс [HTMLUnknownElement](http://www.w3.org/TR/html5/dom.html#htmlunknownelement), и у него нет каких-либо особых методов.
|
||||
Она в том, что обычно элемент с нестандартным названием (например `<mytag>`) воспринимается браузером, как нечто неопределённо-непонятное. Ему соответствует класс [HTMLUnknownElement](http://www.w3.org/TR/html5/dom.html#htmlunknownelement), и у него нет каких-либо особых методов.
|
||||
|
||||
**Стандарт Web Components позволяет описывать для новых элементов свои свойства, методы, объявлять свой DOM, подобие конструктора и многое другое.**
|
||||
**Стандарт Custom Elements позволяет описывать для новых элементов свои свойства, методы, объявлять свой DOM, подобие конструктора и многое другое.**
|
||||
|
||||
Давайте посмотрим это на примерах.
|
||||
|
||||
[warn header="Только Chrome"]
|
||||
Так как спецификация не окончательна, то для запуска примеров рекомендуется использовать Chrome Canary.
|
||||
[warn header="Для примеров рекомендуется Chrome"]
|
||||
Так как спецификация не окончательна, то для запуска примеров рекомендуется использовать Google Chrome, лучше -- последнюю сборку [Chrome Canary](https://www.google.ru/chrome/browser/canary.html), в которой, как правило, отражены последние изменения.
|
||||
[/warn]
|
||||
|
||||
## Новый элемент
|
||||
|
@ -22,7 +24,7 @@
|
|||
|
||||
Здесь:
|
||||
<ul>
|
||||
<li>`имя` -- имя нового тега, например `"mega-select"`. Оно обязано содержать дефис `-`. Спецификация требует дефис, чтобы избежать в будущем конфликтов со стандартными элементами HTML. Нельзя создать элемент `timer` или `myTimer` -- будет ошибка.</li>
|
||||
<li>`имя` -- имя нового тега, например `"mega-select"`. Оно обязано содержать дефис `"-"`. Спецификация требует дефис, чтобы избежать в будущем конфликтов со стандартными элементами HTML. Нельзя создать элемент `timer` или `myTimer` -- будет ошибка.</li>
|
||||
<li>`прототип` -- объект-прототип для нового элемента, он должен наследовать от `HTMLElement`, чтобы у элемента были стандартные свойства и методы.</li>
|
||||
</ul>
|
||||
|
||||
|
@ -62,7 +64,7 @@
|
|||
</script>
|
||||
```
|
||||
|
||||
**Метод `registerElement` может быть вызван в любое время, даже и после появления `<my-timer>` в HTML.**
|
||||
Использовать новый элемент в HTML можно и до его объявления через `registerElement`.
|
||||
|
||||
Для этого в браузере предусмотрен специальный режим "обновления" существующих элементов.
|
||||
|
||||
|
@ -72,7 +74,7 @@
|
|||
<li>При вызове `registerElement` такие элементы автоматически обновятся до нужного класса.</li>
|
||||
</ul>
|
||||
|
||||
Вот демо того, что происходит, когда регистрация элемента происходит после его появления в разметке:
|
||||
В примере ниже регистрация элемента происходит через 2 секунды после его появления в разметке:
|
||||
|
||||
```html
|
||||
<!--+ run -->
|
||||
|
@ -85,7 +87,7 @@
|
|||
}
|
||||
|
||||
hello-world {
|
||||
transition: color 5s;
|
||||
transition: color 3s;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
@ -103,12 +105,15 @@
|
|||
}
|
||||
});
|
||||
|
||||
*!*
|
||||
// у нового типа элементов есть метод sayHi
|
||||
*/!*
|
||||
hello.sayHi();
|
||||
}, 2000);
|
||||
</script>
|
||||
```
|
||||
|
||||
Создание таких элементов в JavaScript никак не отличается от обычных:
|
||||
Можно создавать такие элементы и в JavaScript -- обычным вызовом `createElement`:
|
||||
|
||||
```js
|
||||
var timer = document.createElement('my-timer');
|
||||
|
@ -116,9 +121,11 @@ var timer = document.createElement('my-timer');
|
|||
|
||||
## Расширение встроенных элементов
|
||||
|
||||
Выше мы видели пример создания элемента на основе базового `HTMLElement`. Но можно расширить и другие, более конкретные HTML-элементы.
|
||||
|
||||
Для расширения встроенных элементов у `registerElement` предусмотрен параметр `extends`, в котором можно задать, какой тег мы расширяем.
|
||||
|
||||
Например:
|
||||
Например, кнопку:
|
||||
|
||||
```html
|
||||
<!--+ run -->
|
||||
|
@ -167,7 +174,7 @@ var timer = document.createElement("button", "my-timer");
|
|||
|
||||
## Жизненный цикл
|
||||
|
||||
Следующие методы автоматически вызываются во время жизненного цикла элемента:
|
||||
В прототипе своего элемента мы можем задать специальные методы, которые будут вызываться при создании, добавлении и удалении элемента из DOM:
|
||||
|
||||
<table>
|
||||
<tr><td>`createdCallback`</td><td>Элемент создан</td></tr>
|
||||
|
@ -176,9 +183,9 @@ var timer = document.createElement("button", "my-timer");
|
|||
<tr><td>`attributeChangedCallback(name, prevValue, newValue)`</td><td>Атрибут добавлен, изменён или удалён</td></tr>
|
||||
</table>
|
||||
|
||||
Как вы, наверняка, заметили, `createdCallback` является подобием конструктора. Можно добавить к элементу свойства, запросить с сервера данные и так далее.
|
||||
Как вы, наверняка, заметили, `createdCallback` является подобием конструктора. Он вызывается только при создании элемента, поэтому всю дополнительную инициализацию имеет смысл описывать в нём.
|
||||
|
||||
Давайте используем `attachedCallback`, чтобы автоматически запускать таймер при вставке в документ:
|
||||
Давайте используем `createdCallback`, чтобы инициализовать таймер, а `attachedCallback` -- чтобы автоматически запускать таймер при вставке в документ:
|
||||
|
||||
```html
|
||||
<!--+ run -->
|
||||
|
@ -186,9 +193,16 @@ var timer = document.createElement("button", "my-timer");
|
|||
var MyTimerProto = Object.create(HTMLElement.prototype);
|
||||
|
||||
MyTimerProto.tick = function() {
|
||||
this.innerHTML++;
|
||||
this.timer++;
|
||||
this.innerHTML = this.timer;
|
||||
};
|
||||
|
||||
*!*
|
||||
MyTimerProto.createdCallback = function() {
|
||||
this.timer = 0;
|
||||
};
|
||||
*/!*
|
||||
|
||||
*!*
|
||||
MyTimerProto.attachedCallback = function() {
|
||||
setInterval(this.tick.bind(this), 1000);
|
||||
|
@ -203,10 +217,9 @@ var timer = document.createElement("button", "my-timer");
|
|||
<my-timer id="timer">0</my-timer>
|
||||
```
|
||||
|
||||
Спецификация:
|
||||
<ul>
|
||||
<li>[Introduction to Web Components](http://w3c.github.io/webcomponents/explainer/)</li>
|
||||
<li>[Custom Elements](http://w3c.github.io/webcomponents/spec/custom/)</li>
|
||||
</ul>
|
||||
## Итого
|
||||
|
||||
Мы рассмотрели, как создавать свои DOM-элементы при помощи стандарта [Custom Elements](http://www.w3.org/TR/custom-elements/).
|
||||
|
||||
Далее мы перейдём к изучению дополнительных возможностей по работе с DOM.
|
||||
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
# Shadow DOM
|
||||
# Shadow DOM, шаблоны и стили
|
||||
|
||||
Спецификация [Shadow DOM](http://w3c.github.io/webcomponents/spec/shadow/index.html) готовится как отдельный стандарт и применяется, в том числе, отдельно от веб-компонентов.
|
||||
Спецификация [Shadow DOM](http://w3c.github.io/webcomponents/spec/shadow/) является отдельным стандартом. Частично он уже используется для обычных DOM-элементов, но также применяется для создания веб-компонентов.
|
||||
|
||||
*Shadow DOM* -- это внутренний DOM элемента, который существует отдельно от внешнего документа. В нём могут быть свои ID, свои стили и так далее. Причём снаружи оно, без применения специальных техник, не видно, поэтому не возникает конфликтов.
|
||||
*Shadow DOM* -- это внутренний DOM элемента, который существует отдельно от внешнего документа. В нём могут быть свои ID, свои стили и так далее. Причём снаружи его, без применения специальных техник, не видно, поэтому не возникает конфликтов.
|
||||
[cut]
|
||||
|
||||
## Внутри браузера
|
||||
|
@ -14,13 +14,11 @@
|
|||
Например, вот такое содержимое будет у `<input type="date">`:
|
||||
<img src="shadow-dom-chrome.png">
|
||||
|
||||
То, что находится под `shadow-root` -- это и есть Shadow DOM.
|
||||
То, что находится под `#shadow-root` -- это и есть Shadow DOM.
|
||||
|
||||
**Получить элементы из Shadow DOM можно только при помощи специальных JavaScript-вызовов или селекторов. Это не обычные дети, а намного более мощное средство отделения содержимого.**
|
||||
|
||||
В Shadow DOM выше можно увидеть полезный атрибут `pseudo`. С его помощью можно стилизовать подэлементы через CSS.
|
||||
|
||||
Например, сделаем поле редактирования даты красным:
|
||||
В Shadow DOM выше можно увидеть полезный атрибут `pseudo`. Он нестандартный, существует по историческим причинам. С его помощью можно стилизовать подэлементы через CSS, например, сделаем поле редактирования даты красным:
|
||||
|
||||
```html
|
||||
<!--+ run -->
|
||||
|
@ -35,7 +33,9 @@ input::-webkit-datetime-edit {
|
|||
<input type="date">
|
||||
```
|
||||
|
||||
Если говорить хронологически, то сначала браузеры начали экспериментировать внутри себя с инкапсуляцией внутренних DOM-структур, а уже потом, через некоторое время, появился стандарт Shadow DOM, который позволяет делать то же самое разработчикам.
|
||||
Ещё раз заметим, что `pseudo` -- нестандартный атрибут. Если говорить хронологически, то сначала браузеры начали экспериментировать внутри себя с инкапсуляцией внутренних DOM-структур, а уже потом, через некоторое время, появился стандарт Shadow DOM, который позволяет делать то же самое разработчикам.
|
||||
|
||||
Далее мы рассмотрим работу с Shadow DOM из JavaScript, по стандарту [Shadow DOM](http://w3c.github.io/webcomponents/spec/shadow/).
|
||||
|
||||
## Создание Shadow DOM
|
||||
|
||||
|
@ -44,7 +44,7 @@ Shadow DOM можно создать внутри любого элемента
|
|||
Например:
|
||||
|
||||
```html
|
||||
<!--+ run -->
|
||||
<!--+ run autorun -->
|
||||
<p id="elem">Доброе утро, страна!</p>
|
||||
|
||||
<script>
|
||||
|
@ -57,12 +57,10 @@ Shadow DOM можно создать внутри любого элемента
|
|||
|
||||
**С момента создания Shadow DOM обычное содержимое (дети) элемента не отображается, а показывается только Shadow DOM.**
|
||||
|
||||
Внутрь этого Shadow DOM, при желании, можно поместить обычное содержимое. Для этого нужно указать, куда, то есть "точку вставки" (insertion point). Это делается при помощи тега `<content>`.
|
||||
|
||||
Укажем его:
|
||||
Внутрь этого Shadow DOM, при желании, можно поместить обычное содержимое. Для этого нужно указать, куда. В Shadow DOM это делается через "точку вставки" (insertion point). Она объявляется при помощи тега `<content>`, например:
|
||||
|
||||
```html
|
||||
<!--+ run -->
|
||||
<!--+ run autorun -->
|
||||
<p id="elem">Доброе утро, страна!</p>
|
||||
|
||||
<script>
|
||||
|
@ -71,7 +69,7 @@ Shadow DOM можно создать внутри любого элемента
|
|||
</script>
|
||||
```
|
||||
|
||||
Теперь, если у вас последний Chrome, вы увидите две строчки: "Доброе утро, страна!" в заголовке, а затем "Привет из подполья".
|
||||
Теперь вы увидите две строчки: "Доброе утро, страна!" в заголовке, а затем "Привет из подполья".
|
||||
|
||||
Shadow DOM примера выше в инструментах разработки:
|
||||
|
||||
|
@ -80,21 +78,21 @@ Shadow DOM примера выше в инструментах разработ
|
|||
Важные детали:
|
||||
<ul>
|
||||
<li>Тег `<content>` влияет только на отображение, он не перемещает узлы физически. Как видно из картинки выше, текстовый узел "Доброе утро, страна!" остался внутри `p#elem`. Его можно даже получить при помощи `elem.firstElementChild`. </li>
|
||||
<li>Внутри `<content>` показывается не `<p>`, а его содержимое, то есть в данном случае текст "Привет из подполья".</li>
|
||||
<li>Внутри `<content>` показывается не элемент целиком `<p id="elem">`, а его содержимое, то есть в данном случае текст "Доброе утро, страна!".</li>
|
||||
</ul>
|
||||
|
||||
**Атрибутом `select` можно указать конкретный селектор содержимого, которое нужно переносить. Например, `<content select="h3"></content>` перенесёт только заголовки.**
|
||||
**В `<content>` атрибутом `select` можно указать конкретный селектор содержимого, которое нужно переносить. Например, `<content select="h3"></content>` перенесёт только заголовки.**
|
||||
|
||||
Внутри Shadow DOM можно использовать `<content>` много раз, указывая таким образом, где конкретно какие части исходного содержимого разместить, но при этом дублирование узлов невозможно. Если узел показан в одном `<content>`, то в следующем он будет пропущен.
|
||||
Внутри Shadow DOM можно использовать `<content>` много раз с разными значениями `select`, указывая таким образом, где конкретно какие части исходного содержимого разместить. Но при этом дублирование узлов невозможно. Если узел показан в одном `<content>`, то в следующем он будет пропущен.
|
||||
|
||||
Например, если сначала идёт `<content select="h3.big">`, а затем `<content select="h3">`, то в первом `<content>` будут показаны заголовки `<h3>` с классом `big`, а во втором -- все остальные.</li>
|
||||
Например, если сначала идёт `<content select="h3.title">`, а затем `<content select="h3">`, то в первом `<content>` будут показаны заголовки `<h3>` с классом `title`, а во втором -- все остальные, кроме уже показанных.</li>
|
||||
|
||||
**Содержимое внутри `<content>...</content>` будет показано только в том случае, если узлов для вставки нет.**
|
||||
В примере выше тег `<content></content>` внутри пуст. Если в нём указать содержимое, то оно будет показано только в том случае, если узлов для вставки нет. Например потому что ни один узел не подпал под указанный `select`, или все они уже отображены другими, более ранними `<content>`.
|
||||
|
||||
Ещё пример:
|
||||
Например:
|
||||
|
||||
```html
|
||||
<!--+ run -->
|
||||
<!--+ run autorun -->
|
||||
|
||||
<section id="elem">
|
||||
<h1>Новости</h1>
|
||||
|
@ -103,23 +101,24 @@ Shadow DOM примера выше в инструментах разработ
|
|||
|
||||
<script>
|
||||
var root = elem.createShadowRoot();
|
||||
|
||||
root.innerHTML = "<content select='h1'></content> \
|
||||
<content select='.author'>Без автора.</content> \
|
||||
<content></content>";
|
||||
|
||||
alert(elem.innerHTML);
|
||||
|
||||
</script>
|
||||
|
||||
<button onclick="alert(root.innerHTML)">root.innerHTML</button>
|
||||
```
|
||||
|
||||
При запуске мы увидим, что:
|
||||
<ul>
|
||||
<li>Первый `<content select='h1'>` выведет заголовок.</li>
|
||||
<li>Второй `<content select=".author">` вывел бы автора, но так как такого элемента нет -- выводится содержимое самого `<content select=".author">`.</li>
|
||||
<li>Третий `<content>` выведет остальное содержимое (уже без заголовка `<h1>`, он выведен ранее!)</li>
|
||||
<li>Второй `<content select=".author">` вывел бы автора, но так как такого элемента нет -- выводится содержимое самого `<content select=".author">`, то есть "Без автора".</li>
|
||||
<li>Третий `<content>` выведет остальное содержимое исходного элемента -- уже без заголовка `<h1>`, он выведен ранее!</li>
|
||||
</ul>
|
||||
|
||||
Ещё раз обратим внимание, что `<content>` физически не перемещает узлы по DOM. Он только показывает, где их отображать, а также, как мы увидим далее, влияет на применение стилей. Поэтому в примере выше `innerHTML` выведет содержимое `elem`, как если бы не было никакого Shadow DOM.
|
||||
Ещё раз обратим внимание, что `<content>` физически не перемещает узлы по DOM. Он только показывает, где их отображать, а также, как мы увидим далее, влияет на применение стилей.
|
||||
|
||||
## Корень shadowRoot
|
||||
|
||||
|
@ -127,23 +126,30 @@ Shadow DOM примера выше в инструментах разработ
|
|||
|
||||
Он представляет собой специальный объект, поддерживающий основные методы CSS-запросов и подробно описанный в стандарте как [ShadowRoot](http://w3c.github.io/webcomponents/spec/shadow/#shadowroot-object).
|
||||
|
||||
Например:
|
||||
Если нужно работать с содержимым в Shadow DOM, то нужно перейти к нему через `elem.shadowRoot`. Можно и создать новое Shadow DOM-дерево из JavaScript, например:
|
||||
|
||||
```html
|
||||
<!--+ run -->
|
||||
<!--+ run autorun -->
|
||||
<p id="elem">Доброе утро, страна!</p>
|
||||
|
||||
<script>
|
||||
*!*
|
||||
// создать новое дерево Shadow DOM для elem
|
||||
*/!*
|
||||
var root = elem.createShadowRoot();
|
||||
root.innerHTML = "<h3><content></content></h3> <p>Привет из подполья!</p>";
|
||||
|
||||
root.innerHTML = "<h3><content></content></h3> <p>Привет из подполья!</p> <hr>";
|
||||
</script>
|
||||
|
||||
<script>
|
||||
*!*
|
||||
var root = elem.shadowRoot; // ...где-то в другом месте кода
|
||||
alert(root.querySelector('p').innerHTML); // Привет из подполья!
|
||||
alert(root.querySelector('content').innerHTML); // пусто, физически узлы - вне content
|
||||
// прочитать данные из Shadow DOM для elem
|
||||
*/!*
|
||||
var root = elem.shadowRoot;
|
||||
// Привет из подполья!
|
||||
document.write("<p>p:" + root.querySelector('p').innerHTML);
|
||||
// пусто, так как физически узлы - вне content
|
||||
document.write("<p>content:" + root.querySelector('content').innerHTML);
|
||||
</script>
|
||||
```
|
||||
|
||||
|
@ -153,23 +159,25 @@ Shadow DOM примера выше в инструментах разработ
|
|||
|
||||
## Шаблоны <template>
|
||||
|
||||
Элемент `<template>` предназначен для хранения "образца" разметки, предназначенного для вставки куда-либо.
|
||||
Элемент `<template>` предназначен для хранения "образца" разметки, невидимого и предназначенного для вставки куда-либо.
|
||||
|
||||
Его отличие от обычных тегов в том, что его содержимое обрабатывается особым образом. Оно не показывается и считается находящимся вообще "вне документа".
|
||||
Конечно, есть много способов записать произвольный невидимый текст в HTML. В чём же особенность `<template>`?
|
||||
|
||||
Однако, вместе с тем, оно всё же парсится браузером и записывается в виде `DocumentFragment` в свойство `content`. Предполагается, что мы, при необходимости, возьмём фрагмент `content` и вставим, куда надо.
|
||||
Его отличие от обычных тегов в том, что его содержимое обрабатывается особым образом. Оно не только показывается, но и считается находящимся вообще "вне документа".
|
||||
|
||||
Пример использования:
|
||||
Однако, вместе с тем, оно всё же обрабатывается браузером (а значит должно быть корректным HTML) и записывается как `DocumentFragment` в свойство тега `content`. Предполагается, что мы, при необходимости, возьмём `content` и вставим, куда надо.
|
||||
|
||||
Пример вставки шаблона `tmpl` в Shadow DOM элемента `elem`:
|
||||
|
||||
```html
|
||||
<!--+ run -->
|
||||
<!--+ run autorun -->
|
||||
|
||||
<p id="elem">Доброе утро, страна!</p>
|
||||
|
||||
<template id="tmpl">
|
||||
<h3><content></content></h3>
|
||||
<p>Привет из подполья!</p>
|
||||
<script> alert('Новость!'); </script>
|
||||
<script> document.write('...document.write:Новость!'); </script>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
@ -178,10 +186,17 @@ Shadow DOM примера выше в инструментах разработ
|
|||
</script>
|
||||
```
|
||||
|
||||
Детали:
|
||||
У нас получилось, что:
|
||||
<ol>
|
||||
<li>В элементе `#elem` содержатся данные в некоторой оговорённой разметке.</li>
|
||||
<li>Шаблон `#tmpl` указывает, как их отобразить, куда и в какие HTML-теги завернуть содержимое `#elem`.</li>
|
||||
<li>Это содержимое добавляется в Shadow DOM тега. Технически, шаблон можно использовать и без Shadow DOM, но тогда не сработает тег `<content>`.</li>
|
||||
</ol>
|
||||
|
||||
Важные детали:
|
||||
<ul>
|
||||
<li>В отличие от вставки через `innerHTML` и от обычного `DocumentFragment`, скрипт внутри шаблона выполнится при вставке. Содержимое шаблона изначально "вне документа" и "оживает", когда оно попадает в него. Это относится ко всему -- картинки начинают загружаться, видео -- проигрываться и т.п.</li>
|
||||
<li>Вставляется не сам `tmpl.content`, а его клон. Это обычная практика, чтобы можно было использовать содержимое одного шаблона во многих однотипных элементах.</li>
|
||||
<li>Мы вставляем не сам `tmpl.content`, а его клон. Это обычная практика, чтобы можно было использовать один шаблон много раз.</li>
|
||||
</ul>
|
||||
|
||||
## Стили
|
||||
|
@ -193,7 +208,7 @@ Shadow DOM примера выше в инструментах разработ
|
|||
Например:
|
||||
|
||||
```html
|
||||
<!--+ run -->
|
||||
<!--+ run autorun -->
|
||||
<p>Жили мы тихо-мирно, и тут...</p>
|
||||
|
||||
<p id="elem">Доброе утро, страна!</p>
|
||||
|
|
|
@ -1,2 +1,5 @@
|
|||
# Веб-компоненты: взгляд в будущее
|
||||
|
||||
Веб-компоненты -- "платформа будущего": совокупность стандартов, которые позволяют описывать "свои теги" -- новые типы DOM-элементов, со своими свойствами и методами, инкапсулировать их DOM и стили.
|
||||
|
||||
Современные браузеры поддерживают их частично.
|
79
archive/menu.html
Normal file
|
@ -0,0 +1,79 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<style>
|
||||
|
||||
.menu ul {
|
||||
margin: 0;
|
||||
list-style: none;
|
||||
padding-left: 20px;
|
||||
|
||||
display: none;
|
||||
}
|
||||
|
||||
.menu .title {
|
||||
font-size: 18px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.menu .title::before {
|
||||
color: green;
|
||||
font-size: 80%;
|
||||
content: '▶ ';
|
||||
}
|
||||
|
||||
.menu_open .title::before {
|
||||
color: green;
|
||||
font-size: 80%;
|
||||
content: '▼ ';
|
||||
}
|
||||
|
||||
|
||||
.menu_open ul {
|
||||
display: block;
|
||||
}
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div id="sweeties" class="menu">
|
||||
<span class="title">Сладости (нажми меня)!</span>
|
||||
<ul>
|
||||
<li>Торт</li>
|
||||
<li>Пончик</li>
|
||||
<li>Пирожное</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function Menu(options) {
|
||||
var elem = options.elem;
|
||||
|
||||
function toggle() {
|
||||
elem.classList.toggle('menu_open');
|
||||
}
|
||||
|
||||
elem.onclick = function(event) {
|
||||
console.log(event);
|
||||
console.log(this == elem);
|
||||
toggle();
|
||||
}
|
||||
|
||||
this.toggle = toggle;
|
||||
}
|
||||
|
||||
var menu = new Menu({
|
||||
elem: document.querySelector('#sweeties')
|
||||
});
|
||||
|
||||
setInterval(function() {
|
||||
menu.toggle();
|
||||
}, 3000)
|
||||
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.9 KiB |
Before Width: | Height: | Size: 2 KiB After Width: | Height: | Size: 2 KiB |