up
This commit is contained in:
parent
1f61c2ab1d
commit
af0ee2a49e
66 changed files with 12263 additions and 2059 deletions
|
@ -150,6 +150,86 @@ Click on the button in `<iframe>` below to set the handler, and then click the l
|
||||||
Some browsers like Chrome and Firefox ignore the string and shows its own message instead. That's for sheer safety, to protect the user from potentially misleading and hackish messages.
|
Some browsers like Chrome and Firefox ignore the string and shows its own message instead. That's for sheer safety, to protect the user from potentially misleading and hackish messages.
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## readyState
|
||||||
|
|
||||||
|
What happens if we set the `DOMContentLoaded` handler after the document is loaded?
|
||||||
|
|
||||||
|
Naturally, it never runs.
|
||||||
|
|
||||||
|
There are cases when we are not sure whether the document is ready or not, for instance an external script with `async` attribute loads and runs asynchronously. Depending on the network, it may load and execute before the document is complete or after that, we can't be sure. So we should be able to know the current state of the document.
|
||||||
|
|
||||||
|
The `document.readyState` property gives us information about it. There are 3 possible values:
|
||||||
|
|
||||||
|
- `"loading"` -- the document is loading.
|
||||||
|
- `"interactive"` -- the document was fully read.
|
||||||
|
- `"complete"` -- the document was fully read and all resources (like images) are loaded too.
|
||||||
|
|
||||||
|
So we can check `document.readyState` and setup a handler or execute the code immediately if it's ready.
|
||||||
|
|
||||||
|
Like this:
|
||||||
|
|
||||||
|
```js
|
||||||
|
function work() { /*...*/ }
|
||||||
|
|
||||||
|
if (document.readyState == 'loading') {
|
||||||
|
document.addEventListener('DOMContentLoaded', work);
|
||||||
|
} else {
|
||||||
|
work();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
There's a `readystatechange` event that triggers when the state changes, so we can print all these states like this:
|
||||||
|
|
||||||
|
```js run
|
||||||
|
// current state
|
||||||
|
console.log(document.readyState);
|
||||||
|
|
||||||
|
// print state changes
|
||||||
|
document.addEventListener('readystatechange', () => console.log(document.readyState));
|
||||||
|
```
|
||||||
|
|
||||||
|
The `readystatechange` event is an alternative mechanics of tracking the document loading state, it appeared long ago. Nowadays, it is rarely used, but let's cover it for completeness.
|
||||||
|
|
||||||
|
What is the place of `readystatechange` among other events?
|
||||||
|
|
||||||
|
To see the timing, here's a document with `<iframe>`, `<img>` and handlers that log events:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<script>
|
||||||
|
function log(text) { /* output the time and message */ }
|
||||||
|
log('initial readyState:' + document.readyState);
|
||||||
|
|
||||||
|
document.addEventListener('readystatechange', () => log('readyState:' + document.readyState));
|
||||||
|
document.addEventListener('DOMContentLoaded', () => log('DOMContentLoaded'));
|
||||||
|
|
||||||
|
window.onload = () => log('window onload');
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<iframe src="iframe.html" onload="log('iframe onload')"></iframe>
|
||||||
|
|
||||||
|
<img src="http://en.js.cx/clipart/train.gif" id="img">
|
||||||
|
<script>
|
||||||
|
img.onload = () => log('img onload');
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
The working example is [in the sandbox](sandbox:readystate).
|
||||||
|
|
||||||
|
The typical output:
|
||||||
|
1. [1] initial readyState:loading
|
||||||
|
2. [2] readyState:interactive
|
||||||
|
3. [2] DOMContentLoaded
|
||||||
|
4. [3] iframe onload
|
||||||
|
5. [4] readyState:complete
|
||||||
|
6. [4] img onload
|
||||||
|
7. [4] window onload
|
||||||
|
|
||||||
|
The numbers in square brackets denote the approximate time of when it happens. The real time is a bit greater, but events labeled with the same digit happen approximately at the same time (+- a few ms).
|
||||||
|
|
||||||
|
- `document.readyState` becomes `interactive` right before `DOMContentLoaded`. These two events actually mean the same.
|
||||||
|
- `document.readyState` becomes `complete` when all resources (`iframe` and `img`) are loaded. Here we can see that it happens in about the same time as `img.onload` (`img` is the last resource) and `window.onload`. Switching to `complete` state means the same as `window.onload`. The difference is that `window.onload` always works after all other `load` handlers.
|
||||||
|
|
||||||
|
|
||||||
## Summary
|
## Summary
|
||||||
|
|
||||||
Page lifecycle events:
|
Page lifecycle events:
|
||||||
|
@ -157,7 +237,10 @@ Page lifecycle events:
|
||||||
- `DOMContentLoaded` event triggers on `document` when DOM is ready. We can apply Javascript to elements at this stage.
|
- `DOMContentLoaded` event triggers on `document` when DOM is ready. We can apply Javascript to elements at this stage.
|
||||||
- All scripts are executed except those that are external with `async` or `defer`
|
- All scripts are executed except those that are external with `async` or `defer`
|
||||||
- Images and other resources may still continue loading.
|
- Images and other resources may still continue loading.
|
||||||
|
|
||||||
- `load` event on `window` triggers when the page and all resources are loaded. We rarely use it, because there's usually no need to wait for so long.
|
- `load` event on `window` triggers when the page and all resources are loaded. We rarely use it, because there's usually no need to wait for so long.
|
||||||
- `beforeload` event on `window` triggers when the user wants to leave the page. If it returns a string, the browser shows a question whether the user really wants to leave or not.
|
- `beforeload` event on `window` triggers when the user wants to leave the page. If it returns a string, the browser shows a question whether the user really wants to leave or not.
|
||||||
- `unload` event on `window` triggers when the user is finally leaving, in the handler we can only do simple things that do not involve delays or asking a user. Because of that limitation, it's rarely used.
|
- `unload` event on `window` triggers when the user is finally leaving, in the handler we can only do simple things that do not involve delays or asking a user. Because of that limitation, it's rarely used.
|
||||||
|
- `document.readyState` is the current state of the document, changes can be tracked in the `readystatechange` event:
|
||||||
|
- `loading` -- the document is loading.
|
||||||
|
- `interactive` -- the document is parsed, happens at about the same time as `DOMContentLoaded`, but before it.
|
||||||
|
- `complete` -- the document and resources are loaded, happens at about the same time as `window.onload`, but before it.
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
test iframe
|
||||||
|
</body>
|
||||||
|
</html>
|
File diff suppressed because it is too large
Load diff
|
@ -87,3 +87,5 @@ Pictures `<img>`, external styles, scripts and other resources provide `load` an
|
||||||
- `error` triggers on a failed load.
|
- `error` triggers on a failed load.
|
||||||
|
|
||||||
The only exception is `<iframe>`: for historical reasons it always triggers `load`, for any load completion, even if the page is not found.
|
The only exception is `<iframe>`: for historical reasons it always triggers `load`, for any load completion, even if the page is not found.
|
||||||
|
|
||||||
|
The `readystatechange` event also works for resources, but is rarely used, because `load/error` events are simpler.
|
||||||
|
|
|
@ -1,24 +1,21 @@
|
||||||
Решение:
|
The solution, step by step:
|
||||||
|
|
||||||
```html run
|
```html run
|
||||||
<select>
|
<select id="genres">
|
||||||
<option value="Rock">Рок</option>
|
<option value="rock">Rock</option>
|
||||||
<option value="Blues" selected>Блюз</option>
|
<option value="blues" selected>Blues</option>
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
var select = document.body.children[0];
|
|
||||||
|
|
||||||
// 1)
|
// 1)
|
||||||
var selectedOption = select.options[select.selectedIndex];
|
let selectedOption = genres.options[select.selectedIndex];
|
||||||
alert( selectedOption.value );
|
alert( selectedOption.value );
|
||||||
|
|
||||||
// 2)
|
// 2)
|
||||||
var newOption = new Option("Classic", "Классика");
|
let newOption = new Option("classic", "Classic");
|
||||||
select.appendChild(newOption);
|
select.append(newOption);
|
||||||
|
|
||||||
// 3)
|
// 3)
|
||||||
newOption.selected = true;
|
newOption.selected = true;
|
||||||
</script>
|
</script>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
@ -2,20 +2,19 @@ importance: 5
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
# Добавьте опцию к селекту
|
# Add an option to select
|
||||||
|
|
||||||
Есть селект:
|
There's a `<select>`:
|
||||||
|
|
||||||
```html
|
```html
|
||||||
<select>
|
<select id="genres">
|
||||||
<option value="Rock">Рок</option>
|
<option value="rock">Rock</option>
|
||||||
<option value="Blues" selected>Блюз</option>
|
<option value="blues" selected>Blues</option>
|
||||||
</select>
|
</select>
|
||||||
```
|
```
|
||||||
|
|
||||||
При помощи JavaScript:
|
Use Javascript to:
|
||||||
|
|
||||||
1. Выведите значение и текст текущей выбранной опции.
|
|
||||||
2. Добавьте опцию: `<option value="Classic">Классика</option>`.
|
|
||||||
3. Сделайте её выбранной.
|
|
||||||
|
|
||||||
|
1. Show the value and the text of the selected option.
|
||||||
|
2. Add an option: `<option value="classic">Classic</option>`.
|
||||||
|
3. Make it selected.
|
||||||
|
|
|
@ -1,10 +1,9 @@
|
||||||
# Form properties and methods [todo]
|
# Form properties and methods
|
||||||
|
|
||||||
Forms and control elements, such as `<input>` have a lot of special properties and events.
|
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.
|
Working with forms can be much more convenient if we know them.
|
||||||
|
|
||||||
|
|
||||||
[cut]
|
[cut]
|
||||||
|
|
||||||
## Navigation: form and elements
|
## Navigation: form and elements
|
||||||
|
@ -35,7 +34,7 @@ For instance:
|
||||||
// get the element
|
// get the element
|
||||||
let elem = form.elements.one; // <input name="one"> element
|
let elem = form.elements.one; // <input name="one"> element
|
||||||
|
|
||||||
alert( elem.value ); // 1
|
alert(elem.value); // 1
|
||||||
</script>
|
</script>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -151,126 +150,139 @@ For instance:
|
||||||
</script>
|
</script>
|
||||||
```
|
```
|
||||||
|
|
||||||
## Values: input and textarea
|
## Form elements
|
||||||
|
|
||||||
Normally, we can read the value as `input.value` or `input.checked` (for radio)
|
Let's talk about form controls, pay attention to their specific features.
|
||||||
|
|
||||||
Для большинства типов `input` значение ставится/читается через свойство `value`.
|
### input and textarea
|
||||||
|
|
||||||
|
Normally, we can access the value as `input.value` or `input.checked` for checkboxes.
|
||||||
|
|
||||||
|
Like this:
|
||||||
|
|
||||||
```js
|
```js
|
||||||
input.value = "Новое значение";
|
input.value = "New value";
|
||||||
textarea.value = "Новый текст";
|
textarea.value = "New text";
|
||||||
|
|
||||||
|
input.checked = true; // for a checkbox or radio button
|
||||||
```
|
```
|
||||||
|
|
||||||
```warn header="Не используйте `textarea.innerHTML`"
|
```warn header="Use `textarea.value`, not `textarea.innerHTML`"
|
||||||
Для элементов `textarea` также доступно свойство `innerHTML`, но лучше им не пользоваться: оно хранит только HTML, изначально присутствовавший в элементе, и не меняется при изменении значения.
|
Please note that we should never use `textarea.innerHTML`: it stores only the HTML that was initially on the page, not the current value.
|
||||||
```
|
```
|
||||||
|
|
||||||
Исключения -- `input type="checkbox"` и `input type="radio"`
|
### select and option
|
||||||
|
|
||||||
**Текущее "отмеченное" состояние для `checkbox` и `radio` находится в свойстве `checked` (`true/false`).**
|
A `<select>` element has 3 important properties:
|
||||||
|
|
||||||
```js
|
1. `select.options` -- the collection of `<option>` elements,
|
||||||
if (input.checked) {
|
2. `select.value` -- the value of the chosen option,
|
||||||
alert( "Чекбокс выбран" );
|
3. `select.selectedIndex` -- the number of the selected option.
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Элементы select и option
|
So we have three ways to set the value of a `<select>`:
|
||||||
|
|
||||||
Селект в JavaScript можно установить двумя путями: поставив значение `select.value`, либо установив свойство `select.selectedIndex` в номер нужной опции.:
|
1. Find the needed `<option>` and set `option.selected` to `true`.
|
||||||
|
2. Set `select.value` to the value.
|
||||||
|
3. Set `select.selectedIndex` to the number of the option.
|
||||||
|
|
||||||
```js
|
The first way is the most obvious, but `(2)` and `(3)` are usually more convenient.
|
||||||
select.selectedIndex = 0; // первая опция
|
|
||||||
```
|
|
||||||
|
|
||||||
Установка `selectedIndex = -1` очистит выбор.
|
Here is an example:
|
||||||
|
|
||||||
**Список элементов-опций доступен через `select.options`.**
|
|
||||||
|
|
||||||
Если `select` допускает множественный выбор (атрибут `multiple`), то значения можно получить/установить, сделав цикл по `select.options`. При этом выбранные опции будут иметь свойство `option.selected = true`.
|
|
||||||
|
|
||||||
Пример:
|
|
||||||
|
|
||||||
```html run
|
```html run
|
||||||
<form name="form">
|
<select id="select">
|
||||||
<select name="genre" *!*multiple*/!*>
|
<option value="apple">Apple</option>
|
||||||
<option value="blues" selected>Мягкий блюз</option>
|
<option value="pear">Pear</option>
|
||||||
<option value="rock" selected>Жёсткий рок</option>
|
<option value="banana">Banana</option>
|
||||||
<option value="classic">Классика</option>
|
</select>
|
||||||
</select>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
var form = document.forms[0];
|
// all three lines do the same thing
|
||||||
var select = form.elements.genre;
|
select.options[2].selected = true;
|
||||||
|
select.selectedIndex = 2;
|
||||||
for (var i = 0; i < select.options.length; i++) {
|
select.value = 'banana';
|
||||||
var option = select.options[i];
|
|
||||||
*!*
|
|
||||||
if(option.selected) {
|
|
||||||
alert( option.value );
|
|
||||||
}
|
|
||||||
*/!*
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
```
|
```
|
||||||
|
|
||||||
Спецификация: [the select element](https://html.spec.whatwg.org/multipage/forms.html#the-select-element).
|
Unlike most other controls, `<select multiple>` allows multiple choice. In that case we need to walk over `select.options` to get all selected values.
|
||||||
|
|
||||||
````smart header="`new Option`"
|
Like this:
|
||||||
В стандарте [the option element](https://html.spec.whatwg.org/multipage/forms.html#the-option-element) есть любопытный короткий синтаксис для создания элемента с тегом `option`:
|
|
||||||
|
```html run
|
||||||
|
<select id="select" *!*multiple*/!*>
|
||||||
|
<option value="blues" selected>Blues</option>
|
||||||
|
<option value="rock" selected>Rock</option>
|
||||||
|
<option value="classic">Classic</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// get all selected values from multi-select
|
||||||
|
let selected = Array.from(select.options)
|
||||||
|
.filter(option => option.selected)
|
||||||
|
.map(option => option.value);
|
||||||
|
|
||||||
|
alert(selected); // blues,rock
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
The full specification of the `<select>` element is available at <https://html.spec.whatwg.org/multipage/forms.html#the-select-element>.
|
||||||
|
|
||||||
|
### new Option
|
||||||
|
|
||||||
|
In the specification of [the option element](https://html.spec.whatwg.org/multipage/forms.html#the-option-element) there's a nice short syntax to create `<option>` elements:
|
||||||
|
|
||||||
```js
|
```js
|
||||||
option = new Option(text, value, defaultSelected, selected);
|
option = new Option(text, value, defaultSelected, selected);
|
||||||
```
|
```
|
||||||
|
|
||||||
Параметры:
|
Parameters:
|
||||||
|
|
||||||
- `text` -- содержимое,
|
- `text` -- the text inside the option,
|
||||||
- `value` -- значение,
|
- `value` -- the option value,
|
||||||
- `defaultSelected` и `selected` поставьте в `true`, чтобы сделать элемент выбранным.
|
- `defaultSelected` -- if `true`, then `selected` attribute is created,
|
||||||
|
- `selected` -- if `true`, then the option is selected.
|
||||||
|
|
||||||
Его можно использовать вместо `document.createElement('option')`, например:
|
For instance:
|
||||||
|
|
||||||
```js
|
```js
|
||||||
var option = new Option("Текст", "value");
|
let option = new Option("Text", "value");
|
||||||
// создаст <option value="value">Текст</option>
|
// creates <option value="value">Текст</option>
|
||||||
```
|
```
|
||||||
|
|
||||||
Такой же элемент, но выбранный:
|
The same element selected:
|
||||||
|
|
||||||
```js
|
```js
|
||||||
var option = new Option("Текст", "value", true, true);
|
let option = new Option("Text", "value", true, true);
|
||||||
```
|
```
|
||||||
````
|
|
||||||
|
|
||||||
```smart header="Дополнительные свойства `option`"
|
```smart header="Additional properties of `<option>`"
|
||||||
У элементов `option` также есть особые свойства, которые могут оказаться полезными (см. [the option element](https://html.spec.whatwg.org/multipage/forms.html#the-option-element)):
|
Option elements have additional properties:
|
||||||
|
|
||||||
`selected`
|
`selected`
|
||||||
: выбрана ли опция
|
: Is the option selected.
|
||||||
|
|
||||||
`index`
|
`index`
|
||||||
: номер опции в списке селекта
|
: The number of the option among the others in its `<select>`.
|
||||||
|
|
||||||
`text`
|
`text`
|
||||||
: Текстовое содержимое опции (то, что видит посетитель).
|
: Text content of the option (seen by what the visitor).
|
||||||
```
|
```
|
||||||
|
|
||||||
## Итого
|
## Summary
|
||||||
|
|
||||||
Свойства для навигации по формам:
|
Form navigation:
|
||||||
|
|
||||||
`document.forms`
|
`document.forms`
|
||||||
: Форму можно получить как `document.forms[name/index]`.
|
: A form is available as `document.forms[name/index]`.
|
||||||
|
|
||||||
`form.elements`
|
`form.elements`
|
||||||
: Элементы в форме: `form.elements[name/index]`. Каждый элемент имеет ссылку на форму в свойстве `form`. Свойство `elements` также есть у `<fieldset>`.
|
: Form elements are available as `form.elements[name/index]`, or can use just `form[name/index]`. The `elements` property also works for `<fieldset>`.
|
||||||
|
|
||||||
Значение элементов читается/ставится через `value` или `checked`.
|
`element.form`
|
||||||
|
: Elements reference their form in the `form` property.
|
||||||
|
|
||||||
Для элемента `select` можно задать опцию по номеру через `select.selectedIndex` и перебрать опции через `select.options`. При этом выбранные опции (в том числе при мультиселекте) будут иметь свойство `option.selected = true`.
|
Value is available as `input.value`, `textarea.value`, `select.value` etc, or `input.checked` for checkboxes and radio buttons.
|
||||||
|
|
||||||
|
For `<select>` we can also get the value by the index `select.selectedIndex` or through the options collection `select.options`. The full specification of this and other elements is at <https://html.spec.whatwg.org/multipage/forms.html>.
|
||||||
|
|
||||||
Спецификация: [HTML5 Forms](https://html.spec.whatwg.org/multipage/forms.html).
|
These are the basics to start working with forms. In the next chapter we'll cover `focus` and `blur` events that may occur on any element, but are mostly handled on forms.
|
||||||
|
|
|
@ -1,3 +0,0 @@
|
||||||
В данном случае достаточно событий `input.focus/input.blur`.
|
|
||||||
|
|
||||||
Если бы мы хотели реализовать это на уровне документа, то применили бы делегирование и события `focusin/focusout` (эмуляцию для firefox), так как обычные `focus/blur` не всплывают.
|
|
|
@ -1,74 +0,0 @@
|
||||||
<!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>
|
|
|
@ -1,48 +0,0 @@
|
||||||
<!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>
|
|
|
@ -1,20 +0,0 @@
|
||||||
importance: 5
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# Улучшенный плейсхолдер
|
|
||||||
|
|
||||||
Реализуйте более удобный плейсхолдер-подсказку на JavaScript через атрибут `data-placeholder`.
|
|
||||||
|
|
||||||
Правила работы плейсхолдера:
|
|
||||||
|
|
||||||
- Элемент изначально содержит плейсхолдер. Специальный класс `placeholder` придает ему синий цвет.
|
|
||||||
- При фокусировке плейсхолдер показывается уже над полем, становясь "подсказкой".
|
|
||||||
- При снятии фокуса, подсказка убирается, если поле пустое -- плейсхолдер возвращается в него.
|
|
||||||
|
|
||||||
Демо:
|
|
||||||
|
|
||||||
[iframe src="solution" height=100]
|
|
||||||
|
|
||||||
В этой задаче плейсхолдер должен работать на одном конкретном input. Подумайте, если input много, как здесь применить делегирование?
|
|
||||||
|
|
|
@ -1,9 +0,0 @@
|
||||||
|
|
||||||
Нам нужно ловить `onclick` на мышонке и в `onkeydown` на нём смотреть коды символов. При скан-кодах стрелок двигать мышонка через `position:absolute` или `position:fixed`.
|
|
||||||
|
|
||||||
Скан-коды для клавиш стрелок можно узнать, нажимая на них на [тестовом стенде](info:keyboard-events#keyboard-test-stand). Вот они: 37-38-39-40 (влево-вверх-вправо-вниз).
|
|
||||||
|
|
||||||
Проблема может возникнуть одна -- `keydown` не возникает на элементе, если на нём нет фокуса.
|
|
||||||
|
|
||||||
Чтобы фокус был -- нужно добавить мышонку атрибут `tabindex` через JS или в HTML.
|
|
||||||
|
|
|
@ -1,13 +0,0 @@
|
||||||
importance: 4
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# Мышонок на "клавиатурном" приводе
|
|
||||||
|
|
||||||
Кликните по мышонку. Затем нажимайте клавиши со стрелками, и он будет двигаться.
|
|
||||||
|
|
||||||
[demo src="solution"]
|
|
||||||
|
|
||||||
В этой задаче запрещается ставить обработчики куда-либо, кроме элемента `#mouse`.
|
|
||||||
|
|
||||||
Можно изменять атрибуты и классы в HTML.
|
|
|
@ -0,0 +1,54 @@
|
||||||
|
<!DOCTYPE HTML>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<link type="text/css" rel="stylesheet" href="my.css">
|
||||||
|
<meta charset="utf-8">
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
<li>Click the div to edit.</li>
|
||||||
|
<li>Enter or blur saves the result.</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
HTML is allowed.
|
||||||
|
|
||||||
|
<div id="view" class="view">Text</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
let area = null;
|
||||||
|
let view = document.getElementById('view');
|
||||||
|
|
||||||
|
view.onclick = function() {
|
||||||
|
editStart();
|
||||||
|
};
|
||||||
|
|
||||||
|
function editStart() {
|
||||||
|
area = document.createElement('textarea');
|
||||||
|
area.className = 'edit';
|
||||||
|
area.value = view.innerHTML;
|
||||||
|
|
||||||
|
area.onkeydown = function(event) {
|
||||||
|
if (event.key == 'Enter') {
|
||||||
|
this.blur();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
area.onblur = function() {
|
||||||
|
editEnd();
|
||||||
|
};
|
||||||
|
|
||||||
|
view.replaceWith(area);
|
||||||
|
area.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
function editEnd() {
|
||||||
|
view.innerHTML = area.value;
|
||||||
|
area.replaceWith(view);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -1,27 +1,25 @@
|
||||||
#view,
|
.view,
|
||||||
#area {
|
.edit {
|
||||||
height: 150px;
|
height: 150px;
|
||||||
width: 400px;
|
width: 400px;
|
||||||
font-family: arial;
|
font-family: arial;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
#view {
|
.view {
|
||||||
/* padding + border = 3px */
|
/* padding + border = 3px */
|
||||||
|
|
||||||
padding: 2px;
|
padding: 2px;
|
||||||
border: 1px solid black;
|
border: 1px solid black;
|
||||||
}
|
}
|
||||||
|
|
||||||
#area {
|
.edit {
|
||||||
display: none;
|
|
||||||
/* replace padding with border (still 3px not to shift the contents) */
|
/* replace padding with border (still 3px not to shift the contents) */
|
||||||
|
border: 3px solid blue;
|
||||||
border: 3px groove blue;
|
|
||||||
padding: 0px;
|
padding: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#area:focus {
|
.edit:focus {
|
||||||
outline: none;
|
outline: none;
|
||||||
/* remove focus border in Safari */
|
/* remove focus border in Safari */
|
||||||
}
|
}
|
|
@ -0,0 +1,27 @@
|
||||||
|
<!DOCTYPE HTML>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<link type="text/css" rel="stylesheet" href="my.css">
|
||||||
|
<meta charset="utf-8">
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
<li>Click the div to edit.</li>
|
||||||
|
<li>Enter or blur saves the result.</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
HTML is allowed.
|
||||||
|
|
||||||
|
<div id="view" class="view">Text</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// ...your code...
|
||||||
|
// Note: <textarea> should have class="edit"
|
||||||
|
// my.css has styles to make it the same size as div
|
||||||
|
</script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -1,27 +1,25 @@
|
||||||
#view,
|
.view,
|
||||||
#area {
|
.edit {
|
||||||
height: 150px;
|
height: 150px;
|
||||||
width: 400px;
|
width: 400px;
|
||||||
font-family: arial;
|
font-family: arial;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
#view {
|
.view {
|
||||||
/* padding + border = 3px */
|
/* padding + border = 3px */
|
||||||
|
|
||||||
padding: 2px;
|
padding: 2px;
|
||||||
border: 1px solid black;
|
border: 1px solid black;
|
||||||
}
|
}
|
||||||
|
|
||||||
#area {
|
.edit {
|
||||||
display: none;
|
|
||||||
/* replace padding with border (still 3px not to shift the contents) */
|
/* replace padding with border (still 3px not to shift the contents) */
|
||||||
|
border: 3px solid blue;
|
||||||
border: 3px groove blue;
|
|
||||||
padding: 0px;
|
padding: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#area:focus {
|
.edit:focus {
|
||||||
outline: none;
|
outline: none;
|
||||||
/* remove focus border in Safari */
|
/* remove focus border in Safari */
|
||||||
}
|
}
|
13
2-ui/4-forms-controls/2-focus-blur/3-editable-div/task.md
Normal file
13
2-ui/4-forms-controls/2-focus-blur/3-editable-div/task.md
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
importance: 5
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Editable div
|
||||||
|
|
||||||
|
Create a `<div>` that turns into `<textarea>` when clicked.
|
||||||
|
|
||||||
|
The textarea allows to edit the HTML in the `<div>`.
|
||||||
|
|
||||||
|
When the user presses `key:Enter` or it looses focus, the `<textarea>` turns back into `<div>`, and its content becomes HTML in `<div>`.
|
||||||
|
|
||||||
|
[demo src="solution"]
|
|
@ -1,99 +0,0 @@
|
||||||
# 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';
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
|
@ -1,61 +0,0 @@
|
||||||
<!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>
|
|
|
@ -1,22 +0,0 @@
|
||||||
<!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>
|
|
|
@ -1,14 +0,0 @@
|
||||||
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 @@
|
||||||
|
|
||||||
|
We can use `mouse.onclick` to handle the click and make the mouse "moveable" with `position:fixed`, then then `mouse.onkeydown` to handle arrow keys.
|
||||||
|
|
||||||
|
The only pitfall is that `keydown` only triggers on elements with focus. So we need to add `tabindex` to the element. As we're forbidden to change HTML, we can use `mouse.tabIndex` property for that.
|
||||||
|
|
||||||
|
P.S. We also can replace `mouse.onclick` with `mouse.onfocus`.
|
|
@ -9,7 +9,7 @@
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
#mouse:focus {
|
#mouse:focus {
|
||||||
outline: 1px dashed black;
|
outline: 1px dashed black;
|
||||||
}
|
}
|
||||||
|
@ -18,9 +18,9 @@
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
<p>Кликните на мышонка и передвигайте его, нажимая клавиши со стрелками.</p>
|
<p>Click on the mouse and move it with arrow keys.</p>
|
||||||
|
|
||||||
<pre id="mouse" tabindex="0">
|
<pre id="mouse">
|
||||||
_ _
|
_ _
|
||||||
(q\_/p)
|
(q\_/p)
|
||||||
/. .\
|
/. .\
|
||||||
|
@ -34,7 +34,9 @@
|
||||||
|
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
document.getElementById('mouse').onclick = function() {
|
mouse.tabIndex = 0;
|
||||||
|
|
||||||
|
mouse.onclick = function() {
|
||||||
this.style.left = this.getBoundingClientRect().left + 'px';
|
this.style.left = this.getBoundingClientRect().left + 'px';
|
||||||
this.style.top = this.getBoundingClientRect().top + 'px';
|
this.style.top = this.getBoundingClientRect().top + 'px';
|
||||||
|
|
||||||
|
@ -42,18 +44,18 @@
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
document.getElementById('mouse').onkeydown = function(e) {
|
mouse.onkeydown = function(e) {
|
||||||
switch (e.keyCode) {
|
switch (e.key) {
|
||||||
case 37: // влево
|
case 'ArrowLeft':
|
||||||
this.style.left = parseInt(this.style.left) - this.offsetWidth + 'px';
|
this.style.left = parseInt(this.style.left) - this.offsetWidth + 'px';
|
||||||
return false;
|
return false;
|
||||||
case 38: // вверх
|
case 'ArrowUp':
|
||||||
this.style.top = parseInt(this.style.top) - this.offsetHeight + 'px';
|
this.style.top = parseInt(this.style.top) - this.offsetHeight + 'px';
|
||||||
return false;
|
return false;
|
||||||
case 39: // вправо
|
case 'ArrowRight':
|
||||||
this.style.left = parseInt(this.style.left) + this.offsetWidth + 'px';
|
this.style.left = parseInt(this.style.left) + this.offsetWidth + 'px';
|
||||||
return false;
|
return false;
|
||||||
case 40: // вниз
|
case 'ArrowDown':
|
||||||
this.style.top = parseInt(this.style.top) + this.offsetHeight + 'px';
|
this.style.top = parseInt(this.style.top) + this.offsetHeight + 'px';
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -61,5 +63,4 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
|
</html>
|
||||||
</html>
|
|
|
@ -9,7 +9,7 @@
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
#mouse:focus {
|
#mouse:focus {
|
||||||
outline: 1px dashed black;
|
outline: 1px dashed black;
|
||||||
}
|
}
|
||||||
|
@ -18,7 +18,7 @@
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
<p>Кликните на мышонка и передвигайте его, нажимая клавиши со стрелками.</p>
|
<p>Click on the mouse and move it with arrow keys.</p>
|
||||||
|
|
||||||
<pre id="mouse">
|
<pre id="mouse">
|
||||||
_ _
|
_ _
|
||||||
|
@ -34,9 +34,9 @@
|
||||||
|
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// ваш код
|
// ...your code...
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
12
2-ui/4-forms-controls/2-focus-blur/5-keyboard-mouse/task.md
Normal file
12
2-ui/4-forms-controls/2-focus-blur/5-keyboard-mouse/task.md
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
importance: 4
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Keyboard-driven mouse
|
||||||
|
|
||||||
|
Focus on the mouse. Then use arrow keys to move it:
|
||||||
|
|
||||||
|
[demo src="solution"]
|
||||||
|
|
||||||
|
P.S. Don't put event handlers anywhere except the `#mouse` element.
|
||||||
|
P.P.S. Don't modify HTML/CSS, the approach should be generic and work with any element.
|
|
@ -1,21 +0,0 @@
|
||||||
# Вёрстка
|
|
||||||
|
|
||||||
Для вёрстки можно использовать отрицательный `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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
|
@ -1,33 +0,0 @@
|
||||||
<!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>
|
|
|
@ -1,17 +0,0 @@
|
||||||
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;
|
|
||||||
}
|
|
|
@ -1,21 +0,0 @@
|
||||||
<!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>
|
|
|
@ -1,17 +0,0 @@
|
||||||
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;
|
|
||||||
}
|
|
|
@ -1,11 +0,0 @@
|
||||||
importance: 5
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# Красивый плейсхолдер для INPUT
|
|
||||||
|
|
||||||
Создайте для `<input type="password">` красивый, стилизованный плейсхолдер, например (кликните на тексте):
|
|
||||||
|
|
||||||
[iframe src="solution" height=90]
|
|
||||||
|
|
||||||
При клике плейсхолдер просто исчезает и дальше не показывается.
|
|
|
@ -1,86 +0,0 @@
|
||||||
# Алгоритм
|
|
||||||
|
|
||||||
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>
|
|
||||||
```
|
|
||||||
|
|
|
@ -1,77 +0,0 @@
|
||||||
<!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>
|
|
|
@ -1,19 +0,0 @@
|
||||||
<!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>
|
|
|
@ -1,12 +0,0 @@
|
||||||
importance: 3
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# Поле, предупреждающее о включенном CapsLock
|
|
||||||
|
|
||||||
Создайте поле, которое будет предупреждать пользователя, если включен `key:CapsLock`. Выключение `key:CapsLock` уберёт предупреждение.
|
|
||||||
|
|
||||||
Такое поле может помочь избежать ошибок при вводе пароля.
|
|
||||||
|
|
||||||
[iframe height=80 src="solution"]
|
|
||||||
|
|
|
@ -1,58 +1,63 @@
|
||||||
# Фокусировка: focus/blur
|
# Focusing: focus/blur
|
||||||
|
|
||||||
Говорят, что элемент "получает фокус", когда посетитель фокусируется на нём. Обычно фокусировка автоматически происходит при нажатии на элементе мышкой, но также можно перейти на нужный элемент клавиатурой -- через клавишу `key:Tab`, нажатие пальцем на планшете и так далее.
|
An element receives a focus when the user either clicks on it or uses the `key:Tab` key on the keyboard. There's also an `autofocus` HTML attribute that puts the focus into an element by default when a page loads and other means of getting a focus.
|
||||||
|
|
||||||
Момент получения фокуса и потери очень важен.
|
Focusing generally means: "prepare to accept the data here", so that's the moment when we can run the code to initialize or load something.
|
||||||
|
|
||||||
При получении фокуса мы можем подгрузить данные для автодополнения, начать отслеживать изменения. При потере -- проверить данные, которые ввёл посетитель.
|
The moment of loosing the focus ("blur") can be even more important. That's when a user clicks somewhere else or presses `key:Tab` to go to the next form field, or there are other means as well.
|
||||||
|
|
||||||
Кроме того, иногда полезно "вручную", из JavaScript перевести фокус на нужный элемент, например, на поле в динамически созданной форме.
|
Loosing the focus generally means: "the data has been entered", so we can run the code to check it or even to save it to the server and so on.
|
||||||
|
|
||||||
|
There are important peculiarities when working with focus events. We'll do the best to cover them here.
|
||||||
|
|
||||||
[cut]
|
[cut]
|
||||||
|
|
||||||
## События focus/blur
|
## Events focus/blur
|
||||||
|
|
||||||
Событие `focus` вызывается тогда, когда пользователь фокусируется на элементе, а `blur` -- когда фокус исчезает, например посетитель кликает на другом месте экрана.
|
The `focus` event is called on focusing, and `blur` -- when the element loooses the focus.
|
||||||
|
|
||||||
Давайте сразу посмотрим на них в деле, используем для проверки ("валидации") введённых в форму значений.
|
Let's use them for validation of an input field.
|
||||||
|
|
||||||
В примере ниже:
|
In the example below:
|
||||||
|
|
||||||
- Обработчик `onblur` проверяет, что в поле введено число, если нет -- показывает ошибку.
|
- The `blur` handler checks if the field the email is entered, and if not -- shows an error.
|
||||||
- Обработчик `onfocus`, если текущее состояние поля ввода -- "ошибка" -- скрывает её (потом при `onblur` будет повторная проверка).
|
- The `focus` handler hides the error message (on `blur` it will be checked again):
|
||||||
|
|
||||||
В примере ниже, если набрать что-нибудь в поле "возраст" и завершить ввод, нажав `key:Tab` или кликнув в другое место страницы, то введённое значение будет автоматически проверено:
|
|
||||||
|
|
||||||
```html run autorun height=60
|
```html run autorun height=60
|
||||||
<style> .error { border-color: red; } </style>
|
<style>
|
||||||
|
.invalid { border-color: red; }
|
||||||
|
#error { color: red }
|
||||||
|
</style>
|
||||||
|
|
||||||
Введите ваш возраст: <input type="text" id="input">
|
Your email please: <input type="email" id="input">
|
||||||
|
|
||||||
<div id="error"></div>
|
<div id="error"></div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
*!*input.onblur*/!* = function() {
|
*!*input.onblur*/!* = function() {
|
||||||
if (isNaN(this.value)) { // введено не число
|
if (!input.value.includes('@')) { // not email
|
||||||
// показать ошибку
|
input.classList.add('invalid');
|
||||||
this.className = "error";
|
error.innerHTML = 'Please enter a correct email.'
|
||||||
error.innerHTML = 'Вы ввели не число. Исправьте, пожалуйста.'
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
*!*input.onfocus*/!* = function() {
|
*!*input.onfocus*/!* = function() {
|
||||||
if (this.className == 'error') { // сбросить состояние "ошибка", если оно есть
|
if (this.classList.contains('invalid')) { // сбросить состояние "ошибка", если оно есть
|
||||||
this.className = "";
|
this.classList.remove('invalid');
|
||||||
error.innerHTML = "";
|
error.innerHTML = "";
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
```
|
```
|
||||||
|
|
||||||
## Методы focus/blur
|
Modern HTML allows to do many validations using input attributes: `required`, `pattern` and so on. And sometimes they are just what we need. Javascript can be used when we want more flexibility. Also we could automatically send the changed value on the server if it's correct.
|
||||||
|
|
||||||
Методы с теми же названиями переводят/уводят фокус с элемента.
|
|
||||||
|
|
||||||
Для примера модифицируем пример выше, чтобы при неверном вводе посетитель просто не мог уйти с элемента:
|
## Methods focus/blur
|
||||||
|
|
||||||
|
Methods `elem.focus()` and `elem.blur()` set/unset the focus on the element.
|
||||||
|
|
||||||
|
For instance, let's make the visitor unable to leave the input if the value is invalid:
|
||||||
|
|
||||||
```html run autorun height=80
|
```html run autorun height=80
|
||||||
<style>
|
<style>
|
||||||
|
@ -61,22 +66,16 @@
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<div>Возраст:
|
Your email please: <input type="email" id="input">
|
||||||
<input type="text" id="age">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>Имя:
|
|
||||||
<input type="text">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
age.onblur = function() {
|
input.onblur = function() {
|
||||||
if (isNaN(this.value)) { // введено не число
|
if (!this.value.includes('@')) { // not email
|
||||||
// показать ошибку
|
// show the error
|
||||||
this.classList.add("error");
|
this.classList.add("error");
|
||||||
*!*
|
*!*
|
||||||
//... и вернуть фокус обратно
|
// ...and put the focus back
|
||||||
age.focus();
|
input.focus();
|
||||||
*/!*
|
*/!*
|
||||||
} else {
|
} else {
|
||||||
this.classList.remove("error");
|
this.classList.remove("error");
|
||||||
|
@ -85,115 +84,42 @@
|
||||||
</script>
|
</script>
|
||||||
```
|
```
|
||||||
|
|
||||||
Этот пример работает во всех браузерах, кроме Firefox ([ошибка](https://bugzilla.mozilla.org/show_bug.cgi?id=53579)).
|
It works in all browsers except Firefox ([bug](https://bugzilla.mozilla.org/show_bug.cgi?id=53579)).
|
||||||
|
|
||||||
Если ввести что-то нецифровое в поле "возраст", и потом попытаться табом или мышкой перейти на другой `<input>`, то обработчик `onblur` вернёт фокус обратно.
|
If we enter something into the input and then try to use `key:Tab` or click away from the `<input>`, then `onblur` returns the focus back.
|
||||||
|
|
||||||
Обратим внимание -- если из `onblur` сделать `event.preventDefault()`, то такого же эффекта не будет, потому что `onblur` срабатывает уже *после* того, как элемент потерял фокус.
|
Please note that we can't "prevent loosing focus" by calling `event.preventDefault()` in `onblur`, because `onblur` works *after* the element lost the focus.
|
||||||
|
|
||||||
## HTML5 и CSS3 вместо focus/blur
|
## Allow focusing on any element: tabindex
|
||||||
|
|
||||||
Прежде чем переходить к более сложным примерам, использующим JavaScript, мы рассмотрим три примера, когда его использовать не надо, а достаточно современного HTML/CSS.
|
By default many element do not support focusing.
|
||||||
|
|
||||||
### Подсветка при фокусировке
|
The list varies between browsers, but one thing is always correct: `focus/blur` support is guaranteed for elements that a visitor can interact with: `<button>`, `<input>`, `<select>`, `<a>` and so on.
|
||||||
|
|
||||||
Стилизация полей ввода может быть решена средствами CSS (CSS2.1), а именно -- селектором `:focus`:
|
From the other hand, elements that exist to format something like `<div>`, `<span>`, `<table>` -- are unfocusable by default. The method `elem.focus()` doesn't work on them, and `focus/blur` events are never triggered.
|
||||||
|
|
||||||
```html autorun height=100
|
This can be changed using HTML-attribute `tabindex`.
|
||||||
<style>
|
|
||||||
*!*input:focus*/!* {
|
|
||||||
background: #FA6;
|
|
||||||
outline: none; /* убрать рамку */
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
<input type="text">
|
|
||||||
|
|
||||||
<p>Селектор :focus выделит элемент при фокусировке на нем и уберёт рамку, которой браузер выделяет этот элемент по умолчанию.</p>
|
The purpose of this attribute is to specify the order number of the element when `key:Tab` is used to switch between them.
|
||||||
```
|
|
||||||
|
|
||||||
В IE (включая более старые) скрыть фокус также может установка специального атрибута [hideFocus](http://msdn.microsoft.com/en-us/library/ie/ms533783.aspx).
|
That is: if we have two elements, the first has `tabindex="1"`, and the second has `tabindex="2"`, then pressing `key:Tab` while in the first element -- moves us to the second one.
|
||||||
|
|
||||||
### Автофокус
|
There are two special values:
|
||||||
|
|
||||||
При загрузке страницы, если на ней существует элемент с атрибутом `autofocus` -- браузер автоматически фокусируется на этом элементе. Работает во всех браузерах, кроме IE9-.
|
- `tabindex="0"` makes the element the last one.
|
||||||
|
- `tabindex="-1"` means that `key:Tab` should ignore that element.
|
||||||
|
|
||||||
```html run link
|
**Any element supports focusing if it has `tabindex`.**
|
||||||
<input type="text" name="search" *!*autofocus*/!*>
|
|
||||||
```
|
|
||||||
|
|
||||||
Если нужны старые IE, то же самое может сделать JavaScript:
|
For instance, here's a list. Click the first item and press `key:Tab`:
|
||||||
|
|
||||||
```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
|
```html autorun no-beautify
|
||||||
Кликните на первый элемент списка и нажмите Tab. Внимание! Дальнейшие нажатия Tab могут вывести за границы iframe'а с примером.
|
Click the first item and press Tab. Keep track of the order. Please note that many subsequent Tabs can move the focus out of the iframe with the example.
|
||||||
<ul>
|
<ul>
|
||||||
<li tabindex="1">Один</li>
|
<li tabindex="1">One</li>
|
||||||
<li tabindex="0">Ноль</li>
|
<li tabindex="0">Zero</li>
|
||||||
<li tabindex="2">Два</li>
|
<li tabindex="2">Two</li>
|
||||||
<li tabindex="-1">Минус один</li>
|
<li tabindex="-1">Minus one</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
@ -202,121 +128,78 @@
|
||||||
</style>
|
</style>
|
||||||
```
|
```
|
||||||
|
|
||||||
Порядок перемещения по клавише "Tab" в примере выше должен быть таким: `1 - 2 - 0` (ноль всегда последний). Продвинутые пользователи частенько используют "Tab" для навигации, и ваше хорошее отношение к ним будет вознаграждено :)
|
The order is like this: `1 - 2 - 0` (zero is always the last). Normally, `<li>` does not support focusing, but `tabindex` full enables it, along with events and styling with `:focus`.
|
||||||
|
|
||||||
Обычно `<li>` не поддерживает фокусировку, но здесь есть `tabindex`.
|
## Delegation: focusin/focusout
|
||||||
|
|
||||||
## Делегирование с focus/blur
|
Events `focus` and `blur` do not bubble.
|
||||||
|
|
||||||
События `focus` и `blur` не всплывают.
|
For instance, we can't put `onfocus` on the `<form>` to highlight it, like this:
|
||||||
|
|
||||||
Это грустно, поскольку мы не можем использовать делегирование с ними. Например, мы не можем сделать так, чтобы при фокусировке в форме она вся подсвечивалась:
|
```html autorun height=80
|
||||||
|
<!-- on focusing in the form -- add the class -->
|
||||||
```html autorun height=100
|
|
||||||
<!-- при фокусировке на форме ставим ей класс -->
|
|
||||||
<form *!*onfocus="this.className='focused'"*/!*>
|
<form *!*onfocus="this.className='focused'"*/!*>
|
||||||
<input type="text" name="name" value="Ваше имя">
|
<input type="text" name="name" value="Name">
|
||||||
<input type="text" name="surname" value="Ваша фамилия">
|
<input type="text" name="surname" value="Surname">
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<style> .focused { outline: 1px solid red; } </style>
|
<style> .focused { outline: 1px solid red; } </style>
|
||||||
```
|
```
|
||||||
|
|
||||||
Пример выше не работает, т.к. при фокусировке на любом `<input>` событие `focus` срабатывает только на этом элементе и не всплывает наверх. Так что обработчик `onfocus` на форме никогда не сработает.
|
The example above doesn't work, because when user focuses on an `<input>`, the `focus` event triggers on that input only. It doesn't bubble up. So `form.onfocus` never triggers.
|
||||||
|
|
||||||
Что делать? Неужели мы должны присваивать обработчик каждому полю `<input>`?
|
There are two solutions.
|
||||||
|
|
||||||
**Это забавно, но хотя `focus/blur` не всплывают, они могут быть пойманы на фазе перехвата.**
|
First, there's a funny historical feature: `focus/blur` do not bubble up, but propagate down on the capturing phase.
|
||||||
|
|
||||||
Вот так сработает:
|
This will work:
|
||||||
|
|
||||||
```html autorun height=100
|
```html autorun height=80
|
||||||
<form id="form">
|
<form id="form">
|
||||||
<input type="text" name="name" value="Ваше имя">
|
<input type="text" name="name" value="Name">
|
||||||
<input type="text" name="surname" value="Ваша фамилия">
|
<input type="text" name="surname" value="Surname">
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<style>
|
<style> .focused { outline: 1px solid red; } </style>
|
||||||
.focused {
|
|
||||||
outline: 1px solid red;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
*!*
|
*!*
|
||||||
// ставим обработчики на фазе перехвата, последний аргумент true
|
// put the handler on capturing phase (last argument true)
|
||||||
form.addEventListener("focus", function() {
|
form.addEventListener("focus", () => form.classList.add('focused'), true);
|
||||||
this.classList.add('focused');
|
form.addEventListener("blur", () => form.classList.remove('focused'), true);
|
||||||
}, true);
|
|
||||||
|
|
||||||
form.addEventListener("blur", function() {
|
|
||||||
this.classList.remove('focused');
|
|
||||||
}, true);
|
|
||||||
*/!*
|
*/!*
|
||||||
</script>
|
</script>
|
||||||
```
|
```
|
||||||
|
|
||||||
### События focusin/focusout
|
Second, there are `focusin` and `focusout` events -- exactly the same as `focus/blur`, but they bubble.
|
||||||
|
|
||||||
События `focusin/focusout` -- то же самое, что и `focus/blur`, только они всплывают.
|
Note that they must be assigned using `elem.addEventListener`, not `on<event>`.
|
||||||
|
|
||||||
У них две особенности:
|
So here's another working variant:
|
||||||
|
|
||||||
- Не поддерживаются Firefox (хотя поддерживаются даже старейшими IE), см. <https://bugzilla.mozilla.org/show_bug.cgi?id=687787>.
|
```html autorun height=80
|
||||||
- Должны быть назначены не через `on`-свойство, а при помощи `elem.addEventListener`.
|
<form id="form">
|
||||||
|
<input type="text" name="name" value="Name">
|
||||||
Из-за отсутствия подержки Firefox эти события используют редко. Получается, что во всех браузерах можно использовать `focus` на стадии перехвата, ну а `focusin/focusout` -- в IE8-, где стадии перехвата нет.
|
<input type="text" name="surname" value="Surname">
|
||||||
|
|
||||||
Подсветка формы в примере ниже работает во всех браузерах.
|
|
||||||
|
|
||||||
```html autorun height=60 run
|
|
||||||
<form name="form">
|
|
||||||
<input type="text" name="name" value="Ваше имя">
|
|
||||||
<input type="text" name="surname" value="Ваша фамилия">
|
|
||||||
</form>
|
</form>
|
||||||
<style>
|
|
||||||
.focused {
|
<style> .focused { outline: 1px solid red; } </style>
|
||||||
outline: 1px solid red;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
function onFormFocus() {
|
*!*
|
||||||
this.className = 'focused';
|
// put the handler on capturing phase (last argument true)
|
||||||
}
|
form.addEventListener("focusin", () => form.classList.add('focused'));
|
||||||
|
form.addEventListener("focusout", () => form.classList.remove('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>
|
</script>
|
||||||
```
|
```
|
||||||
|
|
||||||
## Итого
|
## Summary
|
||||||
События `focus/blur` происходят при получении и снятия фокуса с элемента.
|
|
||||||
|
|
||||||
У них есть особенности:
|
Events `focus` and `blur` trigger on focusing/loosing focus on the element.
|
||||||
|
|
||||||
- Они не всплывают. Но на фазе перехвата их можно перехватить. Это странно, но это так, не спрашивайте почему.
|
Their specials are:
|
||||||
|
- They do not bubble. Can use capturing state instead or `focusin/focusout`.
|
||||||
Везде, кроме Firefox, поддерживаются всплывающие альтернативы `focusin/focusout`.
|
- Most elements do not support focus by default. Use `tabindex` to make anything focusable.
|
||||||
- По умолчанию многие элементы не могут получить фокус. Например, если вы кликните по `DIV`, то фокусировка на нем не произойдет.
|
|
||||||
|
|
||||||
Но это можно изменить, если поставить элементу атрибут `tabIndex`. Этот атрибут также дает возможность контролировать порядок перехода при нажатии `key:Tab`.
|
|
||||||
|
|
||||||
```smart header="Текущий элемент: `document.activeElement`"
|
|
||||||
Кстати, текущий элемент, на котором фокус, доступен как `document.activeElement`.
|
|
||||||
```
|
|
||||||
|
|
||||||
|
The current focused element is available as `document.activeElement`.
|
||||||
|
|
193
7-frames-and-windows/01-popup-windows/article.md
Normal file
193
7-frames-and-windows/01-popup-windows/article.md
Normal file
|
@ -0,0 +1,193 @@
|
||||||
|
# Popups and window methods
|
||||||
|
|
||||||
|
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]
|
||||||
|
|
||||||
|
## Popup blocking
|
||||||
|
|
||||||
|
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`.**
|
||||||
|
|
||||||
|
If you think about it, that's a bit tricky. If the code is directly in an `onclick` handler, then that's easy. But what is the popup opens in `setTimeout`?
|
||||||
|
|
||||||
|
Try this code:
|
||||||
|
|
||||||
|
```js run
|
||||||
|
// open after 3 seconds
|
||||||
|
setTimeout(() => window.open('http://google.com'), 3000);
|
||||||
|
```
|
||||||
|
|
||||||
|
The popup opens in Chrome, but gets blocked in Firefox.
|
||||||
|
|
||||||
|
...And this works in Firefox too:
|
||||||
|
|
||||||
|
```js run
|
||||||
|
// open after 1 seconds
|
||||||
|
setTimeout(() => window.open('http://google.com'), 1000);
|
||||||
|
```
|
||||||
|
|
||||||
|
The difference is that Firefox treats a timeout of 2000ms or less are acceptable, but after it -- removes the "trust", assuming that now it's "outside of the user action". So the first one is blocked, and the second one is not.
|
||||||
|
|
||||||
|
## Modern usage
|
||||||
|
|
||||||
|
As of now, we have many methods to load and show data on-page with Javascript. But there are still 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, why not in-page?
|
||||||
|
|
||||||
|
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. A popup is very simple to attach to a site, little to no overhead. It's only a small button, without additional scripts.
|
||||||
|
3. A 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 to open a popup is: `window.open(url, name, params)`:
|
||||||
|
|
||||||
|
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 already a window with such name -- the given URL opens in it, 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`.
|
||||||
|
|
||||||
|
Settings for `params`:
|
||||||
|
|
||||||
|
- 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
|
||||||
|
let params = `scrollbars=no,resizable=no,status=no,location=no,toolbar=no,menubar=no,
|
||||||
|
width=0,height=0,left=-1000,top=-1000`;
|
||||||
|
|
||||||
|
open('/', 'test', params);
|
||||||
|
```
|
||||||
|
|
||||||
|
Here most "window features" are disabled and window is positioned offscreen. Run it and see what really happens. Most browsers "fix" odd things like zero `width/height` and offscreen `left/top`. For instance, Chrome open such a window with full width/height, so that it occupies the full screen.
|
||||||
|
|
||||||
|
Let's add normal positioning options and reasonable `width`, `height`, `left`, `top` coordinates:
|
||||||
|
|
||||||
|
```js run
|
||||||
|
let params = `scrollbars=no,resizable=no,status=no,location=no,toolbar=no,menubar=no,
|
||||||
|
width=600,height=300,left=100,top=100`;
|
||||||
|
|
||||||
|
open('/', 'test', params);
|
||||||
|
```
|
||||||
|
|
||||||
|
Most browsers show the example above as required.
|
||||||
|
|
||||||
|
Rules for omitted settings:
|
||||||
|
|
||||||
|
- 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 explicitly 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 a popup
|
||||||
|
|
||||||
|
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
|
||||||
|
let newWindow = open('/', 'example', 'width=300,height=300')
|
||||||
|
newWindow.focus();
|
||||||
|
|
||||||
|
newWindow.onload = function() {
|
||||||
|
let html = `<div style="font-size:30px">Welcome!</div>`;
|
||||||
|
*!*
|
||||||
|
newWindow.document.body.insertAdjacentHTML('afterbegin', html);
|
||||||
|
*/!*
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
Please note that external `document` content is only accessible for windows from the same origin (the same protocol://domain:port).
|
||||||
|
|
||||||
|
For windows with URLs from another sites, we are able to change the location by assigning `newWindow.location=...`, but we can't read the location or access the content. That's for user safety, so that an evil page can't open a popup with `http://gmail.com` and read the data. We'll talk more about it later.
|
||||||
|
|
||||||
|
## Accessing the opener window
|
||||||
|
|
||||||
|
A popup may access the "opener" window as well. A Javascript in it may use `window.opener` to access the window that opened it. It is `null` for all windows except popups.
|
||||||
|
|
||||||
|
So both the main window and the popup have a reference to each other. Thay may modify each other freely assuming that they come from the same origin. If that's not so, then there are still means to communicate, to be covered later [todo when].
|
||||||
|
|
||||||
|
## Closing a popup
|
||||||
|
|
||||||
|
If we don't need a popup any more, we can call `newWindow.close()` on it.
|
||||||
|
|
||||||
|
Technically, the `close()` method is available for any `window`, but `window.close()` is ignored by most browsers if `window` is not created with `window.open()`.
|
||||||
|
|
||||||
|
The `newWindow.closed` is `true` if the window is closed. That's useful to check if the popup (or the main window) is still open or not. A user could close it, and our code should take that possibility into account.
|
||||||
|
|
||||||
|
This code loads and then closes the window:
|
||||||
|
|
||||||
|
```js run
|
||||||
|
let newWindow = open('/', 'example', 'width=300,height=300')
|
||||||
|
newWindow.onload = function() {
|
||||||
|
newWindow.close();
|
||||||
|
alert(newWindow.closed); // true
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## Focus/blur on a popup
|
||||||
|
|
||||||
|
Theoretically, there are `window.focus()` and `window.blur()` methods to focus/unfocus on a window. Also there are `focus/blur` events that allow to focus a window and catch the moment when the visitor switches elsewhere.
|
||||||
|
|
||||||
|
In the past evil pages abused those. For instance, look at this code:
|
||||||
|
|
||||||
|
```js run
|
||||||
|
window.onblur = () => window.focus();
|
||||||
|
```
|
||||||
|
|
||||||
|
When a user attempts to switch out of the window (`blur`), it brings it back to focus. The intention is to "lock" the user within the `window`.
|
||||||
|
|
||||||
|
So, there are limitations that forbid the code like that. There are many limitations to protect the user from ads and evils pages. They depend on the browser.
|
||||||
|
|
||||||
|
For instance, a mobile browser usually ignores that call completely. Also focusing doesn't work when a popup opens in a separate tab rather than a new window.
|
||||||
|
|
||||||
|
Still, there are some things that can be done.
|
||||||
|
|
||||||
|
For instance:
|
||||||
|
|
||||||
|
- When we open a popup, it's might be a good idea to run a `newWindow.focus()` on it. Just in case, for some OS/browser combinations it ensures that the user is in the new window now.
|
||||||
|
- If we want to track when a visitor actually uses our web-app, we can track `window.onfocus/onblur`. That allows us to suspend/resume in-page activities, animations etc. But please note that the `blur` event means that the visitor switched out from the window, but he still may observe it. The window is in the background, but still may be visible.
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
- A popup can be opened by the `open(url, name, params)` call. It returns the reference to the newly opened window.
|
||||||
|
- By default, browsers block `open` calls from the code outside of user actions. Usually a notification appears, so that a user may allow them.
|
||||||
|
- The popup may access the opener window using the `window.opener` property, so the two are connected.
|
||||||
|
- If the main window and the popup come from the same origin, they can freely read and modify each other. Otherwise, they can change location of each other and communicate using messages (to be covered).
|
||||||
|
- To close the popup: use `close()` call. Also the user may close them (just like any other windows). The `window.closed` is `true` after that.
|
||||||
|
- Methods `focus()` and `blur()` allow to focus/unfocus a window. Sometimes.
|
||||||
|
- Events `focus` and `blur` allow to track switching in and out of the window. But please note that a window may still be visible even in the background state, after `blur`.
|
||||||
|
|
||||||
|
Also if we open a popup, a good practice is to notify the user about it. An icon with the opening window can help the visitor to survive the focus shift and keep both windows in mind.
|
346
7-frames-and-windows/03-cross-window-communication/article.md
Normal file
346
7-frames-and-windows/03-cross-window-communication/article.md
Normal file
|
@ -0,0 +1,346 @@
|
||||||
|
# Cross-window communication
|
||||||
|
|
||||||
|
The "Same Origin" (same site) policy limits access of windows and frame to each other.
|
||||||
|
|
||||||
|
The idea is that if we have two windows open: one from `john-smith.com`, and another one is `gmail.com`, then we wouldn't want a script from `john-smith.com` to read our mail.
|
||||||
|
|
||||||
|
[cut]
|
||||||
|
|
||||||
|
## Same Origin [#same-origin]
|
||||||
|
|
||||||
|
Two URLs are said to have the "same origin" if they have the same protocol, domain and port.
|
||||||
|
|
||||||
|
These URLs have the same origin:
|
||||||
|
|
||||||
|
- `http://site.com`
|
||||||
|
- `http://site.com/`
|
||||||
|
- `http://site.com/my/page.html`
|
||||||
|
|
||||||
|
These ones are not:
|
||||||
|
|
||||||
|
- `http://www.site.com` (another domain: `www.` matters)
|
||||||
|
- `http://site.org` (another domain: `.org` matters)
|
||||||
|
- `https://site.com` (another protocol: `https`)
|
||||||
|
- `http://site.com:8080` (another port: `8080`)
|
||||||
|
|
||||||
|
If we have a reference to another window (a popup or iframe), and that window comes from the same origin, then we can do everything with it.
|
||||||
|
|
||||||
|
Otherwise, we can only change its location. Please note: not *read*, but modify it, redirect it to another place. That's possible, because such action does not reveal any data. Also such windows windows may exchange messages. Soon about that later.
|
||||||
|
|
||||||
|
````warn header="Exclusion: subdomains may be same-origin"
|
||||||
|
|
||||||
|
There's an important exclusion in the same-origin policy.
|
||||||
|
|
||||||
|
If windows share the same second-level domain, for instance `john.site.com`, `peter.site.com` and `site.com`, and assign to `document.domain` their common second-level domain `site.com`, then limitations are removed.
|
||||||
|
|
||||||
|
In other words, all such documents (including the one from `site.com`) should have the code:
|
||||||
|
|
||||||
|
```js
|
||||||
|
document.domain = 'site.com';
|
||||||
|
```
|
||||||
|
|
||||||
|
Then they can interact without limitations.
|
||||||
|
````
|
||||||
|
|
||||||
|
## Managing iframes
|
||||||
|
|
||||||
|
An `<iframe>` is a two-faced beast. From one side it's a tag, just like `<script>` or `<img>`. From another side it's a window-in-window.
|
||||||
|
|
||||||
|
The embedded window has a separate `window` and `document` objects, scripts and so on.
|
||||||
|
|
||||||
|
We can access them:
|
||||||
|
|
||||||
|
- `iframe.contentWindow` is a reference to the window inside the `<iframe>`.
|
||||||
|
- `iframe.contentDocument` is a reference to the document inside it.
|
||||||
|
|
||||||
|
When we access an embedded window, the browser checks if the iframe has the same origin. If that's not so then the access to almost everything is denied.
|
||||||
|
|
||||||
|
For instance:
|
||||||
|
|
||||||
|
```html run
|
||||||
|
<iframe src="https://example.com" id="iframe"></iframe>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
iframe.onload = function() {
|
||||||
|
// can get the reference to the inner window
|
||||||
|
let iframeWindow = iframe.contentWindow;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// ...but not to the document inside it
|
||||||
|
let doc = iframe.contentDocument;
|
||||||
|
} catch(e) {
|
||||||
|
alert(e); // Security Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// also can't read iframe.contentWindow.location:
|
||||||
|
try {
|
||||||
|
alert(iframe.contentWindow.location);
|
||||||
|
} catch(e) {
|
||||||
|
alert(e); // Security Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// ...but can modify it!
|
||||||
|
iframe.contentWindow.location = '/'; // works
|
||||||
|
|
||||||
|
iframe.onload = null; // run this code only once
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
The code above shows errors for any operations except:
|
||||||
|
|
||||||
|
- Getting the reference to the inner window `iframe.contentWindow`
|
||||||
|
- Changing its `location`.
|
||||||
|
|
||||||
|
```smart header="`iframe.onload` vs `iframe.contentWindow.onload`"
|
||||||
|
The `iframe.onload` event is actually the same as `iframe.contentWindow.onload`. It triggers when the embedded window fully loads with all resources.
|
||||||
|
|
||||||
|
...But `iframe.onload` is always available, while `iframe.contentWindow.onload` needs the same origin.
|
||||||
|
```
|
||||||
|
|
||||||
|
And here's an example with the same origin:
|
||||||
|
|
||||||
|
```html run
|
||||||
|
<iframe src="/" id="iframe"></iframe>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
iframe.onload = function() {
|
||||||
|
// just do anything
|
||||||
|
iframe.contentDocument.body.prepend("Hello, world!");
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Wait until the iframe loads
|
||||||
|
|
||||||
|
When an iframe is created, it immediately has a document. But that document is different from the one that finally loads into it!
|
||||||
|
|
||||||
|
Here, look:
|
||||||
|
|
||||||
|
|
||||||
|
```html run
|
||||||
|
<iframe src="/" id="iframe"></iframe>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
let oldDoc = iframe.contentDocument;
|
||||||
|
iframe.onload = function() {
|
||||||
|
let newDoc = iframe.contentDocument;
|
||||||
|
*!*
|
||||||
|
alert(oldDoc == newDoc); // false
|
||||||
|
*/!*
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
That's actually a well-known pitfall for novice developers. We shouldn't work with the document immediately, because that's the *wrong document*. If we set any event handlers on it, they will be ignored.
|
||||||
|
|
||||||
|
...But the `onload` event triggers when the whole iframe with all resources is loaded. What if we want to act sooner, on `DOMContentLoaded`?
|
||||||
|
|
||||||
|
We can try to catch the moment when a new document appears, and then setup necessary handlers, like this:
|
||||||
|
|
||||||
|
```html run
|
||||||
|
<iframe src="/" id="iframe"></iframe>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
let oldDoc = iframe.contentDocument;
|
||||||
|
|
||||||
|
// every 100 ms check if the document is the new one
|
||||||
|
let timer = setInterval(() => {
|
||||||
|
if (iframe.contentDocument == oldDoc) return;
|
||||||
|
|
||||||
|
// yeah, now set handlers and do whatever we want
|
||||||
|
clearInterval(timer);
|
||||||
|
|
||||||
|
iframe.contentDocument.addEventListener('DOMContentLoaded', () => {
|
||||||
|
iframe.contentDocument.body.prepend('Hello, world!');
|
||||||
|
});
|
||||||
|
}, 100);
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
Let me know in comments if you know a better solution here.
|
||||||
|
|
||||||
|
### window.frames
|
||||||
|
|
||||||
|
An alternative way to get a window object for `<iframe>` -- is to get it from the named collection `window.frames`:
|
||||||
|
|
||||||
|
- By number: `window.frames[0]` -- the window object for the first frame in the document.
|
||||||
|
- By name: `window.frames.iframeName` -- the window object for the frame with `name="iframeName"`.
|
||||||
|
|
||||||
|
For instance:
|
||||||
|
|
||||||
|
```html run
|
||||||
|
<iframe src="/" style="height:80px" name="win" id="iframe"></iframe>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
alert(iframe.contentWindow == frames[0]); // true
|
||||||
|
alert(iframe.contentWindow == frames.win); // true
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
An iframe may have other iframes inside. The corresponding `window` objects form a hierarchy.
|
||||||
|
|
||||||
|
Navigation links are:
|
||||||
|
|
||||||
|
- `window.frames` -- the collection of "children" windows (for nested frames).
|
||||||
|
- `window.parent` -- the reference to the "parent" (outer) window.
|
||||||
|
- `window.top` -- the reference to the topmost parent window.
|
||||||
|
|
||||||
|
For instance:
|
||||||
|
|
||||||
|
```js run
|
||||||
|
window.frames[0].parent === window; // true
|
||||||
|
```
|
||||||
|
|
||||||
|
We can use the `top` property to check if the current document is open inside a frame or not:
|
||||||
|
|
||||||
|
```js run
|
||||||
|
if (window == top) { // current window == window.top?
|
||||||
|
alert('The script is in the topmost window, not in a frame');
|
||||||
|
} else {
|
||||||
|
alert('The script runs in a frame!');
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### The sandbox attribute
|
||||||
|
|
||||||
|
The `sandbox` attribute allows to run untrusted content inside an `<iframe>`. It "sandboxes" the iframe by treating it as coming from another origin and forbidding certain actions.
|
||||||
|
|
||||||
|
There are many restrictions. By default, for `<iframe sandbox src="...">` all of them are applied. But if we specify them in a value of the attribute, like this: `<iframe sandbox="allow-forms allow-popups">`, then they are lifted.
|
||||||
|
|
||||||
|
In other words, an empty `"sandbox"` attribute puts the strictest limitations possible, but we can put a space-delimited list of those that we want to lift:
|
||||||
|
|
||||||
|
`allow-same-origin`
|
||||||
|
: By default `"sandbox"` forces the browser to treat the `iframe` as coming from another origin, even if it's `src` points to the same site. This option removes that feature.
|
||||||
|
|
||||||
|
`allow-top-navigation`
|
||||||
|
: Allows the `iframe` to change `parent.location`.
|
||||||
|
|
||||||
|
`allow-forms`
|
||||||
|
: Allows to submit forms from `iframe`.
|
||||||
|
|
||||||
|
`allow-scripts`
|
||||||
|
: Allows to run scripts from the `iframe`.
|
||||||
|
|
||||||
|
`allow-popups`
|
||||||
|
: Allows to `window.open` popups from the `iframe`
|
||||||
|
|
||||||
|
See [the manual](mdn:/HTML/Element/iframe) for more.
|
||||||
|
|
||||||
|
The example below demonstrates a sandboxed iframe with some Javascript and a form. Neither one works:
|
||||||
|
|
||||||
|
[codetabs src="sandbox" height=140]
|
||||||
|
|
||||||
|
|
||||||
|
```smart
|
||||||
|
The purpose of the `"sandbox"` attribute is to add restrictions. It cannot remove them. In particular, it can't relax same-origin restrictions if the iframe comes from another origin.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Cross-window messaging
|
||||||
|
|
||||||
|
The `postMessage` interface allows windows to talk to each other no matter which origin they are from.
|
||||||
|
|
||||||
|
It has two parts.
|
||||||
|
|
||||||
|
### postMessage
|
||||||
|
|
||||||
|
The window that wants to send a message calls [postMessage](mdn:api/Window.postMessage) method of the receiving window. In other words, if we want to send the message to `win`, we should call `win.postMessage(data, targetOrigin)`.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
|
||||||
|
`data`
|
||||||
|
: The data to send. Can be any object, the data is cloned using the "structured cloning algorithm". IE supports only strings, so we can `JSON.stringify` complex objects.
|
||||||
|
|
||||||
|
`targetOrigin`
|
||||||
|
: Allow only a window from the given origin to get the message.
|
||||||
|
|
||||||
|
The `targetOrigin` is a safety measure. If the target window comes from another origin, we can't read it's `location`. We can't be sure which site is open in it right now, the user could navigate away.
|
||||||
|
|
||||||
|
Specifying `targetOrigin` ensures that the window only receives the data if it's still at that site. Good when the data is secure.
|
||||||
|
|
||||||
|
If we don't want that check, we can set `targetOrigin` to `*`.
|
||||||
|
|
||||||
|
For instance:
|
||||||
|
|
||||||
|
```html no-beautify
|
||||||
|
<iframe src="http://example.com" name="example">
|
||||||
|
|
||||||
|
<script>
|
||||||
|
let win = window.frames.example;
|
||||||
|
|
||||||
|
win.postMessage("message", "http://example.com");
|
||||||
|
// win.postMessage("message", "*");
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
### onmessage
|
||||||
|
|
||||||
|
To receive a message, the window should have a handler on the `message` event.
|
||||||
|
|
||||||
|
The event object has special properties:
|
||||||
|
|
||||||
|
`data`
|
||||||
|
: The data from `postMessage`
|
||||||
|
|
||||||
|
`origin`
|
||||||
|
: The origin of the sender, for instance `http://javascript.info`.
|
||||||
|
|
||||||
|
`source`
|
||||||
|
: The reference to the sender window. We can immediately `postMessage` back if we want.
|
||||||
|
|
||||||
|
To assign that handler, we should use `addEventListener`, a short syntax `window.onmessage` does not work.
|
||||||
|
|
||||||
|
Here's an example:
|
||||||
|
|
||||||
|
```js
|
||||||
|
window.addEventListener("message", function(event) {
|
||||||
|
if (event.origin != 'http://javascript.info') {
|
||||||
|
// something from an unknown domain, let's ignore it
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
alert( "received: " + event.data );
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
The full example:
|
||||||
|
|
||||||
|
[codetabs src="postmessage" height=120]
|
||||||
|
|
||||||
|
```smart header="There's no delay"
|
||||||
|
There's totally no delay between `postMessage` and the `message` event. That happens synchronously, even faster than `setTimeout(...,0)`.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
To call methods and access the content of another window, we should first have a reference to it.
|
||||||
|
|
||||||
|
For popups:
|
||||||
|
- `window.open` -- opens a new window and returns a reference to it,
|
||||||
|
- `window.opener` -- a reference to the opener window from a popup
|
||||||
|
|
||||||
|
For iframes:
|
||||||
|
- `window.frames` is a collection of nested window objects,
|
||||||
|
- `window.parent`, `window.top` are the references to parent and top windows,
|
||||||
|
- `iframe.contentWindow` is the window inside an `<iframe>` tag.
|
||||||
|
|
||||||
|
Then if they come from the same origin (host, port, protocol), then windows can do whatever they want with each other.
|
||||||
|
|
||||||
|
Otherwise, only possible actions are:
|
||||||
|
- Change the location of another window (write-only access).
|
||||||
|
- Post a message to it.
|
||||||
|
|
||||||
|
Exclusions are:
|
||||||
|
- Windows share the same main domain: `a.site.com` and `b.site.com`. Then setting `document.domain='site.com'` in both of them puts them into "same origin".
|
||||||
|
- If an iframe has a `sandbox` attribute, it is forcefully put into the "different origin" state, unless the `allow-same-origin` is specified in the attribute value. That can be used to run untrusted code in iframes from the same site.
|
||||||
|
|
||||||
|
The `postMessage` interface allows two windows to talk with security checks:
|
||||||
|
|
||||||
|
1. The sender calls `targetWin.postMessage(data, targetOrigin)`.
|
||||||
|
2. If `targetOrigin` is not `'*'`, then the browser checks if window `targetWin` has the URL from `targetWin` site.
|
||||||
|
3. If it is so, then `targetWin` triggers the `message` event with special properties:
|
||||||
|
- `origin` -- the origin of the sender window (like `http://my.site.com`)
|
||||||
|
- `source` -- the reference to the sender window.
|
||||||
|
- `data` -- the data, any object in everywhere except IE that supports only strings.
|
||||||
|
|
||||||
|
We should use `addEventListener` to set the handler for this event inside the target window.
|
|
@ -0,0 +1,18 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
|
||||||
|
Receiving iframe.
|
||||||
|
<script>
|
||||||
|
window.addEventListener('message', function(event) {
|
||||||
|
alert(`Received ${event.data} from ${event.origin}`);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1,25 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<form id="form">
|
||||||
|
<input type="text" placeholder="Enter message" name="message">
|
||||||
|
<input type="submit" value="Click to send">
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<iframe src="iframe.html" id="iframe" style="display:block;height:60px"></iframe>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
form.onsubmit = function() {
|
||||||
|
iframe.contentWindow.postMessage(this.message.value, '*');
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1,15 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div>The iframe below is has <code>sandbox</code> attribute.</div>
|
||||||
|
|
||||||
|
<iframe sandbox src="sandboxed.html" style="height:60px;width:90%"></iframe>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1,18 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<button onclick="alert(123)">Click to run a script (doesn't work)</button>
|
||||||
|
|
||||||
|
<form action="http://google.com">
|
||||||
|
<input type="text">
|
||||||
|
<input type="submit" value="Submit (doesn't work)">
|
||||||
|
</form>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
200
7-frames-and-windows/06-clickjacking/article.md
Normal file
200
7-frames-and-windows/06-clickjacking/article.md
Normal file
|
@ -0,0 +1,200 @@
|
||||||
|
# The clickjacking attack
|
||||||
|
|
||||||
|
The "clickjacking" attack allows an evil page to click on a "victim site" *on behalf of the visitor*.
|
||||||
|
|
||||||
|
Many sites were hacked this way, including Twitter, Facebook, Paypal and other sites. They are all fixed, of course.
|
||||||
|
|
||||||
|
[cut]
|
||||||
|
|
||||||
|
## The idea
|
||||||
|
|
||||||
|
The idea is very simple.
|
||||||
|
|
||||||
|
Here's how clickjacking was done with Facebook:
|
||||||
|
|
||||||
|
1. A visitor is lured to the evil page. No matter how.
|
||||||
|
2. The page has a harmlessly-looking link on it (like "get rich now" or "click here, very funny" and so on).
|
||||||
|
3. Over that link the evil page positions a transparent `<iframe>` with `src` from facebook.com, in such a way that the "Like" button is right above that link. Usually that's done with `z-index`.
|
||||||
|
4. Clicking on that link, the visitor in fact presses that button.
|
||||||
|
|
||||||
|
## The demo
|
||||||
|
|
||||||
|
Here's how the evil page looks like. To make things clear, the `<iframe>` is half-transparent (in real evil pages it's fully transparent):
|
||||||
|
|
||||||
|
```html run height=120 no-beautify
|
||||||
|
<style>
|
||||||
|
iframe { /* iframe from the victim site */
|
||||||
|
width: 400px;
|
||||||
|
height: 100px;
|
||||||
|
position: absolute;
|
||||||
|
top:0; left:-20px;
|
||||||
|
*!*
|
||||||
|
opacity: 0.5; /* in real opacity:0 */
|
||||||
|
*/!*
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div>Click to get rich now:</div>
|
||||||
|
|
||||||
|
<!-- The url from the victim site -->
|
||||||
|
*!*
|
||||||
|
<iframe src="facebook.html"></iframe>
|
||||||
|
|
||||||
|
<button>Click here!</button>
|
||||||
|
*/!*
|
||||||
|
|
||||||
|
<div>...And you're cool (I'm a cool hacker actually)!</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
The full demo of the attack:
|
||||||
|
|
||||||
|
[codetabs src="clickjacking-visible" height=160]
|
||||||
|
|
||||||
|
Here we have a half-transparent `<iframe src="facebook.html">`, and in the example we can see it hovering over the button. A click on the button actually clicks on the iframe, but that's not visible to the user, because the iframe is transparent.
|
||||||
|
|
||||||
|
As a result if the visitor is authorized on facebook ("remember me" is usually turned on), then it adds a "Like". On Twitter that would be a "Follow" button.
|
||||||
|
|
||||||
|
Here's the same example, but closer to reality, with `opacity:0` for `<iframe>`:
|
||||||
|
|
||||||
|
[codetabs src="clickjacking" height=160]
|
||||||
|
|
||||||
|
All we need to attack -- is to position the `<iframe>` on the evil page in such a way that the button is right over the link. That's usually possible with CSS.
|
||||||
|
|
||||||
|
```smart header="Clickjacking is for clicks, not for keyboard"
|
||||||
|
The attack only affects mouse actions.
|
||||||
|
|
||||||
|
Technically, if we have a text field to hack, then we can position an iframe in such a way that text fields overlap each other. So when a visitor tries to focus on the input he sees on the page, he actually focuses on the input inside the iframe.
|
||||||
|
|
||||||
|
But then there's a problem. Everything that the visitor types will be hidden, because the iframe is not visible.
|
||||||
|
|
||||||
|
So that would look really odd to the user, and he will stop.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Old-school defences (weak)
|
||||||
|
|
||||||
|
The oldest defence method is the piece of Javascript that forbids to open the page in a frame (so-called "framebusting").
|
||||||
|
|
||||||
|
Like this:
|
||||||
|
|
||||||
|
```js
|
||||||
|
if (top != window) {
|
||||||
|
top.location = window.location;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
That is: if the window finds out that it's not on the top, then it automatically makes itself the top.
|
||||||
|
|
||||||
|
As of now, that's not reliable, because there are many ways to hack around it. Let's cover a few.
|
||||||
|
|
||||||
|
### Blocking top-navigation
|
||||||
|
|
||||||
|
We can block the transition caused by changing `top.location` in the [beforeunload](info:onload-ondomcontentloaded#window.onbeforeunload) event.
|
||||||
|
|
||||||
|
The top page (that belongs to the hacker) sets a handler to it, and when the `iframe` tries to change `top.location` the visitor gets a message asking him whether he wants to leave.
|
||||||
|
|
||||||
|
Like this:
|
||||||
|
```js
|
||||||
|
window.onbeforeunload = function() {
|
||||||
|
window.onbeforeunload = null;
|
||||||
|
return "Want to leave without learning all the secrets (he-he)?";
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
In most cases the visitor would answer negatively, because he doesn't know about the iframe, all he can see is the top page, there's no reason to leave. And so the `top.location` won't change!
|
||||||
|
|
||||||
|
In action:
|
||||||
|
|
||||||
|
[codetabs src="top-location"]
|
||||||
|
|
||||||
|
### Sandbox attribute
|
||||||
|
|
||||||
|
One of the things restricted by the `sandbox` attribute is navigation. A sandboxed iframe may not change `top.location`.
|
||||||
|
|
||||||
|
So we can add the iframe with `sandbox="allow-scripts allow-forms"`. That would relax the restrictions allowing scripts and forms. But we don't put `allow-top-navigation` in the value so that the navigation is still forbidden. And the change of `top.location` won't work.
|
||||||
|
|
||||||
|
Here's the code:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<iframe *!*sandbox="allow-scripts allow-forms"*/!* src="facebook.html"></iframe>
|
||||||
|
```
|
||||||
|
|
||||||
|
There are other ways to work around that simple protection too.
|
||||||
|
|
||||||
|
## X-Frame-Options
|
||||||
|
|
||||||
|
Server-side header `X-Frame-Options` can allow or forbid showing the page inside a frame.
|
||||||
|
|
||||||
|
It must be sent by the server: browser ignore it if found in `<meta>` tags. So `<meta http-equiv="X-Frame-Options"...>` won't do anything.
|
||||||
|
|
||||||
|
The header may have 3 values:
|
||||||
|
|
||||||
|
|
||||||
|
`DENY`
|
||||||
|
: Never ever show the page inside an iframe.
|
||||||
|
|
||||||
|
`SAMEORIGIN`
|
||||||
|
: Allow to show inside an iframe if the parent document comes from the same origin.
|
||||||
|
|
||||||
|
`ALLOW-FROM domain`
|
||||||
|
: Allows to show inside an iframe if the parent document is from the given domain.
|
||||||
|
|
||||||
|
For instance, Twitter uses `X-Frame-Options: SAMEORIGIN`. Here's the result:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<iframe src="https://twitter.com"></iframe>
|
||||||
|
```
|
||||||
|
|
||||||
|
<iframe src="https://twitter.com"></iframe>
|
||||||
|
|
||||||
|
Depending on the browser, `iframe` above is either empty or it has a message telling that "the browser can't show it".
|
||||||
|
|
||||||
|
## Showing with disabled functionality
|
||||||
|
|
||||||
|
The protecting `X-Frame-Options` header has a side-effect. Other sites can't show our page in an `iframe`, even if they have "legal" reasons to do so.
|
||||||
|
|
||||||
|
So there are other solutions. For instance, we can "cover" the page with a `<div>` with `height:100%;width:100%`, so that it handles all clicks. That `<div>` should disappear if `window == top` or we figure out that we don't need protection.
|
||||||
|
|
||||||
|
Like this:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<style>
|
||||||
|
#protector {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
z-index: 99999999;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div id="protector">
|
||||||
|
<a href="/" target="_blank">Go to the site</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// there will be an error if top window is from the different origin
|
||||||
|
// but that's ok here
|
||||||
|
if (top.document.domain == document.domain) {
|
||||||
|
protector.remove();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
The demo:
|
||||||
|
|
||||||
|
[codetabs src="protector"]
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
Clickjacking is a way to "trick" users into a clicking on a victim site without even knowing what happens. That's dangerous if there are important click-activated actions.
|
||||||
|
|
||||||
|
A hacker can post a link to his evil page in a message or lure visitors to his page by other means. There are many variants.
|
||||||
|
|
||||||
|
From one side -- the attack is "not deep": all a hacker can do is one click. But from another side, if the hacker knows that after the click another control appears, then it may use cunning messages to make the user to click on it as well.
|
||||||
|
|
||||||
|
The attack is quite dangerous, because when we engineer the UI we usually don't think that a hacker can click on behalf of the visitor. So vulnerabilities can be found in totally unexpeced places.
|
||||||
|
|
||||||
|
- It's recommended to use `X-Frame-Options: SAMEORIGIN` on pages that are totally not meant to be shown inside iframes (or just for the whole site).
|
||||||
|
- Use a covering `<div>` if we want to allow our pages to be shown in iframes, and still stay safe.
|
|
@ -0,0 +1,10 @@
|
||||||
|
<!DOCTYPE HTML>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<body style="margin:10px;padding:10px">
|
||||||
|
|
||||||
|
<input type="button" onclick="alert('Like pressed on facebook.html!')" value="I LIKE IT !">
|
||||||
|
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
|
@ -0,0 +1,32 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
iframe {
|
||||||
|
width: 400px;
|
||||||
|
height: 100px;
|
||||||
|
position: absolute;
|
||||||
|
top: 5px;
|
||||||
|
left: -14px;
|
||||||
|
opacity: 0.5;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div>Click to get rich now:</div>
|
||||||
|
|
||||||
|
<!-- The url from the victim site -->
|
||||||
|
<iframe src="facebook.html"></iframe>
|
||||||
|
|
||||||
|
<button>Click here!</button>
|
||||||
|
|
||||||
|
<div>...And you're cool (I'm a cool hacker actually)!</div>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1,10 @@
|
||||||
|
<!DOCTYPE HTML>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<body style="margin:10px;padding:10px">
|
||||||
|
|
||||||
|
<input type="button" onclick="alert('Like pressed on facebook.html!')" value="I LIKE IT !">
|
||||||
|
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
|
@ -0,0 +1,32 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
iframe {
|
||||||
|
width: 400px;
|
||||||
|
height: 100px;
|
||||||
|
position: absolute;
|
||||||
|
top: 5px;
|
||||||
|
left: -14px;
|
||||||
|
opacity: 0;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div>Click to get rich now:</div>
|
||||||
|
|
||||||
|
<!-- The url from the victim site -->
|
||||||
|
<iframe src="facebook.html"></iframe>
|
||||||
|
|
||||||
|
<button>Click here!</button>
|
||||||
|
|
||||||
|
<div>...And you're cool (I'm a cool hacker actually)!</div>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1,41 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
|
||||||
|
<style>
|
||||||
|
#protector {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
z-index: 99999999;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div id="protector">
|
||||||
|
<a href="/" target="_blank">Go to the site</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
|
||||||
|
if (top.document.domain == document.domain) {
|
||||||
|
protector.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
This text is always visible.
|
||||||
|
|
||||||
|
But if the page is shown inside a document from another domain, the div over it prevents any actions.
|
||||||
|
|
||||||
|
<button onclick="alert(1)">Click doesn't work in that case</button>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -4,11 +4,9 @@
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
<iframe sandbox src="sandboxed.html"></iframe>
|
<iframe src="iframe.html"></iframe>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
|
</html>
|
||||||
</html>
|
|
|
@ -0,0 +1,41 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
|
||||||
|
<style>
|
||||||
|
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 "Want to leave without learning all the secrets (he-he)?";
|
||||||
|
};
|
||||||
|
|
||||||
|
document.body.insertAdjacentHTML('beforeend', '<iframe src="iframe.html">');
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<p>After a click on the button the visitor gets a "strange" question about whether he wants to leave.</p>
|
||||||
|
|
||||||
|
<p>Probably he would respond "No", and the iframe protection is hacked.</p>
|
||||||
|
|
||||||
|
<button onclick="attack()">Add a "protected" iframe</button>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -1,132 +0,0 @@
|
||||||
# Открытие окон и методы 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.
|
|
|
@ -1,169 +0,0 @@
|
||||||
# Общение между окнами и фреймами
|
|
||||||
|
|
||||||
Элемент `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` -- наложить дополнительные ограничения. Он не может снять уже существующие, в частности, убрать ограничения безопасности, если ифрейм с другого источника.
|
|
||||||
```
|
|
||||||
|
|
|
@ -1,21 +0,0 @@
|
||||||
<!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>
|
|
|
@ -1,127 +0,0 @@
|
||||||
# Кросс-доменные ограничения и их обход
|
|
||||||
|
|
||||||
Ограничение "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 не включает порт в понятие источника. Кроме того, он позволяет снять ограничения для конкретного сайта включением в доверенную зону.
|
|
||||||
|
|
|
@ -1,97 +0,0 @@
|
||||||
# Общение окон с разных доменов: 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`.
|
|
||||||
|
|
|
@ -1,156 +0,0 @@
|
||||||
# Привлечение внимания к окну
|
|
||||||
|
|
||||||
Проверить, находится ли окно в фокусе, а также перевести внимание посетителя на него -- сложно.
|
|
||||||
|
|
||||||
В первую очередь, это потому, что 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`.
|
|
||||||
|
|
|
@ -1,199 +0,0 @@
|
||||||
# Атака 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>`, если это допустимо вашим проектом и вы хотите разрешить безопасный показ документа во фреймах с любых доменов.
|
|
||||||
|
|
|
@ -1,10 +0,0 @@
|
||||||
<!DOCTYPE HTML>
|
|
||||||
<html>
|
|
||||||
|
|
||||||
<body style="margin:10px;padding:10px">
|
|
||||||
|
|
||||||
<input type="button" onclick="alert('Нажата кнопка Like с другого домена!')" value="I LIKE IT !">
|
|
||||||
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
|
|
@ -1,35 +0,0 @@
|
||||||
<!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>
|
|
|
@ -1,10 +0,0 @@
|
||||||
<!DOCTYPE HTML>
|
|
||||||
<html>
|
|
||||||
|
|
||||||
<body style="margin:10px;padding:10px">
|
|
||||||
|
|
||||||
<input type="button" onclick="alert('Нажата кнопка Like с другого домена!')" value="I LIKE IT !">
|
|
||||||
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
|
|
@ -1,35 +0,0 @@
|
||||||
<!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>
|
|
|
@ -1,44 +0,0 @@
|
||||||
<!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>
|
|
|
@ -1,2 +1 @@
|
||||||
# Окна и Фреймы
|
# Frames and windows
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue