ok
This commit is contained in:
parent
aeb74092b6
commit
1f61c2ab1d
100 changed files with 4120 additions and 659 deletions
|
@ -0,0 +1,6 @@
|
|||
|
||||
The algorithm:
|
||||
1. Make `img` for every source.
|
||||
2. Add `onload/onerror` for every image.
|
||||
3. Increase the counter when either `onload` or `onerror` triggers.
|
||||
4. When the counter value equals to the sources count -- we're done: `callback()`.
|
|
@ -0,0 +1,54 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<script>
|
||||
function preloadImages(sources, callback) {
|
||||
let counter = 0;
|
||||
|
||||
function onLoad() {
|
||||
counter++;
|
||||
if (counter == sources.length) callback();
|
||||
}
|
||||
|
||||
for(let source of sources) {
|
||||
let img = document.createElement('img');
|
||||
img.onload = img.onerror = onLoad;
|
||||
img.src = source;
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- The test ----------
|
||||
|
||||
let sources = [
|
||||
"https://en.js.cx/images-load/1.jpg",
|
||||
"https://en.js.cx/images-load/2.jpg",
|
||||
"https://en.js.cx/images-load/3.jpg"
|
||||
];
|
||||
|
||||
// add random characters to prevent browser caching
|
||||
for (let i = 0; i < sources.length; i++) {
|
||||
sources[i] += '?' + Math.random();
|
||||
}
|
||||
|
||||
// for each image,
|
||||
// let's create another img with the same src and check that we have its width immediately
|
||||
function testLoaded() {
|
||||
let widthSum = 0;
|
||||
for (var i = 0; i < sources.length; i++) {
|
||||
let img = document.createElement('img');
|
||||
img.src = sources[i];
|
||||
widthSum += img.width;
|
||||
}
|
||||
alert(widthSum);
|
||||
}
|
||||
|
||||
// should output 300
|
||||
preloadImages(sources, testLoaded);
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,43 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<script>
|
||||
function preloadImages(sources, callback) {
|
||||
/* your code */
|
||||
}
|
||||
|
||||
// ---------- The test ----------
|
||||
|
||||
let sources = [
|
||||
"https://en.js.cx/images-load/1.jpg",
|
||||
"https://en.js.cx/images-load/2.jpg",
|
||||
"https://en.js.cx/images-load/3.jpg"
|
||||
];
|
||||
|
||||
// add random characters to prevent browser caching
|
||||
for (let i = 0; i < sources.length; i++) {
|
||||
sources[i] += '?' + Math.random();
|
||||
}
|
||||
|
||||
// for each image,
|
||||
// let's create another img with the same src and check that we have its width immediately
|
||||
function testLoaded() {
|
||||
let widthSum = 0;
|
||||
for (var i = 0; i < sources.length; i++) {
|
||||
let img = document.createElement('img');
|
||||
img.src = sources[i];
|
||||
widthSum += img.width;
|
||||
}
|
||||
alert(widthSum);
|
||||
}
|
||||
|
||||
// every image is 100x100, the total width should be 300
|
||||
preloadImages(sources, testLoaded);
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,36 @@
|
|||
importance: 4
|
||||
|
||||
---
|
||||
|
||||
# Load images with a callback
|
||||
|
||||
Normally, images are loaded when they are created. So i when we add `<img>` to the page, the user does not see the picture immediately. The browser needs to load it first.
|
||||
|
||||
To show an image immediately, we can create it "in advance", like this:
|
||||
|
||||
```js
|
||||
let img = document.createElement('img');
|
||||
img.src = 'my.jpg';
|
||||
```
|
||||
|
||||
The browser starts loading the image and remembers it in the cache. Later, when the same image appears in the document (no matter how), it shows up immediately.
|
||||
|
||||
**Create a function `preloadImages(sources, callback)` that loads all images from the array `sources` and, when ready, runs `callback`.**
|
||||
|
||||
For instance, this will show an `alert` after the images are loaded:
|
||||
|
||||
```js
|
||||
function loaded() {
|
||||
alert("Images loaded")
|
||||
}
|
||||
|
||||
preloadImages(["1.jpg", "2.jpg", "3.jpg"], loaded);
|
||||
```
|
||||
|
||||
In case of an error, the function should still assume the picture "loaded".
|
||||
|
||||
In other words, the `callback` is executed when all images are either loaded or errored out.
|
||||
|
||||
The function is useful, for instance, when we plan to show a gallery with many scrollable images, and want to be sure that all images are loaded.
|
||||
|
||||
In the source document you can find links to test images, and also the code to check whether they are loaded or not. It should output `300`.
|
|
@ -1,8 +0,0 @@
|
|||
# Подсказка
|
||||
|
||||
Текст на странице пусть будет изначально `DIV`, с классом `img-replace` и атрибутом `data-src` для картинки.
|
||||
|
||||
Функция `replaceImg()` должна искать такие `DIV` и загружать изображение с указанным `src`. По `onload` осуществляется замена `DIV` на картинку.
|
||||
|
||||
# Решение
|
||||
|
|
@ -1,49 +0,0 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<style>
|
||||
.img-replace {
|
||||
float: left;
|
||||
border: 1px solid black;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<button onclick="window.location.reload(true)">Перезагрузить ифрейм</button>
|
||||
<hr>
|
||||
<div style="width:114px;height:40px;font-size:32px;letter-spacing:3px" data-src="https://js.cx/search/google.png" class="img-replace">
|
||||
<span style="color:#1A53F7">G</span><span style="color:#E42131">o</span><span style="color:#FEB819">o</span><span style="color:#164AF2">g</span><span style="color:#00a315">l</span><span style="color:#E42131">e</span>
|
||||
</div>
|
||||
|
||||
<div style="width:101px;height:40px;font-size:32px" data-src="https://js.cx/search/yandex.png" class="img-replace">
|
||||
<span style="color:#F00">Я</span>ндекс
|
||||
</div>
|
||||
|
||||
<div style="width:100;height:40px;font-size:32px;color:#006dd4;font-weight:bold;letter-spacing:3px;font-family:Arial" data-src="bing.png" class="img-replace">bing</div>
|
||||
|
||||
|
||||
<script>
|
||||
function replaceImg() {
|
||||
var divs = document.querySelectorAll('div.img-replace');
|
||||
for (var i = 0; i < divs.length; i++)(function(i) {
|
||||
var img = document.createElement('img');
|
||||
img.src = divs[i].getAttribute('data-src');
|
||||
img.className = 'img-replace';
|
||||
|
||||
img.onload = function() {
|
||||
divs[i].parentNode.replaceChild(img, divs[i]);
|
||||
}
|
||||
|
||||
}(i))
|
||||
}
|
||||
|
||||
setTimeout(replaceImg, 1000); // задержка на 1 сек для демонстрации
|
||||
</script>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
|
@ -1,40 +0,0 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<style>
|
||||
.img-replace {
|
||||
float: left;
|
||||
border: 1px solid black;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<!-- Google -->
|
||||
|
||||
<div style="width:114px;height:40px;font-size:32px;letter-spacing:3px" class="img-replace">
|
||||
<span style="color:#1A53F7">G</span><span style="color:#E42131">o</span><span style="color:#FEB819">o</span><span style="color:#164AF2">g</span><span style="color:#00a315">l</span><span style="color:#E42131">e</span>
|
||||
</div>
|
||||
|
||||
<!-- Яндекс -->
|
||||
<div style="width:101px;height:40px;font-size:32px" class="img-replace">
|
||||
<span style="color:#F00">Я</span>ндекс
|
||||
</div>
|
||||
|
||||
<!-- Bing -->
|
||||
<div style="width:100;height:40px;font-size:32px;color:#006dd4;font-weight:bold;letter-spacing: 3px; font-family:Arial">bing</div>
|
||||
|
||||
<hr>
|
||||
<!-- картинки (для bing картинки специально нет, чтобы протестировать случай "загрузка не удалась") -->
|
||||
|
||||
<img src="https://js.cx/search/yandex.png" width="114" height="40" alt="Яндекс">
|
||||
<img src="https://js.cx/search/google.png" width="101" height="40" alt="Google">
|
||||
<img src="https://js.cx/search/bing.png" width="101" height="40" alt="Файла нет (bing)">
|
||||
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
|
@ -1,20 +0,0 @@
|
|||
importance: 5
|
||||
|
||||
---
|
||||
|
||||
# Красивый "ALT"
|
||||
|
||||
Обычно, до того как изображение загрузится (или при отключенных картинках), посетитель видит пустое место с текстом из "ALT". Но этот атрибут не допускает HTML-форматирования.
|
||||
|
||||
При мобильном доступе скорость небольшая, и хочется, чтобы посетитель сразу видел красивый текст.
|
||||
|
||||
**Реализуйте "красивый" (HTML) аналог `alt` при помощи CSS/JavaScript, который затем будет заменён картинкой сразу же как только она загрузится.** А если загрузка не состоится -- то не заменён.
|
||||
|
||||
Демо: (нажмите "перезагрузить", чтобы увидеть процесс загрузки и замены)
|
||||
|
||||
[iframe src="solution" height="100"]
|
||||
|
||||
Картинки для `bing` специально нет, так что текст остается "как есть".
|
||||
|
||||
Исходный документ содержит разметку текста и ссылки на изображения.
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
|
||||
Создайте переменную-счетчик для подсчёта количества загруженных картинок, и увеличивайте при каждом `onload/onerror`.
|
||||
|
||||
Когда счетчик станет равен количеству картинок -- вызывайте `callback`.
|
||||
|
|
@ -1,57 +0,0 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<script>
|
||||
function preloadImages(sources, callback) {
|
||||
var counter = 0;
|
||||
|
||||
function onLoad() {
|
||||
counter++;
|
||||
if (counter == sources.length) callback();
|
||||
}
|
||||
|
||||
for (var i = 0; i < sources.length; i++) {
|
||||
var img = document.createElement('img');
|
||||
// сначала onload/onerror, затем src - важно для IE8-
|
||||
img.onload = img.onerror = onLoad;
|
||||
img.src = sources[i];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// ---------- Проверка ----------
|
||||
|
||||
var sources = [
|
||||
"https://js.cx/images-load/1.jpg",
|
||||
"https://js.cx/images-load/2.jpg",
|
||||
"https://js.cx/images-load/3.jpg"
|
||||
];
|
||||
for (var i = 0; i < sources.length; i++) {
|
||||
sources[i] += '?' + Math.random();
|
||||
}
|
||||
|
||||
function testLoaded() {
|
||||
var widthSum = 0;
|
||||
for (var i = 0; i < sources.length; i++) {
|
||||
var img = document.createElement('img');
|
||||
img.src = sources[i];
|
||||
widthSum += img.width;
|
||||
}
|
||||
alert(widthSum); // 300!
|
||||
}
|
||||
|
||||
// до загрузки - 0
|
||||
testLoaded();
|
||||
// после загрузки - 300
|
||||
preloadImages(sources, testLoaded);
|
||||
</script>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
|
@ -1,50 +0,0 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<script>
|
||||
function preloadImages(sources, callback) {
|
||||
/* ваш код */
|
||||
}
|
||||
|
||||
// ---------- Проверка ----------
|
||||
|
||||
/* файлы для загрузки */
|
||||
var sources = [
|
||||
"https://js.cx/images-load/1.jpg",
|
||||
"https://js.cx/images-load/2.jpg",
|
||||
"https://js.cx/images-load/3.jpg"
|
||||
];
|
||||
for (var i = 0; i < sources.length; i++) {
|
||||
sources[i] += '?' + Math.random(); // добавляем параметр, чтобы без кеша (для теста)
|
||||
}
|
||||
|
||||
/** если картинка загружена, то можно будет сразу получить её ширину
|
||||
* создадим все картинки и проверим, есть ли у них ширина
|
||||
*/
|
||||
function testLoaded() {
|
||||
var widthSum = 0;
|
||||
for (var i = 0; i < sources.length; i++) {
|
||||
var img = document.createElement('img');
|
||||
img.src = sources[i];
|
||||
widthSum += img.width;
|
||||
}
|
||||
// каждое изображение 100x100, общая ширина должна быть 300px
|
||||
alert(widthSum);
|
||||
}
|
||||
|
||||
// до загрузки - выведет 0
|
||||
testLoaded();
|
||||
|
||||
// после загрузки - выведет 300
|
||||
preloadImages(sources, testLoaded);
|
||||
</script>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
|
@ -1,20 +0,0 @@
|
|||
importance: 4
|
||||
|
||||
---
|
||||
|
||||
# Загрузить изображения с коллбэком
|
||||
|
||||
Создайте функцию `preloadImages(sources, callback)`, которая предзагружает изображения из массива `sources`, и после загрузки вызывает функцию `callback`.
|
||||
|
||||
Пример использования:
|
||||
|
||||
```js
|
||||
preloadImages(["1.jpg", "2.jpg", "3.jpg"], callback);
|
||||
```
|
||||
|
||||
Если вдруг возникает ошибка при загрузке -- считаем такое изображение загруженным, чтобы не ломать поток выполнения.
|
||||
|
||||
Такая функция может полезна, например, для фоновой загрузки картинок в онлайн-галерею.
|
||||
|
||||
В исходном документе содержатся ссылки на картинки, а также код для проверки, действительно ли изображения загрузились. Он должен выводить "0", затем "300".
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
# Подсказка
|
||||
|
||||
Добавляйте `SCRIPT` при помощи методов `DOM`:
|
||||
|
||||
```js
|
||||
var script = document.createElement('script');
|
||||
script.src = src;
|
||||
|
||||
// в документе может не быть HEAD или BODY,
|
||||
// но хотя бы один (текущий) SCRIPT в документе есть
|
||||
var s = document.getElementsByTagName('script')[0];
|
||||
s.parentNode.insertBefore(script, s); // перед ним и вставим
|
||||
```
|
||||
|
||||
На скрипт повесьте обработчики `onload/onreadystatechange`.
|
||||
|
||||
# Решение
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
function go() {
|
||||
alert("ok");
|
||||
}
|
|
@ -1,42 +0,0 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<script>
|
||||
function addScript(src, callback) {
|
||||
var script = document.createElement('script');
|
||||
script.src = src;
|
||||
var s = document.getElementsByTagName('script')[0]
|
||||
s.parentNode.insertBefore(script, s);
|
||||
|
||||
var loaded = false;
|
||||
|
||||
function onload() {
|
||||
if (loaded) return; // повторный вызов
|
||||
loaded = true;
|
||||
callback();
|
||||
}
|
||||
|
||||
script.onload = onload; // все браузеры, IE с версии 9
|
||||
|
||||
script.onreadystatechange = function() { // IE8-
|
||||
if (this.readyState == 'loaded' || this.readyState == 'complete') {
|
||||
setTimeout(onload, 0);
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
addScript("go.js", function() {
|
||||
go();
|
||||
});
|
||||
</script>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
|
@ -1,3 +0,0 @@
|
|||
function go() {
|
||||
alert("ok");
|
||||
}
|
|
@ -1,22 +0,0 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<script>
|
||||
function addScript(src, callback) {
|
||||
/* ваш код */
|
||||
}
|
||||
|
||||
addScript("go.js", function() {
|
||||
go();
|
||||
});
|
||||
</script>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
|
@ -1,21 +0,0 @@
|
|||
importance: 4
|
||||
|
||||
---
|
||||
|
||||
# Скрипт с коллбэком
|
||||
|
||||
Создайте функцию `addScript(src, callback)`, которая загружает скрипт с данным `src`, и после его загрузки и выполнения вызывает функцию `callback`.
|
||||
|
||||
Скрипт может быть любым, работа функции не должна зависеть от его содержимого.
|
||||
|
||||
Пример использования:
|
||||
|
||||
```js
|
||||
// go.js содержит функцию go()
|
||||
addScript("go.js", function() {
|
||||
go();
|
||||
});
|
||||
```
|
||||
|
||||
Ошибки загрузки обрабатывать не нужно.
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
# Подсказки
|
||||
|
||||
Создайте переменную-счетчик для подсчёта количества загруженных скриптов.
|
||||
|
||||
Чтобы один скрипт не учитывался два раза (например, `onreadystatechange` запустился при `loaded` и `complete`), учитывайте его состояние в объекте `loaded`. Свойство `loaded[i] = true` означает что `i`-й скрипт уже учтён.
|
||||
|
||||
# Решение
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
function a() {
|
||||
b();
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
function b() {
|
||||
c();
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
function c() {
|
||||
alert('ok');
|
||||
}
|
|
@ -1,54 +0,0 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<script>
|
||||
function addScript(src) {
|
||||
var script = document.createElement('script');
|
||||
script.src = src;
|
||||
var s = document.getElementsByTagName('script')[0]
|
||||
s.parentNode.insertBefore(script, s);
|
||||
return script;
|
||||
}
|
||||
|
||||
function addScripts(scripts, callback) {
|
||||
var loaded = {}; // Для загруженных файлов loaded[i] = true
|
||||
var counter = 0;
|
||||
|
||||
function onload(i) {
|
||||
if (loaded[i]) return; // лишний вызов onload/onreadystatechange
|
||||
loaded[i] = true;
|
||||
counter++;
|
||||
if (counter == scripts.length) callback();
|
||||
}
|
||||
|
||||
for (var i = 0; i < scripts.length; i++)(function(i) {
|
||||
var script = addScript(scripts[i]);
|
||||
|
||||
script.onload = function() {
|
||||
onload(i);
|
||||
};
|
||||
|
||||
script.onreadystatechange = function() { // IE8-
|
||||
if (this.readyState == 'loaded' || this.readyState == 'complete') {
|
||||
setTimeout(this.onload, 0); // возможны повторные вызовы onload
|
||||
}
|
||||
};
|
||||
|
||||
}(i));
|
||||
|
||||
}
|
||||
|
||||
addScripts(["a.js", "b.js", "c.js"], function() {
|
||||
a()
|
||||
});
|
||||
</script>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
|
@ -1,3 +0,0 @@
|
|||
function a() {
|
||||
b();
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
function b() {
|
||||
c();
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
function c() {
|
||||
alert('ok');
|
||||
}
|
|
@ -1,21 +0,0 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<script>
|
||||
/* ваш код */
|
||||
|
||||
// функция a() сработает только если загружены a.js, b.js, c.js
|
||||
addScripts(["a.js", "b.js", "c.js"], function() {
|
||||
a()
|
||||
});
|
||||
</script>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
|
@ -1,22 +0,0 @@
|
|||
importance: 5
|
||||
|
||||
---
|
||||
|
||||
# Скрипты с коллбэком
|
||||
|
||||
Создайте функцию `addScripts(scripts, callback)`, которая загружает скрипты из массива `scripts`, и *после загрузки и выполнения их всех* вызывает функцию `callback`.
|
||||
|
||||
Скрипт может быть любым, работа функции не должна зависеть от его содержимого.
|
||||
|
||||
Пример использования:
|
||||
|
||||
```js no-beautify
|
||||
addScripts(["a.js", "b.js", "c.js"], function() { a() });
|
||||
/* функция a() описана в a.js и использует b.js,c.js */
|
||||
```
|
||||
|
||||
- Ошибки загрузки обрабатывать не нужно.</li>
|
||||
- Один скрипт не ждёт другого. Они все загружаются, а по окончании вызывается обработчик `callback`.
|
||||
|
||||
Исходный код содержит скрипты `a.js`, `b.js`, `c.js`:
|
||||
|
|
@ -1,227 +1,89 @@
|
|||
# Resource loading: onload and onerror
|
||||
|
||||
Браузер позволяет отслеживать загрузку внешних ресурсов -- скриптов, ифреймов, картинок и других.
|
||||
The browser allows to track the loading of external resources -- scripts, iframes, pictures and so on.
|
||||
|
||||
Для этого есть два события:
|
||||
There are two events for it:
|
||||
|
||||
- `onload` -- если загрузка успешна.
|
||||
- `onerror` -- если при загрузке произошла ошибка.
|
||||
- `onload` -- successful load,
|
||||
- `onerror` -- an error occured.
|
||||
|
||||
## Загрузка SCRIPT
|
||||
## Loading a script
|
||||
|
||||
Рассмотрим следующую задачу.
|
||||
Let's say we need to call a function that resides in an external script.
|
||||
|
||||
В браузере работает сложный интерфейс и, чтобы создать очередной компонент, нужно загрузить скрипт с сервера.
|
||||
|
||||
Подгрузить внешний скрипт -- достаточно просто:
|
||||
We can load it dynamically, like this:
|
||||
|
||||
```js
|
||||
var script = document.createElement('script');
|
||||
let script = document.createElement('script');
|
||||
script.src = "my.js";
|
||||
|
||||
document.body.appendChild(script);
|
||||
document.head.append(script);
|
||||
```
|
||||
|
||||
...Но как после подгрузки выполнить функцию, которая объявлена в этом скрипте? Для этого нужно отловить момент окончания загрузки и выполнения тега `<script>`.
|
||||
...But how to run the function that is declared inside that script? We need to wait until the script loads, and only then we can call it.
|
||||
|
||||
### script.onload
|
||||
|
||||
Главным помощником станет событие `onload`. Оно сработает, когда скрипт загрузился и выполнился.
|
||||
The main helper is the `load` event. It triggers after the script was loaded and executed.
|
||||
|
||||
Например:
|
||||
For instance:
|
||||
|
||||
```js run
|
||||
var script = document.createElement('script');
|
||||
```js run untrusted
|
||||
let script = document.createElement('script');
|
||||
|
||||
// can load any script, from any domain
|
||||
script.src = "https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.3.0/lodash.js"
|
||||
document.body.appendChild(script);
|
||||
document.head.append(script);
|
||||
|
||||
*!*
|
||||
script.onload = function() {
|
||||
// после выполнения скрипта становится доступна функция _
|
||||
alert( _ ); // её код
|
||||
}
|
||||
// the script creates a helper function "_"
|
||||
alert(_); // the function is available
|
||||
};
|
||||
*/!*
|
||||
```
|
||||
|
||||
Это даёт возможность, как в примере выше, получить переменные из скрипта и выполнять объявленные в нём функции.
|
||||
So in `onload` we can use script variables, run functions etc.
|
||||
|
||||
...А что, если загрузка скрипта не удалась? Например, такого скрипта на сервере нет (ошибка 404) или сервер "упал" (ошибка 500).
|
||||
|
||||
Такую ситуацию тоже нужно как-то обрабатывать, хотя бы сообщить посетителю о возникшей проблеме.
|
||||
...And what if the loading failed? For instance, there's no such script (error 404) or the server or the server is down (unavailable).
|
||||
|
||||
### script.onerror
|
||||
|
||||
Любые ошибки загрузки (но не выполнения) скрипта отслеживаются обработчиком `onerror`.
|
||||
Errors that occur during the loading (but not execution) of the script can be tracked on `error` event.
|
||||
|
||||
Например, сделаем запрос заведомо отсутствующего скрипта:
|
||||
For instance, let's request a script that doesn't exist:
|
||||
|
||||
```js run
|
||||
var script = document.createElement('script');
|
||||
script.src = "https://example.com/404.js"
|
||||
document.body.appendChild(script);
|
||||
let script = document.createElement('script');
|
||||
script.src = "https://example.com/404.js"; // no such script
|
||||
document.head.append(script);
|
||||
|
||||
*!*
|
||||
script.onerror = function() {
|
||||
alert( "Ошибка: " + this.src );
|
||||
alert("Error loading " + this.src); // Error loading https://example.com/404.js
|
||||
};
|
||||
*/!*
|
||||
```
|
||||
|
||||
### IE8-: script.onreadystatechange [#onreadystatechange]
|
||||
Please note that we can't get error details here. We don't know was it error 404 or 500 or something else. Just that the loading failed.
|
||||
|
||||
Примеры выше работают во всех браузерах, кроме IE8-.
|
||||
## Other resources
|
||||
|
||||
В IE для отслеживания загрузки есть другое событие: `onreadystatechange`. Оно срабатывает многократно, при каждом обновлении состояния загрузки.
|
||||
The `load` and `error` events also work for other resources. There may be minor differences though.
|
||||
|
||||
Текущая стадия процесса находится в `script.readyState`:
|
||||
For instance:
|
||||
|
||||
`loading`
|
||||
: В процессе загрузки.
|
||||
|
||||
`loaded`
|
||||
: Получен ответ с сервера -- скрипт или ошибка. Скрипт на фазе `loaded` может быть ещё не выполнен.
|
||||
|
||||
`complete`
|
||||
: Скрипт выполнен.
|
||||
|
||||
Например, рабочий скрипт:
|
||||
|
||||
```js run no-beautify
|
||||
var script = document.createElement('script');
|
||||
script.src = "https://code.jquery.com/jquery.js";
|
||||
document.documentElement.appendChild(script);
|
||||
|
||||
*!*
|
||||
script.onreadystatechange = function() {
|
||||
alert(this.readyState); // loading -> loaded -> complete
|
||||
}
|
||||
*/!*
|
||||
```
|
||||
|
||||
Скрипт с ошибкой:
|
||||
|
||||
```js run no-beautify
|
||||
var script = document.createElement('script');
|
||||
script.src = "http://ajax.googleapis.com/404.js";
|
||||
document.documentElement.appendChild(script);
|
||||
|
||||
*!*
|
||||
script.onreadystatechange = function() {
|
||||
alert(this.readyState); // loading -> loaded
|
||||
}
|
||||
*/!*
|
||||
```
|
||||
|
||||
Обратим внимание на две особенности:
|
||||
|
||||
- **Стадии могут пропускаться.**
|
||||
|
||||
Если скрипт в кэше браузера -- он сразу даст `complete`. Вы можете увидеть это, если несколько раз запустите первый пример.
|
||||
- **Нет особой стадии для ошибки.**
|
||||
|
||||
В примере выше это видно, обработка останавливается на `loaded`.
|
||||
|
||||
Итак, самое надёжное средство для IE8- поймать загрузку (или ошибку загрузки) -- это повесить обработчик на событие `onreadystatechange`, который будет срабатывать и на стадии `complete` и на стадии `loaded`. Так как скрипт может быть ещё не выполнен к этому моменту, то вызов функции лучше сделать через `setTimeout(.., 0)`.
|
||||
|
||||
Пример ниже вызывает `afterLoad` после загрузки скрипта и работает только в IE:
|
||||
|
||||
```js run no-beautify
|
||||
var script = document.createElement('script');
|
||||
script.src = "https://code.jquery.com/jquery.js";
|
||||
document.documentElement.appendChild(script);
|
||||
|
||||
function afterLoad() {
|
||||
alert("Загрузка завершена: " + typeof(jQuery));
|
||||
}
|
||||
|
||||
*!*
|
||||
script.onreadystatechange = function() {
|
||||
if (this.readyState == "complete") { // на случай пропуска loaded
|
||||
afterLoad(); // (2)
|
||||
}
|
||||
|
||||
if (this.readyState == "loaded") {
|
||||
setTimeout(afterLoad, 0); // (1)
|
||||
|
||||
// убираем обработчик, чтобы не сработал на complete
|
||||
this.onreadystatechange = null;
|
||||
}
|
||||
}
|
||||
*/!*
|
||||
```
|
||||
|
||||
Вызов `(1)` выполнится при первой загрузке скрипта, а `(2)` -- при второй, когда он уже будет в кеше, и стадия станет сразу `complete`.
|
||||
|
||||
Функция `afterLoad` может и не обнаружить `jQuery`, если при загрузке была ошибка, причём не важно какая -- файл не найден или синтаксис скрипта ошибочен.
|
||||
|
||||
### Кросс-браузерное решение
|
||||
|
||||
Для кросс-браузерной обработки загрузки скрипта или её ошибки поставим обработчик на все три события: `onload`, `onerror`, `onreadystatechange`.
|
||||
|
||||
Пример ниже выполняет функцию `afterLoad` после загрузки скрипта *или* при ошибке.
|
||||
|
||||
Работает во всех браузерах:
|
||||
|
||||
```js run
|
||||
var script = document.createElement('script');
|
||||
script.src = "https://code.jquery.com/jquery.js";
|
||||
document.documentElement.appendChild(script);
|
||||
|
||||
function afterLoad() {
|
||||
alert( "Загрузка завершена: " + typeof(jQuery) );
|
||||
}
|
||||
|
||||
script.onload = script.onerror = function() {
|
||||
if (!this.executed) { // выполнится только один раз
|
||||
this.executed = true;
|
||||
afterLoad();
|
||||
}
|
||||
};
|
||||
|
||||
script.onreadystatechange = function() {
|
||||
var self = this;
|
||||
if (this.readyState == "complete" || this.readyState == "loaded") {
|
||||
setTimeout(function() {
|
||||
self.onload()
|
||||
}, 0); // сохранить "this" для onload
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
## Загрузка других ресурсов
|
||||
|
||||
Поддержка этих событий для других типов ресурсов различна:
|
||||
|
||||
`<img>`, `<link>` (стили)
|
||||
: Поддерживает `onload/onerror` во всех браузерах.
|
||||
`<img>`, `<link>` (external stylesheets)
|
||||
: Both `load` and `error` events work as expected.
|
||||
|
||||
`<iframe>`
|
||||
: Поддерживает `onload` во всех браузерах. Это событие срабатывает как при успешной загрузке, так и при ошибке.
|
||||
: Only `load` event when the iframe loading finished. It triggers both for successful load and in case of an error. That's for historical reasons.
|
||||
|
||||
Обратим внимание, что если `<iframe>` загружается с того же домена, то можно, используя `iframe.contentWindow.document` получить ссылку на документ и поставить обработчик `DOMContentLoaded`. А вот если `<iframe>` -- с другого домена, то так не получится, однако сработает `onload`.
|
||||
## Summary
|
||||
|
||||
## Итого
|
||||
Pictures `<img>`, external styles, scripts and other resources provide `load` and `error` events to track their loading:
|
||||
|
||||
В этой статье мы рассмотрели события `onload/onerror` для ресурсов.
|
||||
- `load` triggers on a successful load,
|
||||
- `error` triggers on a failed load.
|
||||
|
||||
Их можно обобщить, разделив на рецепты:
|
||||
|
||||
Отловить загрузку скрипта (включая ошибку)
|
||||
: Ставим обработчики на `onload` + `onerror` + (для IE8-) `onreadystatechange`, как указано в рецепте выше
|
||||
|
||||
Отловить загрузку картинки `<img>` или стиля `<link>`
|
||||
: Ставим обработчики на `onload` + `onerror`
|
||||
|
||||
```js no-beautify
|
||||
var img = document.createElement('img');
|
||||
img.onload = function() { alert("Успех "+this.src };
|
||||
img.onerror = function() { alert("Ошибка "+this.src };
|
||||
img.src = ...
|
||||
```
|
||||
|
||||
Изображения начинают загружаться сразу при создании, не нужно их для этого вставлять в HTML.
|
||||
|
||||
**Чтобы работало в IE8-, `src` нужно ставить *после* `onload/onerror`.**
|
||||
|
||||
Отловить загрузку `<iframe>`
|
||||
: Поддерживается только обработчик `onload`. Он сработает, когда `IFRAME` загрузится, со всеми подресурсами, а также в случае ошибки.
|
||||
The only exception is `<iframe>`: for historical reasons it always triggers `load`, for any load completion, even if the page is not found.
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
Решение:
|
||||
|
||||
```html run
|
||||
<select>
|
||||
<option value="Rock">Рок</option>
|
||||
<option value="Blues" selected>Блюз</option>
|
||||
</select>
|
||||
|
||||
<script>
|
||||
var select = document.body.children[0];
|
||||
|
||||
// 1)
|
||||
var selectedOption = select.options[select.selectedIndex];
|
||||
alert( selectedOption.value );
|
||||
|
||||
// 2)
|
||||
var newOption = new Option("Classic", "Классика");
|
||||
select.appendChild(newOption);
|
||||
|
||||
// 3)
|
||||
newOption.selected = true;
|
||||
</script>
|
||||
```
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
importance: 5
|
||||
|
||||
---
|
||||
|
||||
# Добавьте опцию к селекту
|
||||
|
||||
Есть селект:
|
||||
|
||||
```html
|
||||
<select>
|
||||
<option value="Rock">Рок</option>
|
||||
<option value="Blues" selected>Блюз</option>
|
||||
</select>
|
||||
```
|
||||
|
||||
При помощи JavaScript:
|
||||
|
||||
1. Выведите значение и текст текущей выбранной опции.
|
||||
2. Добавьте опцию: `<option value="Classic">Классика</option>`.
|
||||
3. Сделайте её выбранной.
|
||||
|
276
2-ui/4-forms-controls/1-form-elements/article.md
Normal file
276
2-ui/4-forms-controls/1-form-elements/article.md
Normal file
|
@ -0,0 +1,276 @@
|
|||
# Form properties and methods [todo]
|
||||
|
||||
Forms and control elements, such as `<input>` have a lot of special properties and events.
|
||||
|
||||
Working with forms can be much more convenient if we know them.
|
||||
|
||||
|
||||
[cut]
|
||||
|
||||
## Navigation: form and elements
|
||||
|
||||
Document forms are members of the special collection `document.forms`.
|
||||
|
||||
That's a *named* collection: we can use both the name and the number to get the form.
|
||||
|
||||
```js no-beautify
|
||||
document.forms.my - the form with name="my"
|
||||
document.forms[0] - the first form in the document
|
||||
```
|
||||
|
||||
When we have a form, then any element is available in the named collection `form.elements`.
|
||||
|
||||
For instance:
|
||||
|
||||
```html run height=40
|
||||
<form name="my">
|
||||
<input name="one" value="1">
|
||||
<input name="two" value="2">
|
||||
</form>
|
||||
|
||||
<script>
|
||||
// get the form
|
||||
let form = document.forms.my; // <form name="my"> element
|
||||
|
||||
// get the element
|
||||
let elem = form.elements.one; // <input name="one"> element
|
||||
|
||||
alert( elem.value ); // 1
|
||||
</script>
|
||||
```
|
||||
|
||||
There may be multiple elements with the same name, that's often the case with radio buttons.
|
||||
|
||||
In that case `form.elements[name]` is a collection, for instance:
|
||||
|
||||
```html run height=40
|
||||
<form>
|
||||
<input type="radio" *!*name="age"*/!* value="10">
|
||||
<input type="radio" *!*name="age"*/!* value="20">
|
||||
</form>
|
||||
|
||||
<script>
|
||||
let form = document.forms[0];
|
||||
|
||||
let ageElems = form.elements.age;
|
||||
|
||||
alert(ageElems[0].value); // 10, the first input value
|
||||
</script>
|
||||
```
|
||||
|
||||
These navigation properties do not depend on the tag structure. All elements, no matter how deep they are in the form, are available in `form.elements`.
|
||||
|
||||
|
||||
````smart header="Fieldsets as \"subforms\""
|
||||
A form may have one or many `<fieldset>` elements inside it. They also support the `elements` property.
|
||||
|
||||
For instance:
|
||||
|
||||
```html run height=80
|
||||
<body>
|
||||
<form id="form">
|
||||
<fieldset name="userFields">
|
||||
<legend>info</legend>
|
||||
<input name="login" type="text">
|
||||
</fieldset>
|
||||
</form>
|
||||
|
||||
<script>
|
||||
alert(form.elements.login); // <input name="login">
|
||||
|
||||
*!*
|
||||
let fieldset = form.elements.userFields;
|
||||
alert(fieldset); // HTMLFieldSetElement
|
||||
|
||||
// we can get the input both from the form and from the fieldset
|
||||
alert(fieldset.elements.login == form.elements.login); // true
|
||||
*/!*
|
||||
</script>
|
||||
</body>
|
||||
```
|
||||
````
|
||||
|
||||
````warn header="Shorter notation: `form.name`"
|
||||
There's a shorter notation: we can access the element as `form[index/name]`.
|
||||
|
||||
Instead of `form.elements.login` we can write `form.login`.
|
||||
|
||||
That also works, but there's a minor issue: if we access an element, and then change its `name`, then it is still available under the old name (as well as under the new one).
|
||||
|
||||
That's easy to see in an example:
|
||||
|
||||
```html run height=40
|
||||
<form id="form">
|
||||
<input name="login">
|
||||
</form>
|
||||
|
||||
<script>
|
||||
alert(form.elements.login == form.login); // true, the same <input>
|
||||
|
||||
form.login.name = "username"; // change the name of the input
|
||||
|
||||
// form.elements updated the name:
|
||||
alert(form.elements.login); // undefined
|
||||
alert(form.elements.username); // input
|
||||
|
||||
*!*
|
||||
// the direct access now can use both names: the new one and the old one
|
||||
alert(form.username == form.login); // true
|
||||
*/!*
|
||||
</script>
|
||||
```
|
||||
|
||||
That's usually not a problem, because we rarely change names of form elements.
|
||||
|
||||
````
|
||||
|
||||
## Backreference: element.form
|
||||
|
||||
For any element, the form is available as `element.form`. So a form references all elements, and elements
|
||||
reference the form.
|
||||
|
||||
Here's the picture:
|
||||
|
||||

|
||||
|
||||
For instance:
|
||||
|
||||
```html run height=40
|
||||
<form id="form">
|
||||
<input type="text" name="login">
|
||||
</form>
|
||||
|
||||
<script>
|
||||
*!*
|
||||
// form -> element
|
||||
let login = form.login;
|
||||
|
||||
// element -> form
|
||||
alert(login.form); // HTMLFormElement
|
||||
*/!*
|
||||
</script>
|
||||
```
|
||||
|
||||
## Values: input and textarea
|
||||
|
||||
Normally, we can read the value as `input.value` or `input.checked` (for radio)
|
||||
|
||||
Для большинства типов `input` значение ставится/читается через свойство `value`.
|
||||
|
||||
```js
|
||||
input.value = "Новое значение";
|
||||
textarea.value = "Новый текст";
|
||||
```
|
||||
|
||||
```warn header="Не используйте `textarea.innerHTML`"
|
||||
Для элементов `textarea` также доступно свойство `innerHTML`, но лучше им не пользоваться: оно хранит только HTML, изначально присутствовавший в элементе, и не меняется при изменении значения.
|
||||
```
|
||||
|
||||
Исключения -- `input type="checkbox"` и `input type="radio"`
|
||||
|
||||
**Текущее "отмеченное" состояние для `checkbox` и `radio` находится в свойстве `checked` (`true/false`).**
|
||||
|
||||
```js
|
||||
if (input.checked) {
|
||||
alert( "Чекбокс выбран" );
|
||||
}
|
||||
```
|
||||
|
||||
## Элементы select и option
|
||||
|
||||
Селект в JavaScript можно установить двумя путями: поставив значение `select.value`, либо установив свойство `select.selectedIndex` в номер нужной опции.:
|
||||
|
||||
```js
|
||||
select.selectedIndex = 0; // первая опция
|
||||
```
|
||||
|
||||
Установка `selectedIndex = -1` очистит выбор.
|
||||
|
||||
**Список элементов-опций доступен через `select.options`.**
|
||||
|
||||
Если `select` допускает множественный выбор (атрибут `multiple`), то значения можно получить/установить, сделав цикл по `select.options`. При этом выбранные опции будут иметь свойство `option.selected = true`.
|
||||
|
||||
Пример:
|
||||
|
||||
```html run
|
||||
<form name="form">
|
||||
<select name="genre" *!*multiple*/!*>
|
||||
<option value="blues" selected>Мягкий блюз</option>
|
||||
<option value="rock" selected>Жёсткий рок</option>
|
||||
<option value="classic">Классика</option>
|
||||
</select>
|
||||
</form>
|
||||
|
||||
<script>
|
||||
var form = document.forms[0];
|
||||
var select = form.elements.genre;
|
||||
|
||||
for (var i = 0; i < select.options.length; i++) {
|
||||
var option = select.options[i];
|
||||
*!*
|
||||
if(option.selected) {
|
||||
alert( option.value );
|
||||
}
|
||||
*/!*
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
Спецификация: [the select element](https://html.spec.whatwg.org/multipage/forms.html#the-select-element).
|
||||
|
||||
````smart header="`new Option`"
|
||||
В стандарте [the option element](https://html.spec.whatwg.org/multipage/forms.html#the-option-element) есть любопытный короткий синтаксис для создания элемента с тегом `option`:
|
||||
|
||||
```js
|
||||
option = new Option(text, value, defaultSelected, selected);
|
||||
```
|
||||
|
||||
Параметры:
|
||||
|
||||
- `text` -- содержимое,
|
||||
- `value` -- значение,
|
||||
- `defaultSelected` и `selected` поставьте в `true`, чтобы сделать элемент выбранным.
|
||||
|
||||
Его можно использовать вместо `document.createElement('option')`, например:
|
||||
|
||||
```js
|
||||
var option = new Option("Текст", "value");
|
||||
// создаст <option value="value">Текст</option>
|
||||
```
|
||||
|
||||
Такой же элемент, но выбранный:
|
||||
|
||||
```js
|
||||
var option = new Option("Текст", "value", true, true);
|
||||
```
|
||||
````
|
||||
|
||||
```smart header="Дополнительные свойства `option`"
|
||||
У элементов `option` также есть особые свойства, которые могут оказаться полезными (см. [the option element](https://html.spec.whatwg.org/multipage/forms.html#the-option-element)):
|
||||
|
||||
`selected`
|
||||
: выбрана ли опция
|
||||
|
||||
`index`
|
||||
: номер опции в списке селекта
|
||||
|
||||
`text`
|
||||
: Текстовое содержимое опции (то, что видит посетитель).
|
||||
```
|
||||
|
||||
## Итого
|
||||
|
||||
Свойства для навигации по формам:
|
||||
|
||||
`document.forms`
|
||||
: Форму можно получить как `document.forms[name/index]`.
|
||||
|
||||
`form.elements`
|
||||
: Элементы в форме: `form.elements[name/index]`. Каждый элемент имеет ссылку на форму в свойстве `form`. Свойство `elements` также есть у `<fieldset>`.
|
||||
|
||||
Значение элементов читается/ставится через `value` или `checked`.
|
||||
|
||||
Для элемента `select` можно задать опцию по номеру через `select.selectedIndex` и перебрать опции через `select.options`. При этом выбранные опции (в том числе при мультиселекте) будут иметь свойство `option.selected = true`.
|
||||
|
||||
|
||||
Спецификация: [HTML5 Forms](https://html.spec.whatwg.org/multipage/forms.html).
|
BIN
2-ui/4-forms-controls/1-form-elements/form-navigation.png
Normal file
BIN
2-ui/4-forms-controls/1-form-elements/form-navigation.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 13 KiB |
BIN
2-ui/4-forms-controls/1-form-elements/form-navigation@2x.png
Normal file
BIN
2-ui/4-forms-controls/1-form-elements/form-navigation@2x.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 29 KiB |
|
@ -0,0 +1,3 @@
|
|||
В данном случае достаточно событий `input.focus/input.blur`.
|
||||
|
||||
Если бы мы хотели реализовать это на уровне документа, то применили бы делегирование и события `focusin/focusout` (эмуляцию для firefox), так как обычные `focus/blur` не всплывают.
|
|
@ -0,0 +1,74 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<style>
|
||||
.placeholder {
|
||||
color: blue;
|
||||
font-family: Georgia;
|
||||
}
|
||||
|
||||
.placeholder-tooltip {
|
||||
color: blue;
|
||||
font-family: Georgia;
|
||||
position: fixed;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<p>Красивый placeholder:</p>
|
||||
|
||||
<input type="email" data-placeholder="E-mail">
|
||||
|
||||
|
||||
<script>
|
||||
var input = document.querySelector('[data-placeholder]');
|
||||
|
||||
showPlaceholder(input);
|
||||
|
||||
// Показать placeholder внутри input
|
||||
// Также можно сделать это при помощи вёрстки, отдельным элементом
|
||||
function showPlaceholder(input) {
|
||||
input.classList.add('placeholder');
|
||||
input.value = input.dataset.placeholder;
|
||||
}
|
||||
|
||||
// Показать подсказку над элементом (будет вместо placeholder)
|
||||
function showTooltip(input) {
|
||||
var tooltip = document.createElement('span');
|
||||
tooltip.innerHTML = input.dataset.placeholder;
|
||||
tooltip.className = 'placeholder-tooltip';
|
||||
tooltip.style.fontSize = getComputedStyle(input).fontSize;
|
||||
tooltip.style.left = input.getBoundingClientRect().left + 'px';
|
||||
document.body.appendChild(tooltip);
|
||||
tooltip.style.top = input.getBoundingClientRect().top - tooltip.offsetHeight - 4 + 'px';
|
||||
input.tooltip = tooltip;
|
||||
}
|
||||
|
||||
input.onfocus = function() {
|
||||
if (input.classList.contains('placeholder')) {
|
||||
input.classList.remove('placeholder');
|
||||
input.value = '';
|
||||
}
|
||||
|
||||
showTooltip(input);
|
||||
};
|
||||
|
||||
input.onblur = function() {
|
||||
document.body.removeChild(input.tooltip);
|
||||
delete input.tooltip;
|
||||
|
||||
// показываем placeholder обратно, если input пуст
|
||||
if (input.value == '') {
|
||||
showPlaceholder(input);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
|
@ -0,0 +1,48 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<style>
|
||||
/* стиль для input с плейсхолдером */
|
||||
|
||||
.placeholder {
|
||||
color: blue;
|
||||
font-family: Georgia;
|
||||
}
|
||||
/* стиль для подсказки над элементом (вместо плейсхолдера при фокусировке) */
|
||||
|
||||
.placeholder-tooltip {
|
||||
color: blue;
|
||||
font-family: Georgia;
|
||||
position: fixed;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<p>Красивый placeholder:</p>
|
||||
|
||||
<input type="email" data-placeholder="E-mail">
|
||||
|
||||
|
||||
<script>
|
||||
var input = document.querySelector('[data-placeholder]');
|
||||
|
||||
showPlaceholder(input);
|
||||
|
||||
// Показать placeholder внутри input
|
||||
// Также можно сделать это при помощи вёрстки, отдельным элементом
|
||||
function showPlaceholder(input) {
|
||||
input.classList.add('placeholder');
|
||||
input.value = input.dataset.placeholder;
|
||||
}
|
||||
|
||||
// ...ваш код для input...
|
||||
</script>
|
||||
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
|
@ -0,0 +1,20 @@
|
|||
importance: 5
|
||||
|
||||
---
|
||||
|
||||
# Улучшенный плейсхолдер
|
||||
|
||||
Реализуйте более удобный плейсхолдер-подсказку на JavaScript через атрибут `data-placeholder`.
|
||||
|
||||
Правила работы плейсхолдера:
|
||||
|
||||
- Элемент изначально содержит плейсхолдер. Специальный класс `placeholder` придает ему синий цвет.
|
||||
- При фокусировке плейсхолдер показывается уже над полем, становясь "подсказкой".
|
||||
- При снятии фокуса, подсказка убирается, если поле пустое -- плейсхолдер возвращается в него.
|
||||
|
||||
Демо:
|
||||
|
||||
[iframe src="solution" height=100]
|
||||
|
||||
В этой задаче плейсхолдер должен работать на одном конкретном input. Подумайте, если input много, как здесь применить делегирование?
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
|
||||
Нам нужно ловить `onclick` на мышонке и в `onkeydown` на нём смотреть коды символов. При скан-кодах стрелок двигать мышонка через `position:absolute` или `position:fixed`.
|
||||
|
||||
Скан-коды для клавиш стрелок можно узнать, нажимая на них на [тестовом стенде](info:keyboard-events#keyboard-test-stand). Вот они: 37-38-39-40 (влево-вверх-вправо-вниз).
|
||||
|
||||
Проблема может возникнуть одна -- `keydown` не возникает на элементе, если на нём нет фокуса.
|
||||
|
||||
Чтобы фокус был -- нужно добавить мышонку атрибут `tabindex` через JS или в HTML.
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<style>
|
||||
#mouse {
|
||||
display: inline-block;
|
||||
cursor: pointer;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#mouse:focus {
|
||||
outline: 1px dashed black;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<p>Кликните на мышонка и передвигайте его, нажимая клавиши со стрелками.</p>
|
||||
|
||||
<pre id="mouse" tabindex="0">
|
||||
_ _
|
||||
(q\_/p)
|
||||
/. .\
|
||||
=\_t_/= __
|
||||
/ \ (
|
||||
(( )) )
|
||||
/\) (/\ /
|
||||
\ Y /-'
|
||||
nn^nn
|
||||
</pre>
|
||||
|
||||
|
||||
<script>
|
||||
document.getElementById('mouse').onclick = function() {
|
||||
this.style.left = this.getBoundingClientRect().left + 'px';
|
||||
this.style.top = this.getBoundingClientRect().top + 'px';
|
||||
|
||||
this.style.position = 'fixed';
|
||||
};
|
||||
|
||||
|
||||
document.getElementById('mouse').onkeydown = function(e) {
|
||||
switch (e.keyCode) {
|
||||
case 37: // влево
|
||||
this.style.left = parseInt(this.style.left) - this.offsetWidth + 'px';
|
||||
return false;
|
||||
case 38: // вверх
|
||||
this.style.top = parseInt(this.style.top) - this.offsetHeight + 'px';
|
||||
return false;
|
||||
case 39: // вправо
|
||||
this.style.left = parseInt(this.style.left) + this.offsetWidth + 'px';
|
||||
return false;
|
||||
case 40: // вниз
|
||||
this.style.top = parseInt(this.style.top) + this.offsetHeight + 'px';
|
||||
return false;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
|
@ -0,0 +1,42 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<style>
|
||||
#mouse {
|
||||
display: inline-block;
|
||||
cursor: pointer;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#mouse:focus {
|
||||
outline: 1px dashed black;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<p>Кликните на мышонка и передвигайте его, нажимая клавиши со стрелками.</p>
|
||||
|
||||
<pre id="mouse">
|
||||
_ _
|
||||
(q\_/p)
|
||||
/. .\
|
||||
=\_t_/= __
|
||||
/ \ (
|
||||
(( )) )
|
||||
/\) (/\ /
|
||||
\ Y /-'
|
||||
nn^nn
|
||||
</pre>
|
||||
|
||||
|
||||
<script>
|
||||
// ваш код
|
||||
</script>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
13
2-ui/4-forms-controls/2-focus-blur/2-keyboard-mouse/task.md
Normal file
13
2-ui/4-forms-controls/2-focus-blur/2-keyboard-mouse/task.md
Normal file
|
@ -0,0 +1,13 @@
|
|||
importance: 4
|
||||
|
||||
---
|
||||
|
||||
# Мышонок на "клавиатурном" приводе
|
||||
|
||||
Кликните по мышонку. Затем нажимайте клавиши со стрелками, и он будет двигаться.
|
||||
|
||||
[demo src="solution"]
|
||||
|
||||
В этой задаче запрещается ставить обработчики куда-либо, кроме элемента `#mouse`.
|
||||
|
||||
Можно изменять атрибуты и классы в HTML.
|
99
2-ui/4-forms-controls/2-focus-blur/3-hotkeys/solution.md
Normal file
99
2-ui/4-forms-controls/2-focus-blur/3-hotkeys/solution.md
Normal file
|
@ -0,0 +1,99 @@
|
|||
# CSS для решения
|
||||
|
||||
Как видно из исходного кода, `#view` -- это `<div>`, который будет содержать результат, а `#area` - это редактируемое текстовое поле.
|
||||
|
||||
Так как мы преобразуем `<div>` в `<textarea>` и обратно, нам нужно сделать их практически одинаковыми с виду:
|
||||
|
||||
```css
|
||||
#view,
|
||||
#area {
|
||||
height: 150px;
|
||||
width: 400px;
|
||||
font-family: arial;
|
||||
font-size: 14px;
|
||||
}
|
||||
```
|
||||
|
||||
Текстовое поле нужно как-то выделить. Можно добавить границу, но тогда изменится блок: он увеличится в размерах и немного съедет текст.
|
||||
|
||||
Для того, чтобы сделать размер `#area` таким же, как и `#view`, добавим поля(padding):
|
||||
|
||||
```css
|
||||
#view {
|
||||
/* padding + border = 3px */
|
||||
|
||||
padding: 2px;
|
||||
border: 1px solid black;
|
||||
}
|
||||
```
|
||||
|
||||
CSS для `#area` заменяет поля границами:
|
||||
|
||||
```css
|
||||
#area {
|
||||
border: 3px groove blue;
|
||||
padding: 0px;
|
||||
display: none;
|
||||
}
|
||||
```
|
||||
|
||||
По умолчанию, текстовое поле скрыто. Кстати, этот код убирает дополнительную рамку в ряде браузеров, которая появляется вокруг поля, когда на него попадает фокус:
|
||||
|
||||
```css
|
||||
/*+ no-beautify */
|
||||
#area:focus {
|
||||
outline: none; /* убирает рамку при фокусе */
|
||||
}
|
||||
```
|
||||
|
||||
# Горячие клавиши
|
||||
|
||||
Чтобы отследить горячие клавиши, нам нужны их скан-коды, а не символы. Это важно, потому что горячие клавиши должны работать независимо от языковой раскладки. Поэтому, мы будем использовать <code>keydown</code>:
|
||||
|
||||
```js
|
||||
document.onkeydown = function(e) {
|
||||
if (e.keyCode == 27) { // escape
|
||||
cancel();
|
||||
return false;
|
||||
}
|
||||
|
||||
if ((e.ctrlKey && e.keyCode == 'E'.charCodeAt(0)) && !area.offsetHeight) {
|
||||
edit();
|
||||
return false;
|
||||
}
|
||||
|
||||
if ((e.ctrlKey && e.keyCode == 'S'.charCodeAt(0)) && area.offsetHeight) {
|
||||
save();
|
||||
return false;
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
В примере выше, `offsetHeight` используется для того, чтобы проверить, отображается элемент или нет. Это очень надежный способ для всех элементов, кроме `<tr>` в некоторых старых браузерах.
|
||||
|
||||
В отличие от простой проверки `display=='none'`, этот способ работает с элементом, спрятанным с помощью стилей, а так же для элементов, у которых скрыты родители.
|
||||
|
||||
# Редактирование
|
||||
|
||||
Следующие функции переключают режимы. HTML-код разрешен, поэтому возможна прямая трансформация в `<textarea>` и обратно.
|
||||
|
||||
```js
|
||||
function edit() {
|
||||
view.style.display = 'none';
|
||||
area.value = view.innerHTML;
|
||||
area.style.display = 'block';
|
||||
area.focus();
|
||||
}
|
||||
|
||||
function save() {
|
||||
area.style.display = 'none';
|
||||
view.innerHTML = area.value;
|
||||
view.style.display = 'block';
|
||||
}
|
||||
|
||||
function cancel() {
|
||||
area.style.display = 'none';
|
||||
view.style.display = 'block';
|
||||
}
|
||||
```
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<link type="text/css" rel="stylesheet" href="my.css">
|
||||
<meta charset="utf-8">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<ul>
|
||||
<li>Ctrl-E для начала редактирования.</li>
|
||||
<li>Во время редактирования: Ctrl-S для сохранения, Esc для отмены.</li>
|
||||
</ul>
|
||||
|
||||
HTML разрешён.
|
||||
|
||||
<textarea id="area"></textarea>
|
||||
<div id="view">Текст</div>
|
||||
|
||||
<script>
|
||||
document.onkeydown = function(e) {
|
||||
if (e.keyCode == 27) { // escape
|
||||
cancel();
|
||||
return false;
|
||||
}
|
||||
|
||||
if ((e.ctrlKey && e.keyCode == 'E'.charCodeAt(0)) && !area.offsetHeight) {
|
||||
edit();
|
||||
return false;
|
||||
}
|
||||
|
||||
if ((e.ctrlKey && e.keyCode == 'S'.charCodeAt(0)) && area.offsetHeight) {
|
||||
save();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function edit() {
|
||||
view.style.display = 'none';
|
||||
area.value = view.innerHTML;
|
||||
area.style.display = 'block';
|
||||
area.focus();
|
||||
}
|
||||
|
||||
function save() {
|
||||
area.style.display = 'none';
|
||||
view.innerHTML = area.value;
|
||||
view.style.display = 'block';
|
||||
}
|
||||
|
||||
function cancel() {
|
||||
area.style.display = 'none';
|
||||
view.style.display = 'block';
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
|
@ -0,0 +1,27 @@
|
|||
#view,
|
||||
#area {
|
||||
height: 150px;
|
||||
width: 400px;
|
||||
font-family: arial;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
#view {
|
||||
/* padding + border = 3px */
|
||||
|
||||
padding: 2px;
|
||||
border: 1px solid black;
|
||||
}
|
||||
|
||||
#area {
|
||||
display: none;
|
||||
/* replace padding with border (still 3px not to shift the contents) */
|
||||
|
||||
border: 3px groove blue;
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
#area:focus {
|
||||
outline: none;
|
||||
/* remove focus border in Safari */
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<link type="text/css" rel="stylesheet" href="my.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<ul>
|
||||
<li>Ctrl-E to start editing.</li>
|
||||
<li>While editing: Ctrl-S to save, Esc to cancel.</li>
|
||||
</ul>
|
||||
|
||||
|
||||
<textarea id="area"></textarea>
|
||||
<div id="view">Text</div>
|
||||
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
|
@ -0,0 +1,27 @@
|
|||
#view,
|
||||
#area {
|
||||
height: 150px;
|
||||
width: 400px;
|
||||
font-family: arial;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
#view {
|
||||
/* padding + border = 3px */
|
||||
|
||||
padding: 2px;
|
||||
border: 1px solid black;
|
||||
}
|
||||
|
||||
#area {
|
||||
display: none;
|
||||
/* replace padding with border (still 3px not to shift the contents) */
|
||||
|
||||
border: 3px groove blue;
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
#area:focus {
|
||||
outline: none;
|
||||
/* remove focus border in Safari */
|
||||
}
|
14
2-ui/4-forms-controls/2-focus-blur/3-hotkeys/task.md
Normal file
14
2-ui/4-forms-controls/2-focus-blur/3-hotkeys/task.md
Normal file
|
@ -0,0 +1,14 @@
|
|||
importance: 5
|
||||
|
||||
---
|
||||
|
||||
# Горячие клавиши
|
||||
|
||||
Создайте `<div>`, который при нажатии `key:Ctrl+E` превращается в `<textarea>`.
|
||||
|
||||
Изменения, внесенные в поле, можно сохранить обратно в `<div>` сочетанием клавиш `key:Ctrl+S`, при этом `<div>` получит в виде HTML содержимое `<textarea>`.
|
||||
|
||||
Если же нажать `key:Esc`, то `<textarea>` снова превращается в `<div>`, изменения не сохраняются.
|
||||
|
||||
[demo src="solution"].
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
|
||||
1. При клике -- заменяем `innerHTML` ячейки на `<textarea>` с размерами "под ячейку", без рамки.
|
||||
2. В `textarea.value` присваиваем содержимое ячейки.
|
||||
3. Фокусируем посетителя на ячейке вызовом `focus()`.
|
||||
4. Показываем кнопки OK/CANCEL под ячейкой.
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
/* общие стили для таблицы */
|
||||
|
||||
th {
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
td {
|
||||
width: 150px;
|
||||
white-space: nowrap;
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.nw {
|
||||
background-color: #999;
|
||||
}
|
||||
|
||||
.n {
|
||||
background-color: #03f;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.ne {
|
||||
background-color: #ff6;
|
||||
}
|
||||
|
||||
.w {
|
||||
background-color: #ff0;
|
||||
}
|
||||
|
||||
.c {
|
||||
background-color: #60c;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.e {
|
||||
background-color: #09f;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.sw {
|
||||
background-color: #963;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.s {
|
||||
background-color: #f60;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.se {
|
||||
background-color: #0c3;
|
||||
color: #fff;
|
||||
}
|
|
@ -0,0 +1,78 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<link rel="stylesheet" href="bagua.css">
|
||||
<link rel="stylesheet" href="my.css">
|
||||
|
||||
|
||||
<p>Кликните на ячейке для начала редактирования. Когда закончите -- нажмите OK или CANCEL.</p>
|
||||
|
||||
<table id="bagua-table">
|
||||
<tr>
|
||||
<th colspan="3"><em>Bagua</em> Chart: Direction, Element, Color, Meaning</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="nw"><strong>Northwest</strong>
|
||||
<br>Metal
|
||||
<br>Silver
|
||||
<br>Elders
|
||||
</td>
|
||||
<td class="n"><strong>North</strong>
|
||||
<br>Water
|
||||
<br>Blue
|
||||
<br>Change
|
||||
</td>
|
||||
<td class="ne"><strong>Northeast</strong>
|
||||
<br>Earth
|
||||
<br>Yellow
|
||||
<br>Direction
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="w"><strong>West</strong>
|
||||
<br>Metal
|
||||
<br>Gold
|
||||
<br>Youth
|
||||
</td>
|
||||
<td class="c"><strong>Center</strong>
|
||||
<br>All
|
||||
<br>Purple
|
||||
<br>Harmony
|
||||
</td>
|
||||
<td class="e"><strong>East</strong>
|
||||
<br>Wood
|
||||
<br>Blue
|
||||
<br>Future
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="sw"><strong>Southwest</strong>
|
||||
<br>Earth
|
||||
<br>Brown
|
||||
<br>Tranquility
|
||||
</td>
|
||||
<td class="s"><strong>South</strong>
|
||||
<br>Fire
|
||||
<br>Orange
|
||||
<br>Fame
|
||||
</td>
|
||||
<td class="se"><strong>Southeast</strong>
|
||||
<br>Wood
|
||||
<br>Green
|
||||
<br>Romance
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
|
||||
|
||||
<script src="script.js"></script>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
|
@ -0,0 +1,23 @@
|
|||
.edit-td .edit-area {
|
||||
border: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: block;
|
||||
resize: none;
|
||||
/* remove resizing handle in Firefox */
|
||||
|
||||
outline: none;
|
||||
/* remove outline on focus in Chrome */
|
||||
|
||||
overflow: auto;
|
||||
/* remove scrollbar in IE */
|
||||
}
|
||||
|
||||
.edit-controls {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.edit-td {
|
||||
position: relative;
|
||||
padding: 0;
|
||||
}
|
|
@ -0,0 +1,62 @@
|
|||
var table = document.getElementById('bagua-table');
|
||||
|
||||
var editingTd;
|
||||
|
||||
table.onclick = function(event) {
|
||||
|
||||
var target = event.target;
|
||||
|
||||
while (target != table) {
|
||||
if (target.className == 'edit-cancel') {
|
||||
finishTdEdit(editingTd.elem, false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (target.className == 'edit-ok') {
|
||||
finishTdEdit(editingTd.elem, true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (target.nodeName == 'TD') {
|
||||
if (editingTd) return; // already editing
|
||||
|
||||
makeTdEditable(target);
|
||||
return;
|
||||
}
|
||||
|
||||
target = target.parentNode;
|
||||
}
|
||||
}
|
||||
|
||||
function makeTdEditable(td) {
|
||||
editingTd = {
|
||||
elem: td,
|
||||
data: td.innerHTML
|
||||
};
|
||||
|
||||
td.classList.add('edit-td'); // td, not textarea! the rest of rules will cascade
|
||||
|
||||
var textArea = document.createElement('textarea');
|
||||
textArea.style.width = td.clientWidth + 'px';
|
||||
textArea.style.height = td.clientHeight + 'px';
|
||||
textArea.className = 'edit-area';
|
||||
|
||||
textArea.value = td.innerHTML;
|
||||
td.innerHTML = '';
|
||||
td.appendChild(textArea);
|
||||
textArea.focus();
|
||||
|
||||
td.insertAdjacentHTML("beforeEnd",
|
||||
'<div class="edit-controls"><button class="edit-ok">OK</button><button class="edit-cancel">CANCEL</button></div>'
|
||||
);
|
||||
}
|
||||
|
||||
function finishTdEdit(td, isOk) {
|
||||
if (isOk) {
|
||||
td.innerHTML = td.firstChild.value;
|
||||
} else {
|
||||
td.innerHTML = editingTd.data;
|
||||
}
|
||||
td.classList.remove('edit-td'); // remove edit class
|
||||
editingTd = null;
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
/* общие стили для таблицы */
|
||||
|
||||
th {
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
td {
|
||||
width: 150px;
|
||||
white-space: nowrap;
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.nw {
|
||||
background-color: #999;
|
||||
}
|
||||
|
||||
.n {
|
||||
background-color: #03f;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.ne {
|
||||
background-color: #ff6;
|
||||
}
|
||||
|
||||
.w {
|
||||
background-color: #ff0;
|
||||
}
|
||||
|
||||
.c {
|
||||
background-color: #60c;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.e {
|
||||
background-color: #09f;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.sw {
|
||||
background-color: #963;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.s {
|
||||
background-color: #f60;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.se {
|
||||
background-color: #0c3;
|
||||
color: #fff;
|
||||
}
|
|
@ -0,0 +1,78 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<link rel="stylesheet" href="bagua.css">
|
||||
<link rel="stylesheet" href="my.css">
|
||||
|
||||
|
||||
<p>Кликните на ячейке для начала редактирования. Когда закончите -- нажмите OK или CANCEL.</p>
|
||||
|
||||
<table id="bagua-table">
|
||||
<tr>
|
||||
<th colspan="3"><em>Bagua</em> Chart: Direction, Element, Color, Meaning</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="nw"><strong>Northwest</strong>
|
||||
<br>Metal
|
||||
<br>Silver
|
||||
<br>Elders
|
||||
</td>
|
||||
<td class="n"><strong>North</strong>
|
||||
<br>Water
|
||||
<br>Blue
|
||||
<br>Change
|
||||
</td>
|
||||
<td class="ne"><strong>Northeast</strong>
|
||||
<br>Earth
|
||||
<br>Yellow
|
||||
<br>Direction
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="w"><strong>West</strong>
|
||||
<br>Metal
|
||||
<br>Gold
|
||||
<br>Youth
|
||||
</td>
|
||||
<td class="c"><strong>Center</strong>
|
||||
<br>All
|
||||
<br>Purple
|
||||
<br>Harmony
|
||||
</td>
|
||||
<td class="e"><strong>East</strong>
|
||||
<br>Wood
|
||||
<br>Blue
|
||||
<br>Future
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="sw"><strong>Southwest</strong>
|
||||
<br>Earth
|
||||
<br>Brown
|
||||
<br>Tranquility
|
||||
</td>
|
||||
<td class="s"><strong>South</strong>
|
||||
<br>Fire
|
||||
<br>Orange
|
||||
<br>Fame
|
||||
</td>
|
||||
<td class="se"><strong>Southeast</strong>
|
||||
<br>Wood
|
||||
<br>Green
|
||||
<br>Romance
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
|
||||
|
||||
<script src="script.js"></script>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
|
@ -0,0 +1 @@
|
|||
/* ваши стили */
|
|
@ -0,0 +1,3 @@
|
|||
var table = document.getElementById('bagua-table');
|
||||
|
||||
/* ваш код */
|
16
2-ui/4-forms-controls/2-focus-blur/4-edit-td-click/task.md
Normal file
16
2-ui/4-forms-controls/2-focus-blur/4-edit-td-click/task.md
Normal file
|
@ -0,0 +1,16 @@
|
|||
importance: 5
|
||||
|
||||
---
|
||||
|
||||
# Редактирование TD по клику
|
||||
|
||||
Сделать ячейки таблицы `td` редактируемыми по клику.
|
||||
|
||||
- При клике -- ячейка `<td>` превращается в редактируемую, можно менять HTML. Размеры ячеек при этом не должны меняться.
|
||||
- В один момент может редактироваться одна ячейка.
|
||||
- При редактировании под ячейкой появляются кнопки для приема и отмена редактирования, только клик на них заканчивает редактирование.
|
||||
|
||||
Демо:
|
||||
|
||||
[iframe src="solution"]
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
# Вёрстка
|
||||
|
||||
Для вёрстки можно использовать отрицательный `margin` у текста с подсказкой.
|
||||
|
||||
Решение в плане вёрстка есть в решении задачи <info:task/position-text-into-input>.
|
||||
|
||||
# Решение
|
||||
|
||||
```js
|
||||
placeholder.onclick = function() {
|
||||
input.focus();
|
||||
}
|
||||
|
||||
// onfocus сработает и вызове input.focus() и при клике на input
|
||||
input.onfocus = function() {
|
||||
if (placeholder.parentNode) {
|
||||
placeholder.parentNode.removeChild(placeholder);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<link rel="stylesheet" href="style.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<div>Добро пожаловать</div>
|
||||
<input type="password" id="input">
|
||||
|
||||
<div id="placeholder">Скажи пароль, друг</div>
|
||||
|
||||
<div>.. и заходи</div>
|
||||
|
||||
<script>
|
||||
var placeholder = document.getElementById('placeholder');
|
||||
var input = document.getElementById('input');
|
||||
|
||||
placeholder.onclick = function() {
|
||||
input.focus();
|
||||
}
|
||||
|
||||
input.onfocus = function() {
|
||||
placeholder.parentNode && placeholder.parentNode.removeChild(placeholder);
|
||||
}
|
||||
</script>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
|
@ -0,0 +1,17 @@
|
|||
body {
|
||||
font: 14px/14px Arial, sans-serif;
|
||||
}
|
||||
|
||||
input {
|
||||
font: 14px/14px Arial, sans-serif;
|
||||
width: 12em;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#placeholder {
|
||||
font: 14px/14px Arial, sans-serif;
|
||||
position: absolute;
|
||||
margin: -1.2em 0 0 0.2em;
|
||||
color: red;
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<link rel="stylesheet" href="style.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
|
||||
<div>Добро пожаловать</div>
|
||||
<input type="password" id="input">
|
||||
|
||||
<div id="placeholder">Скажи пароль, друг</div>
|
||||
|
||||
<div>.. и заходи</div>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
|
@ -0,0 +1,17 @@
|
|||
body {
|
||||
font: 14px/14px Arial, sans-serif;
|
||||
}
|
||||
|
||||
input {
|
||||
font: 14px/14px Arial, sans-serif;
|
||||
width: 12em;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#placeholder {
|
||||
font: 14px/14px Arial, sans-serif;
|
||||
position: absolute;
|
||||
margin: -1.2em 0 0 0.2em;
|
||||
color: red;
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
importance: 5
|
||||
|
||||
---
|
||||
|
||||
# Красивый плейсхолдер для INPUT
|
||||
|
||||
Создайте для `<input type="password">` красивый, стилизованный плейсхолдер, например (кликните на тексте):
|
||||
|
||||
[iframe src="solution" height=90]
|
||||
|
||||
При клике плейсхолдер просто исчезает и дальше не показывается.
|
|
@ -0,0 +1,86 @@
|
|||
# Алгоритм
|
||||
|
||||
JavaScript не имеет доступа к текущему состоянию `key:CapsLock`. При загрузке страницы не известно, включён он или нет.
|
||||
|
||||
Но мы можем догадаться о его состоянии из событий:
|
||||
|
||||
1. Проверив символ, полученный по `keypress`. Символ в верхнем регистре без нажатого `key:Shift` означает, что включён `key:CapsLock`. Аналогично, символ в нижнем регистре, но с `key:Shift` говорят о включенном `key:CapsLock`. Свойство `event.shiftKey` показывает, нажат ли `key:Shift`. Так мы можем точно узнать, нажат ли `key:CapsLock`.
|
||||
2. Проверять `keydown`. Если нажат CapsLock (скан-код равен `20`), то переключить состояние, но лишь в том случае, когда оно уже известно.
|
||||
Под Mac так делать не получится, поскольку клавиатурные события с CapsLock [работают некорректно](info:keyboard-events#keyboard-events-order).
|
||||
|
||||
Имея состояние `CapsLock` в переменной, можно при фокусировке на `INPUT` выдавать предупреждение.
|
||||
|
||||
Отслеживать оба события: `keydown` и `keypress` хорошо бы на уровне документа, чтобы уже на момент входа в поле ввода мы знали состояние CapsLock.
|
||||
|
||||
Но при вводе сразу в нужный `input` событие `keypress` событие доплывёт до `document` и поставит состояние CapsLock *после того, как сработает на `input`*. Как это обойти -- подумайте сами.
|
||||
|
||||
# Решение
|
||||
|
||||
При загрузке страницы, когда еще ничего не набрано, мы ничего не знаем о состоянии `key:CapsLock`, поэтому оно равно `null`:
|
||||
|
||||
```js
|
||||
var capsLockEnabled = null;
|
||||
```
|
||||
|
||||
Когда нажата клавиша, мы можем попытаться проверить, совпадает ли регистр символа и состояние `key:Shift`:
|
||||
|
||||
```js
|
||||
document.onkeypress = function(e) {
|
||||
|
||||
var chr = getChar(e);
|
||||
if (!chr) return; // специальная клавиша
|
||||
|
||||
if (chr.toLowerCase() == chr.toUpperCase()) {
|
||||
// символ, который не имеет регистра, такой как пробел,
|
||||
// мы не можем использовать для определения состояния CapsLock
|
||||
return;
|
||||
}
|
||||
|
||||
capsLockEnabled = (chr.toLowerCase() == chr && e.shiftKey) || (chr.toUpperCase() == chr && !e.shiftKey);
|
||||
}
|
||||
```
|
||||
|
||||
Когда пользователь нажимает `key:CapsLock`, мы должны изменить его текущее состояние. Но мы можем сделать это только если знаем, что был нажат `key:CapsLock`.
|
||||
|
||||
Например, когда пользователь открыл страницу, мы не знаем, включен ли `key:CapsLock`. Затем, мы получаем событие `keydown` для `key:CapsLock`. Но мы все равно не знаем его состояния, был ли `key:CapsLock` *выключен* или, наоборот, включен.
|
||||
|
||||
```js
|
||||
if (navigator.platform.substr(0, 3) != 'Mac') { // событие для CapsLock глючит под Mac
|
||||
document.onkeydown = function(e) {
|
||||
if (e.keyCode == 20 && capsLockEnabled !== null) {
|
||||
capsLockEnabled = !capsLockEnabled;
|
||||
}
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
Теперь поле. Задание состоит в том, чтобы предупредить пользователя о включенном CapsLock, чтобы уберечь его от неправильного ввода.
|
||||
|
||||
1. Для начала, когда пользователь сфокусировался на поле, мы должны вывести предупреждение о CapsLock, если он включен.
|
||||
2. Пользователь начинает ввод. Каждое событие `keypress` всплывает до обработчика `document.keypress`, который обновляет состояние `capsLockEnabled`.
|
||||
|
||||
Мы не можем использовать событие `input.onkeypress`, для отображения состояния пользователю, потому что оно сработает *до* `document.onkeypress` (из-за всплытия) и, следовательно, до того, как мы узнаем состояние `key:CapsLock`.
|
||||
|
||||
Есть много способов решить эту проблему. Можно, например, назначить обработчик состояния CapsLock на событие `input.onkeyup`. То есть, индикация будет с задержкой, но это несущественно.
|
||||
|
||||
Альтернативное решение -- добавить на `input` такой же обработчик, как и на `document.onkeypress`.
|
||||
3. ...И наконец, пользователь убирает фокус с поля. Предупреждение может быть видно, если `key:CapsLock` включен, но так как пользователь уже ушел с поля, то нам нужно спрятать предупреждение.
|
||||
|
||||
Код проверки поля:
|
||||
|
||||
```html
|
||||
<input type="text" onkeyup="checkCapsWarning(event)" onfocus="checkCapsWarning(event)" onblur="removeCapsWarning()" />
|
||||
|
||||
<div style="display:none;color:red" id="caps">Внимание: нажат CapsLock!</div>
|
||||
|
||||
<script>
|
||||
function checkCapsWarning() {
|
||||
document.getElementById('caps').style.display = capsLockEnabled ? 'block' : 'none';
|
||||
}
|
||||
|
||||
function removeCapsWarning() {
|
||||
document.getElementById('caps').style.display = 'none';
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
Введите текст(например, пароль) с нажатым CapsLock:
|
||||
<input type="text" onkeyup="checkCapsWarning(event)" onfocus="checkCapsWarning(event)" onblur="removeCapsWarning()" />
|
||||
|
||||
<div style="display:none;color:red" id="capsIndicator">Внимание: нажат CapsLock!</div>
|
||||
|
||||
|
||||
<script>
|
||||
/**
|
||||
* Текущее состояние CapsLock
|
||||
* - null : неизвестно
|
||||
* - true/false : CapsLock включен/выключен
|
||||
*/
|
||||
var capsLockEnabled = null;
|
||||
|
||||
function getChar(event) {
|
||||
if (event.which == null) {
|
||||
if (event.keyCode < 32) return null;
|
||||
return String.fromCharCode(event.keyCode) // IE
|
||||
}
|
||||
|
||||
if (event.which != 0 && event.charCode != 0) {
|
||||
if (event.which < 32) return null;
|
||||
return String.fromCharCode(event.which) // остальные
|
||||
}
|
||||
|
||||
return null; // специальная клавиша
|
||||
}
|
||||
|
||||
if (navigator.platform.substr(0, 3) != 'Mac') { // событие для CapsLock глючит под Mac
|
||||
document.onkeydown = function(e) {
|
||||
if (e.keyCode == 20 && capsLockEnabled !== null) {
|
||||
capsLockEnabled = !capsLockEnabled;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
document.onkeypress = function(e) {
|
||||
e = e || event;
|
||||
|
||||
var chr = getChar(e);
|
||||
if (!chr) return // special key
|
||||
|
||||
if (chr.toLowerCase() == chr.toUpperCase()) {
|
||||
// символ, не зависящий от регистра, например пробел
|
||||
// не может быть использован для определения CapsLock
|
||||
return;
|
||||
}
|
||||
|
||||
capsLockEnabled = (chr.toLowerCase() == chr && e.shiftKey) || (chr.toUpperCase() == chr && !e.shiftKey);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Проверить CapsLock
|
||||
*/
|
||||
function checkCapsWarning() {
|
||||
document.getElementById('capsIndicator').style.display = capsLockEnabled ? 'block' : 'none';
|
||||
}
|
||||
|
||||
function removeCapsWarning() {
|
||||
document.getElementById('capsIndicator').style.display = 'none';
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
|
@ -0,0 +1,19 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
|
||||
Введите текст(например, пароль) с нажатым CapsLock:
|
||||
<input type="text" />
|
||||
|
||||
<div style="display:none;color:red" id="capsIndicator">Внимание: нажат CapsLock!</div>
|
||||
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
|
@ -0,0 +1,12 @@
|
|||
importance: 3
|
||||
|
||||
---
|
||||
|
||||
# Поле, предупреждающее о включенном CapsLock
|
||||
|
||||
Создайте поле, которое будет предупреждать пользователя, если включен `key:CapsLock`. Выключение `key:CapsLock` уберёт предупреждение.
|
||||
|
||||
Такое поле может помочь избежать ошибок при вводе пароля.
|
||||
|
||||
[iframe height=80 src="solution"]
|
||||
|
322
2-ui/4-forms-controls/2-focus-blur/article.md
Normal file
322
2-ui/4-forms-controls/2-focus-blur/article.md
Normal file
|
@ -0,0 +1,322 @@
|
|||
# Фокусировка: focus/blur
|
||||
|
||||
Говорят, что элемент "получает фокус", когда посетитель фокусируется на нём. Обычно фокусировка автоматически происходит при нажатии на элементе мышкой, но также можно перейти на нужный элемент клавиатурой -- через клавишу `key:Tab`, нажатие пальцем на планшете и так далее.
|
||||
|
||||
Момент получения фокуса и потери очень важен.
|
||||
|
||||
При получении фокуса мы можем подгрузить данные для автодополнения, начать отслеживать изменения. При потере -- проверить данные, которые ввёл посетитель.
|
||||
|
||||
Кроме того, иногда полезно "вручную", из JavaScript перевести фокус на нужный элемент, например, на поле в динамически созданной форме.
|
||||
|
||||
[cut]
|
||||
|
||||
## События focus/blur
|
||||
|
||||
Событие `focus` вызывается тогда, когда пользователь фокусируется на элементе, а `blur` -- когда фокус исчезает, например посетитель кликает на другом месте экрана.
|
||||
|
||||
Давайте сразу посмотрим на них в деле, используем для проверки ("валидации") введённых в форму значений.
|
||||
|
||||
В примере ниже:
|
||||
|
||||
- Обработчик `onblur` проверяет, что в поле введено число, если нет -- показывает ошибку.
|
||||
- Обработчик `onfocus`, если текущее состояние поля ввода -- "ошибка" -- скрывает её (потом при `onblur` будет повторная проверка).
|
||||
|
||||
В примере ниже, если набрать что-нибудь в поле "возраст" и завершить ввод, нажав `key:Tab` или кликнув в другое место страницы, то введённое значение будет автоматически проверено:
|
||||
|
||||
```html run autorun height=60
|
||||
<style> .error { border-color: red; } </style>
|
||||
|
||||
Введите ваш возраст: <input type="text" id="input">
|
||||
|
||||
<div id="error"></div>
|
||||
|
||||
<script>
|
||||
*!*input.onblur*/!* = function() {
|
||||
if (isNaN(this.value)) { // введено не число
|
||||
// показать ошибку
|
||||
this.className = "error";
|
||||
error.innerHTML = 'Вы ввели не число. Исправьте, пожалуйста.'
|
||||
}
|
||||
};
|
||||
|
||||
*!*input.onfocus*/!* = function() {
|
||||
if (this.className == 'error') { // сбросить состояние "ошибка", если оно есть
|
||||
this.className = "";
|
||||
error.innerHTML = "";
|
||||
}
|
||||
};
|
||||
</script>
|
||||
```
|
||||
|
||||
## Методы focus/blur
|
||||
|
||||
Методы с теми же названиями переводят/уводят фокус с элемента.
|
||||
|
||||
Для примера модифицируем пример выше, чтобы при неверном вводе посетитель просто не мог уйти с элемента:
|
||||
|
||||
```html run autorun height=80
|
||||
<style>
|
||||
.error {
|
||||
background: red;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div>Возраст:
|
||||
<input type="text" id="age">
|
||||
</div>
|
||||
|
||||
<div>Имя:
|
||||
<input type="text">
|
||||
</div>
|
||||
|
||||
<script>
|
||||
age.onblur = function() {
|
||||
if (isNaN(this.value)) { // введено не число
|
||||
// показать ошибку
|
||||
this.classList.add("error");
|
||||
*!*
|
||||
//... и вернуть фокус обратно
|
||||
age.focus();
|
||||
*/!*
|
||||
} else {
|
||||
this.classList.remove("error");
|
||||
}
|
||||
};
|
||||
</script>
|
||||
```
|
||||
|
||||
Этот пример работает во всех браузерах, кроме Firefox ([ошибка](https://bugzilla.mozilla.org/show_bug.cgi?id=53579)).
|
||||
|
||||
Если ввести что-то нецифровое в поле "возраст", и потом попытаться табом или мышкой перейти на другой `<input>`, то обработчик `onblur` вернёт фокус обратно.
|
||||
|
||||
Обратим внимание -- если из `onblur` сделать `event.preventDefault()`, то такого же эффекта не будет, потому что `onblur` срабатывает уже *после* того, как элемент потерял фокус.
|
||||
|
||||
## HTML5 и CSS3 вместо focus/blur
|
||||
|
||||
Прежде чем переходить к более сложным примерам, использующим JavaScript, мы рассмотрим три примера, когда его использовать не надо, а достаточно современного HTML/CSS.
|
||||
|
||||
### Подсветка при фокусировке
|
||||
|
||||
Стилизация полей ввода может быть решена средствами CSS (CSS2.1), а именно -- селектором `:focus`:
|
||||
|
||||
```html autorun height=100
|
||||
<style>
|
||||
*!*input:focus*/!* {
|
||||
background: #FA6;
|
||||
outline: none; /* убрать рамку */
|
||||
}
|
||||
</style>
|
||||
<input type="text">
|
||||
|
||||
<p>Селектор :focus выделит элемент при фокусировке на нем и уберёт рамку, которой браузер выделяет этот элемент по умолчанию.</p>
|
||||
```
|
||||
|
||||
В IE (включая более старые) скрыть фокус также может установка специального атрибута [hideFocus](http://msdn.microsoft.com/en-us/library/ie/ms533783.aspx).
|
||||
|
||||
### Автофокус
|
||||
|
||||
При загрузке страницы, если на ней существует элемент с атрибутом `autofocus` -- браузер автоматически фокусируется на этом элементе. Работает во всех браузерах, кроме IE9-.
|
||||
|
||||
```html run link
|
||||
<input type="text" name="search" *!*autofocus*/!*>
|
||||
```
|
||||
|
||||
Если нужны старые IE, то же самое может сделать JavaScript:
|
||||
|
||||
```html
|
||||
<input type="text" name="search">
|
||||
<script>
|
||||
document.getElementsByName('search')[0].focus();
|
||||
</script>
|
||||
```
|
||||
|
||||
Как правило, этот атрибут используется при изначальной загрузке, для страниц поиска и так далее, где главный элемент очевиден.
|
||||
|
||||
### Плейсхолдер
|
||||
|
||||
*Плейсхолдер* -- это значение-подсказка внутри `INPUT`, которое автоматически исчезает при фокусировке и существует, пока посетитель не начал вводить текст.
|
||||
|
||||
Во всех браузерах, кроме IE9-, это реализуется специальным атрибутом `placeholder`:
|
||||
|
||||
```html autorun height=80
|
||||
<input type="text" placeholder="E-mail">
|
||||
```
|
||||
|
||||
В некоторых браузерах этот текст можно стилизовать:
|
||||
|
||||
```html autorun height=80
|
||||
<style>
|
||||
.my*!*::-webkit-input-placeholder*/!* {
|
||||
color: red;
|
||||
font-style: italic;
|
||||
}
|
||||
.my*!*::-moz-input-placeholder*/!* {
|
||||
color: red;
|
||||
font-style: italic;
|
||||
}
|
||||
.my*!*::-ms-input-placeholder*/!* {
|
||||
color: red;
|
||||
font-style: italic;
|
||||
}
|
||||
</style>
|
||||
|
||||
<input class="my" type="text" placeholder="E-mail">
|
||||
Стилизованный плейсхолдер
|
||||
```
|
||||
|
||||
## Разрешаем фокус на любом элементе: tabindex
|
||||
|
||||
По умолчанию не все элементы поддерживают фокусировку.
|
||||
|
||||
Перечень элементов немного рознится от браузера к браузеру, например, список для IE описан <a href="http://msdn.microsoft.com/en-us/library/ms536934.aspx">в MSDN</a>, одно лишь верно всегда -- заведомо поддерживают `focus/blur` те элементы, c которыми посетитель может взаимодействовать: `<button>`, `<input>`, `<select>`, `<a>` и т.д.
|
||||
|
||||
С другой стороны, на элементах для форматирования, таких как `<div>`, `<span>`, `<table>` -- по умолчанию сфокусироваться нельзя. Впрочем, существует способ включить фокусировку и для них.
|
||||
|
||||
В HTML есть атрибут `tabindex`.
|
||||
|
||||
Его основной смысл -- это указать номер элемента при переборе клавишей `key:Tab`.
|
||||
|
||||
То есть, если есть два элемента, первый имеет `tabindex="1"`, а второй `tabindex="2"`, то нажатие `key:Tab` при фокусе на первом элементе -- переведёт его на второй.
|
||||
|
||||
Исключением являются специальные значения:
|
||||
|
||||
- `tabindex="0"` делает элемент всегда последним.
|
||||
- `tabindex="-1"` означает, что клавиша `key:Tab` будет элемент игнорировать.
|
||||
|
||||
**Любой элемент поддерживает фокусировку, если у него есть `tabindex`.**
|
||||
|
||||
В примере ниже есть список элементов. Кликните на любой из них и нажмите "tab".
|
||||
|
||||
```html autorun no-beautify
|
||||
Кликните на первый элемент списка и нажмите Tab. Внимание! Дальнейшие нажатия Tab могут вывести за границы iframe'а с примером.
|
||||
<ul>
|
||||
<li tabindex="1">Один</li>
|
||||
<li tabindex="0">Ноль</li>
|
||||
<li tabindex="2">Два</li>
|
||||
<li tabindex="-1">Минус один</li>
|
||||
</ul>
|
||||
|
||||
<style>
|
||||
li { cursor: pointer; }
|
||||
:focus { outline: 1px dashed green; }
|
||||
</style>
|
||||
```
|
||||
|
||||
Порядок перемещения по клавише "Tab" в примере выше должен быть таким: `1 - 2 - 0` (ноль всегда последний). Продвинутые пользователи частенько используют "Tab" для навигации, и ваше хорошее отношение к ним будет вознаграждено :)
|
||||
|
||||
Обычно `<li>` не поддерживает фокусировку, но здесь есть `tabindex`.
|
||||
|
||||
## Делегирование с focus/blur
|
||||
|
||||
События `focus` и `blur` не всплывают.
|
||||
|
||||
Это грустно, поскольку мы не можем использовать делегирование с ними. Например, мы не можем сделать так, чтобы при фокусировке в форме она вся подсвечивалась:
|
||||
|
||||
```html autorun height=100
|
||||
<!-- при фокусировке на форме ставим ей класс -->
|
||||
<form *!*onfocus="this.className='focused'"*/!*>
|
||||
<input type="text" name="name" value="Ваше имя">
|
||||
<input type="text" name="surname" value="Ваша фамилия">
|
||||
</form>
|
||||
|
||||
<style> .focused { outline: 1px solid red; } </style>
|
||||
```
|
||||
|
||||
Пример выше не работает, т.к. при фокусировке на любом `<input>` событие `focus` срабатывает только на этом элементе и не всплывает наверх. Так что обработчик `onfocus` на форме никогда не сработает.
|
||||
|
||||
Что делать? Неужели мы должны присваивать обработчик каждому полю `<input>`?
|
||||
|
||||
**Это забавно, но хотя `focus/blur` не всплывают, они могут быть пойманы на фазе перехвата.**
|
||||
|
||||
Вот так сработает:
|
||||
|
||||
```html autorun height=100
|
||||
<form id="form">
|
||||
<input type="text" name="name" value="Ваше имя">
|
||||
<input type="text" name="surname" value="Ваша фамилия">
|
||||
</form>
|
||||
|
||||
<style>
|
||||
.focused {
|
||||
outline: 1px solid red;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
*!*
|
||||
// ставим обработчики на фазе перехвата, последний аргумент true
|
||||
form.addEventListener("focus", function() {
|
||||
this.classList.add('focused');
|
||||
}, true);
|
||||
|
||||
form.addEventListener("blur", function() {
|
||||
this.classList.remove('focused');
|
||||
}, true);
|
||||
*/!*
|
||||
</script>
|
||||
```
|
||||
|
||||
### События focusin/focusout
|
||||
|
||||
События `focusin/focusout` -- то же самое, что и `focus/blur`, только они всплывают.
|
||||
|
||||
У них две особенности:
|
||||
|
||||
- Не поддерживаются Firefox (хотя поддерживаются даже старейшими IE), см. <https://bugzilla.mozilla.org/show_bug.cgi?id=687787>.
|
||||
- Должны быть назначены не через `on`-свойство, а при помощи `elem.addEventListener`.
|
||||
|
||||
Из-за отсутствия подержки Firefox эти события используют редко. Получается, что во всех браузерах можно использовать `focus` на стадии перехвата, ну а `focusin/focusout` -- в IE8-, где стадии перехвата нет.
|
||||
|
||||
Подсветка формы в примере ниже работает во всех браузерах.
|
||||
|
||||
```html autorun height=60 run
|
||||
<form name="form">
|
||||
<input type="text" name="name" value="Ваше имя">
|
||||
<input type="text" name="surname" value="Ваша фамилия">
|
||||
</form>
|
||||
<style>
|
||||
.focused {
|
||||
outline: 1px solid red;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
function onFormFocus() {
|
||||
this.className = 'focused';
|
||||
}
|
||||
|
||||
function onFormBlur() {
|
||||
this.className = '';
|
||||
}
|
||||
|
||||
var form = document.forms.form;
|
||||
|
||||
if (form.addEventListener) {
|
||||
// focus/blur на стадии перехвата срабатывают во всех браузерах
|
||||
// поэтому используем их
|
||||
form.addEventListener('focus', onFormFocus, true);
|
||||
form.addEventListener('blur', onFormBlur, true);
|
||||
} else {
|
||||
// ветка для IE8-, где нет стадии перехвата, но есть focusin/focusout
|
||||
form.onfocusin = onFormFocus;
|
||||
form.onfocusout = onFormBlur;
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
## Итого
|
||||
События `focus/blur` происходят при получении и снятия фокуса с элемента.
|
||||
|
||||
У них есть особенности:
|
||||
|
||||
- Они не всплывают. Но на фазе перехвата их можно перехватить. Это странно, но это так, не спрашивайте почему.
|
||||
|
||||
Везде, кроме Firefox, поддерживаются всплывающие альтернативы `focusin/focusout`.
|
||||
- По умолчанию многие элементы не могут получить фокус. Например, если вы кликните по `DIV`, то фокусировка на нем не произойдет.
|
||||
|
||||
Но это можно изменить, если поставить элементу атрибут `tabIndex`. Этот атрибут также дает возможность контролировать порядок перехода при нажатии `key:Tab`.
|
||||
|
||||
```smart header="Текущий элемент: `document.activeElement`"
|
||||
Кстати, текущий элемент, на котором фокус, доступен как `document.activeElement`.
|
||||
```
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
Алгоритм решения такой.
|
||||
|
||||
Только численный ввод в поле с суммой разрешаем, повесив обработчик на `keypress`.
|
||||
|
||||
Отслеживаем события изменения для перевычисления результатов:
|
||||
|
||||
- На `input`: событие `input` и дополнительно `propertychange/keyup` для совместимости со старыми IE.
|
||||
- На `checkbox`: событие `click` вместо `change` для совместимости с IE8-.
|
||||
- На `select`: событие `change`.
|
|
@ -0,0 +1,153 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<style>
|
||||
td select,
|
||||
td input {
|
||||
width: 150px;
|
||||
}
|
||||
|
||||
#diagram td {
|
||||
vertical-align: bottom;
|
||||
text-align: center;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
#diagram div {
|
||||
margin: auto;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
|
||||
Калькулятор процентов, из расчёта 12% годовых.
|
||||
<form name="calculator">
|
||||
<table>
|
||||
<tr>
|
||||
<td>Сумма</td>
|
||||
<td>
|
||||
<input name="money" type="text" value="10000">
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Срок в месяцах</td>
|
||||
<td>
|
||||
<select name="months">
|
||||
<option value="3">3 (минимум)</option>
|
||||
<option value="6">6 (полгода)</option>
|
||||
<option value="12" selected>12 (год)</option>
|
||||
<option value="18">18 (1.5 года)</option>
|
||||
<option value="24">24 (2 года)</option>
|
||||
<option value="30">30 (2.5 года)</option>
|
||||
<option value="36">36 (3 года)</option>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>С капитализацией</td>
|
||||
<td>
|
||||
<input name="capitalization" type="checkbox">
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
||||
</form>
|
||||
|
||||
|
||||
<table id="diagram">
|
||||
<tr>
|
||||
<th>Было:</th>
|
||||
<th>Станет:</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th id="money-before"></th>
|
||||
<th id="money-after"></th>
|
||||
</tr>
|
||||
<td>
|
||||
<div style="background: red;width:40px;height:100px"></div>
|
||||
</td>
|
||||
<td>
|
||||
<div style="background: green;width:40px;height:0" id="height-after"></div>
|
||||
</td>
|
||||
</table>
|
||||
|
||||
<script>
|
||||
// event.type должен быть keypress
|
||||
function getChar(event) {
|
||||
if (event.which == null) {
|
||||
if (event.keyCode < 32) return null;
|
||||
return String.fromCharCode(event.keyCode) // IE
|
||||
}
|
||||
|
||||
if (event.which != 0 && event.charCode != 0) {
|
||||
if (event.which < 32) return null;
|
||||
return String.fromCharCode(event.which) // остальные
|
||||
}
|
||||
|
||||
return null; // специальная клавиша
|
||||
}
|
||||
|
||||
var form = document.forms.calculator;
|
||||
|
||||
|
||||
var moneyElem = form.elements.money;
|
||||
moneyElem.onkeypress = function(e) {
|
||||
e = e || event;
|
||||
var chr = getChar(e);
|
||||
|
||||
if (e.ctrlKey || e.altKey || chr == null) return; // специальная клавиша
|
||||
if (chr < '0' || chr > '9') return false;
|
||||
}
|
||||
|
||||
// клавиатура, вставить/вырезать клавиатурой
|
||||
moneyElem.onkeyup = calculate;
|
||||
|
||||
// любые действия, кроме IE. В IE9 также работает, кроме удаления
|
||||
moneyElem.oninput = calculate;
|
||||
|
||||
moneyElem.onpropertychange = function() { // для IE8- изменение значения, кроме удаления
|
||||
event.propertyName == "value" && calculate();
|
||||
}
|
||||
|
||||
var capitalizationElem = form.elements.capitalization;
|
||||
capitalizationElem.onclick = calculate;
|
||||
|
||||
var monthsElem = form.elements.months;
|
||||
monthsElem.onchange = calculate;
|
||||
|
||||
|
||||
function calculate() {
|
||||
var sum = +moneyElem.value;
|
||||
if (!sum) return;
|
||||
|
||||
var monthlyIncrease = 0.01;
|
||||
|
||||
if (!capitalizationElem.checked) {
|
||||
sum = sum * (1 + monthlyIncrease * monthsElem.value);
|
||||
} else {
|
||||
|
||||
for (var i = 0; i < monthsElem.value; i++) {
|
||||
// 1000 1010 1020.1
|
||||
sum = sum * (1 + monthlyIncrease);
|
||||
}
|
||||
}
|
||||
sum = Math.round(sum);
|
||||
|
||||
var height = sum / moneyElem.value * 100 + 'px';
|
||||
document.getElementById('height-after').style.height = height;
|
||||
document.getElementById('money-before').innerHTML = moneyElem.value;
|
||||
document.getElementById('money-after').innerHTML = sum;
|
||||
}
|
||||
|
||||
calculate();
|
||||
</script>
|
||||
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
|
@ -0,0 +1,104 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<style>
|
||||
td select,
|
||||
td input {
|
||||
width: 150px;
|
||||
}
|
||||
|
||||
#diagram td {
|
||||
vertical-align: bottom;
|
||||
text-align: center;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
#diagram div {
|
||||
margin: auto;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
|
||||
Калькулятор процентов, из расчёта 12% годовых.
|
||||
<form name="calculator">
|
||||
<table>
|
||||
<tr>
|
||||
<td>Сумма</td>
|
||||
<td>
|
||||
<input name="money" type="text" value="10000">
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Срок в месяцах</td>
|
||||
<td>
|
||||
<select name="months">
|
||||
<option value="3">3 (минимум)</option>
|
||||
<option value="6">6 (полгода)</option>
|
||||
<option value="12" selected>12 (год)</option>
|
||||
<option value="18">18 (1.5 года)</option>
|
||||
<option value="24">24 (2 года)</option>
|
||||
<option value="30">30 (2.5 года)</option>
|
||||
<option value="36">36 (3 года)</option>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>С капитализацией</td>
|
||||
<td>
|
||||
<input name="capitalization" type="checkbox">
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
||||
</form>
|
||||
|
||||
|
||||
<table id="diagram">
|
||||
<tr>
|
||||
<th>Было:</th>
|
||||
<th>Станет:</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th id="money-before"></th>
|
||||
<th id="money-after"></th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<div style="background: red;width:40px;height:100px"></div>
|
||||
</td>
|
||||
<td>
|
||||
<div style="background: green;width:40px;height:0" id="height-after"></div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<script>
|
||||
// вспомогательная функция для получения символа из события keypress
|
||||
// (вдруг понадобится))
|
||||
function getChar(event) {
|
||||
if (event.which == null) {
|
||||
if (event.keyCode < 32) return null;
|
||||
return String.fromCharCode(event.keyCode) // IE
|
||||
}
|
||||
|
||||
if (event.which != 0 && event.charCode != 0) {
|
||||
if (event.which < 32) return null;
|
||||
return String.fromCharCode(event.which) // остальные
|
||||
}
|
||||
|
||||
return null; // специальная клавиша
|
||||
}
|
||||
|
||||
// ваш код...
|
||||
</script>
|
||||
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
|
@ -0,0 +1,19 @@
|
|||
importance: 5
|
||||
|
||||
---
|
||||
|
||||
# Автовычисление процентов по вкладу
|
||||
|
||||
Создайте интерфейс для автоматического вычисления процентов по вкладу.
|
||||
|
||||
Ставка фиксирована: 12% годовых. При включённом поле "капитализация" -- проценты приплюсовываются к сумме вклада каждый месяц ([сложный процент](http://damoney.ru/finance/slozniy-procent.php)).
|
||||
|
||||
Пример:
|
||||
|
||||
[iframe src="solution" height="350" border="1"]
|
||||
|
||||
Технические требования:
|
||||
|
||||
- В поле с суммой должно быть нельзя ввести не-цифру. При этом пусть в нём работают специальные клавиши и сочетания Ctrl-X/Ctrl-V.
|
||||
- Изменения в форме отражаются в результатах сразу.
|
||||
|
185
2-ui/4-forms-controls/3-events-change/article.md
Normal file
185
2-ui/4-forms-controls/3-events-change/article.md
Normal file
|
@ -0,0 +1,185 @@
|
|||
# Изменение: change, input, cut, copy, paste
|
||||
|
||||
На элементах формы происходят события клавиатуры и мыши, но есть и несколько других, особенных событий.
|
||||
|
||||
## Событие change
|
||||
|
||||
Событие [change](http://www.w3.org/TR/html5/forms.html#event-input-change) происходит по окончании изменении значения элемента формы, когда это изменение зафиксировано.
|
||||
|
||||
Для текстовых элементов это означает, что событие произойдёт не при каждом вводе, а при потере фокуса.
|
||||
|
||||
Например, пока вы набираете что-то в текстовом поле ниже -- события нет. Но как только вы уведёте фокус на другой элемент, например, нажмёте кнопку -- произойдет событие `onchange`.
|
||||
|
||||
```html autorun height=40
|
||||
<input type="text" onchange="alert(this.value)">
|
||||
<input type="button" value="Кнопка">
|
||||
```
|
||||
|
||||
Для остальных же элементов: `select`, `input type=checkbox/radio` оно срабатывает сразу при выборе значения.
|
||||
|
||||
```warn header="Поздний `onchange` в IE8-"
|
||||
В IE8- `checkbox/radio` при изменении мышью не инициируют событие сразу, а ждут потери фокуса.
|
||||
|
||||
Для того, чтобы видеть изменения `checkbox/radio` тут же -- в IE8- нужно повесить обработчик на событие `click` (оно произойдет и при изменении значения с клавиатуры) или воспользоваться событием `propertychange`, описанным далее.
|
||||
```
|
||||
|
||||
## Событие input
|
||||
|
||||
Событие `input` срабатывает *тут же* при изменении значения текстового элемента и поддерживается всеми браузерами, кроме IE8-.
|
||||
|
||||
В IE9 оно поддерживается частично, а именно -- *не возникает при удалении символов* (как и `onpropertychange`).
|
||||
|
||||
Пример использования (не работает в IE8-):
|
||||
|
||||
```html autorun height=40
|
||||
<input type="text"> oninput: <span id="result"></span>
|
||||
<script>
|
||||
var input = document.body.children[0];
|
||||
|
||||
input.oninput = function() {
|
||||
document.getElementById('result').innerHTML = input.value;
|
||||
};
|
||||
</script>
|
||||
```
|
||||
|
||||
В современных браузерах `oninput` -- самое главное событие для работы с элементом формы. Именно его, а не `keydown/keypress` следует использовать.
|
||||
|
||||
Если бы ещё не проблемы со старыми IE... Впрочем, их можно решить при помощи события `propertychange`.
|
||||
|
||||
## IE10-, событие propertychange
|
||||
|
||||
Это событие происходит только в IE10-, при любом изменении свойства. Оно позволяет отлавливать изменение тут же. Оно нестандартное, и его основная область использования -- исправление недочётов обработки событий в старых IE.
|
||||
|
||||
Если поставить его на `checkbox` в IE8-, то получится "правильное" событие `change`:
|
||||
|
||||
```html autorun height=40
|
||||
<input type="checkbox"> Чекбокс с "onchange", работающим везде одинаково
|
||||
<script>
|
||||
var checkbox = document.body.children[0];
|
||||
|
||||
if ("onpropertychange" in checkbox) {
|
||||
// старый IE
|
||||
*!*
|
||||
checkbox.onpropertychange = function() {
|
||||
// проверим имя изменённого свойства
|
||||
if (event.propertyName == "checked") {
|
||||
alert( checkbox.checked );
|
||||
}
|
||||
};
|
||||
*/!*
|
||||
} else {
|
||||
// остальные браузеры
|
||||
checkbox.onchange = function() {
|
||||
alert( checkbox.checked );
|
||||
};
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
Это событие также срабатывает при изменении значения текстового элемента. Поэтому его можно использовать в старых IE вместо `oninput`.
|
||||
|
||||
К сожалению, в IE9 у него недочёт: оно не срабатывает при удалении символов. Поэтому сочетания `onpropertychange` + `oninput` недостаточно, чтобы поймать любое изменение поля в старых IE. Далее мы рассмотрим пример, как это можно сделать иначе.
|
||||
|
||||
## События cut, copy, paste
|
||||
|
||||
Эти события используются редко. Они происходят при вырезании/вставке/копировании значения.
|
||||
|
||||
К сожалению, кросс-браузерного способа получить данные, которые вставляются/копируются, не существует, поэтому их основное применение -- это отмена соответствующей операции.
|
||||
|
||||
Например, вот так:
|
||||
|
||||
```html autorun height=40
|
||||
<input type="text" id="input"> event: <span id="result"></span>
|
||||
<script>
|
||||
input.oncut = input.oncopy = input.onpaste = function(event) {
|
||||
result.innerHTML = event.type + ' ' + input.value;
|
||||
return false;
|
||||
};
|
||||
</script>
|
||||
```
|
||||
|
||||
## Пример: поле с контролем СМС
|
||||
|
||||
Как видим, событий несколько и они взаимно дополняют друг друга.
|
||||
|
||||
Посмотрим, как их использовать, на примере.
|
||||
|
||||
Сделаем поле для СМС, рядом с которым должно показываться число символов, обновляющееся при каждом изменении поля.
|
||||
|
||||
Как такое реализовать?
|
||||
|
||||
Событие `input` идеально решит задачу во всех браузерах, кроме IE9-. Собственно, если IE9- нам не нужен, то на этом можно и остановиться.
|
||||
|
||||
### IE9-
|
||||
|
||||
В IE8- событие `input` не поддерживается, но, как мы видели ранее, есть `onpropertychange`, которое может заменить его.
|
||||
|
||||
Что же касается IE9 -- там поддерживаются и `input` и `onpropertychange`, но они оба не работают при удалении символов. Поэтому мы будем отслеживать удаление при помощи `keyup` на `key:Delete` и `key:BackSpace` . А вот удаление командой "вырезать" из меню -- сможет отловить лишь `oncut`.
|
||||
|
||||
Получается вот такая комбинация:
|
||||
|
||||
```html autorun run height=60
|
||||
<input type="text" id="sms"> символов: <span id="result"></span>
|
||||
<script>
|
||||
function showCount() {
|
||||
result.innerHTML = sms.value.length;
|
||||
}
|
||||
|
||||
sms.onkeyup = sms.oninput = showCount;
|
||||
sms.onpropertychange = function() {
|
||||
if (event.propertyName == "value") showCount();
|
||||
}
|
||||
sms.oncut = function() {
|
||||
setTimeout(showCount, 0); // на момент oncut значение еще старое
|
||||
};
|
||||
</script>
|
||||
```
|
||||
|
||||
Здесь мы добавили вызов `showCount` на все события, которые могут приводить к изменению значения. Да, иногда изменение будет обрабатываться несколько раз, но зато с гарантией. А лишние вызовы легко убрать, например, при помощи `throttle`-декоратора, описанного в задаче <info:task/throttle>.
|
||||
|
||||
**Есть и совсем другой простой, но действенный вариант: через `setInterval` регулярно проверять значение и, если оно слишком длинное, обрезать его.**
|
||||
|
||||
Чтобы сэкономить ресурсы браузера, мы можем начинать отслеживание по `onfocus`, а прекращать -- по `onblur`, вот так:
|
||||
|
||||
```html autorun height=60
|
||||
<input type="text" id="sms"> символов: <span id="result"></span>
|
||||
|
||||
<script>
|
||||
var timerId;
|
||||
|
||||
sms.onfocus = function() {
|
||||
|
||||
var lastValue = sms.value;
|
||||
timerId = setInterval(function() {
|
||||
if (sms.value != lastValue) {
|
||||
showCount();
|
||||
lastValue = sms.value;
|
||||
}
|
||||
}, 20);
|
||||
};
|
||||
|
||||
sms.onblur = function() {
|
||||
clearInterval(timerId);
|
||||
};
|
||||
|
||||
function showCount() {
|
||||
result.innerHTML = sms.value.length;
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
Обратим внимание -- весь этот "танец с бубном" нужен только для поддержки IE8-, в которых не поддерживается `oninput` и IE9, где `oninput` не работает при удалении.
|
||||
|
||||
## Итого
|
||||
|
||||
События изменения данных:
|
||||
|
||||
| Событие | Описание | Особенности |
|
||||
|---------|----------|-------------|
|
||||
| `change`| Изменение значения любого элемента формы. Для текстовых элементов срабатывает при потере фокуса. | В IE8- на чекбоксах ждет потери фокуса, поэтому для мгновенной реакции ставят также <code>onclick</code>-обработчик или <code>onpropertychange</code>. |
|
||||
| `input` | Событие срабатывает только на текстовых элементах. Оно не ждет потери фокуса, в отличие от <code>change</code>. | В IE8- не поддерживается, в IE9 не работает при удалении символов. |
|
||||
| `propertychange` | Только для IE10-. Универсальное событие для отслеживания изменения свойств элементов. Имя изменённого свойства содержится в `event.propertyName`. Используют для мгновенной реакции на изменение значения в старых IE. | В IE9 не срабатывает при удалении символов. |
|
||||
| `cut/copy/paste` | Срабатывают при вставке/копировании/удалении текста. Если в их обработчиках отменить действие браузера, то вставки/копирования/удаления не произойдёт. | Вставляемое значение получить нельзя: на момент срабатывания события в элементе всё ещё *старое* значение, а новое недоступно. |
|
||||
|
||||
Ещё особенность: в IE8- события `change`, `propertychange`, `cut` и аналогичные не всплывают. То есть, обработчики нужно назначать на сам элемент, без делегирования.
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
Модальное окно делается путём добавления к документу `DIV`, полностью перекрывающего документ и имеющего больший `z-index`.
|
||||
|
||||
В результате все клики будут доставаться этому `DIV'у`:
|
||||
|
||||
Стиль:
|
||||
|
||||
```css
|
||||
#cover-div {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 9000;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: gray;
|
||||
opacity: 0.3;
|
||||
}
|
||||
```
|
||||
|
||||
Самой форме можно дать еще больший `z-index`, чтобы она была над `DIV'ом`. Мы не помещаем форму в контейнер, чтобы она не унаследовала полупрозрачность.
|
||||
|
|
@ -0,0 +1,155 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<style>
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#prompt-form {
|
||||
display: inline-block;
|
||||
padding: 5px 5px 5px 70px;
|
||||
width: 200px;
|
||||
border: 1px solid black;
|
||||
background: white url(https://js.cx/clipart/prompt.png) no-repeat left 5px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
#prompt-form-container {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 9999;
|
||||
display: none;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#prompt-form-container:before {
|
||||
display: inline-block;
|
||||
height: 100%;
|
||||
content: '';
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
#cover-div {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 9000;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: gray;
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
#prompt-form input[name="text"] {
|
||||
display: block;
|
||||
margin: 5px;
|
||||
width: 180px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body style="height:3000px">
|
||||
|
||||
<h1>Нажмите на кнопку ниже</h1>
|
||||
|
||||
<input type="button" value="Нажмите для показа формы ввода" id="show-button">
|
||||
|
||||
|
||||
<div id="prompt-form-container">
|
||||
<form id="prompt-form">
|
||||
<div id="prompt-message"></div>
|
||||
<input name="text" type="text">
|
||||
<input type="submit" value="Ок">
|
||||
<input type="button" name="cancel" value="Отмена">
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Показать полупрозрачный DIV, затеняющий всю страницу
|
||||
// (а форма будет не в нем, а рядом с ним, чтобы не полупрозрачная)
|
||||
function showCover() {
|
||||
var coverDiv = document.createElement('div');
|
||||
coverDiv.id = 'cover-div';
|
||||
document.body.appendChild(coverDiv);
|
||||
}
|
||||
|
||||
function hideCover() {
|
||||
document.body.removeChild(document.getElementById('cover-div'));
|
||||
}
|
||||
|
||||
function showPrompt(text, callback) {
|
||||
showCover();
|
||||
var form = document.getElementById('prompt-form');
|
||||
var container = document.getElementById('prompt-form-container');
|
||||
document.getElementById('prompt-message').innerHTML = text;
|
||||
form.elements.text.value = '';
|
||||
|
||||
function complete(value) {
|
||||
hideCover();
|
||||
container.style.display = 'none';
|
||||
document.onkeydown = null;
|
||||
callback(value);
|
||||
}
|
||||
|
||||
form.onsubmit = function() {
|
||||
var value = form.elements.text.value;
|
||||
if (value == '') return false; // игнорировать пустой submit
|
||||
|
||||
complete(value);
|
||||
return false;
|
||||
};
|
||||
|
||||
form.elements.cancel.onclick = function() {
|
||||
complete(null);
|
||||
};
|
||||
|
||||
document.onkeydown = function(e) {
|
||||
if (e.keyCode == 27) { // escape
|
||||
complete(null);
|
||||
}
|
||||
};
|
||||
|
||||
var lastElem = form.elements[form.elements.length - 1];
|
||||
var firstElem = form.elements[0];
|
||||
|
||||
lastElem.onkeydown = function(e) {
|
||||
if (e.keyCode == 9 && !e.shiftKey) {
|
||||
firstElem.focus();
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
firstElem.onkeydown = function(e) {
|
||||
if (e.keyCode == 9 && e.shiftKey) {
|
||||
lastElem.focus();
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
container.style.display = 'block';
|
||||
form.elements.text.focus();
|
||||
}
|
||||
|
||||
document.getElementById('show-button').onclick = function() {
|
||||
showPrompt("Введите что-нибудь<br>...умное :)", function(value) {
|
||||
alert("Вы ввели: " + value);
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
|
@ -0,0 +1,65 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<style>
|
||||
html,
|
||||
body {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#prompt-form {
|
||||
display: inline-block;
|
||||
padding: 5px 5px 5px 70px;
|
||||
width: 200px;
|
||||
border: 1px solid black;
|
||||
background: white url(https://js.cx/clipart/prompt.png) no-repeat left 5px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
#prompt-form-container {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 9999;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#prompt-form-container:before {
|
||||
display: inline-block;
|
||||
height: 100%;
|
||||
content: '';
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
#prompt-form input[name="text"] {
|
||||
display: block;
|
||||
margin: 5px;
|
||||
width: 180px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
|
||||
<div id="prompt-form-container">
|
||||
<form id="prompt-form">
|
||||
<div id="prompt-message">Введите, пожалуйста...
|
||||
<br>Что-то..</div>
|
||||
<input name="text" type="text">
|
||||
<input type="submit" value="Ок">
|
||||
<input type="button" name="cancel" value="Отмена">
|
||||
</form>
|
||||
</div>
|
||||
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
33
2-ui/4-forms-controls/4-forms-submit/1-modal-dialog/task.md
Normal file
33
2-ui/4-forms-controls/4-forms-submit/1-modal-dialog/task.md
Normal file
|
@ -0,0 +1,33 @@
|
|||
importance: 5
|
||||
|
||||
---
|
||||
|
||||
# Модальное диалоговое окно
|
||||
|
||||
Создайте функцию `showPrompt(text, callback)`, которая выводит форму для ввода с сообщением `text` и кнопками `ОК/Отмена`.
|
||||
|
||||
- При отправке формы (OK/ввод в текстовом поле) -- должна вызываться функция `callback` со значением поля.
|
||||
- При нажатии на `Отмена` или на клавишу `key:Esc` -- должна вызываться функция `callback(null)`. Клавиша `key:Esc` должна закрывать форму всегда, даже если поле для ввода сообщения не в фокусе.
|
||||
|
||||
Особенности реализации:
|
||||
|
||||
- Форма должна показываться в центре окна (и оставаться в центре при изменении его размеров, а также при прокрутке окна!).
|
||||
- Текст может состоять из нескольких строк, возможен любой HTML
|
||||
- При показе формы остальные элементы страницы использовать нельзя, не работают другие кнопки и т.п, это окно -- *модальное*.
|
||||
- При показе формы -- сразу фокус на `INPUT` для ввода.
|
||||
- Нажатия `key:Tab`/`key:Shift+Tab` переключают в цикле только по полям формы, они не позволяют переключиться на другие элементы страницы.
|
||||
|
||||
Пример использования:
|
||||
|
||||
```js
|
||||
showPrompt("Введите что-нибудь<br>... умное :)", function(value) {
|
||||
alert( value );
|
||||
});
|
||||
```
|
||||
|
||||
Демо в ифрейме:
|
||||
|
||||
[iframe src="solution" height=160 border=1]
|
||||
|
||||
Исходный HTML/CSS для формы с готовым fixed-позиционированием - в песочнице.
|
||||
|
|
@ -0,0 +1,118 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<style>
|
||||
td select,
|
||||
td input {
|
||||
width: 150px;
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.error input,
|
||||
.error textarea {
|
||||
border: 1px solid red;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: red;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
|
||||
<form>
|
||||
<table>
|
||||
<tr>
|
||||
<td>От кого</td>
|
||||
<td>
|
||||
<input name="from" type="text">
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Ваш пароль</td>
|
||||
<td>
|
||||
<input name="password" type="password">
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Повторите пароль</td>
|
||||
<td>
|
||||
<input name="password2" type="password">
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Куда</td>
|
||||
<td>
|
||||
<select name="to">
|
||||
<option></option>
|
||||
<option value="1">Отдел снабжения</option>
|
||||
<option value="2">Отдел разработки</option>
|
||||
<option value="3">Директору</option>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
Сообщение:
|
||||
<label>
|
||||
<textarea name="message" style="display:block;width:400px;height:80px"></textarea>
|
||||
</label>
|
||||
|
||||
<input type="button" onclick="validate(this.form)" value="Проверить">
|
||||
</form>
|
||||
|
||||
<script>
|
||||
function showError(container, errorMessage) {
|
||||
container.className = 'error';
|
||||
var msgElem = document.createElement('span');
|
||||
msgElem.className = "error-message";
|
||||
msgElem.innerHTML = errorMessage;
|
||||
container.appendChild(msgElem);
|
||||
}
|
||||
|
||||
function resetError(container) {
|
||||
container.className = '';
|
||||
if (container.lastChild.className == "error-message") {
|
||||
container.removeChild(container.lastChild);
|
||||
}
|
||||
}
|
||||
|
||||
function validate(form) {
|
||||
var elems = form.elements;
|
||||
|
||||
resetError(elems.from.parentNode);
|
||||
if (!elems.from.value) {
|
||||
showError(elems.from.parentNode, ' Укажите от кого.');
|
||||
}
|
||||
|
||||
resetError(elems.password.parentNode);
|
||||
if (!elems.password.value) {
|
||||
showError(elems.password.parentNode, ' Укажите пароль.');
|
||||
} else if (elems.password.value != elems.password2.value) {
|
||||
showError(elems.password.parentNode, ' Пароли не совпадают.');
|
||||
}
|
||||
|
||||
resetError(elems.to.parentNode);
|
||||
if (!elems.to.value) {
|
||||
showError(elems.to.parentNode, ' Укажите, куда.');
|
||||
}
|
||||
|
||||
resetError(elems.message.parentNode);
|
||||
if (!elems.message.value) {
|
||||
showError(elems.message.parentNode, ' Отсутствует текст.');
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
|
@ -0,0 +1,74 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<style>
|
||||
td select,
|
||||
td input {
|
||||
width: 150px;
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
}
|
||||
/* ваши стили */
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
|
||||
<form>
|
||||
<table>
|
||||
<tr>
|
||||
<td>От кого</td>
|
||||
<td>
|
||||
<input name="from" type="text">
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Ваш пароль</td>
|
||||
<td>
|
||||
<input name="password" type="password">
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Повторите пароль</td>
|
||||
<td>
|
||||
<input name="password2" type="password">
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Куда</td>
|
||||
<td>
|
||||
<select name="to">
|
||||
<option></option>
|
||||
<option value="1">Отдел снабжения</option>
|
||||
<option value="2">Отдел разработки</option>
|
||||
<option value="3">Директору</option>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
Сообщение:
|
||||
<label>
|
||||
<textarea name="message" style="display:block;width:400px;height:100px"></textarea>
|
||||
</label>
|
||||
|
||||
<input type="button" onclick="validate(this.form)" value="Проверить">
|
||||
</form>
|
||||
|
||||
<script>
|
||||
function validate(form) {
|
||||
/* ваш код */
|
||||
}
|
||||
|
||||
/* ваш код */
|
||||
</script>
|
||||
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
|
@ -0,0 +1,17 @@
|
|||
importance: 3
|
||||
|
||||
---
|
||||
|
||||
# Валидация формы
|
||||
|
||||
Напишите функцию `validate(form)`, которая проверяет содержимое формы по клику на кнопку "Проверить".
|
||||
|
||||
Ошибки:
|
||||
|
||||
1. Одно из полей не заполнено.
|
||||
2. Пароли не совпадают.
|
||||
|
||||
Ошибка должна сопровождаться сообщением у поля. Например:
|
||||
|
||||
[iframe height=280 src="solution"]
|
||||
|
56
2-ui/4-forms-controls/4-forms-submit/article.md
Normal file
56
2-ui/4-forms-controls/4-forms-submit/article.md
Normal file
|
@ -0,0 +1,56 @@
|
|||
# Формы: отправка, событие и метод submit
|
||||
|
||||
Событие `submit` возникает при отправке формы. Наиболее частое его применение -- это *валидация* (проверка) формы перед отправкой.
|
||||
|
||||
Метод `submit` позволяет инициировать отправку формы из JavaScript, без участия пользователя. Далее мы рассмотрим детали их использования.
|
||||
|
||||
[cut]
|
||||
|
||||
## Событие submit
|
||||
|
||||
Чтобы отправить форму на сервер, у посетителя есть два способа:
|
||||
|
||||
1. **Первый -- это нажать кнопку `<input type="submit">` или `<input type="image">`.**
|
||||
2. **Второй -- нажать Enter, находясь на каком-нибудь поле.**
|
||||
|
||||
Какой бы способ ни выбрал посетитель -- будет сгенерировано событие `submit`. Обработчик в нём может проверить данные и, если они неверны, то вывести ошибку и сделать `event.preventDefault()` -- тогда форма не отправится на сервер.
|
||||
|
||||
Например, в таком HTML оба способа выведут `alert`, форма не будет отправлена:
|
||||
|
||||
```html autorun height=80 no-beautify
|
||||
<form onsubmit="alert('submit!');return false">
|
||||
Первый: Enter в текстовом поле <input type="text" value="Текст"><br>
|
||||
Второй: Нажать на "Отправить": <input type="submit" value="Отправить">
|
||||
</form>
|
||||
```
|
||||
|
||||
Ожидаемое поведение:
|
||||
|
||||
1. Перейдите в текстовое поле и нажмите Enter, будет событие, но форма не отправится на сервер благодаря `return false` в обработчике.
|
||||
2. То же самое произойдет при клике на `<input type="submit">`.
|
||||
|
||||
````smart header="Взаимосвязь событий `submit` и `click`"
|
||||
При отправке формы путём нажатия Enter на текстовом поле, на элементе `<input type="submit">` везде, кроме IE8-, генерируется событие `click`.
|
||||
|
||||
Это довольно забавно, учитывая что клика-то и не было.
|
||||
|
||||
```html autorun height=80
|
||||
<form onsubmit="alert('submit');return false">
|
||||
<input type="text" size="30" value="При нажатии Enter будет click">
|
||||
<input type="submit" value="Submit" *!*onclick="alert('click')"*/!*>
|
||||
</form>
|
||||
```
|
||||
````
|
||||
|
||||
```warn header="В IE8- событие `submit` не всплывает"
|
||||
В IE8- событие `submit` не всплывает. Нужно вешать обработчик `submit` на сам элемент формы, без использования делегирования.
|
||||
```
|
||||
|
||||
## Метод submit
|
||||
|
||||
Чтобы отправить форму на сервер из JavaScript -- нужно вызвать на элементе формы метод `form.submit()`.
|
||||
|
||||
При этом само событие `submit` не генерируется. Предполагается, что если программист вызывает метод `form.submit()`, то он выполнил все проверки.
|
||||
|
||||
Это используют, в частности, для искусственной генерации и отправки формы.
|
||||
|
3
2-ui/4-forms-controls/index.md
Normal file
3
2-ui/4-forms-controls/index.md
Normal file
|
@ -0,0 +1,3 @@
|
|||
# Forms, controls
|
||||
|
||||
Special properties and events for forms `<form>` and controls: `<input>`, `<select>` and other.
|
132
7-frames-and-windows/1-window-methods/article.md
Normal file
132
7-frames-and-windows/1-window-methods/article.md
Normal file
|
@ -0,0 +1,132 @@
|
|||
# Открытие окон и методы window
|
||||
|
||||
A popup window is one of the oldest methods to show additional document to user.
|
||||
|
||||
Basically, you just run:
|
||||
```js
|
||||
window.open('http://javascript.info/')
|
||||
```
|
||||
|
||||
... And it will open a new window with given URL. Most modern browsers are configured to open new tabs instead of separate windows.
|
||||
|
||||
[cut]
|
||||
|
||||
# The modern use
|
||||
|
||||
Popups exist from really ancient times. The initial idea was to show another content without closing the main window. As of now, there are other ways to do that: Javascript is able to send requests for server, so popups are rarely used. But sometimes they are still handy.
|
||||
|
||||
In the past evil sites abused popups a lot. A bad page could open tons of popup windows with ads. So now most browsers try to block popups and protect the user.
|
||||
|
||||
**Most browsers block popups if they are called outside of user-triggered event handlers like `onclick`.**
|
||||
|
||||
Despite of this problem, there are situations when a popup works best.
|
||||
|
||||
For instance, many shops use online chats for consulting people. A visitor clicks on the button, it runs `window.open` and opens the popup with the chat.
|
||||
|
||||
Why a popup is good here?
|
||||
|
||||
1. A popup is a separate window with its own independent Javascript environment. So a chat service doesn't need to integrate with scripts of the main shop site.
|
||||
2. The popup is very simple to add to site and needs no overhead. It's only a small button, without additional scripts.
|
||||
3. The popup may persist even if the user left the page. For example, a consult advices the user to visit the page of a new "Super-Cooler" goodie. The user goes there in the main window without leaving the chat.
|
||||
|
||||
# window.open
|
||||
|
||||
The syntax is: `window.open(url, name, params)`, where:
|
||||
|
||||
url
|
||||
: An URL to load into the new window.
|
||||
|
||||
name
|
||||
: A name of the new window. Each window has a `window.name`, and here we can specify which window to use for the popup. If there's a window with such name -- it opens the given URL, otherwise a new window is opened.
|
||||
|
||||
params
|
||||
: The configuration string for the new window. It contains settings, delimited by a comma. There must be no spaces in params, for instance: `width:200,height=100`.
|
||||
|
||||
Parameters:
|
||||
|
||||
- Position:
|
||||
- `left/top` (numeric) -- coordinates of the window top-left corner on the screen. There is a limitation: a new window cannot be positioned offscreen.
|
||||
- `width/height` (numeric) -- width and height of a new window. There is a limit on minimal width/height, so it's impossible to create an invisible window.
|
||||
- Window features:
|
||||
- `menubar` (yes/no) -- shows or hides the browser menu on the new window.
|
||||
- `toolbar` (yes/no) -- shows or hides the browser navigation bar (back, forward, reload etc) on the new window.
|
||||
- `location` (yes/no) -- shows or hides the URL field in the new window. FF and IE don't allow to hide it by default.
|
||||
- `status` (yes/no) -- shows or hides the status bar. Again, most browsers force it to show.
|
||||
- `resizable` (yes/no) -- allows to disable the resize for the new window. Not recommended.
|
||||
- `scrollbars` (yes/no) -- allows to disable the scrollbars for the new window. Not recommended.
|
||||
|
||||
|
||||
There is also a number of less supported browser-specific features, which are usually not used. Check <a href="https://developer.mozilla.org/en/DOM/window.open">window.open in MDN</a> for examples.
|
||||
|
||||
|
||||
# Example: a minimalistic window
|
||||
|
||||
Let's open a window with minimal set of features just to see which of them browser allows to disable:
|
||||
|
||||
[js run]
|
||||
var p1 = 'scrollbars=no,resizable=no,status=no,location=no,toolbar=no,menubar=no'
|
||||
var p2 = 'width=0,height=0,left=-1000,top=-1000'
|
||||
open('/', 'test', p1+p2)
|
||||
[/js]
|
||||
|
||||
Here most parameters are disabled and window is positioned offscreen. Run it and see what really happens.
|
||||
|
||||
Let's add normal positioning options, a reasonable width, height, left and top coordinates:
|
||||
|
||||
[js run]
|
||||
var p1 = 'scrollbars=no,resizable=no,status=no,location=no,toolbar=no,menubar=no'
|
||||
var p2 = 'width=100,height=100,left=100,top=100'
|
||||
open('/', 'test', p1+p2)
|
||||
[/js]
|
||||
|
||||
The browser shows the example above as it is meant.
|
||||
|
||||
If there is no 3rd argument in the `open` call or it is empty, then the default window parameters are used.
|
||||
|
||||
If there is a string of params, but some yes/no features are omitted, then the omitted features are disabled, if the browser allows that. So if you specify params, make sure you set all required features to yes.
|
||||
|
||||
If there is no `left/top` in params, then the browser tries to open a new window near the last opened window.
|
||||
|
||||
If there is no `width/height`, then the new window will be the same size as the last opened.
|
||||
|
||||
|
||||
|
||||
# Accessing the new window
|
||||
|
||||
The `open` call returns a reference to the new window. It can be used to manipulate it's properties, change location and even more.
|
||||
|
||||
In the example below, the contents of the new window is modified after loading.
|
||||
|
||||
[js run]
|
||||
var win = open('/', 'example', 'width=300,height=300')
|
||||
win.focus()
|
||||
win.onload = function() {
|
||||
var div = win.document.createElement('div')
|
||||
div.innerHTML = 'Welcome into the future!'
|
||||
div.style.fontSize = '30px'
|
||||
win.document.body.insertBefore( div, win.document.body.firstChild )
|
||||
}
|
||||
[/js]
|
||||
|
||||
Note that the `document` object of the new window is used. That won't work otherwise.
|
||||
|
||||
It is always possible to change the location by assigning `win.location=...`. Most other actions, especially modifying or accessing the new window content are only available if the URL of new window comes from the same protocol://domain:port (or, shortly, from the same origin). More about it in the section [](#142).
|
||||
|
||||
|
||||
# Accessing the opener window
|
||||
|
||||
There is a `window.opener` property which references the window which opened this one. It is `null` for all windows except popups.
|
||||
|
||||
So it is possible to manipulate the opener window, change it's location etc. The popup can do with the opener window all that the window can do with it's popup.
|
||||
|
||||
|
||||
|
||||
# Summary
|
||||
|
||||
The popup is opened by the `win = open(url, name, params)` call. It returns the reference to the newly opened window.
|
||||
|
||||
The popup may access the opener window using the `window.opener` property.
|
||||
|
||||
Also, if you use a popup, inform the visitor about it. The same applies to opening a new window by a link or form with `target="_blank"`.
|
||||
|
||||
An icon with the opening window can help the visitor to survive the focus shift and keep both windows in mind. That's especially true if the new window opens in another tab.
|
169
7-frames-and-windows/2-iframes/article.md
Normal file
169
7-frames-and-windows/2-iframes/article.md
Normal file
|
@ -0,0 +1,169 @@
|
|||
# Общение между окнами и фреймами
|
||||
|
||||
Элемент `iframe` является обычным узлом DOM, как и любой другой. Существенное отличие -- в том, что с ним связан объект `window` внутреннего окна. Он доступен по ссылке `iframe.contentWindow`.
|
||||
|
||||
[cut]
|
||||
|
||||
Таким образом, `iframe.contentWindow.document` будет внутренним документом, `iframe.contentWindow.document.body` -- его `<body>` и так далее.
|
||||
|
||||
```smart header="Когда-то..."
|
||||
В старых браузерах использовались другие свойства, такие как `iframe.contentDocument` и даже `iframe.document`, но они давно не нужны.
|
||||
```
|
||||
|
||||
## Переход внутрь ифрейма
|
||||
|
||||
В примере ниже JavaScript получает документ внутри ифрейма и модифицирует его:
|
||||
|
||||
```html run height=100
|
||||
<iframe src="javascript:'тест'" style="height:60px"></iframe>
|
||||
|
||||
<script>
|
||||
var iframe = document.getElementsByTagName('iframe')[0];
|
||||
|
||||
var iframeDoc = iframe.contentWindow.document;
|
||||
|
||||
if (iframeDoc.readyState == 'complete') {
|
||||
iframeDoc.body.style.backgroundColor = 'green';
|
||||
}
|
||||
iframe.onload = function() {
|
||||
var iframeDoc2 = iframe.contentWindow.document;
|
||||
iframeDoc2.body.style.backgroundColor = 'orange';
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
```smart header="src='javascript:\"текст\"'"
|
||||
Атрибут `src` может использовать протокол `javascript`, как указано выше: `src="javascript:код"`. При этом код выполняется и его результат будет содержимым ифрейма. Этот способ описан в стандарте и поддерживается всеми браузерами.
|
||||
|
||||
Атрибут `src` является обязательным, и его отсутствие может привести к проблемам, вплоть до игнорирования ифрейма браузером. Чтобы ничего не загружать в ифрейм, можно указать пустую строку: `src="javascript:''"` или специальную страницу: `src="about:blank"`.
|
||||
```
|
||||
|
||||
В некоторых браузерах (Chrome) пример выше покажет `iframe` зелёным. А в некоторых (Firefox) -- оранжевым.
|
||||
|
||||
Дело в том, что, когда `iframe` только создан, документ в нём обычно ещё не загружен.
|
||||
|
||||
При обычных значениях `iframe src="..."`, которые указывают на HTML-страницу (даже если она уже в кеше), это всегда так. Документ, который в `iframe` на момент срабатывания скрипта `iframeDoc` -- временный, он будет заменён на новый очень скоро. И работать надо уже с новым документом `iframeDoc2` -- например, по событию `iframe.onload`.
|
||||
|
||||
В случае с `javascript`-протоколом, по идее, ифрейм уже загружен, и тогда `onload` у него уже не будет. Но здесь мнения браузеров расходятся, некоторые (Firefox) всё равно "подгрузят" документ позже. Поэтому факт "готовности" документа в скрипте проверяется через `iframeDoc.readyState`.
|
||||
|
||||
Ещё раз заметим, что при обычных URL в качестве `src` нужно работать не с начальным документом, а с тем, который появится позже.
|
||||
|
||||
## Кросс-доменность: ограничение доступа к окну
|
||||
|
||||
Элемент `<iframe>` является "двуличным". С одной стороны, это обычный узел DOM, с другой -- внутри находится окно, которое может иметь совершенно другой URL, содержать независимый документ из другого источника.
|
||||
|
||||
Внешний документ имеет полный доступ к `<iframe>` как к DOM-узлу. А вот к окну -- если они с одного источника.
|
||||
|
||||
Это приводит к забавным последствиям. Например, чтобы узнать об окончании загрузки `<iframe>`, мы можем повесить обработчик `iframe.onload`. По сути, это то же самое что `iframe.contentWindow.onload`, но его мы можем поставить лишь в случае, если окно с того же источника.
|
||||
|
||||
```html run height=120
|
||||
<iframe src="https://example.com" style="height:100px"></iframe>
|
||||
|
||||
<script>
|
||||
var iframe = document.getElementsByTagName('iframe')[0];
|
||||
|
||||
// сработает
|
||||
iframe.onload = function() {
|
||||
alert( "iframe onload" );
|
||||
};
|
||||
|
||||
// не сработает
|
||||
iframe.contentWindow.onload = function() {
|
||||
alert( "contentWindow onload" );
|
||||
};
|
||||
</script>
|
||||
```
|
||||
|
||||
Если бы в примере выше `<iframe src>` был с текущего сайта, то оба обработчика сработали бы.
|
||||
|
||||
## Иерархия window.frames
|
||||
|
||||
Альтернативный способ доступа к окну ифрейма -- это получить его из коллекции `window.frames`.
|
||||
|
||||
Есть два способа доступа:
|
||||
|
||||
1. `window.frames[0]` -- доступ по номеру.
|
||||
2. `window.frames.iframeName` -- доступ по `name` ифрейма.
|
||||
|
||||
Обратим внимание: в коллекции хранится именно окно (`contentWindow`), а не DOM-элемент.
|
||||
|
||||
Демонстрация всех способов доступа к окну:
|
||||
|
||||
```html run
|
||||
<iframe src="javascript:''" style="height:80px" name="i"></iframe>
|
||||
|
||||
<script>
|
||||
var iframeTag = document.body.children[0];
|
||||
|
||||
var iframeWindow = iframeTag.contentWindow; // окно из тега
|
||||
|
||||
alert( frames[0] === iframeWindow ); // true, окно из коллекции frames
|
||||
alert( frames.i == iframeWindow ); // true, окно из frames по имени
|
||||
</script>
|
||||
```
|
||||
|
||||
Внутри ифрейма могут быть свои вложенные ифреймы. Всё это вместе образует иерархию.
|
||||
|
||||
Ссылки для навигации по ней:
|
||||
|
||||
- `window.frames` -- коллекция "детей" (вложенных ифреймов)
|
||||
- `window.parent` -- содержит ссылку на родительское окно, позволяет обратиться к нему из ифрейма.
|
||||
|
||||
Всегда верно:
|
||||
|
||||
```js
|
||||
// (из окна со фреймом)
|
||||
window.frames[0].parent === window; // true
|
||||
```
|
||||
- `window.top` -- содержит ссылку на самое верхнее окно (вершину иерархии).
|
||||
|
||||
Всегда верно (в предположении, что вложенные фреймы существуют):
|
||||
|
||||
```js
|
||||
window.frames[0].frames[0].frames[0].top === window
|
||||
```
|
||||
|
||||
**Свойство `top` позволяет легко проверить, во фрейме ли находится текущий документ:**
|
||||
|
||||
```js run
|
||||
if (window == top) {
|
||||
alert( 'Этот скрипт является окном верхнего уровня в браузере' );
|
||||
} else {
|
||||
alert( 'Этот скрипт исполняется во фрейме!' );
|
||||
}
|
||||
```
|
||||
|
||||
## Песочница sandbox
|
||||
|
||||
Атрибут `sandbox` позволяет построить "песочницу" вокруг ифрейма, запретив ему выполнять ряд действий.
|
||||
|
||||
Наличие атрибута `sandbox`:
|
||||
|
||||
- Заставляет браузер считать ифрейм загруженным с другого источника, так что он и внешнее окно больше не могут обращаться к переменным друг друга.
|
||||
- Отключает формы и скрипты в ифрейме.
|
||||
- Запрещает менять `parent.location` из ифрейма.
|
||||
|
||||
Пример ниже загружает в `<iframe sandbox>` документ с JavaScript и формой. Ни то ни другое не сработает:
|
||||
|
||||
[codetabs src="sandbox"]
|
||||
|
||||
Если у атрибута `sandbox` нет значения, то браузер применяет максимум ограничений.
|
||||
|
||||
Атрибут `sandbox` может содержать через пробел список ограничений, которые не нужны:
|
||||
|
||||
allow-same-origin
|
||||
: Браузер будет считать документ в ифрейме пришедшим с другого домена и накладывать соответствующие ограничения на работу с ним. Если ифрейм и так с другого домена, то ничего не меняется.
|
||||
|
||||
allow-top-navigation
|
||||
: Разрешает ифрейму менять `parent.location`.
|
||||
|
||||
allow-forms
|
||||
: Разрешает отправлять формы из `iframe`.
|
||||
|
||||
allow-scripts
|
||||
: Разрешает выполнение скриптов из ифрейма. Но скриптам, всё же, будет запрещено открывать попапы.
|
||||
|
||||
```smart
|
||||
Цель атрибута `sandbox` -- наложить дополнительные ограничения. Он не может снять уже существующие, в частности, убрать ограничения безопасности, если ифрейм с другого источника.
|
||||
```
|
||||
|
14
7-frames-and-windows/2-iframes/sandbox.view/index.html
Normal file
14
7-frames-and-windows/2-iframes/sandbox.view/index.html
Normal file
|
@ -0,0 +1,14 @@
|
|||
<!doctype html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<iframe sandbox src="sandboxed.html"></iframe>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
21
7-frames-and-windows/2-iframes/sandbox.view/sandboxed.html
Normal file
21
7-frames-and-windows/2-iframes/sandbox.view/sandboxed.html
Normal file
|
@ -0,0 +1,21 @@
|
|||
<!doctype html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<script>
|
||||
alert(1);
|
||||
</script>
|
||||
|
||||
<form action="http://google.ru">
|
||||
<input type="text">
|
||||
<input type="submit" value="Отправить форму на http://google.ru">
|
||||
</form>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
127
7-frames-and-windows/3-same-origin-policy/article.md
Normal file
127
7-frames-and-windows/3-same-origin-policy/article.md
Normal file
|
@ -0,0 +1,127 @@
|
|||
# Кросс-доменные ограничения и их обход
|
||||
|
||||
Ограничение "Same Origin" ("тот же источник") ограничивает доступ окон и фреймов друг к другу, а также влияет на AJAX-запросы к серверу.
|
||||
|
||||
Причина, по которой оно существует -- безопасность. Если есть два окна, в одном из которых `vasya-pupkin.com`, а в другом `gmail.com`, то мы бы не хотели, чтобы скрипт из первого мог читать нашу почту.
|
||||
|
||||
Сама концепция проста, но есть много важных исключений и особенностей, которые нужно знать для полного понимания этого правила.
|
||||
|
||||
[cut]
|
||||
|
||||
## Концепция Same Origin [#same-origin]
|
||||
|
||||
Два URL считаются имеющим один источник ("same origin"), если у них одинаковый протокол, домен и порт.
|
||||
|
||||
Эти URL имеют один источник:
|
||||
|
||||
- `http://site.com`
|
||||
- `http://site.com`/
|
||||
- `http://site.com/my/page.html`
|
||||
|
||||
А вот эти -- все из других источников:
|
||||
|
||||
- http://<span style="color:red;font-weight:bold">www.</span>site.com (другой домен)
|
||||
- http://site.<span style="color:red;font-weight:bold">org</span> (другой домен)
|
||||
- http<span style="color:red; font-weight:bold">s</span>://site.com (другой протокол)
|
||||
- http://site.com<span style="color:red; font-weight:bold">:8080</span> (другой порт)
|
||||
|
||||
Существует ряд исключений, позволяющих-таки окнам с разных доменов обмениваться информацией, но прямой вызов методов друг друга и чтение свойств запрещены.
|
||||
|
||||
## В действии
|
||||
|
||||
Если одно окно попытается обратиться к другому, то браузер проверит, из одного ли они источника. Если нет -- доступ будет запрещён.
|
||||
|
||||
Например:
|
||||
|
||||
```html run
|
||||
<iframe src="https://example.com"></iframe>
|
||||
|
||||
<script>
|
||||
var iframe = document.body.children[0];
|
||||
|
||||
iframe.onload = function() {
|
||||
try {
|
||||
alert( iframe.contentWindow.document );
|
||||
} catch (e) {
|
||||
alert( "Ошибка: " + e.message );
|
||||
}
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
Пример выше выведет ошибку.
|
||||
|
||||
## Исключение: запись в location
|
||||
|
||||
Окна могут менять `location` друг друга, даже если они из разных источников.
|
||||
|
||||
Причём *читать* свойства `location` нельзя, одно окно не имеет право знать, на каком URL пользователь в другом. А вот *запись* браузеры считают безопасной.
|
||||
|
||||
Например, открыв на `javascript.ru` iframe с `http://example.com`, из этого ифрейма нельзя будет прочитать URL, а вот поменять его -- запросто:
|
||||
|
||||
```html run
|
||||
<iframe src="https://example.com"></iframe>
|
||||
|
||||
<script>
|
||||
var iframe = document.body.children[0];
|
||||
|
||||
iframe.onload = function() {
|
||||
try {
|
||||
// не сработает (чтение)
|
||||
alert( iframe.contentWindow.location.href );
|
||||
} catch (e) {
|
||||
alert( "Ошибка при чтении: " + e.message );
|
||||
}
|
||||
|
||||
// сработает (запись)
|
||||
iframe.contentWindow.location.href = 'https://wikipedia.org';
|
||||
|
||||
iframe.onload = null;
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
Если запустить код выше, то окно сначала загрузит `example.com`, а потом будет перенаправлено на `wikipedia.org`.
|
||||
|
||||
## Исключение: поддомен 3-го уровня
|
||||
|
||||
Ещё одно важное исключение касается доменов третьего уровня.
|
||||
|
||||
Если несколько окон имеют общий домен второго уровня, к примеру `john.site.com`, `peter.site.com`, `site.com`, и присваивают в `document.domain` свой общий поддомен 2-го уровня `site.com`, то все ограничения снимаются.
|
||||
|
||||
То есть, на всех этих сайтах должен быть код:
|
||||
```js
|
||||
document.domain = 'site.com';
|
||||
```
|
||||
|
||||
Тогда между ними не будет кросс-доменных ограничений.
|
||||
|
||||
Обратим внимание: свойство `document.domain` должно быть присвоено на всех окнах, участвующих в коммуникации. Выглядит абсурдно, но даже на документе с `site.com` нужно вызвать: `document.domain="site.com"`. Иначе не будет работать.
|
||||
|
||||
Таким образом разные подсайты в рамках одного общего проекта могут взаимодействовать без ограничений.
|
||||
|
||||
## Исключения в IE
|
||||
|
||||
В браузере Internet Explorer есть два своих, дополнительных исключения из Same Origin Policy.
|
||||
|
||||
1. Порт не входит в понятие "источник" (origin).
|
||||
|
||||
Это означает, что окно с `http://site.com` может свободно общаться с `http://site.com:8080`.
|
||||
|
||||
Это иногда используют для общения серверов, использующих один IP-адрес. Но допустимо такое только в IE.
|
||||
2. Если сайт находится в зоне "Надёжные узлы", то в Internet Explorer ограничения к нему не применяются.
|
||||
|
||||
При этом подразумевается, что для этой зоны в параметрах "Безопасность" включена опция "Доступ к источникам данных за пределами домена".
|
||||
|
||||
## Итого
|
||||
|
||||
Ограничение "одного источника" запрещает окнам и фреймам с разных источников вызывать методы друг друга и читать данные друг из друга.
|
||||
|
||||
При этом "из одного источника" означает "совпадают протокол, домен и порт".
|
||||
|
||||
У этого подхода ряд существенных исключений:
|
||||
|
||||
- Свойства `window.location.*` нельзя читать, но можно менять.
|
||||
- Домены третьего уровня с общим наддоменом могут поменять `document.domain` на их общий домен второго уровня, и тогда они смогут взаимодействовать без ограничений.
|
||||
- IE не включает порт в понятие источника. Кроме того, он позволяет снять ограничения для конкретного сайта включением в доверенную зону.
|
||||
|
|
@ -0,0 +1,97 @@
|
|||
# Общение окон с разных доменов: postMessage
|
||||
|
||||
Интерфейс `postMessage` позволяет общаться друг с другом окнам и ифреймам с разных доменов.
|
||||
|
||||
Он очень удобен, например, для взаимодействия внешних виджетов и сервисов, подключённых через ифрейм с основной страницей.
|
||||
|
||||
[cut]
|
||||
|
||||
## Отправитель: метод postMessage
|
||||
|
||||
Первая часть интерфейса состоит из метода [postMessage](https://developer.mozilla.org/en-US/docs/Web/API/Window.postMessage). Его вызывает окно, которое хочет отправить сообщение, в контексте окна-получателя.
|
||||
|
||||
Проще говоря, если мы хотим отправить сообщение в окно `win`, то нужно вызвать `win.postMessage(data, targetOrigin)`.
|
||||
|
||||
Аргументы:
|
||||
|
||||
data
|
||||
: Данные. По спецификации, это может быть любой объект, который будет *клонирован с сохранением структуры* при передаче.
|
||||
|
||||
Но IE поддерживает только строки, поэтому обычно данные JSON-сериализуют.
|
||||
|
||||
targetOrigin
|
||||
: Разрешить получение сообщения только окнам с данного источника.
|
||||
|
||||
Мы ведь не можем из JavaScript узнать, на каком именно URL находится другое окно. Но иногда хочется быть уверенным, что данные передаются в доверенный документ. Для этого и нужен этот параметр. Проверку осуществляет браузер. При указании `'*'` ограничений нет.
|
||||
|
||||
Например:
|
||||
```html no-beautify
|
||||
<iframe src="http://target.com" name="target">
|
||||
|
||||
<script>
|
||||
var win = window.frames.target;
|
||||
win.postMessage("сообщение", "http://javascript.ru");
|
||||
</script>
|
||||
```
|
||||
|
||||
```warn header="В IE11- можно использовать `postMessage` только для ифреймов"
|
||||
В браузере IE, интерфейс `postMessage` работает только с ифреймами. Он не работает между табами и окнами.
|
||||
|
||||
Это ошибка в данном конкретном браузере, в других -- всё в порядке. Детали по этой и связанным с ней ошибкам: [HTML5 Implementation Issues in IE8 and later](http://blogs.msdn.com/b/ieinternals/archive/2009/09/16/bugs-in-ie8-support-for-html5-postmessage-sessionstorage-and-localstorage.aspx).
|
||||
```
|
||||
|
||||
## Получатель: событие onmessage
|
||||
|
||||
Чтобы получить сообщение, окно должно поставить обработчик на событие `onmessage`.
|
||||
|
||||
Свойства объекта события:
|
||||
|
||||
`data`
|
||||
: Присланные данные
|
||||
|
||||
`origin`
|
||||
: Источник, из которого пришло сообщение, например `http://javascript.ru`.
|
||||
|
||||
`source`
|
||||
: Ссылка на окно, с которого пришло сообщение. Можно тут же ответить.
|
||||
|
||||
Назначать обработчик нужно обязательно через методы `addEventListener/attachEvent`, например:
|
||||
|
||||
```js
|
||||
function listener(event) {
|
||||
if (event.origin != 'http://javascript.ru') {
|
||||
// что-то прислали с неизвестного домена - проигнорируем..
|
||||
return;
|
||||
}
|
||||
|
||||
alert( "получено: " + event.data );
|
||||
}
|
||||
|
||||
if (window.addEventListener) {
|
||||
window.addEventListener("message", listener);
|
||||
} else {
|
||||
// IE8
|
||||
window.attachEvent("onmessage", listener);
|
||||
}
|
||||
```
|
||||
|
||||
```smart header="Задержка отсутствуют"
|
||||
Задержки между отправкой и получением нет, совсем.
|
||||
|
||||
Если для `setTimeout` стандарт предусматривает минимальную задержку 4 мс, то для `postMessage` она равна 0 мс.
|
||||
Поэтому `postMessage` можно, в том числе, использовать как мгновенную альтернативу `setTimeout`.
|
||||
```
|
||||
|
||||
## Итого
|
||||
|
||||
Интерфейс `postMessage` позволяет общаться окнам и ифреймам с разных доменов (в IE8 -- только ифреймы), при этом обеспечивая проверки безопасности.
|
||||
|
||||
1. Отправитель вызывает `targetWin.postMessage(data, targetOrigin)`.
|
||||
2. Если `targetOrigin` не `'*'`, то браузер проверяет, совпадает ли источник с `targetWin`.
|
||||
3. Если совпадает, то на `targetWin` генерируется событие `onmessage`, в котором передаются:
|
||||
|
||||
- `origin` -- источник, с которого пришло сообщение.
|
||||
- `source` -- ссылка на окно-отправитель.
|
||||
- `data` -- данные. Везде, кроме IE, допустимы объекты, которые клонируются, а в IE -- только строка.
|
||||
4. Обработчик на `onmessage` необходимо вешать при помощи специализированных методов `addEventListener/attachEvent`.
|
||||
|
156
7-frames-and-windows/5-window-focus/article.md
Normal file
156
7-frames-and-windows/5-window-focus/article.md
Normal file
|
@ -0,0 +1,156 @@
|
|||
# Привлечение внимания к окну
|
||||
|
||||
Проверить, находится ли окно в фокусе, а также перевести внимание посетителя на него -- сложно.
|
||||
|
||||
В первую очередь, это потому, что JavaScript не интегрирован с оконным менеджером ОС. Кроме того, браузер охраняет права посетителя: если он хочет скрыть окно, то JavaScript не может его остановить.
|
||||
|
||||
Но кое-что сделать, конечно, можно. Об этом и поговорим.
|
||||
|
||||
[cut]
|
||||
|
||||
## Метод window.focus
|
||||
|
||||
Метод `window.focus` позволяет сфокусироваться на окне. Он работает по-разному в разных ОС и браузерах.
|
||||
|
||||
Проверьте, например:
|
||||
|
||||
```js run no-beautify
|
||||
setInterval(function() { window.focus() }, 1000);
|
||||
```
|
||||
|
||||
Что будет, если запустить этот код, и затем переключиться в другое окно или вкладку?
|
||||
|
||||
Можно подумать, что окно будет оказываться в фокусе раз в секунду. Но это не так.
|
||||
|
||||
Произойдет одно из трех:
|
||||
|
||||
1. Вообще никакого эффекта. Самый распространённый случай, если в окне много вкладок.
|
||||
2. Окно развернется (при необходимости) и выйдет на передний план. Обычно это происходит, когда метод `window.focus()` вызывается для попапа, а активно сейчас -- главное окно. То есть, в этом случае вызов сработает.
|
||||
3. Заголовок окна начнет мигать. Чтобы увидеть это в действии -- откройте данную страницу в IE, запустите код и переключитесь на другое окно. Браузер попытается привлечь Ваше внимание миганием/мерцанием заголовка окна.
|
||||
|
||||
## Мерцание заголовка
|
||||
|
||||
В дополнение к `window.focus()` используют мерцание заголовка окна, как показано в примере ниже:
|
||||
|
||||
```html run
|
||||
<script>
|
||||
var win = open('/', 'test', 'width=300,height=300')
|
||||
|
||||
function getAttention(win) {
|
||||
if (win.closed) {
|
||||
alert( "Окно закрыто, привлечь внимание к нему нельзя" );
|
||||
return;
|
||||
}
|
||||
|
||||
win.focus();
|
||||
var i = 0;
|
||||
var show = ['************', win.document.title];
|
||||
|
||||
var focusTimer = setInterval(function() {
|
||||
if (win.closed) {
|
||||
clearInterval(focusTimer);
|
||||
return;
|
||||
}
|
||||
win.document.title = show[i++ % 2];
|
||||
}, 1000);
|
||||
|
||||
win.document.onmousemove = function() {
|
||||
clearInterval(focusTimer);
|
||||
win.document.title = show[1];
|
||||
win.document.onmousemove = null;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<input type="button" onclick="getAttention(win)" value="getAttention(win)">
|
||||
```
|
||||
|
||||
Запустите код и сверните всплывающее окно. А затем -- нажмите кнопку с надписью "getAttention(win)". Браузер будет привлекать ваше внимание, как умеет ;)
|
||||
|
||||
Обратите внимание: в коде есть проверка на `win.closed`. Попытка манипулирования закрытым окном вызовет исключение.
|
||||
|
||||
Как только посетитель сфокусировался на окне индикация прекращается. Для определения момента фокусировки в примере выше используется событие `document.onmousemove`.
|
||||
|
||||
Можно было использовать событие `window.onfocus`, но, оказывается, оно ненадежно.
|
||||
|
||||
## Событие window.onfocus
|
||||
|
||||
Вот переписанный вариант функции `getAttention(win)`, с использованием события `onfocus`:
|
||||
|
||||
```html run
|
||||
<script>
|
||||
var win = open('/', 'test', 'width=300,height=300')
|
||||
|
||||
function getAttention(win) {
|
||||
if (win.closed) {
|
||||
alert( "Окно закрыто, привлечь внимание к нему нельзя" );
|
||||
return;
|
||||
}
|
||||
|
||||
var i = 0;
|
||||
var show = ['************', win.document.title];
|
||||
|
||||
function stop() {
|
||||
clearInterval(focusTimer);
|
||||
win.document.title = show[1];
|
||||
}
|
||||
|
||||
win.onfocus = function() {
|
||||
stop();
|
||||
win.onfocus = null;
|
||||
}
|
||||
|
||||
var focusTimer = setInterval(function() {
|
||||
if (win.closed) {
|
||||
clearInterval(focusTimer);
|
||||
return;
|
||||
}
|
||||
|
||||
win.document.title = show[i++ % 2];
|
||||
}, 1000);
|
||||
|
||||
win.focus();
|
||||
}
|
||||
</script>
|
||||
|
||||
<input type="button" onclick="getAttention(win)" value="getAttention(win)">
|
||||
```
|
||||
|
||||
Далее мы посмотрим случаи, когда он не срабатывает, и почему нам всё же нужно `document.onmousemove`.
|
||||
|
||||
### Когда событие onfocus не работает?
|
||||
|
||||
Возможно такое, что посетитель переключается на окно, а `window.onfocus` не происходит.
|
||||
|
||||
Это потому, что переключение между окнами и фокусировка -- это разные вещи. Например, если курсор находится в поле для ввода URL браузера, то считается, что окно не в фокусе, хотя посетитель переключился на это окно.
|
||||
|
||||
Попробуйте проделать следующее:
|
||||
|
||||
1. Запустите пример с `getAttention` в Chrome или IE (кстати, в них нельзя отключить адресную панель).
|
||||
2. Поместите курсор в панель адреса всплывающего окна.
|
||||
3. Перейдите обратно к главному окну и нажмите кнопку `getAttention(win)`
|
||||
|
||||
**Вы увидите, что несмотря на то, что вы переключились на окно, и оно сейчас на переднем плане, событие `onfocus` не срабатывает.**
|
||||
|
||||
Есть и другие случаи, когда переключение между окнами не вызывает `window.onfocus`. Скажем, если окно сфокусировать щелчком в поле ввода формы, то в IE события `window.onfocus` (а также `window.onfocusin`) -- не сработают!
|
||||
|
||||
Можно попробовать поймать момент фокусировки и по-другому, повесив дополнительные обработчики событий на `document`. В главе <info:focus-blur> описана техника делегирования для `focus/blur`.
|
||||
|
||||
Но этот способ получает фокус только если посетитель сфокусируется где-то в документе: щелкнет или сделает еще какое-то действие в документе, а не просто посмотрит на него и проведет над ним мышкой.
|
||||
|
||||
Впрочем, никто не мешает использовать сочетание всех описанных методов.
|
||||
|
||||
## Итого
|
||||
|
||||
Фокусировка и привлечение внимания к окну:
|
||||
|
||||
- Метод `focus` для `window` не надёжен. Окнами и вкладками браузера можно уверенно управлять только на уровне ОС.
|
||||
|
||||
Поэтому для привлечения внимания посетителя к окну стоит также использовать мерцающий заголовок окна.
|
||||
|
||||
Обнаружение переключения на окно:
|
||||
|
||||
- У `window` есть событие `onfocus`, но оно также ненадёжно.
|
||||
|
||||
Поэтому для определения переключения на окно -- используйте его вместе с делегируемым `focus` на документе, а также `document.onmousemove`.
|
||||
|
199
7-frames-and-windows/6-clickjacking/article.md
Normal file
199
7-frames-and-windows/6-clickjacking/article.md
Normal file
|
@ -0,0 +1,199 @@
|
|||
# Атака Clickjacking и защита от неё
|
||||
|
||||
Атака "кликджекинг" (англ. Clickjacking) позволяет хакеру выполнить клик на сайте-жертве *от имени посетителя*.
|
||||
|
||||
В русском языке встречается дословный перевод термина clickjacking: "угон клика". Так же применительно к clickjacking-атаке можно встретить термины "перекрытие iframe" и "подмена пользовательского интерфейса".
|
||||
|
||||
Кликджекингу подверглись в своё время Twitter, Facebook , PayPal, YouTube и многие другие сайты. Сейчас, конечно, они уже защищены.
|
||||
|
||||
[cut]
|
||||
|
||||
## Идея атаки
|
||||
|
||||
В целом идея очень проста.
|
||||
|
||||
Вот как выглядел "угон клика" пользователя, который зарегистрирован на Facebook:
|
||||
|
||||
1. На вредоносной странице пользователю подсовывается безобидная ссылка (скажем, что-то скачать, "разбогатеть сейчас", посмотреть ролик или просто перейти по ссылке на интересный ресурс).
|
||||
2. Поверх этой заманчивой ссылки помещен прозрачный iframe со страницей facebook.com, так что кнопка "Like" находится чётко над ней.
|
||||
3. Кликая на ссылку, посетитель на самом деле нажимает на эту кнопку.
|
||||
|
||||
## Демо
|
||||
|
||||
Вот пример вредоносной страницы (для наглядности `iframe` -- полупрозрачный):
|
||||
|
||||
```html run height=120 no-beautify
|
||||
<style>
|
||||
iframe { /* iframe с сайта-жертвы */
|
||||
width: 400px;
|
||||
height: 100px;
|
||||
position: absolute;
|
||||
top:0; left:-20px;
|
||||
*!*
|
||||
opacity: 0.5; /* в реальности opacity:0 */
|
||||
*/!*
|
||||
z-index: 1;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div>Нажмите, чтобы разбогатеть сейчас:</div>
|
||||
|
||||
<!-- URL в реальности - с другого домена (атакуемого сайта) -->
|
||||
*!*
|
||||
<iframe src="facebook.html"></iframe>
|
||||
|
||||
<button>Жми тут!</button>
|
||||
*/!*
|
||||
|
||||
<div>..И всё получится (хе-хе, у меня, злого хакера, получится)!</div>
|
||||
```
|
||||
|
||||
В действии:
|
||||
|
||||
[codetabs src="clickjacking-visible" height=200]
|
||||
|
||||
Так как `<iframe src="facebook.html">` полупрозрачный, то в примере выше легко видеть, как он перекрывает кнопку. При клике на "Жми тут" на самом деле происходит клик на `<iframe>` (на "Like").
|
||||
|
||||
В итоге, если посетитель авторизован на facebook (а в большинстве случаев так и есть), то facebook.com получает щелчок от имени посетителя.
|
||||
|
||||
На Twitter это была бы кнопка "Follow".
|
||||
|
||||
Тот же самый пример, но ближе к реальности, с `opacity:0` для `<iframe>`. Вообще незаметно, что на самом деле посетитель кликает на `<iframe>`:
|
||||
|
||||
[codetabs src="clickjacking" height=200]
|
||||
|
||||
Итак, все, что нужно для проведения атаки -- это правильно расположить iframe на вредоносной странице, так чтобы кнопка с Facebook оказалась над "Жми тут!". В большинстве случаев это возможно и делается обычным CSS-позиционированием.
|
||||
|
||||
```smart header="С клавиатурой так не сделаешь"
|
||||
Атака называется "Clickjacking", то есть "угон клика", так как события клавиатуры "угнать" гораздо труднее.
|
||||
Посетителя можно заставить сфокусироваться на `<input>` прозрачного `<iframe>` с сайтом-жертвой, но этот `<input>` невидим, а значит текст в нём также будет невидимым. Посетитель начнёт печатать, но, не увидев текст, прекратит свои действия.
|
||||
```
|
||||
|
||||
## Плохая защита
|
||||
|
||||
Самый старый метод защиты -- это код JavaScript, не позволяющий отобразить веб-страницу внутри фрейма ("framebusting", также его называют "framekilling" и "framebreaking").
|
||||
|
||||
Примерно такой:
|
||||
|
||||
```js
|
||||
if (top != window) {
|
||||
top.location = window.location;
|
||||
}
|
||||
```
|
||||
|
||||
То есть, если окно обнаруживает, что оно загружено во фрейме, то оно автоматически делает себя верхним.
|
||||
|
||||
Увы, в настоящий момент это уже не является сколько-нибудь надежной защитой. Есть несколько способов обхода framebusting. Давайте рассмотрим некоторые из них.
|
||||
|
||||
### Блокировка top-навигации.
|
||||
|
||||
Можно заблокировать переход, инициированный сменой `top.location`, в событии [onbeforeunload](info:onload-ondomcontentloaded#window.onbeforeunload).
|
||||
|
||||
Обработчик этого события ставится на внешней (хакерской) странице и, при попытке `iframe` поменять `top.location`, спросит посетителя, хочет он покинуть данную страницу. В большинстве браузеров хакер может спросить посетителя, используя своё сообщение.
|
||||
|
||||
```js
|
||||
window.onbeforeunload = function() {
|
||||
window.onbeforeunload = null;
|
||||
return "Хотите уйти с этой страницы, не узнав все её тайны (хе-хе)?";
|
||||
}
|
||||
```
|
||||
|
||||
Так что, скорее всего, посетитель ответит на такой странный вопрос отрицательно (он же не знает про ифрейм, видит только страницу, причины для ухода нет). А значит, ожидаемая смена `top.location` не произойдёт!
|
||||
|
||||
Пример в действии:
|
||||
|
||||
[codetabs src="top-location"]
|
||||
|
||||
### Атрибут sandbox
|
||||
|
||||
Современные браузеры поддерживают атрибут [sandbox](http://www.whatwg.org/specs/web-apps/current-work/multipage/the-iframe-element.html#attr-iframe-sandbox)
|
||||
|
||||
Он позволяет разрешить во фрейме скрипты `allow-scripts` и формы `allow-forms`, но запретить top-навигацию (не указать `allow-top-navigation`).
|
||||
|
||||
"Защищённый" `<iframe>` хакер может подключить, к примеру, так:
|
||||
|
||||
```html
|
||||
<iframe *!*sandbox="allow-scripts allow-forms"*/!* src="facebook.html"></iframe>
|
||||
```
|
||||
|
||||
Есть и другие приёмы для обхода этой простейшей защиты.
|
||||
|
||||
Firefox и старый IE могут активировать designMode на исходной странице, это также предотвращает framebusting, у IE есть нестандартный атрибут [security](https://msdn.microsoft.com/en-us/library/ie/ms534622.aspx) для ифреймов, который можно использовать с той же целью.
|
||||
|
||||
Как мы видим, эта защита не только не выдерживает реальной атаки, но и может скомпрометировать сайт (программист-то думает, что защитил его).
|
||||
|
||||
## Заголовок X-Frame-Options
|
||||
|
||||
Все современные браузеры поддерживают заголовок `X-Frame-Options`.
|
||||
|
||||
Он разрешает или запрещает отображение страницы, если она открыта во фрейме.
|
||||
|
||||
Браузеры игнорируют заголовок, если он определен в МЕТА теге. Таким образом, `<meta http-equiv="X-Frame-Options"...>` будет проигнорирован.
|
||||
|
||||
У заголовка может быть три значения:
|
||||
|
||||
SAMEORIGIN
|
||||
: Рендеринг документа, при открытии во фрейме, производится только в том случае, когда верхний (top) документ -- с того же домена.
|
||||
|
||||
DENY
|
||||
: Рендеринг документа внутри фрейма запрещён.
|
||||
|
||||
ALLOW-FROM domain
|
||||
: Разрешает рендеринг, если внешний документ с данного домена (не поддерживается в Safari, Firefox).
|
||||
|
||||
К примеру, Twitter использует `X-Frame-Options: SAMEORIGIN`. Результат:
|
||||
|
||||
```html
|
||||
<iframe src="https://twitter.com"></iframe>
|
||||
```
|
||||
|
||||
<iframe src="https://twitter.com"></iframe>
|
||||
|
||||
В зависимости от браузера, `iframe` выше либо пустой, либо в нём находится сообщение о невозможности отобразить его (IE).
|
||||
|
||||
## Показ с отключённым функционалом
|
||||
|
||||
Заголовок `X-Frame-Options` имеет неприятный побочный эффект. Иногда поисковики, анонимайзеры или другие сайты хотели бы отобразить страницу в `iframe`, по вполне "легальным" причинам, но не могут.
|
||||
|
||||
Хорошо бы показывать их посетителям не пустой `iframe`, а нечто, что может быть более интересно.
|
||||
|
||||
Например, можно изначально "накрывать" документ `div` с `height:100%;width:100%`, который будет перехватывать все клики. И поставить на нём ссылку, ведующую на страницу в новом окне.
|
||||
|
||||
```html
|
||||
<style>
|
||||
#iframe-protector {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
z-index: 99999999;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div id="iframe-protector">
|
||||
<a href="/" target="_blank">Перейти на сайт</a>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
if (top.document.domain == document.domain) {
|
||||
убрать iframe - protector
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
Если страница -- не во фрейме или домен совпадает, то посетитель не увидит его.
|
||||
|
||||
## Заключение
|
||||
|
||||
Атаку "Clickjacking" легко осуществить, если на сайте есть действие, активируемое с помощью одного клика.
|
||||
|
||||
Злоумышленник может осуществить атаку целенаправленно на посетителей ресурса -- опубликовав ссылку на форуме, или "счастливой рассылкой". Существует масса вариантов.
|
||||
|
||||
С первого взгляда, она "неглубокая": всё, что можно сделать -- это один клик. С другой стороны, если хакер знает, что после клика появляется какой-то другой управляющий элемент, то он, хитрыми сообщениями, может заставить посетителя кликнуть и по нему. А это уже не один, а два клика.
|
||||
|
||||
Атака особенно опасна, поскольку, проектируя интерфейс сайта, обычно никто и не задумывается о том, что клик от имени юзера может сделать хакер. Точки уязвимости могут быть в совершенно непредсказуемых местах.
|
||||
|
||||
- Рекомендуется использовать `X-Frame-Options` на страницах, заведомо не предназначеных для запуска во фрейме и на важнейших страницах (финансовые транзакции).
|
||||
- Используйте перекрывающий `<div>`, если это допустимо вашим проектом и вы хотите разрешить безопасный показ документа во фреймах с любых доменов.
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
|
||||
<body style="margin:10px;padding:10px">
|
||||
|
||||
<input type="button" onclick="alert('Нажата кнопка Like с другого домена!')" value="I LIKE IT !">
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
|
@ -0,0 +1,35 @@
|
|||
<!doctype html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<style>
|
||||
iframe {
|
||||
/* iframe с сайта-жертвы */
|
||||
|
||||
width: 400px;
|
||||
height: 100px;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -20px;
|
||||
opacity: 0.5;
|
||||
z-index: 1;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div>Нажмите, чтобы разбогатеть сейчас:</div>
|
||||
|
||||
<!-- URL, в реальности - с другого домена (атакуемого сайта) -->
|
||||
<iframe src="facebook.html"></iframe>
|
||||
|
||||
<button>Жми тут!</button>
|
||||
|
||||
<div>..И всё получится (хе-хе, у меня, злого хакера, получится)!</div>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
|
@ -0,0 +1,10 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
|
||||
<body style="margin:10px;padding:10px">
|
||||
|
||||
<input type="button" onclick="alert('Нажата кнопка Like с другого домена!')" value="I LIKE IT !">
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
|
@ -0,0 +1,35 @@
|
|||
<!doctype html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<style>
|
||||
iframe {
|
||||
/* iframe с сайта-жертвы */
|
||||
|
||||
width: 400px;
|
||||
height: 100px;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -20px;
|
||||
opacity: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div>Нажмите, чтобы разбогатеть сейчас:</div>
|
||||
|
||||
<!-- URL, в реальности - с другого домена (атакуемого сайта) -->
|
||||
<iframe src="facebook.html"></iframe>
|
||||
|
||||
<button>Жми тут!</button>
|
||||
|
||||
<div>..И всё получится (хе-хе, у меня, злого хакера, получится)!</div>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
|
@ -0,0 +1,18 @@
|
|||
<!doctype html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<div>Меняет top.location на javascript.ru</div>
|
||||
|
||||
<script>
|
||||
top.location = 'http://javascript.ru';
|
||||
</script>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
|
@ -0,0 +1,44 @@
|
|||
<!doctype html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
|
||||
<style>
|
||||
iframe {
|
||||
/* iframe с сайта-жертвы */
|
||||
|
||||
width: 400px;
|
||||
height: 100px;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -20px;
|
||||
opacity: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
function attack() {
|
||||
|
||||
window.onbeforeunload = function() {
|
||||
window.onbeforeunload = null;
|
||||
return "Хотите уйти с этой страницы, не узнав все её тайны (хе-хе)?";
|
||||
};
|
||||
|
||||
document.body.insertAdjacentHTML('beforeend', '<iframe src="iframe.html">');
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<p>При нажатии на кнопку посетитель получит "странный" вопрос о том, не хочет ли уйти со страницы.</p>
|
||||
|
||||
<p>Наверно, он ответит "хочу остаться" и защита ифрейма будет провалена.</p>
|
||||
|
||||
<button onclick="attack()">Подключить "защищённый" iframe</button>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
2
7-frames-and-windows/index.md
Normal file
2
7-frames-and-windows/index.md
Normal file
|
@ -0,0 +1,2 @@
|
|||
# Окна и Фреймы
|
||||
|
BIN
figures.sketch
BIN
figures.sketch
Binary file not shown.
Loading…
Add table
Add a link
Reference in a new issue