fixes, renovations

This commit is contained in:
Ilya Kantor 2015-03-24 00:03:51 +03:00
parent 3889056599
commit bc34b2fc9d
133 changed files with 189 additions and 10 deletions

View file

@ -0,0 +1,255 @@
# Куки, document.cookie
Для чтения и записи cookie используется свойство `document.cookie`. Однако, оно представляет собой не объект, а строку в специальном формате, для удобной манипуляций с которой нужны дополнительные функции.
[cut]
## Чтение document.cookie
Наверняка у вас есть cookie, которые привязаны к этому сайту. Давайте полюбуемся на них. Вот так:
```js
//+ run
alert( document.cookie );
```
Эта строка состоит из пар `ключ=значение`, которые перечисляются через точку с запятой с пробелом `"; "`.
Значит, чтобы прочитать cookie, достаточно разбить строку по `"; "`, и затем найти нужный ключ. Это можно делать либо через `split` и работу с массивом, либо через регулярное выражение.
## Функция getCookie(name)
Следующая функция `getCookie(name)` возвращает cookie с именем `name`:
```js
// возвращает cookie с именем name, если есть, если нет, то undefined
function getCookie(name) {
var matches = document.cookie.match(new RegExp(
"(?:^|; )" + name.replace(/([\.$?*|{}\(\)\[\]\\\/\+^])/g, '\\$1') + "=([^;]*)"
));
return matches ? decodeURIComponent(matches[1]) : undefined;
}
```
Обратим внимание, что значение может быть любым. Если оно содержит символы, нарушающие форматирование, например, пробелы или `;`, то оно кодируется при помощи `encodeURIComponent`. Функция `getCookie` автоматически раскодирует его.
## Запись в document.cookie
В `document.cookie` можно писать. При этом запись не перезаписывает существующие cookie, а дополняет к ним!
Например, такая строка поставит cookie с именем `userName` и значением `Vasya`:
```js
//+ run
document.cookie = "userName=Vasya";
```
...Однако, всё не так просто. У cookie есть ряд важных настроек, которые очень желательно указать, так как значения по умолчанию у них неудобны.
Эти настройки указываются после пары ключ=значение, каждое -- после точки с запятой:
<dl>
<dt>`path=/mypath`</dt>
<dd>Путь, внутри которого будет доступ к cookie. Если не указать, то имеется в виду текущий путь и все пути ниже него.
Как правило, используется `path=/`, то есть cookie доступно со всех страниц сайта.</dd>
<dt>`domain=site.com`</dt>
<dd>Домен, на котором доступно cookie. Если не указать, то текущий домен. Допустимо указывать текущий домен `site.com` и его поддомены, например `forum.site.com`.
Если указать специальную маску `.site.com`, то cookie будет доступно на сайте и всех его поддоменах. Это используется, например, в случаях, когда кука содержит данные авторизации и должна быть доступна как на `site.com`, так и на `forum.site.com`.
</dd>
<dt>`expires=Tue, 19 Jan 2038 03:14:07 GMT`</dt>
<dd>Дата истечения куки в формате GMT. Получить нужную дату можно, используя объект `Date`. Его можно установить в любое время, а потом вызвать `toUTCString()`, например:
```js
// +1 день от текущего момента
var date = new Date;
date.setDate(date.getDate() + 1);
alert( date.toUTCString() );
```
Если дату не указать, то cookie будет считаться "сессионным". Такое cookie удаляется при закрытии браузера.
Если дата в прошлом, то кука будет удалена.
</dd>
<dt>`secure`</dt>
<dd>Cookie можно передавать только по HTTPS.</dd>
</dl>
Например, чтобы поставить cookie `name=value` по текущему пути с датой истечения через 60 секунд:
```js
//+ run
var date = new Date(new Date().getTime() + 60 * 1000);
document.cookie = "name=value; path=/; expires=" + date.toUTCString();
```
Чтобы удалить это cookie:
```js
//+ run
var date = new Date(0);
document.cookie = "name=; path=/; expires=" + date.toUTCString();
```
При удалении значение не важно. Можно его не указывать, как сделано в коде выше.
## Функция setCookie(name, value, options)
Если собрать все настройки воедино, вот такая функция ставит куки:
```js
function setCookie(name, value, options) {
options = options || {};
var expires = options.expires;
if (typeof expires == "number" && expires) {
var d = new Date();
d.setTime(d.getTime() + expires * 1000);
expires = options.expires = d;
}
if (expires && expires.toUTCString) {
options.expires = expires.toUTCString();
}
value = encodeURIComponent(value);
var updatedCookie = name + "=" + value;
for (var propName in options) {
updatedCookie += "; " + propName;
var propValue = options[propName];
if (propValue !== true) {
updatedCookie += "=" + propValue;
}
}
document.cookie = updatedCookie;
}
```
Аргументы:
<dl>
<dt>name</dt><dd>название cookie</dd>
<dt>value</dt><dd>значение cookie (строка)</dd>
<dt>options</dt><dd>
Объект с дополнительными свойствами для установки cookie:
<dl>
<dt>expires</dt><dd>Время истечения cookie. Интерпретируется по-разному, в зависимости от типа:
<ul>
<li>Число -- количество секунд до истечения. Например, `expires: 3600` -- кука на час.</li>
<li>Объект типа [Date](https://developer.mozilla.org/ru/docs/Web/JavaScript/Reference/Global_Objects/Date) -- дата истечения.</li>
<li>Если expires в прошлом, то cookie будет удалено.</li>
<li>Если expires отсутствует или `0`, то cookie будет установлено как сессионное и исчезнет при закрытии браузера.</li>
</ul>
</dd>
<dt>path</dt><dd>Путь для cookie.</dd>
<dt>domain</dt><dd>Домен для cookie.</dd>
<dt>secure</dt><dd>Если `true`, то пересылать cookie только по защищенному соединению.</dd>
</dl>
</dd>
</dl>
## Функция deleteCookie(name)
Здесь всё просто -- удаляем вызовом `setCookie` с датой в прошлом.
```js
function deleteCookie(name) {
setCookie(name, "", {
expires: -1
})
}
```
## Сторонние cookie
При работе с cookie есть важная тонкость, которая касается внешних ресурсов.
Теоретически, любой ресурс, который загружает браузер, может поставить cookie.
Например:
<ul>
<li>Если на странице есть `<img src="http://mail.ru/counter.gif">`, то вместе с картинкой в ответ сервер может прислать заголовки, устанавливающие cookie.</li>
<li>Если на странице есть `<iframe src="http://facebook.com/button.php">`, то во-первых сервер может вместе с `button.php` прислать cookie, а во-вторых JS-код внутри ифрейма может записать в `document.cookie`</li>
</ul>
При этом cookie будут принадлежать тому домену, который их поставил. То есть, на `mail.ru` для первого случая, и на `facebook.com` во втором.
**Такие cookie, которые не принадлежат основной странице, называются "сторонними" (3rd party) cookies. Не все браузеры их разрешают.**
Как правило, в настройках браузера можно поставить "Блокировать данные и файлы cookie сторонних сайтов" (Chrome).
**В Safari такая настройка включена по умолчанию и выглядит так:**
<img src="safari-nocookie.png">
### Тс-с-с. Большой брат смотрит за тобой.
Цель этого запрета -- защитить посетителей от слежки со стороны рекламодателей, которые вместе с картинкой-баннером присылают и куки, таким образом помечая посетителей.
Например, на многих сайтах стоят баннеры и другая реклама Google Ads. При помощи таких cookie компания Google будет знать, какие именно сайты вы посещаете, сколько времени вы на них проводите и многое другое.
Как? Да очень просто -- на каждом сайте загружается, к примеру, картинка с рекламой. При этом баннер берётся с домена, принадлежащего Google. Вместе с баннером Google ставит cookie со специальным уникальным идентификатором.
Далее, при следующем запросе на баннер, браузер пошлёт стандартные заголовки, которые включают в себя:
<ul>
<li>Cookie с домена баннера, то есть уникальный идентификатор, который был поставлен ранее.</li>
<li>Стандартный заголовок Referrer (его не будет при HTTPS!), который говорит, с какого сайта сделан запрос. Да, впрочем, Google и так знает, с какого сайта запрос, ведь идентификатор сайта есть в URL.</li>
</ul>
Так что Google может хранить в своей базе, какие именно сайты из тех, на которых есть баннер Google, вы посещали, когда вы на них были, и т.п. Этот идентификатор легко привязывается к остальной информации от других сервисов, и таким образом картина слежки получается довольно-таки глобальной.
Здесь я не утверждаю, что в конкретной компании Google всё именно так... Но во-первых, сделать так легко, во-вторых идентификаторы действительно ставятся, а в-третьих, такие знания о человеке позволяют решать, какую именно рекламу и когда ему показать. А это основная доля доходов Google, благодаря которой корпорация существует.
Возможно, компания Apple, которая выпустила Safari, поставила такой флаг по умолчанию именно для уменьшения влияния Google?
### А если очень надо?
Итак, Safari запрещает сторонние cookie по умолчанию. Другие браузеры предоставляют такую возможность, если посетитель захочет.
**А что, если ну очень надо поставить стороннюю cookie, и чтобы это было надёжно?**
Такая задача действительно возникает, например, в системе кросс-доменной авторизации, когда есть несколько доменов 2-го уровня, и хочется, чтобы посетитель, который входит в один сайт, автоматически распознавался во всей сетке. При этом cookie для авторизации ставятся на главный домен -- "мастер", а остальные сайты запрашивают их при помощи специального скрипта (и, как правило, копируют к себе для оптимизации, но здесь это не суть).
Ещё пример -- когда есть внешний виджет, например, `iframe` с информационным сервисом, который можно подключать на разные сайты. И этот `iframe` должен знать что-то о посетителе, опять же, авторизация или какие-то настройки, которые хорошо бы хранить в cookie.
Есть несколько способов поставить 3rd-party cookie для Safari.
<dl>
<dt>Использовать ифрейм.</dt>
<dd>Ифрейм является полноценным окном браузера. В нём должна быть доступна вся функциональность, в том числе cookie. Как браузер решает, что ифрейм "сторонний" и нужно запретить для него и его скриптов установку cookie? Критерий таков: "в ифрейме нет навигации". Если навигация есть, то ифрейм признаётся полноценным окном.
Например, в сторонний `iframe` можно сделать POST. И тогда, в ответ на POST, сервер может поставить cookie. Или прислать документ, который это делает. Ифрейм, в который прошёл POST, считается родным и надёжным.</dd>
<dt>Popup-окно</dt>
<dd>Другой вариант -- использовать popup, то есть при помощи `window.open` открывать именно окно со стороннего домена, и уже там ставить cookie. Это тоже работает.</dd>
<dt>Редирект</dt>
<dd>Ещё одно альтернативное решение, которое подходит не везде - это сделать интеграцию со сторонним доменом, такую что на него можно сделать редирект, он ставит cookie и делает редирект обратно.</dd>
</dl>
## Дополнительно
<ul>
<li>На Cookie наложены ограничения:
<ul>
<li>Имя и значение (после `encodeURIComponent`) вместе не должны превышать 4кб.</li>
<li>Общее количество cookie на домен ограничено 30-50, в зависимости от браузера.</li>
<li>Разные домены 2го уровня полностью изолированы. Но в пределах доменов 3го уровня куки можно ставить свободно с указанием `domain`.</li>
<li>Сервер может поставить cookie с дополнительным флагом `HttpOnly`. Cookie с таким параметром передаётся только в заголовках, оно никак не доступно из JavaScript.</li>
</ul>
</li>
<li>Иногда посетители отключают cookie. Отловить это можно проверкой свойства [navigator.cookieEnabled](https://developer.mozilla.org/en-US/docs/DOM/window.navigator.cookieEnabled)
```js
//+ run
if (!navigator.cookieEnabled) {
alert( 'Включите cookie для комфортной работы с этим сайтом' );
}
```
...Конечно, предполагается, что включён JavaScript. Впрочем, посетитель без JS и cookie с большой вероятностью не человек, а бот.</li>
</ul>
## Итого
Файл с функциями для работы с cookie: [cookie.js](cookie.js).

View file

@ -0,0 +1,45 @@
// возвращает cookie с именем name, если есть, если нет, то undefined
function getCookie(name) {
var matches = document.cookie.match(new RegExp(
"(?:^|; )" + name.replace(/([\.$?*|{}\(\)\[\]\\\/\+^])/g, '\\$1') + "=([^;]*)"
));
return matches ? decodeURIComponent(matches[1]) : undefined;
}
// устанавливает cookie c именем name и значением value
// options - объект с свойствами cookie (expires, path, domain, secure)
function setCookie(name, value, options) {
options = options || {};
var expires = options.expires;
if (typeof expires == "number" && expires) {
var d = new Date();
d.setTime(d.getTime() + expires * 1000);
expires = options.expires = d;
}
if (expires && expires.toUTCString) {
options.expires = expires.toUTCString();
}
value = encodeURIComponent(value);
var updatedCookie = name + "=" + value;
for (var propName in options) {
updatedCookie += "; " + propName;
var propValue = options[propName];
if (propValue !== true) {
updatedCookie += "=" + propValue;
}
}
document.cookie = updatedCookie;
}
// удаляет cookie с именем name
function deleteCookie(name) {
setCookie(name, "", {
expires: -1
})
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

View file

@ -0,0 +1,27 @@
Здесь подойдут стандартные параметры сравнения:
```js
//+ run
var animals = ["тигр", "ёж", "енот", "ехидна", "АИСТ", "ЯК"];
*!*
var collator = new Intl.Collator();
animals.sort(function(a, b) {
return collator.compare(a, b);
});
*/!*
alert( animals ); // АИСТ,ёж,енот,ехидна,тигр,ЯК
```
А вот, что было бы при обычном вызове `sort()`:
```js
//+ run
var animals = ["тигр", "ёж", "енот", "ехидна", "АИСТ", "ЯК"];
*!*
alert( animals.sort() ); // АИСТ,ЯК,енот,ехидна,тигр,ёж
*/!*
```

View file

@ -0,0 +1,17 @@
# Отсортируйте массив с буквой ё
[importance 5]
Используя `Intl.Collate`, отсортируйте массив:
```js
var animals = ["тигр", "ёж", "енот", "ехидна", "АИСТ", "ЯК"];
// ... ваш код ...
alert( animals ); // АИСТ,ёж,енот,ехидна,тигр,ЯК
```
В этом примере порядок сортировки не должен зависеть от регистра.
Что касается бувы `"ё"`, то мы следуем [обычным правилам сортировки буквы ё](http://ru.wikipedia.org/wiki/%D0%81#.D0.A1.D0.BE.D1.80.D1.82.D0.B8.D1.80.D0.BE.D0.B2.D0.BA.D0.B0), по которым "е" и "ё" считаются одной и той же буквой, за исключением случая, когда два слова отличаются только в позиции буквы "е" / "ё" -- тогда слово с "е" ставится первым.

484
10-extra/11-intl/article.md Normal file
View file

@ -0,0 +1,484 @@
# Intl: интернационализация в JavaScript
Общая проблема строк, дат, чисел в JavaScript -- они "не в курсе" языка и особенностей стран, где находится посетитель.
В частности:
<dl>
<dt>Строки</dt>
<dd>При сравнении сравниваются коды символов, а это неправильно, к примеру, в русском языке оказывается, что `"ё" > "я"` и `"а" > "Я"`, хотя всем известно, что `я` -- последняя буква алфавита и это она должна быть больше любой другой.</dd>
<dt>Даты</dt>
<dd>В разных странах принята разная запись дат. Где-то пишут 31.12.2014 (Россия), а где-то 12/31/2014 (США), где-то иначе.</dd>
<dt>Числа</dt>
<dd>В одних странах выводятся цифрами, в других -- иероглифами, длинные числа разделяются где-то пробелом, где-то запятой.</dd>
</dl>
Все современные браузеры, кроме IE10- (но есть библиотеки и для него) поддерживают стандарт [ECMA 402](http://www.ecma-international.org/ecma-402/1.0/ECMA-402.pdf), предназначенный решить эти проблемы навсегда.
[cut]
## Основные объекты
<dl>
<dt>`Intl.Collator`</dt>
<dd>Умеет правильно сравнивать и сортировать строки.</dd>
<dt>`Intl.DateTimeFormat`</dt>
<dd>Умеет форматировать дату и время в соответствии с нужным языком.</dd>
<dt>`Intl.NumberFormat`</dt>
<dd>Умеет форматировать числа в соответствии с нужным языком.</dd>
</dl>
## Локаль
*Локаль* -- первый и самый важный аргумент всех методов, связанных с интернационализацией.
Локаль описывается строкой из трёх компонентов, которые разделяются дефисом:
<ol>
<li>Код языка.</li>
<li>Код способа записи.</li>
<li>Код страны.</li>
</ol>
На практике не всегда указаны три, обычно меньше:
<ol>
<li>`ru` -- русский язык, без уточнений.</li>
<li>`en-GB` -- английский язык, используемый в Англии (`GB`).</li>
<li>`en-US` -- английский язык, используемый в США (`US`).</li>
<li>`zh-Hans-CN` -- китайский язык (`zh`), записываемый упрощённой иероглифической письменностью (`Hans`), используемый в китае.</li>
</ol>
Также через суффикс `-u-*` можно указать расширения локалей, например `"th-TH-u-nu-thai"` -- тайский язык (`th`), используемый в Тайланде (`TH`), с записью чисел тайскими буквами (, ๑, ๒, ๓, ๔, ๕, ๖, ๗, ๘, ๙) .
Стандарт, который описывает локали -- [RFC 5464](http://tools.ietf.org/html/rfc5646), языки описаны в [IANA language registry](http://www.iana.org/assignments/language-subtag-registry/language-subtag-registry).
Все методы принимают локаль в виде строки или массива, содержащего несколько локалей в порядке предпочтения.
Если локаль не указана или `undefined` -- берётся локаль по умолчанию, установленная в окружении (браузере).
### Подбор локали localeMatcher
`localeMatcher` -- вспомогательная настройка, которую тоже можно везде указать, она определяет способ подбора локали, если желаемая недоступна.
У него два значения:
<ul>
<li>`"lookup"` -- означает простейший порядок поиска путём обрезания суффикса, например `zh-Hans-CN` -> `zh-Hans` -> `zh` -> локаль по умолчанию.</li>
<li>`"best fit"` -- использует встроенные алгоритмы и предпочтения браузера (или другого окружения) для выбора подходящей локали.</li>
</ul>
**По умолчанию стоит `"best fit"`.**
Если локалей несколько, например `["zh-Hans-CN", "ru-RU"]` то `localeMatcher` пытается подобрать наиболее подходящую локаль для первой из списка (китайская), если не получается -- переходит ко второй (русской) и так далее. Если ни одной не нашёл, например на компьютере не совсем поддерживается ни китайский ни русский, то используется локаль по умолчанию.
Как правило, `"best fit"` является здесь наилучшим выбором.
## Строки, Intl.Collator [#intl-collator]
Синтаксис:
```js
// создание
var collator = new Intl.Collator([locales, [options]])
```
Параметры:
<dl>
<dt>`locales`</dt>
<dd>Локаль, одна или массив в порядке предпочтения.</dd>
<dt>`options`</dt>
<dd>Объект с дополнительными настройками:
<ul>
<li>`localeMatcher` -- алгоритм выбора подходящей локали.</li>
<li>`usage` -- цель сравнения: сортировка `"sort"` или поиск `"search"`, по умолчанию `"sort"`.</li>
<li>`sensitivity` -- чувствительность: какие различия в символах учитывать, а какие -- нет, варианты:
<ul>
<li>`base` -- учитывать только разные символы, без диакритических знаков и регистра, например: `аб`, `е = ё`, `а = А`.</li>
<li>`accent` -- учитывать символы и диакритические знаки, например: `аб`, `е ≠ ё`, `а = А`.</li>
<li>`case` -- учитывать символы и регистр, например: `аб`, `е = ё`, `аА`.</li>
<li>`variant` -- учитывать всё: символ, диакритические знаки, регистр, например: `аб`, `е ≠ ё`, `аА`, используется по умолчанию.</li>
</ul>
</li>
<li>`ignorePunctuation` -- игнорировать знаки пунктуации: `true/false`, по умолчанию `false`.</li>
<li>`numeric` -- использовать ли численное сравнение: `true/false`, если `true`, то будет `12 > 2`, иначе `2 < 12`.</li>
<li>`caseFirst` -- в сортировке должны идти первыми прописные или строчные буквы, варианты: `"upper"` (прописные), `lower` (строчные) или `false` (стандартное для локали, также является значением по умолчанию). Не поддерживается IE11-.</li>
</ul>
</dd>
</dl>
В подавляющем большинстве случаев подходят стандартные параметры, то есть `options` указывать не нужно.
Использование:
```js
var result = collator.compare(str1, str2);
```
Результат `compare` имеет значение `1` (больше), `0` (равно) или `-1` (меньше).
Например:
```js
//+ run
var collator = new Intl.Collator();
alert( "ёжик" > "яблоко" ); // true (ёжик больше, что неверно)
alert( collator.compare("ёжик", "яблоко") ); // -1 (ёжик меньше, верно)
```
Выше были использованы полностью стандартные настройки. Они различают регистр символа, но это различие можно убрать, если настроить чувствительность `sensitivity`:
```js
//+ run
var collator = new Intl.Collator();
alert( collator.compare("ЁжиК", "ёжик") ); // 1, разные
var collator = new Intl.Collator(undefined, {
sensitivity: "accent"
});
alert( collator.compare("ЁжиК", "ёжик") ); // 0, одинаковые
```
## Даты, Intl.DateFormatter [#intl-dateformatter]
Синтаксис:
```js
// создание
var formatter = new Intl.DateFormatter([locales, [options]])
```
Первый аргумент -- такой же, как и в `Collator`, а в объекте `options` мы можем определить, какие именно части даты показывать (часы, месяц, год...) и в каком формате.
Полный список свойств `options`:
<table>
<thead>
<tr>
<th>Свойство</th>
<th>Описание</th>
<th>Возможные значения</th>
<th>По умолчанию</th>
</tr>
</thead>
<tbody>
<tr>
<td>`localeMatcher` </td>
<td> Алгоритм подбора локали</td>
<td>
`lookup`,`best fit`
</td>
<td>
`best fit`
</td>
</tr>
<tr>
<td>`formatMatcher` </td>
<td>
Алгоритм подбора формата
</td>
<td> `basic`, `best fit` </td>
<td> `best fit` </td>
</tr>
<tr>
<td>`hour12`</td>
<td>Включать ли время в 12-часовом формате</td>
<td>`true` -- 12-часовой формат, `false` -- 24-часовой</td>
<td></td>
</tr>
<tr>
<td>`timeZone`</td>
<td>Временная зона</td>
<td>Временная зона, например `Europe/Moscow`</td>
<td>`UTC`</td>
</tr>
<tr>
<td>`weekday`</td>
<td>День недели</td>
<td>`narrow`, `short`, `long`</td>
<td></td>
</tr>
<tr>
<td>`era`</td>
<td>Эра</td>
<td>`narrow`, `short`, `long`</td>
<td></td>
</tr>
<tr>
<td>`year`</td>
<td>Год</td>
<td>`2-digit`, `numeric`</td>
<td>`undefined` или `numeric`</td>
</tr>
<tr>
<td>`month`</td>
<td>Месяц</td>
<td>`2-digit`, `numeric`, `narrow`, `short`, `long` </td>
<td>`undefined` или `numeric`</td>
</tr>
<tr>
<td>`day`</td>
<td>День</td>
<td>`2-digit`, `numeric`</td>
<td>`undefined` или `numeric`</td>
</tr>
<tr>
<td>`hour`</td>
<td>Час</td>
<td> `2-digit`, `numeric` </td>
<td></td>
</tr>
<tr>
<td>`minute`</td>
<td>Минуты </td>
<td> `2-digit`, `numeric` </td>
<td></td>
</tr>
<tr>
<td>`second`
</td>
<td>Секунды</td>
<td>`2-digit`, `numeric`</td>
<td></td>
</tr>
<tr>
<td>`timeZoneName`</td>
<td>Название таймзоны (нет в IE11)</td>
<td>`short`, `long`</td>
<td></td>
</tr>
</tbody>
</table>
**Все локали обязаны поддерживать следующие наборы настроек:**
<ul>
<li>weekday, year, month, day, hour, minute, second</li>
<li>weekday, year, month, day</li>
<li>year, month, day</li>
<li>year, month</li>
<li>month, day</li>
<li>hour, minute, second</li>
</ul>
Если указанный формат не поддерживается, то настройка `formatMatcher` задаёт алгоритм подбора наиболее близкого формата: `basic` -- по [стандартным правилам](http://www.ecma-international.org/ecma-402/1.0/#BasicFormatMatcher) и `best fit` -- по умолчанию, на усмотрение окружения (браузера).
Использование:
```js
var dateString = formatter.format(date);
```
Например:
```js
//+ run
var date = new Date(2014, 11, 31, 12, 30, 0);
var formatter = new Intl.DateTimeFormat("ru");
alert( formatter.format(date) ); // 31.12.2014
var formatter = new Intl.DateTimeFormat("en-US");
alert( formatter.format(date) ); // 12/31/2014
```
Длинная дата, с настройками:
```js
//+ run
var date = new Date(2014, 11, 31, 12, 30, 0);
var formatter = new Intl.DateTimeFormat("ru", {
weekday: "long",
year: "numeric",
month: "long",
day: "numeric"
});
alert( formatter.format(date) ); // среда, 31 декабря 2014 г.
```
Только время:
```js
//+ run
var date = new Date(2014, 11, 31, 12, 30, 0);
var formatter = new Intl.DateTimeFormat("ru", {
hour: "numeric",
minute: "numeric",
second: "numeric"
});
alert( formatter.format(date) ); // 12:30:00
```
## Числа: Intl.NumberFormat
Форматтер `Intl.NumberFormat` умеет красиво форматировать не только числа, но и валюту, а также проценты.
Синтаксис:
```js
var formatter = new Intl.NumberFormat([locales[, options]]);
formatter.format(number); // форматирование
```
Параметры, как и раньше -- локаль и опции.
Список опций:
<table>
<tr>
<th>Свойство </th>
<th>Описание </th>
<th>Возможные значения </th>
<th>По умолчанию </th>
</tr>
<tr>
<td> `localeMatcher` </td>
<td>Алгоритм подбора локали </td>
<td> `lookup`, `best fit` </td>
<td> `best fit` </td>
</tr>
<tr>
<td> `style` </td>
<td>Стиль форматирования </td>
<td> `decimal`, `percent`, `currency` </td>
<td> `decimal` </td>
</tr>
<tr>
<td> `currency` </td>
<td> Алфавитный код валюты</td>
<td> См. [Список кодов валюты](http://www.currency-iso.org/en/home/tables/table-a1.html), например `USD` </td>
<td> </td>
</tr>
<tr>
<td> `currencyDisplay` </td>
<td>Показывать валюту в виде кода, локализованного символа или локализованного названия
</td>
<td> `code`, `symbol`, `name` </td>
<td> `symbol` </td>
</tr>
<tr>
<td> `useGrouping` </td>
<td>Разделять ли цифры на группы</td>
<td> `true`, `false` </td>
<td> `true` </td>
</tr>
<tr>
<td>`minimumIntegerDigits`</td>
<td>Минимальное количество цифр целой части</td>
<td>от `1` до `21`
</td>
<td>`21`</td>
</tr>
<tr>
<td>`minimumFractionDigits` </td>
<td>Минимальное количество десятичных цифр
</td>
<td>от `0` до `20` </td>
<td>для чисел и процентов `0`, для валюты зависит от кода.</td>
</tr>
<tr>
<td>`maximumFractionDigits`</td>
<td>Максимальное количество десятичных цифр </td>
<td>от `minimumFractionDigits` до `20`. </td>
<td>для чисел `max(minimumFractionDigits, 3)`, для процентов `0`, для валюты зависит от кода.</td>
</tr>
<tr>
<td>`minimumSignificantDigits`</td>
<td>Минимальное количество значимых цифр</td>
<td>от `1` до `21`</td>
<td>`1`</td>
</tr>
<tr>
<td>`maximumSignificantDigits`</td>
<td>Максимальное количество значимых цифр</td>
<td>от `minimumSignificantDigits` до `21`</td>
<td>`minimumSignificantDigits`</td>
</tr>
</table>
Пример без опций:
```js
//+ run
var formatter = new Intl.NumberFormat("ru");
alert( formatter.format(1234567890.123) ); // 1 234 567 890,123
```
С ограничением значимых цифр (важны только первые 3):
```js
//+ run
var formatter = new Intl.NumberFormat("ru", {
maximumSignificantDigits: 3
});
alert( formatter.format(1234567890.123) ); // 1 230 000 000
```
C опциями для валюты:
```js
var formatter = new Intl.NumberFormat("ru", {
style: "currency",
currency: "GBP"
});
alert( formatter.format(1234.5) ); // 1 234,5 £
```
С двумя цифрами после запятой:
```js
var formatter = new Intl.NumberFormat("ru", {
style: "currency",
currency: "GBP",
minimumFractionDigits: 2
});
alert( formatter.format(1234.5) ); // 1 234,50 £
```
## Методы в Date, String, Number
Методы форматирования также поддерживаются в обычных строках, датах, числах:
<dl>
<dt>`String.prototype.localeCompare`(that [, locales [, options]])`</dt>
<dd>Сравнивает строку с другой, с учётом локали, например:
```js
//+ run
var str = "ёжик";
alert( str.localeCompare("яблоко", "ru") ); // -1
```
</dd>
<dt>`Date.prototype.toLocaleString([locales [, options]])`</dt>
<dd>Форматирует дату в соответствии с локалью, например:
```js
//+ run no-beautify
var date = new Date(2014, 11, 31, 12, 00);
alert( date.toLocaleString("ru", { year: 'numeric', month: 'long' }) ); // Декабрь 2014
```
</dd>
<dt>`Date.prototype.toLocaleDateString([locales [, options]])`</dt>
<dd>То же, что и выше, но опции по умолчанию включают в себя год, месяц, день</dd>
<dt>`Date.prototype.toLocaleTimeString`([locales [, options]])`</dt>
<dd>То же, что и выше, но опции по умолчанию включают в себя часы, минуты, секунды</dd>
<dt>`Number.prototype.toLocaleString([locales [, options]])`</dt>
<dd>Форматирует число, используя опции `Intl.NumberFormat`.</dd>
</dl>
Все эти методы при запуске создают соответствующий объект `Intl.*` и передают ему опции, можно рассматривать их как укороченные варианты вызова.
## Старые IE
В IE10- рекомендуется использовать полифилл, например библиотеку [](https://github.com/andyearnshaw/Intl.js).

View file

@ -0,0 +1,193 @@
# Особенности регулярных выражений в Javascript
Регулярные выражения в javascript немного странные. Вроде - перловые, обычные, но с подводными камнями, на которые натыкаются даже опытные javascript-разработчики.
Эта статья ставит целью перечислить неожиданные фишки и особенности `RegExp` в краткой и понятной форме.
[cut]
## Точка и перенос строки
Для поиска в многострочном режиме почти все модификации перловых регэкспов используют специальный multiline-флаг.
И javascript здесь не исключение.
Попробуем же сделать поиск и замену многострочного вхождения. Скажем, будем заменять <code>[u] ... [/u]</code> на тэг подчеркивания: <code>&lt;u&gt;</code>:
```js
//+ run
function bbtagit(text) {
text = text.replace(/\[u\](.*?)\[\/u\]/gim, '<u>$1</u>')
return text
}
var line = "[u]мой\n текст[/u]"
alert(bbtagit(line))
```
Попробуйте запустить. Заменяет? Как бы не так!
Дело в том, что в javascript мультилайн режим (флаг <code>m</code>) влияет только на символы ^ и $, которые начинают матчиться с началом и концом строки, а не всего текста.
Точка по-прежнему - любой символ, кроме новой строки. В javascript нет флага, который устанавливает мультилайн-режим для точки. Для того, чтобы заматчить совсем что угодно - используйте <code>[\s\S]</code>.
Работающий вариант:
```js
//+ run
function bbtagit(text) {
text = text.replace(/\[u\]([\s\S]*)\[\/u\]/gim, '<u>$1</u>')
return text
}
var line = "[u]мой\n текст[/u]"
alert(bbtagit(line))
```
## Жадность
Это не совсем особенность, скорее фича, но все же достойная отдельного абзаца.
Все регулярные выражения в javascript - жадные. То есть, выражение старается отхватить как можно больший кусок строки.
Например, мы хотим заменить все открывающие тэги <code>&lt;a&gt;</code>. На что и почему - не так важно.
```js
//+ run
text = '1 <A href="#">...</A> 2'
text = text.replace(/<A(.*)>/, 'TEST')
alert(text)
```
При запуске вы увидите, что заменяется не открывающий тэг, а вся ссылка, выражение матчит ее от начала и до конца.
Это происходит из-за того, что точка-звездочка в "жадном" режиме пытается захватить как можно больше, в нашем случае - это как раз до последнего <code>&gt;</code>.
Последний символ <code>&gt;</code> точка-звездочка не захватывает, т.к. иначе не будет совпадения.
Как вариант решения используют квадратные скобки: <code>[^&gt;]</code>:
```js
//+ run
text = '1 <A href="#">...</A> 2'
text = text.replace(/<A([^>]*)>/, 'TEST')
alert(text)
```
Это работает. Но самым удобным вариантом является переключение точки-звездочки в нежадный режим. Это осуществляется простым добавлением знака "<code>?</code>" после звездочки.
В нежадном режиме точка-звездочка пустит поиск дальше сразу, как только нашла совпадение:
```js
//+ run
text = '1 <A href="#">...</A> 2'
text = text.replace(/<A(.*?)>/, 'TEST')
alert(text)
```
В некоторых языках программирования можно переключить жадность на уровне всего регулярного выражения, флагом.
В javascript это сделать нельзя.. Вот такая особенность. А вопросительный знак после звездочки рулит - честное слово.
## Backreferences в паттерне и при замене
Иногда нужно в самом паттерне поиска обратиться к предыдущей его части.
Например, при поиске BB-тагов, то есть строк вида <code>[u]...[/u]</code>, <code>[b]...[/b]</code> и <code>[s]...[/s]</code>. Или при поиске атрибутов, которые могут быть в одинарных кавычках или двойных.
Обращение к предыдущей части паттерна в javascript осуществляется как \1, \2 и т.п., бэкслеш + номер скобочной группы:
```js
//+ run
text = ' [b]a [u]b[/u] c [/b] '
var reg = /\[([bus])\](.*?)\[\// * u * /\1/ * /u*/\] /
text = text.replace(reg, '<$1>$2</$1>')
alert(text)
```
Обращение к скобочной группе в строке замены идет уже через доллар: <code>$1</code>. Не знаю, почему, наверное так удобнее..
P.S. Понятно, что при таком способе поиска bb-тагов придется пропустить текст через замену несколько раз - пока результат не перестанет отличаться от оригинала.
## Найти все / Заменить все
Эти две задачи решаются в javascript принципиально по-разному.
Начнем с "простого".
### Заменить все
Для замены всех вхождений используется метод [String#replace](https://developer.mozilla.org/ru/docs/Web/JavaScript/Reference/Global_Objects/String/replace).
Он интересен тем, что допускает первый аргумент - регэксп или строку.
Если первый аргумент - строка, то будет осуществлен поиск подстроки, без преобразования в регулярное выражение.
Попробуйте:
```js
//+ run
alert("2 ++ 1".replace("+", "*"))
```
Как видите, заменился только один плюс, а не оба.
**Чтобы заменить все вхождения, [String#replace](https://developer.mozilla.org/ru/docs/Web/JavaScript/Reference/Global_Objects/String/replace) обязательно нужно использовать с регулярным выражением.**
В режиме регулярного выражения плюс придётся экранировать, но зато <code>replace</code> заменит все вхождения (при указании флага <code>g</code>):
```js
//+ run
alert("2 ++ 1".replace(/\+/g, "*"))
```
Вот такая особенность работы со строкой.
### Заменить функцией
Очень полезной особенностью <code>replace</code> является возможность работать с функцией вместо строки замены. Такая функция получает первым аргументом - все совпадение, а последующими аргументами - скобочные группы.
Следующий пример произведет операции вычитания:
```js
//+ run no-beautify
var str = "count 36 - 26, 18 - 9"
str = str.replace(/(\d+) - (\d+)/g, function(a,b,c) { return b-c })
alert(str)
```
### Найти всё
В javascript нет одного универсального метода для поиска всех совпадений.
Для поиска без запоминания скобочных групп - можно использовать [String#match](https://developer.mozilla.org/ru/docs/Web/JavaScript/Reference/Global_Objects/String/match):
```js
//+ run
var str = "count 36-26, 18-9"
var re = /(\d+)-(\d+)/g
result = str.match(re)
for (var i = 0; i < result.length; i++) alert(result[i])
```
Как видите, оно исправно ищет все совпадения (флаг <code>'g'</code> у регулярного выражения обязателен), но при этом не запоминает скобочные группы. Эдакий "облегченный вариант".
### Найти всё с учётом скобочных групп
В сколько-нибудь сложных задачах важны не только совпадения, но и скобочные группы. Чтобы их найти, предлагается использовать многократный вызов [RegExp#exec](https://developer.mozilla.org/ru/docs/Web/JavaScript/Reference/Global_Objects/RegExp/exec).
Для этого регулярное выражение должно использовать флаг <code>'g'</code>. Тогда результат поиска, запомненный в свойстве <code>lastIndex</code> объекта <code>RegExp</code> используется как точка отсчета для следующего поиска:
```js
//+ run
var str = "count 36-26, 18-9"
var re = /(\d+)-(\d+)/g
var res
while ((res = re.exec(str)) != null) {
alert("Найдено " + res[0] + ": (" + res[1] + ") и (" + res[2] + ")")
alert("Дальше ищу с позиции " + re.lastIndex)
}
```
Проверка <code>while( (res = re.exec(str)) != null)</code> нужна т.к. значение <code>res = 0</code> является хорошим и означает, что вхождение найдено в самом начале строки (поиск успешен). Поэтому необходимо сравнивать именно с <code>null</code>.

View file

@ -0,0 +1,306 @@
# Эволюция шаблонных систем для JavaScript
Различных шаблонных систем -- много.
Они постепенно эволюционировали и развивались.
В этой главе мы разберём, как шёл этот процесс, какие шаблонки "родились", какие бонусы нам даёт использование той или иной шаблонной системы.
[cut]
## Микрошаблоны
*Микрошаблоны* (англ. microtemplate) мы уже видели на примере `_.template`.
Это HTML со вставками переменных и произвольным JS.
Пример:
```html
<!--+ no-beautify -->
<div class="menu">
<span class="title"><%-title%></span>
<ul>
<% items.forEach(function(item) { %>
<li><%-item%></li>
<% }); %>
</ul>
</div>
```
Шаблонная система компилирует этот код в JavaScript-функцию с минимальными модификациями, и она уже, запустившись с данными, генерирует результат.
Достоинства и недостатки такого подхода:
[compare]
-Жёстко привязан к языку JavaScript.
-При ошибке в шаблоне приходится лезть внутрь "страшной" функции
+Простая и быстрая шаблонная система
+Внутри JS-функции доступен полноценный браузерный отладчик, функция хоть и страшна, но понятна.
[/compare]
### Код в шаблоне
Включение произвольного JS-кода в шаблон, в теории, позволяет делать в нём всё, что угодно. Но обратная сторона медали -- шаблон вместо внятного HTML может стать адским нагромождением разделителей вперемешку с вычислениями. Что рекомендуется делать в шаблонах, а что нет?
Можно разделить код на два типа с точки зрения шаблонизации:
<ul>
<li>**Бизнес-логика** -- код, *формирующий данные*, основной код приложения.</li>
<li>**Презентационная логика** -- код, описывающий, как *показываются данные*.</li>
</ul>
Например, код, получающий данные с сервера для вывода в таблице -- бизнес-логика, а код, форматирующий даты для вывода -- презентационная логика.
В шаблонах допустима лишь презентационная логика.
### Кросс-платформенность
Зачастую, нужно использовать один и тот же шаблон и в браузере и на сервере.
Например, серверный код генерирует HTML со списком сообщений, а JavaScript на клиенте добавляет к нему новые по мере появления.
...Но как использовать на сервере шаблон с JavaScript, если его основной язык -- PHP, Ruby, Java?
Эту проблему решили обошли быстро.
На сервер, использующем PHP, Ruby, Java или какой-то другой язык, дополнительно ставится виртуальная машина [V8](http://code.google.com/p/v8/) и настраивается интеграция с ней. Почти все платформы это умеют.
После этого становится возможным запускать JavaScript-шаблоны и передавать им данные в виде объектов, массивов и так далее.
Этот подход может показаться искусственным, но на самом деле он вполне жизнеспособен и используется в ряде крупных проектов.
### Прекомпиляция
Эта шаблонка и большинство других систем, которые мы рассмотрим далее, допускают *прекомпиляцию*.
То есть, можно заранее, до выкладывания сайта на "боевой сервер", обработать шаблоны, создать из них JS-функции, объединить их в единый файл и далее, в "боевом окружении" использовать уже их.
Современные системы сборки ([brunch](http://brunch.io/), [grunt](http://gruntjs.com/) с плагинами и другие) позволяют делать это удобно, а также хранить шаблоны в разных файлах, каждый -- в нужной директории с JS-кодом для виджета.
## Хелперы и фильтры
JavaScript-вставки не всегда просты и элегантны. Иногда, чтобы что-то сделать, нужно написать порядочно кода.
**Для того, чтобы сделать шаблоны компактнее и проще, в них стали добавлять фильтры и хелперы.**
<ul>
<li>**Хелпер** (англ. helper) -- вспомогательная функция, которая доступна в шаблонах и используется для решения часто возникающих задач.
В `_.template`, чтобы объявить хелпер, можно просто сделать глобальную функцию. Но это слишком грубо, так не делают. Гораздо лучше -- использовать объект `_.templateSettings.imports`, в котором можно указать, какие функции добавлять в шаблоны, или опцию `imports` для `_.template`.
Пример хелпера -- функция `t(phrase)`, которая переводит `phrase` на текущий язык:
```js
//+ run
_.templateSettings.imports.t = function(phrase) {
// обычно функция перевода немного сложнее, но здесь это не важно
if (phrase == "Hello") return "Привет";
}
*!*
// в шаблоне используется хелпер t для перевода
var compiled = _.template("<div><%=t('Hello')%></div>");
*/!*
alert( compiled() ); // <div>Привет</div>
```
Такой хелпер очень полезен для мультиязычных сайтов, когда один шаблон нужно выводить на десяти языках. Нечто подобное используется почти во всех языках и платформах, не только в JavaScript.
</li>
<li>**Фильтр** -- это функция, которая трансформирует данные, например, форматирует дату, сортирует элементы массива и так далее.
Обычно для фильтров обычно предусмотрен специальный "особо простой и короткий" синтаксис.
Например, в системе шаблонизации [EJS](https://github.com/visionmedia/ejs), которая по сути такая же, но мощнее, чем `_.template`, фильтры задаются через символ `|`, внутри разделителя `<%=: ... %>`.
Чтобы вывести `item` с большой буквы, можно вместо `<%=item%>` написать `<%=: item | capitalize %>`. Чтобы выводить отсортированный массив, можно использовать `<%=: items | sort %>` и так далее.
</li>
</ul>
## Свой язык
Для того, чтобы сделать шаблон ещё короче, а также с целью "отвязать" их от JavaScript, ряд шаблонных систем предлагают свой язык.
Например:
<ul>
<li>[Mustache](http://mustache.github.com/)</li>
<li>[Handlebars](http://handlebarsjs.com/)</li>
<li>[Closure Templates](https://developers.google.com/closure/templates/docs/javascript_usage)</li>
<li>...тысячи их...</li>
</ul>
Шаблон для меню в Handlerbars, к примеру, будет выглядеть так:
```html
<!--+ no-beautify -->
<div class="menu">
<span class="title">{{title}}</span>
<ul>
{{#each items}}
<li>{{item}}</li>
{{/each}}
</ul>
<div>
```
Как видно, *вместо* JavaScript-конструкций здесь используются хелперы. В примере выше `{{#each}} ... {{/each}` -- "блочный" хелпер: он показывает своё содержимое для каждого элемента `items` и является альтернативой `forEach`.
Есть и другие встроенные в шаблонизатор хелперы, можно легко делать свои.
Использование такого шаблона:
```js
// текст шаблона должен быть в переменной tmpl
var compiled = Handlebars.compile(tmpl);
var result = compiled({
title: "Сладости",
items: ["Торт", "Пирожное", "Пончик"]
});
```
Библиотека шаблонизации [Handlebars](http://handlebarsjs.com/) "понимает" этот язык. Вызов `Handlebars.compile` принимает строку шаблона, разбивает по разделителям и, аналогично предыдущему виду шаблонов, делает JavaScript-функцию, которая затем по данным выдаёт строку-результат.
### Запрет на встроенный JS
Если "свой язык шаблонизатора" очень прост, то библиотеку для его поддержки можно легко написать под PHP, Ruby, Java и других языках, которые тем самым научатся понимать такие шаблоны.
**Если шаблонка действительна нацелена на кросс-платформенность, то явные JS-вызовы в ней запрещены. Всё делается через хелперы.**
Если же нужна какая-то логика, то она либо выносится во внешний код, либо делается через новый хелпер -- он отдельно пишется на JavaScript (для клиента) и для сервера (на его языке). Получается полная совместимость.
Это создаёт определённые сложности. Например, в Handlebars есть хелпер `{{#if cond}} ... {{/if}}`, который выводит содержимое, если истинно условие `cond`. При этом вместо `cond` нельзя поставить, к примеру, `a > b` или вызов `str.toUpperCase()`, будет ошибка. Все вычисления должны быть сделаны на этапе передачи данных в шаблон.
Так сделано как раз для переносимости шаблонной системы на другие языки, но на практике не очень-то удобно.
Продвинутые кросс-платформенные шаблонизаторы, в частности, [Closure Templates](https://developers.google.com/closure/templates/docs/javascript_usage), обладают более мощным языком и умеют самостоятельно разбирать и компилировать многие выражения.
## Шаблонизация компонент
До этого мы говорили о шаблонных системах "общего назначения". По большому счёту, это всего лишь механизмы для преобразования одной строки в другую. Но при описании шаблона для компоненты мы хотим сгенерировать не просто строку, а DOM-элемент, и не просто генерировать, а в дальнейшем -- с ним работать.
Современные шаблонные системы "заточены" на это. Они умеют создавать по шаблону DOM-элементы и автоматически выполнять после этого разные полезные действия.
Например:
<ul>
<li>Можно сохранить важные подэлементы в свойства компоненты, чтобы было проще к ним обращаться из JavaScript.</li>
<li>Можно автоматически назначать обработчики из методов компонента.</li>
<li>Можно запомнить, какие данные относятся к каким элементам и в дальнейшем, при изменении данных автоматически обновлять DOM ("привязка данных" -- англ. data binding).</li>
</ul>
Одной из первых систем шаблонизации, которая поддерживает подобные возможности была [Knockout.JS](http://knockoutjs.com).
Попробуйте поменять значение `<input>` в примере ниже и вы увидите двухстороннюю привязку данных в действии:
```html
<!--+ run height=120 autorun -->
<script src="http://knockoutjs.com/downloads/knockout-3.1.0.js"></script>
Поменяйте имя: <input *!*data-bind="value: name, valueUpdate: 'input'"*/!*>
<hr>
<h1>Привет, <span *!*data-bind="text: name"*/!*></span>!</h1>
<script>
var user = {
name: ko.observable("Вася")
};
ko.applyBindings(user, document.body);
</script>
```
Библиотека Knockout.JS создаёт объект `ko`, который и содержит все её возможности.
В этом примере работу начинает вызов `ko.applyBindings(user, document.body)`.
Его аргументы:
<ul>
<li>`user` -- объект с данными.</li>
<li>`document.body` -- DOM-элемент, который будет использован в качестве шаблона.</li>
</ul>
Он пробегает по всем подэлементам `document.body` и, если видит атрибут `data-bind`, то читает его и выполняет привязку данных.
Значение `<input data-bind="value: name, ...">` означает, что нужно привязать `input.value` к свойству `name` объекта данных.
Привязка осуществляется в две стороны:
<ol>
<li>Во-первых, библиотека ставит на `input` свой обработчик `oninput` (можно выбрать другие события, см. [документацию](http://knockoutjs.com/documentation/value-binding.html)), который будет обновлять `user.name`. То есть, изменение `input` автоматически меняет `user.name`</li>
<li>Во-вторых, свойство `user.name` создано как `ko.observable(...)`. Технически, `ko.observable(value)` -- это функция-обёртка вокруг значения: геттер-сеттер, который умеет рассылать события при изменении.
Например:
```html
<!--+ run no-beautify -->
<script src="http://knockoutjs.com/downloads/knockout-3.1.0.js"></script>
<script>
var user = ko.observable("Вася");
*!*
// вызов user() возвращает значение
*/!*
alert( user() ); // Вася
*!*
// вызов user.subscribe(func) ставит обработчик на изменение значения
*/!*
user.subscribe(function(newValue) {
alert("Новое значение: " + newValue);
});
*!*
// вызов user(newValue) меняет значение
*/!*
user("Петя"); // сработает обработчик, назначенный выше
</script>
```
Библиотека Knockout.JS ставит свой обработчик на изменение значения и при этом обновляет все привязки. Так что при изменении `user.name` меняется и `input.value`.
</li>
</ol>
Далее в том же примере находится `<span data-bind="text: name">` -- здесь атрибут означает привязку текста к `name`. Так как `<span>` по своей инициативе меняться не может, то привязка односторонняя, но если бы мог, то можно сделать и двухстороннюю, это несложно.
**Вызов `ko.applyBindings` можно делать внутри компоненты, и таким образом устанавливать соответствия между её объектом и DOM.**
Библиотка также поддерживает хранение шаблонов в `<script type="text/template">` -- см. документацию [template-binding](http://knockoutjs.com/documentation/template-binding.html), можно организовать прекомпиляцию, добавлять свои привязки и так далее.
### Другие библиотеки
Есть другие библиотеки "продвинутой шаблонизации", которые добавляют свои возможности по работе с DOM, например:
<ul>
<li>[Ractive.JS](http://www.ractivejs.org)</li>
<li>[Rivets.JS](http://www.rivetsjs.com/)</li>
</ul>
Подобная шаблонная система является частью многих фреймворков, например:
<ul>
<li>[React.JS](http://facebook.github.io/react/)</li>
<li>[Angular.JS](http://angularjs.org)</li>
<li>[Ember.JS](http://emberjs.com/)</li>
</ul>
Все эти фреймворки разные:
<ul>
<li>Ember использует надстройку над Handlebars.</li>
<li>React использует JSX ([JavaScript XML syntax transform](http://facebook.github.io/react/docs/jsx-in-depth.html)) -- свой особый способ вставки разметки в JS-код, который нужно обязательно прекомпилировать перед запуском.</li>
<li>Angular вместо работы со строками использует клонирование DOM-узлов.</li>
</ul>
При разработке современного веб-приложения имеет смысл выбрать продвинутую шаблонную систему или даже один из этих архитектурных фреймворков.
## Итого
Системы шаблонизации, в порядке развития и усложнения:
<ul>
<li>Микрошаблонизация -- строка с JS-вставками, которая компилируется в функцию -- самый простой вариант, минимальная работа для шаблонизатора.</li>
<li>Собственный язык шаблонов -- "особо простой" синтаксис для частых операций, с запретом на JS в случае, если нужна кросс-платформенность.</li>
<li>Шаблонизация для компонентов -- современные системы, которые умеют не только генерировать DOM, но и помогать с дальнейшей работой с ним.</li>
</ul>
Для того, чтобы использовать одни и те же шаблоны на клиенте и сервере, применяют либо кросс-платформенную систему шаблонизации, либо, чаще -- интегрируют серверную часть с V8 и, возможно, с сервером Node.JS.
В главе было много ссылок на шаблонные системы. Все они достаточно современные, поддерживаемые и используются во многих проектах. Среди них вы наверняка найдёте нужную вам.

View file

@ -0,0 +1,83 @@
# Книги по JS, HTML/CSS и не только
Мне часто задают вопрос: "Какую литературу порекомендуете?". На этой странице я предлагаю рекомендации по различным темам. Всего несколько книг на каждую тему, из большего количества все равно пришлось бы выбирать.
Кстати, по всем книжкам, особенно тем, которые касаются технологий, всегда ищите последнее издание.
P.S. Скачать книги здесь нельзя. Эта страница содержит только рекомендации.
## CSS
CSS стоит изучать по одной из этих книг. Можно сразу по обеим.
<ul>
<li><a href="http://www.ozon.ru/context/detail/id/24493075/?partner=iliakan">Большая книга CSS3.</a>
<i>Дэвид Макфарланд.</i></li>
<li><a href="http://www.ozon.ru/context/detail/id/3881079/?partner=iliakan">CSS. Каскадные таблицы стилей. Подробное руководство.</a>
<i>Эрик Мейер</i></li>
</ul>
Конечно, [стандарты](http://www.w3.org/Style/CSS/) тоже будут полезны. Подчас их точность куда проще, чем много страниц разъяснений.
## JavaScript
Полезное чтение о языке, встроенных методах и конструкциях JavaScript:
<ul>
<li><a href="http://www.ozon.ru/context/detail/id/19677670/?partner=iliakan">JavaScript. Подробное руководство.</a>
<i>Дэвид Флэнаган.</i></li>
<li><a href="http://www.ozon.ru/context/detail/id/6287517/?partner=iliakan">JavaScript. Шаблоны.</a>
<i>Стоян Стефанов.</i></li>
</ul>
## jQuery
Кроме [документации](http://api.jquery.com/):
<ul>
<li><a href="http://www.ozon.ru/context/detail/id/6277333/?partner=iliakan">jQuery. Подробное руководство по продвинутому JavaScript.</a>
<i>Бер Бибо, Иегуда Кац.</i></li>
</ul>
## Объектно-ориентированное программирование
Объектно-ориентированное программирование (ООП) -- это концепция построения программных систем на основе объектов и взаимодействия между ними. При изучении ООП рассматриваются полезные архитектурные приёмы, как организовать программу более эффективно.
Умение создавать объект, конструктор, вызывать методы -- это основные, самые базовые "кирпичики". Их следует освоить первыми, например используя этот учебник. Затем, когда основы более-менее освоены, стоит уделить внимание теории объектно-ориентированной разработки:
<ul>
<li><a href="http://www.ozon.ru/context/detail/id/3905587/?partner=iliakan">Объектно-ориентированный анализ и проектирование с примерами приложений.</a>
<i>Гради Буч и др.</i>.</li>
<li><a href="http://www.ozon.ru/context/detail/id/20217137/?partner=iliakan">Приемы объектно-ориентированного проектирования. Паттерны проектирования.</a>
<i>Э. Гамма, Р. Хелм, Р. Джонсон, Дж. Влиссидес.</i></li>
</ul>
## Регулярные выражения
<ul>
<li><a href="http://www.ozon.ru/context/detail/id/4066500/?partner=iliakan">Регулярные выражения.</a>
<i>Джеффри Фридл.</i></li>
</ul>
Эта книга описывает более широкий класс регэкспов, по сравнению с текущим JavaScript. С одной стороны, какая-то информация будет лишней, с другой -- регулярные выражения вообще очень важная и полезная тема.
## Алгоритмы и структуры данных
<ul>
<li><a href="http://www.ozon.ru/context/detail/id/2429691/?partner=iliakan">Алгоритмы. Построение и анализ.</a>
<i>Т. Кормен, Ч. Лейзерсон, Р. Ривест, К. Штайн.</i></li>
</ul>
Есть и другая классика, например "Искусство программирования", Дональд Кнут, но она требует более серьёзной математической подготовки. Будьте готовы читать и вникать долго и упорно. Результат -- апгрейд мозговых извилин и общего умения программировать.
## Разработка и организация кода
<ul>
<li><a href="http://www.ozon.ru/context/detail/id/5508646/?partner=iliakan">Совершенный код.</a>
<i>Стив Макконнелл.</i></li>
</ul>
Это желательно изучать уже после получения какого-то опыта в программировании.

View file

@ -0,0 +1,54 @@
# Асинхронное выполнение: setImmediate
Функция, отложенная через `setTimeout(..0)` выполнится не ранее следующего "тика" таймера, минимальная частота которого может составлять от 4 до 1000мс. И, конечно же, это произойдёт после того, как все текущие изменения будут перерисованы.
Но нужна ли нам эта дополнительная задержка? Как правило, используя `setTimeout(func, 0)`, мы хотим перенести выполнение `func` на "ближайшее время после текущего кода", и какая-то дополнительная задержка нам не нужна. Если бы была нужна -- мы бы её указали вторым аргументом вместо `0`.
[cut]
## Метод setImmediate(func)
Для того, чтобы поставить функцию в очередь на выполнение без задержки, в Microsoft предложили метод [setImmediate(func)](http://msdn.microsoft.com/en-us/library/ie/hh773176.aspx). Он реализован в IE10+ и на платформе Node.JS.
У `setImmediate` единственный аргумент -- это функция, выполнение которой нужно запланировать.
В других браузерах `setImmediate` нет, но его можно эмулировать, используя, к примеру, метод [postMessage](https://developer.mozilla.org/en-US/docs/DOM/window.postMessage), предназначенный для пересылки сообщений от одного окна другому. Детали работы с `postMessage` вы найдёте в статье [](/cross-window-messaging-with-postmessage). Желательно читать её после освоения темы "События".
Полифилл для `setImmediate` через `postMessage`:
```js
//+ no-beautify
if (!window.setImmediate) window.setImmediate = (function() {
var head = { }, tail = head; // очередь вызовов, 1-связный список
var ID = Math.random(); // уникальный идентификатор
function onmessage(e) {
if(e.data != ID) return; // не наше сообщение
head = head.next;
var func = head.func;
delete head.func;
func();
}
if(window.addEventListener) { // IE9+, другие браузеры
window.addEventListener('message', onmessage);
} else { // IE8
window.attachEvent( 'onmessage', onmessage );
}
return function(func) {
tail = tail.next = { func: func };
window.postMessage(ID, "*");
};
}());
```
Есть и более сложные эмуляции, включая [MessageChannel](http://www.w3.org/TR/webmessaging/#channel-messaging) для работы с [Web Workers](http://www.w3.org/TR/workers/) и хитрый метод для поддержки IE8-: [](https://github.com/NobleJS/setImmediate). Все они по существу являются "хаками", направленными на то, чтобы обеспечить поддержку `setImmediate` в тех браузерах, где его нет.
## Тест производительности
Чтобы сравнить реальную частоту срабатывания -- измерим время на 100 последовательных вызовов при `setTimeout(..0)` по сравнению с `setImmediate`:
[codetabs src="setImmediate"]
Запустите пример выше -- и вы увидите реальную разницу во времени между `setTimeout(.., 0)` и `setImmediate`. Да, она может быть более в 50, 100 и более раз.

View file

@ -0,0 +1,45 @@
<!DOCTYPE HTML>
<html>
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<script src="setImmediate.js"></script>
</head>
<body>
<button onclick="testTimeout()">testTimeout</button>
<button onclick="testImmediate()">testImmediate</button>
<script>
function testTimeout() {
var start = new Date();
var i = 0;
setTimeout(function go() {
i++;
if (i == 100) {
alert(new Date - start);
} else {
setTimeout(go, 0);
}
}, 0);
}
function testImmediate() {
var start = new Date();
var i = 0;
setImmediate(function go() {
i++;
if (i == 100) {
alert(new Date - start);
} else {
setImmediate(go);
}
});
}
</script>
</body>
</html>

View file

@ -0,0 +1,27 @@
if (!window.setImmediate) window.setImmediate = (function() {
var head = {},
tail = head; // очередь вызовов, 1-связный список
var ID = Math.random(); // уникальный идентификатор
function onmessage(e) {
if (e.data != ID) return; // не наше сообщение
head = head.next;
var func = head.func;
delete head.func;
func();
}
if (window.addEventListener) { // IE9+, другие браузеры
window.addEventListener('message', onmessage);
} else { // IE8
window.attachEvent('onmessage', onmessage);
}
return function(func) {
tail = tail.next = {
func: func
};
window.postMessage(ID, "*");
};
}());

View file

@ -0,0 +1,160 @@
# Позднее связывание "bindLate"
Обычный метод `bind` называется "ранним связыванием", поскольку фиксирует привязку сразу же.
Как только значения привязаны -- они уже не могут быть изменены. В том числе, если метод объекта, который привязали, кто-то переопределит -- "привязанная" функция этого не заметит.
Позднее связывание -- более гибкое, оно позволяет переопределить привязанный метод когда угодно.
[cut]
## Раннее связывание
Например, попытаемся переопределить метод при раннем связывании:
```js
//+ run
function bind(func, context) {
return function() {
return func.apply(context, arguments);
};
}
var user = {
sayHi: function() { alert('Привет!'); }
}
// привязали метод к объекту
*!*
var userSayHi = bind(user.sayHi, user);
*/!*
// понадобилось переопределить метод
user.sayHi = function() { alert('Новый метод!'); }
// будет вызван старый метод, а хотелось бы - новый!
userSayHi(); // *!*выведет "Привет!"*/!*
```
...Привязка всё ещё работает со старым методом, несмотря на то что он был переопределён.
## Позднее связывание
При позднем связывании `bind` вызовет не ту функцию, которая была в `sayHi` на момент привязки, а ту, которая есть на момент вызова.**
Встроенного метода для этого нет, поэтому нужно реализовать.
Синтаксис будет таков:
```js
var func = bindLate(obj, "method");
```
<dl>
<dt>`obj`</dt>
<dd>Объект</dd>
<dt>`method`</dt>
<dd>Название метода (строка)</dd>
</dl>
Код:
```js
function bindLate(context, funcName) {
return function() {
return context[funcName].apply(context, arguments);
};
}
```
Этот вызов похож на обычный `bind`, один из вариантов которого как раз и выглядит как `bind(obj, "method")`, но работает по-другому.
**Поиск метода в объекте: `context[funcName]`, осуществляется при вызове, самой обёрткой**.
**Поэтому, если метод переопределили -- будет использован всегда последний вариант.**
В частности, пример, рассмотренный выше, станет работать правильно:
```js
//+ run
function bindLate(context, funcName) {
return function() {
return context[funcName].apply(context, arguments);
};
}
var user = {
sayHi: function() { alert('Привет!'); }
}
*!*
var userSayHi = bindLate(user, 'sayHi');
*/!*
user.sayHi = function() { alert('Здравствуйте!'); }
userSayHi(); // *!*Здравствуйте!*/!*
```
## Привязка метода, которого нет
Позднее связывание позволяет привязать к объекту даже метод, которого ещё нет!
Конечно, предполагается, что к моменту вызова он уже будет определён ;).
Например:
```js
//+ run
function bindLate(context, funcName) {
return function() {
return context[funcName].apply(context, arguments);
};
}
// *!*метода нет*/!*
var user = { };
// *!*..а привязка возможна!*/!*
*!*
var userSayHi = bindLate(user, 'sayHi');
*/!*
// по ходу выполнения добавили метод..
user.sayHi = function() { alert('Привет!'); }
userSayHi(); // Метод работает: *!*Привет!*/!*
```
В некотором смысле, позднее связывание всегда лучше, чем раннее. Оно удобнее и надежнее, так как всегда вызывает нужный метод, который в объекте сейчас.
Но оно влечет и небольшие накладные расходы -- поиск метода при каждом вызове.
## Итого
*Позднее связывание* ищет функцию в объекте в момент вызова.
Оно используется для привязки в тех случаях, когда метод *может быть переопределён* после привязки или *на момент привязки не существует*.
Обёртка для позднего связывания (без карринга):
```js
function bindLate(context, funcName) {
return function() {
return context[funcName].apply(context, arguments);
};
}
```
[head]
<script>
function bind(func, context /*, args*/) {
var bindArgs = [].slice.call(arguments, 2); // (1)
function wrapper() { // (2)
var args = [].slice.call(arguments);
var unshiftArgs = bindArgs.concat(args); // (3)
return func.apply(context, unshiftArgs); // (4)
}
return wrapper;
}
</script>
[/head]

View file

@ -0,0 +1,47 @@
# Sublime Text: шпаргалка
Одним из наиболее мощных, и при этом простых, редакторов является [Sublime Text](http://www.sublimetext.com/).
В нём хорошо развита система команд и горячих клавиш. На этой странице размещена "шпаргалка" с плагинами и самыми частыми сочетаниями клавиш, которые сильно упрощают жизнь.
[cut]
## Горячие клавиши
Для наибольшего удобства "шпаргалка" должна быть распечатана и повешена перед глазами, поэтому она сделана в виде 3-колоночного PDF.
[Скачать шпаргалку в формате PDF](sheet.pdf)
Эта шпаргалка -- под Mac, для Windows сочетания похожи, обычно вместо Mac-клавиши <code class="key">Cmd</code> под Windows будет <code class="key">Ctrl</code>. А если в сочетании есть и <code class="key">Cmd</code> и <code class="key">Ctrl</code>, то под Windows будет <code class="key">Ctrl</code> + <code class="key">Shift</code>.
**Вы часто используете сочетание, но его нет в списке? Поделитесь им в комментариях!**
## Плагины
Мои любимые плагины:
<ul>
<li>Package Control</li>
<li>sublime-emmet</li>
<li>JsFormat</li>
<li>SideBarEnhancements</li>
<li>AdvancedNewFile</li>
<li>sublime-jsdocs</li>
<li>SublimeCodeIntel</li>
</ul>
Остальные:
<ul>
<li>Alignment</li>
<li>CSSComb</li>
<li>EncodingHelper</li>
<li>GoToRecent</li>
<li>HTML5</li>
<li>jQuery</li>
<li>Prefixr</li>
<li>View In Browser</li>
</ul>
Чтобы узнать о плагине подробнее -- введите его название в Google.
Есть и другие хорошие плагины, кроме перечисленных. Кроме того, Sublime позволяет легко писать свои плагины и команды.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

View file

@ -0,0 +1,428 @@
# Выделение: Range, TextRange и Selection
В этой статье речь пойдет о документированных, но нечасто используемых объектах `Range`, `TextRange` и `Selection`. Мы рассмотрим вольный перевод спецификаций с понятными примерами и различные кроссбраузерные реализации.
Эта статья представляет собой обновлённый вариант статьи Александра Бурцева, которой уже нет онлайн. Публикуется с его разрешения, спасибо, Александр!
[cut]
## Range
`Range`. -- это объект, соответствующий фрагменту документа, который может включать узлы и участки текста из этого документа. Наиболее подробно объект `Range` описан в спецификации [DOM Range](http://www.w3.org/TR/DOM-Level-2-Traversal-Range/ranges.html).
Чтобы понять о чем речь, обратимся к самому простому случаю `Range`, который будет подробно рассмотрен ниже -- к выделениям. В приводимом ниже примере выделите несколько слов в предложении. Будет выводиться текстовое содержимое выделяемой области:
<div id="demo-mix" onmouseup="alert($selection.getText())" style="border:1px dashed #999; color:#666; background:#EEE; padding:2px 5px; margin:10px 0;">
Соберем микс из <b>жирности</b>, <em>курсива</em> и <a href="#">ссылки</a> и повыделяем здесь.
</div>
Но такие области можно создавать не только с помощью пользовательского выделения, но и из JavaScript-сценария, выполняя с ними определенные манипуляции. Однако, написать простой иллюстрирующий код сразу не выйдет, т.к. есть одно НО -- Internet Explorer до версии 9. В Microsoft создали собственную реализацию -- [объект TextRange](http://msdn.microsoft.com/en-us/library/ms535872.aspx). Разбёрем каждую реализацию по-отдельности.
### DOM-реализация Range (кроме IE8-)
`Range` состоит из двух граничных точек (boundary-points), соответствующих началу и концу области. Позиция любой граничной точки определяется в документе с помощью двух свойств: узел (node) и смещение (offset).
Контейнером (container) называют узел, содержащий граничную точку. Сам контейнер и все его предки называются родительскими контейнерами (ancestor containers) для граничной точки. Родительский контейнер, включающий обе граничные точки, называют корневым контейнером (root container).
<img src="57.gif">
На изображении выше граничные точки выделения лежат в текстовых узлах (`#text1` и `#text2`), которые являются контейнерами. Для левой границы родительскими контейнерами являются `#text1`, `H1`, `BODY`, для правой -- `#text2`, `P`, `BODY`. Общий родитель для обоих граничных точек -- `BODY`, этот элемент является корневым контейнером.
Если контейнер является текстовым узлом, то смещение определяется в символах от начала DOM-узла. Если контейнер является элементом (`Document`, `DocumentFragment`, `Element`...), то смещение определяется в дочерних узлах.
Смотрим на иллюстрацию ([источник](http://www.w3.org/TR/DOM-Level-2-Traversal-Range/ranges.html#td-boundarypoint)):
<img src="56.gif" alt="Пример Range">
Граничные точки объекта `Range` <span style="color:green; font-weight:bold;">s1</span> лежат в текстовых узлах, поэтому смещение задается в символах от начала узла. Для <span style="color:red; font-weight:bold;">s2</span> граничные точки расставлены так, что включают весь абзац &lt;p&gt;Blah xyz&lt;/p&gt;, поэтому контейнером является элемент `BODY`, и смещение считается в позициях дочерних узлов.
Объекты `Range` создаются с помощью вызова `document.createRange()`. Объект при этом создается пустой, и граничные точки нужно задать далее его методами `setStart` и `setEnd`. Смотрим пример.
HTML:
```html
<div id="ex2">
<h2>Соз|даем объект `Range`</h2>
<p>От третье|го символа заголовка до десятого символа это абзаца.</p>
</div>
<button onclick="alert(domRangeCreate())">
Создать Range и вывести его текст
</button>
<script>
function domRangeCreate() {
// Найдем корневой контейнер
var root = document.getElementById('ex2');
// Найдем контейнеры граничных точек (в данном случае тестовые)
var start = root.getElementsByTagName('h2')[0].firstChild;
var end = root.getElementsByTagName('p')[0].firstChild;
if (root.createRange) {
// Создаем Range
var rng = root.createRange();
// Задаем верхнюю граничную точку, передав контейнер и смещение
rng.setStart(start, 3);
// Аналогично для нижней границы
rng.setEnd(end, 10);
// Теперь мы можем вернуть текст, который содержится в полученной области
return rng.toString();
} else {
return 'Вероятно, у вас IE8-, смотрите реализацию TextRange ниже';
}
}
</script>
```
<p>В действии:</p>
[iframe border="1" src="domRangeCreate" edit link]
<p>Рассмотрим вкратце [свойства и методы Range](http://www.w3.org/TR/DOM-Level-2-Traversal-Range/ranges.html#Level-2-Range-Interface):</p>
<ul>
<li>Свойство `commonAncestorContainer` вернет ссылку на наиболее вложенный корневой контейнер.</li>
<li>Свойство `startContainer` (`endContainer`) вернет ссылку на контейнер верхней (нижней) граничной точки.</li>
<li>Свойство `startOffset` (`endOffset`) вернет смещение для верхней (нижней) граничной точки.</li>
<li>Свойство `collapsed` вернет `true`, если граничные точки имеют одинаковые контейнеры и смещение (`false` в противном случае).</li>
</ul>
<ul>
<li>Метод `setStart` (`setEnd`) задает контейнер (ссылка на узел) и смещение (целочисленное значение) для соответствующих граничных точек. Пример выше.</li>
<li>Методы `setStartBefore`, `setStartAfter`, `setEndBefore`, `setEndAfter` принимают в качестве единственного аргумента ссылку на узел и устанавливают граничные точки в соот-ии с естественной границей переданного узла. Например:
```html
<span id="s1">First</span>
<span id="s2">Second</span>
```
```js
var rng = document.createRange();
// Установит верхнюю граничную точку по левой границе спана #s1
rng.setStartBefore(document.getElementById('s1'));
// Установит нижнюю граничную точку по правой границе спана #s2
rng.setEndAfter(document.getElementById('s2'));
```
</li>
<li>Методы `selectNode` и `selectNodeContents` позволяют создать объект `Range` по границам узла, ссылку на который они принимают в качестве единственного аргумента. При использовании `selectNode` передаваемый узел также войдет в `Range`, в то время как `selectNodeContents` создаст объект только из содержимого узла:
<img src="58.gif">
</li>
<li>Метод `collapse` объединяет граничные точки объекта `Range`. В качестве единственного аргумента принимает булево значение (`true` -- для объединения в верхней точке, `false` -- в нижней). По-умолчанию `true`.</li>
<li>Метод `toString` вернет текстовое содержимое объекта `Range`.</li>
<li>Метод `cloneContents` вернет копию содержимого объекта `Range` в виде фрагмента документа.</li>
<li>Метод `cloneRange` вернет копию самого объекта `Range`.</li>
<li>Метод `deleteContents` удаляет всё содержимое объекта `Range`.</li>
<li>Метод `detach` извлекает текущий объект из DOM, так что на него больше нельзя сослаться.</li>
<li>Метод `insertNode` принимает в качестве единственного аргумента ссылку на узел (или фрагмент документа) и вставляет его в содержимое объекта `Range` в начальной точке.</li>
<li>Метод `extractContents` вырезает содержимое объекта `Range` и возвращает ссылку на полученный фрагмент документа.</li>
<li>Метод `surroundContents` помещает всё содержимое текущего объекта `Range` в новый родительский элемент, ссылка на который принимается в качестве единственного аргумента.</li>
<li>Метод `compareBoundaryPoints` используется для сравнения граничных точек.</li>
</ul>
Для примера решим небольшую задачку. Найдём в текстовом узле фразу и подсветим её синим фоном.
```html
<div id="ex3">
Найдем в этом тексте слово "бабуля" и подсветим его синим фоном
</div>
<script>
function domRangeHighlight(text) {
// Получим текстовый узел
var root = document.getElementById('ex3').firstChild;
// и его содержимое
var content = root.nodeValue;
// Проверим есть ли совпадения с переданным текстом
if (~content.indexOf(text)) {
if (document.createRange) {
// Если есть совпадение, и браузер поддерживает Range, создаем объект
var rng = document.createRange();
// Ставим верхнюю границу по индексу совпадения,
rng.setStart(root, content.indexOf(text));
// а нижнюю по индексу + длина текста
rng.setEnd(root, content.indexOf(text) + text.length);
// Создаем спан с синим фоном
var highlightDiv = document.createElement('span');
highlightDiv.style.backgroundColor = 'blue';
// Обернем наш Range в спан
rng.surroundContents(highlightDiv);
} else {
alert( 'Вероятно, у вас IE8-, смотрите реализацию TextRange ниже' );
}
} else {
alert( 'Совпадений не найдено' );
}
}
</script>
```
В действии:
[codetabs src="domRangeHighlight"]
С остальными свойствами и методами поэкспериментируйте сами. Перейдем к реализации range в IE.
### TextRange (для IE)
Объект `TextRange` в реализации MSIE -- это текстовый диапазон нулевой и более длины. У данного диапазона также есть свои границы, "перемещать" которые можно на целое число текстовых единиц: character(символ), word (слово), sentence (предложение). То есть можно взять и сдвинуть границу на 2(5, 8 и т.д.) слова (символа, предложения) вправо (влево). При этом у объекта сохраняются данные о HTML-содержимом диапазона и есть методы взаимодействия с DOM.
Объект `TextRange` создается с помощью метода `createTextRange`, который можно вызывать в контексте элементов `BODY`, `BUTTON`, `INPUT` (большинство типов), `TEXTAREA`.
Простой пример с кнопкой:
```html
<!--+ autorun -->
<input id="buttonId" type="button" value="Test button" onclick="alert( ieTextRangeCreate() );" />
<script>
function ieTextRangeCreate() {
// Найдем кнопку
var button = document.getElementById('buttonId');
// Если мы в ИЕ
if (button.createTextRange && button.createTextRange() != undefined) {
// Создаем TextRange
var rng = button.createTextRange();
// И вернем текстовое содержимое полученного объекта
return rng.text;
} else {
return 'Вероятно, у вас не IE, смотрите реализацию Range выше';
}
}
</script>
```
Рассмотрим [свойства и методы объекта TextRange](http://msdn.microsoft.com/en-us/library/ms535872.aspx) (не все, только самые необходимые):
<ul>
<li>Свойство `boundingWidth` (boundingHeight) вернет ширину (высоту), которую занимает объект TextRange в пикселях.</li>
<li>Свойство `boundingTop` (`boundingLeft`) вернет Y(X)-координату верхнего левого угла тестовой области относительно окна документа.</li>
<li>Свойство `htmlText` вернет HTML-содержимое объекта.</li>
<li>Свойство `text` вернет текстовое содержимое объекта (см. пример выше).</li>
<li>Свойство `offsetTop` (`offsetLeft`) вернет Y(X)-координату верхнего левого угла тестовой области относительно предка.</li>
</ul>
<ul>
<li>Метод `collapse` объединяет граничные точки диапазона. В качестве единственного аргумента принимает булево значение (`true` -- для объединения в верхней точке, `false` -- в нижней). По-умолчанию true.</li>
<li>Метод `duplicate` клонирует имеющийся текстовый диапазон, возвращая новый, точно такой же.</li>
<li>Метод `expand` расширяет текущий тектовый диапазон до единицы текста, переданной в качестве единственного текстового аргумента:
<ul>
<li>`"character'` -- символ.</li>
<li>`"word"` -- слово</li>
<li>`"sentence"` -- предложение</li>
<li>`"textedit"` -- сворачивает до первоначального диапазона.
</ul>
Вернет `true` (`false`) в случае успеха (неудачи).
</li>
<li>Метод `findText` ищет в диапазоне совпадения с текстовой строкой, передаваемой в качестве первого аргумента (без учета регистра). Если совпадение найдено, то границы диапазона сворачиваются до него. В качестве второго (необязательного) аргумента можно передать целое число, указывающее число символов от верхней точки, в которых нужно производить поиск. Далее в качестве аргументов можно перечислять INT-флаги, которые вам [вряд ли понадобятся](http://msdn.microsoft.com/en-us/library/ms536422.aspx).</li>
<li>Метод `getBookmark` возвращает в случае успешного вызова строку, по которой можно будет восстановить текущее состояние текстового диапазона с помощью метода `moveToBookmark`.</li>
<li>Метод `inRange` принимает в качестве аргумента другой `TextRange` и проверяет, входит ли его текстовый диапазон в диапазон контекстного объекта. Возвращает булево значение.</li>
<li>Метод `isEqual` проверяет является ли текущий `TextRange` идентичным переданному в качестве аргумента. Возвращает булево значение.</li>
<li>Метод `move(sUnit [, iCount])` сворачивает текущий диапазон до нулевой длины и передвигает на единицу текста, переданного в качестве первого аргумента (character | word | sentence | textedit). В качестве второго (необязательного) аргумента можно передать число единиц, на которое следует передвинуть диапазон.</li>
<li>Метод `moveEnd` (`moveStart`), аналогично методу move, передвигает верхнюю (нижнюю) границу диапазона на единицу текста, число которых также можно задать необязательным вторым параметром.</li>
<li>Метод `moveToElementText` принимает в качестве аргумента ссылку на DOM-элемент и выставляет границы диапазона Textобъекта `Range` по границам полученного элемента.</li>
<li>Метод `moveToPoint` принимает в качестве двух обязательных аргументов X и Y-координаты (в пикселях) относительно верхнего левого угла документа и переносит границы диапазона туда.</li>
<li>Метод `parentElement` вернет ссылку на элемент, который полностью содержит диапазон объекта `TextRange` (или `null`).</li>
<li>Метод `pasteHTML` заменяет HTML-содержимое текущего текстового диапазона на строку, переданную в качестве единственного аргумента.</li>
<li>Метод `select` формирует выделение на основе содержимого объекта `TextRange`, о чем мы подробнее поговорим ниже.</li>
<li>Метод `setEndPoint` принимает в качестве обязательных аргументов текстовый указатель и ссылку на другой `TextRange`, устанавливая в зависимости от значения указателя границы диапазона. Указатели могут быть следующими: 'StartToEnd', 'StartToStart', 'EndToStart', 'EndToEnd'.</li>
</ul>
Также к `TextRange` применимы команды [метода execCommand](http://msdn.microsoft.com/en-us/library/ms536419%28v=vs.85%29.aspx), который умеет делать текст жирным, курсивным, копировать его в буфер обмена (только IE) и т.п.
Для закрепления сделаем задачку по поиску текстового содержимого, аналогичную той, что была выше:
```html
<div id="ex4">
Найдем в этом тексте слово "бабуля" и подсветим его синим фоном
</div>
<script>
function ieTextRangeHighlight(text) {
// Получим ссылку на элемент, в котором будет происходить поиск
var root = document.getElementById('ex4');
// Получим значение его текстового потомка
var content = root.firstChild.nodeValue;
// Если есть совпадение
if (~content.indexOf(text)) {
// и мы в MSIE
if (document.body.createTextRange) {
// Создадим объект TextRange
var rng = document.body.createTextRange();
// Свернем его до root
rng.moveToElementText(root);
// Найдем текст и свернем диапазон до него
if (rng.findText(text))
// Заменим текстовый фрагмент на span с синим фоном
rng.pasteHTML('<span style="background:blue;">' + text + '</span>');
} else {
alert( 'Вероятно, у вас не IE, смотрите реализацию Range выше' );
}
} else {
alert( 'Совпадений не найдено' );
}
}
</script>
```
В действии:
[codetabs border="1" src="ieTextRangeHighlight"]
С остальными свойствами и методами поэкспериментируйте сами.
## Selection
Всем знакомо выделение элементов на странице, когда, зажав левую кнопку мыши и передвигая курсор, мы выделяем нужный фрагмент. Или зажимаем Shift и жмём на стрелочки клавиатуры. Или еще как-то, неважно. В данной части статьи мы кроссбраузерно научимся решать две задачи: получать пользовательское выделение и устанавливать собственное.
### Получаем пользовательское выделение
Эту задачу мы уже решали в самом начале статьи <a href="#demo-mix">в примере с миксом</a>. Теперь рассмотрим код:
```js
//+ no-beautify
function getSelectionText() {
var txt = '';
if (txt = window.getSelection) // Не IE, используем метод getSelection
txt = window.getSelection().toString();
} else { // IE, используем объект selection
txt = document.selection.createRange().text;
}
return txt;
}
```
Все браузеры, кроме IE8- поддерживают метод `window.getSelection()`, который возвращает объект, схожий с рассмотренным ранее `Range`. У этого объекта есть точка начала выделения (anchor) и фокусная точка окончания (focus). Точки могут совпадать. Рассмотрим свойства и методы объекта `Selection`:
<ul>
<li>Свойство `anchorNode` вернет контейнер, в котором начинается выделение. Замечу, что началом выделения считается та граница, от которой вы начали выделение. То есть, если вы выделяете справа налево, то началом будет именно правая граница. Это правило работает везде, кроме браузера Opera, в котором `anchorNode` вернет ссылку на узел левого края выделения.</li>
<li>Свойство `anchorOffset` вернет смещение для начала выделения в пределах контейнера `anchorNode`.</li>
<li>Свойства `focusNode` и `focusOffset` работают аналогично для фокусных точек, то есть точек окончания выделения. Opera и здесь отличилась, возвращает вместо фокусной точки узел правого края выделения.</li>
<li>Свойство `rangeCount` возвращает число объектов `Range`, которые входят в полученное выделение. Это свойство полезно при использовании метода `addRange`.</li>
</ul>
<ul>
<li>Метод `getRangeAt` принимает в качестве аргумента индекс объекта `Range` и возвращает сам объект. Если `rangeCount == 1`, то работать будет только `getRangeAt(0)`. Таким образом, мы можем получить объект `Range`, полностью соответствующий текущему выделению.</li>
<li>Метод `collapse` сворачивает выделение в точку (каретку). Методу можно передать в качестве первого аргумента узел, в который нужно поместить каретку.</li>
<li>Метод `extend` принимает в качестве аргументов ссылку на контейнер и смещение (`parentNode`, `offset`), и перемещает фокусную точку в это положение.</li>
<li>Метод `collapseToStart` (`collapseToEnd`) перемещает фокусную (начальную) границу к начальной (фокусной), тем самым сворачивая выделение в каретку.</li>
<li>Метод `selectAllChildren` принимает в качестве единственного аргумента ссылку на узел и добавляет всех его потомков в выделение.</li>
<li>Метод `addRange` принимает в качестве аргумента объект `Range` и добавляет его в выделение. Таким образом можно увеличить количество объектов `Range`, число которых нам подскажет свойство `rangeCount`.</li>
<li>Метод `removeRange` (`removeAllRanges`) удаляет переданный (все) объект `Range` из выделения.</li>
<li>Метод `toString` вернет текстовое содержимое выделения.</li>
</ul>
IE предоставляет собственный интерфейс взаимодействия с выделениями -- объект `selection` в контексте document. Для работы с этим объектом используются следующие методы:
<ul>
<li>Метод `clear` убирает выделение вместе с содержимым.</li>
<li>Метод `createRange` (ВАЖНО! Не путать со стандартным методом `document.createRange()` для создания объектов `Range`!) создает из содержимого выделения `TextRange`.</li>
<li>Метод `empty` убирает выделение, но оставляет содержимое.</li>
</ul>
Надеюсь, теперь, после знакомства с обеими реализациями выделений, код выше стал более понятен.
### Установка собственного выделения
Допустим, вам хочется, чтобы какой-то текстовый фрагмент на странице был выделен, как пользовательское выделение. Это нужно при клиентской реализации поиска по странице и некоторых других задач.
Проще всего решить эту задачу следующим образом:
<ol>
<li>Создать объект `Range` (`TextRange` для IE8-).</li>
<li>Перевести полученный объект в выделение.</li>
</ol>
Смотрим реализацию:
```html
<div id="ex5">
Снова будем выделять <span>бабулю</span>, на этот раз без поиска.
</div>
<script>
function setSelection() {
var target = document.getElementById('ex5').getElementsByTagName('span')[0];
var rng, sel;
if (document.createRange) {
rng = document.createRange();
rng.selectNode(target)
sel = window.getSelection();
sel.removeAllRanges();
sel.addRange(rng);
} else {
var rng = document.body.createTextRange();
rng.moveToElementText(target);
rng.select();
}
}
</script>
```
В действии:
[codetabs border="1" src="setSelection"]
## Снятие выделения
Код для снятия выделения, использующий соответствующие методы объектов `Selection`:
```js
function clearSelection() {
try {
// современный объект Selection
window.getSelection().removeAllRanges();
} catch (e) {
// для IE8-
document.selection.empty();
}
}
```
## Итого
<ul>
<li>В современных браузерах поддерживается стандартный объект [Range](http://www.w3.org/TR/DOM-Level-2-Traversal-Range/ranges.html)
</li>
<li>В IE8- поддерживается только собственный объект [TextRange](http://help.dottoro.com/ljgbbkjf.php).</li>
</ul>
Есть библиотеки, которые "исправляют" объект `TextRange`, добавляя ему нужные свойства из `Range`.
Код, получающий выделение, при использовании такой библиотеки может выглядеть так:
```js
var range = getRangeObject();
if (range) {
alert( range );
alert( range.startContainer.nodeValue );
alert( range.startOffset );
alert( range.endOffset );
} else {
alert( 'Ничего не выделено' );
}
}
```
В действии:
[codetabs border="1" src="fix-ie"]
Код функций `getRangeObject(win)` для получения выделения в окне и `fixIERangeObject(range, win)` для исправления `TextRange` -- [edit src="fix-ie"]в песочнице вместе с этим примером[/edit].
[head]
<script>
window.$selection = {
getText : function() {
var txt = '';
if (txt = window.getSelection) {
txt = window.getSelection().toString();
} else {
txt = document.selection.createRange().text;
}
return txt;
}
}
</script>
[/head]

View file

@ -0,0 +1,40 @@
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
</head>
<body>
<div id="ex2" style="border:1px dashed #999; color:#666; background:#EEE; padding:2px 5px; margin:10px 0;">
<h2 style="color:#333; margin:0; padding:0 0 7px 0; font:bold 15px Arial;">Соз|даем Range-объект</h2>
<p style="color:#333; margin:0; font:13px Arial;">От третье|го символа заголовка до десятого символа это абзаца.</p>
</div>
<button onclick="alert(domRangeCreate())">Создать Range и вывести его текст</button>
<script>
function domRangeCreate() {
// Найдем корневой контейнер
var root = document.getElementById('ex2');
// Найдем контейнеры граничных точек (в данном случае тестовые)
var start = root.getElementsByTagName('h2')[0].firstChild;
var end = root.getElementsByTagName('p')[0].firstChild;
if (document.createRange) {
// Создаем Range
var rng = document.createRange();
// Задаем верхнюю граничную точку, передав контейнер и смещение
rng.setStart(start, 3);
// Аналогично для нижней границы
rng.setEnd(end, 10);
// Теперь мы можем вернуть текст, который содержится в полученной области
return rng.toString();
} else {
return 'Вероятно, у вас IE8-, смотрите реализацию TextRange ниже';
}
}
</script>
</body>
</html>

View file

@ -0,0 +1,37 @@
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
</head>
<body>
<div id="ex3" style="border:1px dashed #999; color:#666; background:#EEE; padding:2px 5px; margin:10px 0;">
Найдем в этом тексте слово "бабуля" и подсветим его синим фоном
</div>
<div>
<input onclick="domRangehighlight('бабуля'); this.style.display = 'none';" type="button" value="Найти!">
</div>
<script>
function domRangehighlight(text) {
var root = document.getElementById('ex3').firstChild;
var content = root.nodeValue;
if (~content.indexOf(text)) {
if (document.createRange) {
var rng = document.createRange();
rng.setStart(root, content.indexOf(text));
rng.setEnd(root, content.indexOf(text) + text.length);
var highlightDiv = document.createElement('span');
highlightDiv.style.backgroundColor = 'blue';
rng.surroundContents(highlightDiv);
} else
alert('Вероятно, у вас IE8-, смотрите реализацию TextRange ниже');
} else
alert('Совпадений не найдено');
}
</script>
</body>
</html>

View file

@ -0,0 +1,93 @@
/*
This code is to fix Microsoft TextRange object (IE8 and below), to give equivalent of
HTML5 Range object's startContainer,startOffset,endContainer and endOffset properties.
Originally from: https://gist.github.com/1115251 (Munnawwar)
*/
/**
* @param {window object} [win] Optional prameter. You could send an IFrame.contentWindow too.
*/
function fixIERangeObject(range, win) { //Only for IE8 and below.
win = win || window;
if (!range) return null;
if (!range.startContainer && win.document.selection) { //IE8 and below
var _findTextNode = function(parentElement, text) {
//Iterate through all the child text nodes and check for matches
//As we go through each text node keep removing the text value (substring) from the beginning of the text variable.
var container = null,
offset = -1;
for (var node = parentElement.firstChild; node; node = node.nextSibling) {
if (node.nodeType == 3) { //Text node
var find = node.nodeValue;
var pos = text.indexOf(find);
if (pos == 0 && text != find) { //text==find is a special case
text = text.substring(find.length);
} else {
container = node;
offset = text.length - 1; //Offset to the last character of text. text[text.length-1] will give the last character.
break;
}
}
}
//Debug Message
//alert(container.nodeValue);
return {
node: container,
offset: offset
}; //nodeInfo
}
var rangeCopy1 = range.duplicate(),
rangeCopy2 = range.duplicate(); //Create a copy
var rangeObj1 = range.duplicate(),
rangeObj2 = range.duplicate(); //More copies :P
rangeCopy1.collapse(true); //Go to beginning of the selection
rangeCopy1.moveEnd('character', 1); //Select only the first character
rangeCopy2.collapse(false); //Go to the end of the selection
rangeCopy2.moveStart('character', -1); //Select only the last character
//Debug Message
// alert(rangeCopy1.text); //Should be the first character of the selection
var parentElement1 = rangeCopy1.parentElement(),
parentElement2 = rangeCopy2.parentElement();
//If user clicks the input button without selecting text, then moveToElementText throws an error.
if (parentElement1 instanceof HTMLInputElement || parentElement2 instanceof HTMLInputElement) {
return null;
}
rangeObj1.moveToElementText(parentElement1); //Select all text of parentElement
rangeObj1.setEndPoint('EndToEnd', rangeCopy1); //Set end point to the first character of the 'real' selection
rangeObj2.moveToElementText(parentElement2);
rangeObj2.setEndPoint('EndToEnd', rangeCopy2); //Set end point to the last character of the 'real' selection
var text1 = rangeObj1.text; //Now we get all text from parentElement's first character upto the real selection's first character
var text2 = rangeObj2.text; //Here we get all text from parentElement's first character upto the real selection's last character
var nodeInfo1 = _findTextNode(parentElement1, text1);
var nodeInfo2 = _findTextNode(parentElement2, text2);
//Finally we are here
range.startContainer = nodeInfo1.node;
range.startOffset = nodeInfo1.offset;
range.endContainer = nodeInfo2.node;
range.endOffset = nodeInfo2.offset + 1; //End offset comes 1 position after the last character of selection.
}
return range;
}
function getRangeObject(win) { //Gets the first range object
win = win || window;
if (win.getSelection) { // Firefox/Chrome/Safari/Opera/IE9
try {
return win.getSelection().getRangeAt(0); //W3C DOM Range Object
} catch (e) { /*If no text is selected an exception might be thrown*/ }
} else if (win.document.selection) { // IE8
var range = win.document.selection.createRange(); //Microsoft TextRange Object
return fixIERangeObject(range, win);
}
return null;
}

View file

@ -0,0 +1,30 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<script src="fixIERangeObject.js"></script>
<script>
function test() {
var range = getRangeObject();
if (range) {
alert(range);
alert(range.startContainer.nodeValue);
alert(range.startOffset);
alert(range.endOffset);
} else {
alert('Сначала выделите текст');
}
}
</script>
</head>
<body>
Выделите текст:
<pre>The quick brown fox jumped over the lazy dog</pre>
<input type="button" value="Вывести выделение и свойства startContainer, startOffset, endOffset" onclick="test();" />
</body>
</html>

View file

@ -0,0 +1,35 @@
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
</head>
<body>
<div id="ex4" style="border:1px dashed #999; color:#666; background:#EEE; padding:2px 5px; margin:10px 0;">
Найдем в этом тексте слово "бабуля" и подсветим его синим фоном
</div>
<div>
<input onclick="ieTextRangeHighlight('бабуля'); this.style.display = 'none';" type="button" value="Найти!">
</div>
<script>
function ieTextRangeHighlight(text) {
var root = document.getElementById('ex4');
var content = root.firstChild.nodeValue;
if (~content.indexOf(text)) {
if (document.body.createTextRange) {
var rng = document.body.createTextRange();
rng.moveToElementText(root);
if (rng.findText(text))
rng.pasteHTML('<span style="background:blue;">' + text + '</span>');
} else
alert('Вероятно, у вас не IE, смотрите реализацию Range выше');
} else
alert('Совпадений не найдено');
}
</script>
</body>
</html>

View file

@ -0,0 +1,37 @@
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
</head>
<body>
<div id="ex5" style="border:1px dashed #999; color:#666; background:#EEE; padding:2px 5px; margin:10px 0;">
Снова будем выделять <span>бабулю</span>, на этот раз без поиска.
</div>
<div>
<input onclick="setSelection()" type="button" value="Выделить бабулю">
</div>
<script>
function setSelection() {
var target = document.getElementById('ex5').getElementsByTagName('span')[0];
var rng, sel;
if (document.createRange) {
rng = document.createRange();
rng.selectNode(target)
sel = window.getSelection();
sel.removeAllRanges();
sel.addRange(rng);
} else {
var rng = document.body.createTextRange();
rng.moveToElementText(target);
rng.select();
}
}
</script>
</body>
</html>

View file

@ -0,0 +1,83 @@
/**
* "Аватар" - элемент, который перетаскивается.
*
* В простейшем случае аватаром является сам переносимый элемент
* Также аватар может быть клонированным элементом
* Также аватар может быть иконкой и вообще чем угодно.
*/
function DragAvatar(dragZone, dragElem) {
/** "родительская" зона переноса */
this._dragZone = dragZone;
/**
* подэлемент родительской зоны, к которому относится аватар
* по умолчанию - элемент, соответствующий всей зоне
* может быть уточнен в initFromEvent
*/
this._dragZoneElem = dragElem;
/**
* Сам элемент аватара, который будет носиться по экрану.
* Инициализуется в initFromEvent
*/
this._elem = dragElem;
}
/**
* Инициализовать this._elem и позиционировать его
* При необходимости уточнить this._dragZoneElem
* @param downX Координата X нажатия мыши
* @param downY Координата Y нажатия мыши
* @param event Текущее событие мыши
*/
DragAvatar.prototype.initFromEvent = function(downX, downY, event) {
/* override */
};
/**
* Возвращает информацию о переносимом элементе для DropTarget
* @param event
*/
DragAvatar.prototype.getDragInfo = function(event) {
// тут может быть еще какая-то информация, необходимая для обработки конца или процесса переноса
return {
elem: this._elem,
dragZoneElem: this._dragZoneElem,
dragZone: this._dragZone
};
};
/**
* Возвращает текущий самый глубокий DOM-элемент под this._elem
* Приватное свойство _currentTargetElem обновляется при каждом передвижении
*/
DragAvatar.prototype.getTargetElem = function() {
return this._currentTargetElem;
};
/**
* При каждом движении мыши перемещает this._elem
* и записывает текущий элемент под this._elem в _currentTargetElem
* @param event
*/
DragAvatar.prototype.onDragMove = function(event) {
this._elem.style.left = event.pageX - this._shiftX + 'px';
this._elem.style.top = event.pageY - this._shiftY + 'px';
this._currentTargetElem = getElementUnderClientXY(this._elem, event.clientX, event.clientY);
};
/**
* Действия с аватаром, когда перенос не удался
* Например, можно вернуть элемент обратно или уничтожить
*/
DragAvatar.prototype.onDragCancel = function() {
/* override */
};
/**
* Действия с аватаром после успешного переноса
*/
DragAvatar.prototype.onDragEnd = function() {
/* override */
};

View file

@ -0,0 +1,120 @@
var dragManager = new function() {
var dragZone, avatar, dropTarget;
var downX, downY;
var self = this;
function onMouseDown(e) {
if (e.which != 1) { // не левой кнопкой
return false;
}
dragZone = findDragZone(e);
if (!dragZone) {
return;
}
// запомним, что элемент нажат на текущих координатах pageX/pageY
downX = e.pageX;
downY = e.pageY;
return false;
}
function onMouseMove(e) {
if (!dragZone) return; // элемент не зажат
if (!avatar) { // элемент нажат, но пока не начали его двигать
if (Math.abs(e.pageX - downX) < 3 && Math.abs(e.pageY - downY) < 3) {
return;
}
// попробовать захватить элемент
avatar = dragZone.onDragStart(downX, downY, e);
if (!avatar) { // не получилось, значит перенос продолжать нельзя
cleanUp(); // очистить приватные переменные, связанные с переносом
return;
}
}
// отобразить перенос объекта, перевычислить текущий элемент под курсором
avatar.onDragMove(e);
// найти новый dropTarget под курсором: newDropTarget
// текущий dropTarget остался от прошлого mousemove
// *оба значения: и newDropTarget и dropTarget могут быть null
var newDropTarget = findDropTarget(e);
if (newDropTarget != dropTarget) {
// уведомить старую и новую зоны-цели о том, что с них ушли/на них зашли
dropTarget && dropTarget.onDragLeave(newDropTarget, avatar, e);
newDropTarget && newDropTarget.onDragEnter(dropTarget, avatar, e);
}
dropTarget = newDropTarget;
dropTarget && dropTarget.onDragMove(avatar, e);
return false;
}
function onMouseUp(e) {
if (e.which != 1) { // не левой кнопкой
return false;
}
if (avatar) { // если уже начали передвигать
if (dropTarget) {
// завершить перенос и избавиться от аватара, если это нужно
// эта функция обязана вызвать avatar.onDragEnd/onDragCancel
dropTarget.onDragEnd(avatar, e);
} else {
avatar.onDragCancel();
}
}
cleanUp();
}
function cleanUp() {
// очистить все промежуточные объекты
dragZone = avatar = dropTarget = null;
}
function findDragZone(event) {
var elem = event.target;
while (elem != document && !elem.dragZone) {
elem = elem.parentNode;
}
return elem.dragZone;
}
function findDropTarget(event) {
// получить элемент под аватаром
var elem = avatar.getTargetElem();
while (elem != document && !elem.dropTarget) {
elem = elem.parentNode;
}
if (!elem.dropTarget) {
return null;
}
return elem.dropTarget;
}
document.ondragstart = function() {
return false;
}
document.onmousemove = onMouseMove;
document.onmouseup = onMouseUp;
document.onmousedown = onMouseDown;
};

View file

@ -0,0 +1,39 @@
/**
* Зона, из которой можно переносить объекты
* Умеет обрабатывать начало переноса на себе и создавать "аватар"
* @param elem DOM-элемент, к которому привязана зона
*/
function DragZone(elem) {
elem.dragZone = this;
this._elem = elem;
}
/**
* Создать аватар, соответствующий зоне.
* У разных зон могут быть разные типы аватаров
*/
DragZone.prototype._makeAvatar = function() {
/* override */
};
/**
* Обработать начало переноса.
*
* Получает координаты изначального нажатия мышки, событие.
*
* @param downX Координата изначального нажатия по X
* @param downY Координата изначального нажатия по Y
* @param event текущее событие мыши
*
* @return аватар или false, если захватить с данной точки ничего нельзя
*/
DragZone.prototype.onDragStart = function(downX, downY, event) {
var avatar = this._makeAvatar();
if (!avatar.initFromEvent(downX, downY, event)) {
return false;
}
return avatar;
};

View file

@ -0,0 +1,85 @@
/**
* Зона, в которую объекты можно класть
* Занимается индикацией передвижения по себе, добавлением в себя
*/
function DropTarget(elem) {
elem.dropTarget = this;
this._elem = elem;
/**
* Подэлемент, над которым в настоящий момент находится аватар
*/
this._targetElem = null;
}
/**
* Возвращает DOM-подэлемент, над которым сейчас пролетает аватар
*
* @return DOM-элемент, на который можно положить или undefined
*/
DropTarget.prototype._getTargetElem = function(avatar, event) {
return this._elem;
};
/**
* Спрятать индикацию переноса
* Вызывается, когда аватар уходит с текущего this._targetElem
*/
DropTarget.prototype._hideHoverIndication = function(avatar) {
/* override */
};
/**
* Показать индикацию переноса
* Вызывается, когда аватар пришел на новый this._targetElem
*/
DropTarget.prototype._showHoverIndication = function(avatar) {
/* override */
};
/**
* Метод вызывается при каждом движении аватара
*/
DropTarget.prototype.onDragMove = function(avatar, event) {
var newTargetElem = this._getTargetElem(avatar, event);
if (this._targetElem != newTargetElem) {
this._hideHoverIndication(avatar);
this._targetElem = newTargetElem;
this._showHoverIndication(avatar);
}
};
/**
* Завершение переноса.
* Алгоритм обработки (переопределить функцию и написать в потомке):
* 1. Получить данные переноса из avatar.getDragInfo()
* 2. Определить, возможен ли перенос на _targetElem (если он есть)
* 3. Вызвать avatar.onDragEnd() или avatar.onDragCancel()
* Если нужно подтвердить перенос запросом на сервер, то avatar.onDragEnd(),
* а затем асинхронно, если сервер вернул ошибку, avatar.onDragCancel()
* При этом аватар должен уметь "откатываться" после onDragEnd.
*
* При любом завершении этого метода нужно (делается ниже):
* снять текущую индикацию переноса
* обнулить this._targetElem
*/
DropTarget.prototype.onDragEnd = function(avatar, event) {
this._hideHoverIndication(avatar);
this._targetElem = null;
};
/**
* Вход аватара в DropTarget
*/
DropTarget.prototype.onDragEnter = function(fromDropTarget, avatar, event) {};
/**
* Выход аватара из DropTarget
*/
DropTarget.prototype.onDragLeave = function(toDropTarget, avatar, event) {
this._hideHoverIndication();
this._targetElem = null;
};

View file

@ -0,0 +1,43 @@
function TreeDragAvatar(dragZone, dragElem) {
DragAvatar.apply(this, arguments);
}
extend(TreeDragAvatar, DragAvatar);
TreeDragAvatar.prototype.initFromEvent = function(downX, downY, event) {
if (event.target.tagName != 'SPAN') return false;
this._dragZoneElem = event.target;
var elem = this._elem = this._dragZoneElem.cloneNode(true);
elem.className = 'avatar';
// создать вспомогательные свойства shiftX/shiftY
var coords = getCoords(this._dragZoneElem);
this._shiftX = downX - coords.left;
this._shiftY = downY - coords.top;
// инициировать начало переноса
document.body.appendChild(elem);
elem.style.zIndex = 9999;
elem.style.position = 'absolute';
return true;
};
/**
* Вспомогательный метод
*/
TreeDragAvatar.prototype._destroy = function() {
this._elem.parentNode.removeChild(this._elem);
};
/**
* При любом исходе переноса элемент-клон больше не нужен
*/
TreeDragAvatar.prototype.onDragCancel = function() {
this._destroy();
};
TreeDragAvatar.prototype.onDragEnd = function() {
this._destroy();
};

View file

@ -0,0 +1,9 @@
function TreeDragZone(elem) {
DragZone.apply(this, arguments);
}
extend(TreeDragZone, DragZone);
TreeDragZone.prototype._makeAvatar = function() {
return new TreeDragAvatar(this, this._elem);
};

View file

@ -0,0 +1,72 @@
function TreeDropTarget(elem) {
TreeDropTarget.parent.constructor.apply(this, arguments);
}
extend(TreeDropTarget, DropTarget);
TreeDropTarget.prototype._showHoverIndication = function() {
this._targetElem && this._targetElem.classList.add('hover');
};
TreeDropTarget.prototype._hideHoverIndication = function() {
this._targetElem && this._targetElem.classList.remove('hover');
};
TreeDropTarget.prototype._getTargetElem = function(avatar, event) {
var target = avatar.getTargetElem();
if (target.tagName != 'SPAN') {
return;
}
// проверить, может быть перенос узла внутрь самого себя или в себя?
var elemToMove = avatar.getDragInfo(event).dragZoneElem.parentNode;
var elem = target;
while (elem) {
if (elem == elemToMove) return; // попытка перенести родителя в потомка
elem = elem.parentNode;
}
return target;
};
TreeDropTarget.prototype.onDragEnd = function(avatar, event) {
if (!this._targetElem) {
// перенос закончился вне подходящей точки приземления
avatar.onDragCancel();
return;
}
this._hideHoverIndication();
// получить информацию об объекте переноса
var avatarInfo = avatar.getDragInfo(event);
avatar.onDragEnd(); // аватар больше не нужен, перенос успешен
// вставить элемент в детей в отсортированном порядке
var elemToMove = avatarInfo.dragZoneElem.parentNode; // <LI>
var title = avatarInfo.dragZoneElem.innerHTML; // переносимый заголовок
// получить контейнер для узлов дерева, соответствующий точке преземления
var ul = this._targetElem.parentNode.getElementsByTagName('UL')[0];
if (!ul) { // нет детей, создадим контейнер
ul = document.createElement('UL');
this._targetElem.parentNode.appendChild(ul);
}
// вставить новый узел в нужное место среди потомков, в алфавитном порядке
var li = null;
for (var i = 0; i < ul.children.length; i++) {
li = ul.children[i];
var childTitle = li.children[0].innerHTML;
if (childTitle > title) {
break;
}
}
ul.insertBefore(elemToMove, li);
this._targetElem = null;
};

View file

@ -0,0 +1,207 @@
# Применяем ООП: Drag'n'Drop++
Эта статья представляет собой продолжение главы [](/drag-and-drop-objects).
Она посвящена более гибкой и расширяемой реализации переноса.
Рекомендуется прочитать указанную главу перед тем, как двигаться дальше.
[cut]
В сложных приложениях Drag'n'Drop обладает рядом особенностей:
<ol>
<li>Перетаскиваются *элементы* из *зоны переноса `dragZone`* в *зону-цель `dropTarget`*. При этом сама зона не переносится.
Например -- два списка, нужен перенос элемента из одного в другой. В этом случае один список является зоной переноса, второй -- зоной-целью.
Возможно, что перенос осуществляется внутри одного и того же списка. При этом `dragZone == dropTarget`.
</li>
<li>На странице может быть несколько разных зон переноса и зон-целей.</li>
<li>Обработка завершения переноса может быть асинхронной, с уведомлением сервера.</li>
<li>Должно быть легко добавить новый тип зоны переноса или зоны-цели, а также расширить поведение существующей.</li>
<li>Фреймворк для переноса должен быть расширяемым с учётом сложных сценариев.</li>
</ol>
Всё это вполне реализуемо. Но для этого фреймворк, описанный в статье [](/drag-and-drop-objects), нужно отрефакторить, и разделить на сущности.
## Основные сущности
Всего будет 4 сущности:
<dl>
<dt>`DragZone`</dt>
<dd>Зона переноса. С нее начинается перенос. Она принимает нажатие мыши и генерирует аватар нужного типа.</dd>
<dt>`DragAvatar`</dt>
<dd>Переносимый объект. Предоставляет доступ к информации о том, что переносится. Умеет двигать себя по экрану. В зависимости от вида переноса, может что-то делать с собой в конце, например, самоуничтожаться.</dd>
<dt>`DropTarget`</dt>
<dd>Зона-цель, на которую можно положить. В процессе переноса аватара над ней умеет рисовать на себе предполагаемое "место приземления". Обрабатывает окончание переноса.</dd>
<dt>`dragManager`</dt>
<dd>Единый объект, который стоит над всеми ними, ставит обработчики `mousedown/mousemove/mouseup` и управляет процессом. В терминах ООП, это не класс, а [объект-синглтон](http://ru.wikipedia.org/wiki/%D0%9E%D0%B4%D0%B8%D0%BD%D0%BE%D1%87%D0%BA%D0%B0_%28%D1%88%D0%B0%D0%B1%D0%BB%D0%BE%D0%BD_%D0%BF%D1%80%D0%BE%D0%B5%D0%BA%D1%82%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D1%8F%29), поэтому он с маленькой буквы.</dd>
</dl>
На макете страницы ниже возможен перенос студентов из левого списка -- вправо, в одну из команд или в "корзину":
<img src="dragzonetarget.png">
Здесь левый список является зоной переноса `ListDragZone`, а правые списки -- это несколько зон-целей `ListDropTarget`. Кроме того, корзина также является зоной-целью отдельного типа `RemoveDropTarget`.
## Пример
В этой статье мы реализуем пример, когда узлы дерева можно переносить внутри него. То есть, дерево, которое является одновременно `TreeDragZone` и `TreeDropTarget`.
Структура дерева будет состоять из вложенных списков с заголовком в `SPAN`:
```html
<ul>
<li><span>Заголовок 1</span>
<ul>
<li><span>Заголовок 1.1</span></li>
<li><span>Заголовок 1.2</span></li>
...
</ul>
</li>
...
</ul>
```
При переносе:
<ul>
<li>Для аватара нужно клонировать заголовок узла, на котором было нажатие.</li>
<li>Узлы, на которые можно положить, при переносе подсвечиваются красным.</li>
<li>Нельзя перенести узел сам в себя или в своего потомка.</li>
<li>Дерево само поддерживает сортировку по алфавиту среди узлов.</li>
<li>Обязательна расширяемость кода, поддержка большого количества узлов и т.п.</li>
</ul>
[iframe height=450 border=1 src="dragTree"]
## dragManager
Обязанность `dragManager` -- обработка событий мыши и координация всех остальных сущностей в процессе переноса.
**Готовьтесь, дальше будет много кода с комментариями.**
Следующий код должен быть очевиден по смыслу, если вы читали [предыдущую статью](/drag-and-drop-objects). Объект взят оттуда, и из него изъята лишняя функциональность, которая перенесена в другие сущности.
Если вызываемые в нём методы `onDrag*` непонятны -- смотрите далее, в описание остальных объектов.
```js
//+ src="DragManager.js"
```
## DragZone
Основная задача `DragZone` -- создать аватар и инициализовать его. В зависимости от места, где произошел клик, аватар получит соответствующий подэлемент зоны.
Метод для создания аватара `_makeAvatar` вынесен отдельно, чтобы его легко можно было переопределить и подставить собственный тип аватара.
```js
//+ src="DragZone.js"
```
### TreeDragZone
Объект зоны переноса для дерева, по существу, не вносит ничего нового, по сравнению с `DragZone`.
Он только переопределяет `_makeAvatar` для создания `TreeDragAvatar`.
```js
//+ src="TreeDragZone.js"
```
## DragAvatar
Аватар создается только зоной переноса при начале Drag'n'Drop. Он содержит всю необходимую информацию об объекте, который переносится.
В дальнейшем вся работа происходит *только с аватаром*, сама зона напрямую не вызывается.
У аватара есть три основных свойства:
<dl>
<dt>`_dragZone`</dt>
<dd>Зона переноса, которая его создала.</dd>
<dt>`_dragZoneElem`</dt>
<dd>Элемент, соответствующий аватару в зоне переноса. По умолчанию -- DOM-элемент всей зоны. Это подходит в тех случаях, когда зона перетаскивается только целиком.
При инициализации аватара значение этого свойства быть уточнено, например изменено на подэлемент списка, который перетаскивается.</dd>
<dt>`_elem`</dt>
<dd>Основной элемент аватара, который будет двигаться по экрану. По умолчанию равен `_dragZoneElem`, т.е мы переносим сам элемент.
При инициализации мы можем также склонировать `_dragZoneElem`, или создать своё красивое представление переносимого элемента и поместить его в `_elem`.</dd>
</dl>
```js
//+ src="DragAvatar.js"
```
### TreeDragAvatar
Основные изменения -- в методе `initFromEvent`, который создает аватар из узла, на котором был клик.
Обратите внимание, возможно что клик был не на заголовке `SPAN`, а просто где-то на дереве. В этом случае `initFromEvent` возвращает `false` и перенос не начинается.
```js
//+ src="TreeDragAvatar.js"
```
## DropTarget
Именно на `DropTarget` ложится работа по отображению предполагаемой "точки приземления" аватара, а также, по завершению переноса, обработка результата.
Как правило, `DropTarget` принимает переносимый узел в себя, а вот как конкретно организован процесс вставки -- нужно описать в классе-наследнике. Разные типы зон делают разное при вставке: `TreeDropTarget` вставляет элемент в качестве потомка, а `RemoveDropTarget` -- удаляет.
```js
//+ src="DropTarget.js"
```
Как видно, из кода выше, по умолчанию `DropTarget` занимается только отслеживанием и индикацией "точки приземления". По умолчанию, единственной возможной "точкой приземления" является сам элемент зоны. В более сложных ситуациях это может быть подэлемент.
Для применения в реальности необходимо как минимум переопределить обработку результата переноса в `onDragEnd`.
### TreeDropTarget
`TreeDropTarget` содержит код, специфичный для дерева:
<ul>
<li>Индикацию переноса над элементом: методы `_showHoverIndication` и `_hideHoverIndication`.</li>
<li>Получение текущей точки приземления `_targetElem` в методе `_getTargetElem`. Ей может быть только заголовок узла дерева, причем дополнительно проверяется, что это не потомок переносимого узла.</li>
<li>Обработка успешного переноса в `onDragEnd`, вставка исходного узла `avatar.dragZoneElem` в узел, соответствующий `_targetElem`.</li>
</ul>
```js
//+ src="TreeDropTarget.js"
```
## Итого
Реализация Drag'n'Drop оказалась отличным способом применить ООП в JavaScript.
Исходный код примера целиком находится [edit src="dragTree"]в песочнице[/edit].
<ul>
<li>Синглтон `dragManager` и классы `Drag*` задают общий фреймворк. От них наследуются конкретные объекты. Для создания новых зон достаточно унаследовать стандартные классы и переопределить их. </li>
<li>Мини-фреймворк для Drag'n'Drop, который здесь представлен, является переписанным и обновленным вариантом реальной библиотеки, на основе которой было создано много успешных скриптов переноса.
В зависимости от ваших потребностей, вы можете расширить его, добавить перенос нескольких объектов одновременно, поддержку событий и другие возможности.</li>
<li>На сегодняшний день в каждом серьезном фреймворке есть библиотека для Drag'n'Drop. Она работает похожим образом, но сделать универсальный перенос -- штука непростая. Зачастую он перегружен лишним функционалом, либо наоборот -- недостаточно расширяем в нужных местах.
Понимание, как это все может быть устроено, на примере этой статьи, может помочь в адаптации существующего кода под ваши потребности.</li>
</ul>
[head]
<script>
function getElementUnderClientXY(elem, clientX, clientY) {
var display = elem.style.display || '';
elem.style.display = 'none';
var target = document.elementFromPoint(clientX, clientY);
elem.style.display = display;
if (!target || target == document) {
target = document.body;
}
return target;
}
</script>
[/head]
[libs]
getCoords.js
[/libs]

View file

@ -0,0 +1,83 @@
/**
* "Аватар" - элемент, который перетаскивается.
*
* В простейшем случае аватаром является сам переносимый элемент
* Также аватар может быть клонированным элементом
* Также аватар может быть иконкой и вообще чем угодно.
*/
function DragAvatar(dragZone, dragElem) {
/** "родительская" зона переноса */
this._dragZone = dragZone;
/**
* подэлемент родительской зоны, к которому относится аватар
* по умолчанию - элемент, соответствующий всей зоне
* может быть уточнен в initFromEvent
*/
this._dragZoneElem = dragElem;
/**
* Сам элемент аватара, который будет носиться по экрану.
* Инициализуется в initFromEvent
*/
this._elem = dragElem;
}
/**
* Инициализовать this._elem и позиционировать его
* При необходимости уточнить this._dragZoneElem
* @param downX Координата X нажатия мыши
* @param downY Координата Y нажатия мыши
* @param event Текущее событие мыши
*/
DragAvatar.prototype.initFromEvent = function(downX, downY, event) {
/* override */
};
/**
* Возвращает информацию о переносимом элементе для DropTarget
* @param event
*/
DragAvatar.prototype.getDragInfo = function(event) {
// тут может быть еще какая-то информация, необходимая для обработки конца или процесса переноса
return {
elem: this._elem,
dragZoneElem: this._dragZoneElem,
dragZone: this._dragZone
};
};
/**
* Возвращает текущий самый глубокий DOM-элемент под this._elem
* Приватное свойство _currentTargetElem обновляется при каждом передвижении
*/
DragAvatar.prototype.getTargetElem = function() {
return this._currentTargetElem;
};
/**
* При каждом движении мыши перемещает this._elem
* и записывает текущий элемент под this._elem в _currentTargetElem
* @param event
*/
DragAvatar.prototype.onDragMove = function(event) {
this._elem.style.left = event.pageX - this._shiftX + 'px';
this._elem.style.top = event.pageY - this._shiftY + 'px';
this._currentTargetElem = getElementUnderClientXY(this._elem, event.clientX, event.clientY);
};
/**
* Действия с аватаром, когда перенос не удался
* Например, можно вернуть элемент обратно или уничтожить
*/
DragAvatar.prototype.onDragCancel = function() {
/* override */
};
/**
* Действия с аватаром после успешного переноса
*/
DragAvatar.prototype.onDragEnd = function() {
/* override */
};

View file

@ -0,0 +1,120 @@
var dragManager = new function() {
var dragZone, avatar, dropTarget;
var downX, downY;
var self = this;
function onMouseDown(e) {
if (e.which != 1) { // не левой кнопкой
return false;
}
dragZone = findDragZone(e);
if (!dragZone) {
return;
}
// запомним, что элемент нажат на текущих координатах pageX/pageY
downX = e.pageX;
downY = e.pageY;
return false;
}
function onMouseMove(e) {
if (!dragZone) return; // элемент не зажат
if (!avatar) { // элемент нажат, но пока не начали его двигать
if (Math.abs(e.pageX - downX) < 3 && Math.abs(e.pageY - downY) < 3) {
return;
}
// попробовать захватить элемент
avatar = dragZone.onDragStart(downX, downY, e);
if (!avatar) { // не получилось, значит перенос продолжать нельзя
cleanUp(); // очистить приватные переменные, связанные с переносом
return;
}
}
// отобразить перенос объекта, перевычислить текущий элемент под курсором
avatar.onDragMove(e);
// найти новый dropTarget под курсором: newDropTarget
// текущий dropTarget остался от прошлого mousemove
// *оба значения: и newDropTarget и dropTarget могут быть null
var newDropTarget = findDropTarget(e);
if (newDropTarget != dropTarget) {
// уведомить старую и новую зоны-цели о том, что с них ушли/на них зашли
dropTarget && dropTarget.onDragLeave(newDropTarget, avatar, e);
newDropTarget && newDropTarget.onDragEnter(dropTarget, avatar, e);
}
dropTarget = newDropTarget;
dropTarget && dropTarget.onDragMove(avatar, e);
return false;
}
function onMouseUp(e) {
if (e.which != 1) { // не левой кнопкой
return false;
}
if (avatar) { // если уже начали передвигать
if (dropTarget) {
// завершить перенос и избавиться от аватара, если это нужно
// эта функция обязана вызвать avatar.onDragEnd/onDragCancel
dropTarget.onDragEnd(avatar, e);
} else {
avatar.onDragCancel();
}
}
cleanUp();
}
function cleanUp() {
// очистить все промежуточные объекты
dragZone = avatar = dropTarget = null;
}
function findDragZone(event) {
var elem = event.target;
while (elem != document && !elem.dragZone) {
elem = elem.parentNode;
}
return elem.dragZone;
}
function findDropTarget(event) {
// получить элемент под аватаром
var elem = avatar.getTargetElem();
while (elem != document && !elem.dropTarget) {
elem = elem.parentNode;
}
if (!elem.dropTarget) {
return null;
}
return elem.dropTarget;
}
document.ondragstart = function() {
return false;
}
document.onmousemove = onMouseMove;
document.onmouseup = onMouseUp;
document.onmousedown = onMouseDown;
};

View file

@ -0,0 +1,39 @@
/**
* Зона, из которой можно переносить объекты
* Умеет обрабатывать начало переноса на себе и создавать "аватар"
* @param elem DOM-элемент, к которому привязана зона
*/
function DragZone(elem) {
elem.dragZone = this;
this._elem = elem;
}
/**
* Создать аватар, соответствующий зоне.
* У разных зон могут быть разные типы аватаров
*/
DragZone.prototype._makeAvatar = function() {
/* override */
};
/**
* Обработать начало переноса.
*
* Получает координаты изначального нажатия мышки, событие.
*
* @param downX Координата изначального нажатия по X
* @param downY Координата изначального нажатия по Y
* @param event текущее событие мыши
*
* @return аватар или false, если захватить с данной точки ничего нельзя
*/
DragZone.prototype.onDragStart = function(downX, downY, event) {
var avatar = this._makeAvatar();
if (!avatar.initFromEvent(downX, downY, event)) {
return false;
}
return avatar;
};

View file

@ -0,0 +1,85 @@
/**
* Зона, в которую объекты можно класть
* Занимается индикацией передвижения по себе, добавлением в себя
*/
function DropTarget(elem) {
elem.dropTarget = this;
this._elem = elem;
/**
* Подэлемент, над которым в настоящий момент находится аватар
*/
this._targetElem = null;
}
/**
* Возвращает DOM-подэлемент, над которым сейчас пролетает аватар
*
* @return DOM-элемент, на который можно положить или undefined
*/
DropTarget.prototype._getTargetElem = function(avatar, event) {
return this._elem;
};
/**
* Спрятать индикацию переноса
* Вызывается, когда аватар уходит с текущего this._targetElem
*/
DropTarget.prototype._hideHoverIndication = function(avatar) {
/* override */
};
/**
* Показать индикацию переноса
* Вызывается, когда аватар пришел на новый this._targetElem
*/
DropTarget.prototype._showHoverIndication = function(avatar) {
/* override */
};
/**
* Метод вызывается при каждом движении аватара
*/
DropTarget.prototype.onDragMove = function(avatar, event) {
var newTargetElem = this._getTargetElem(avatar, event);
if (this._targetElem != newTargetElem) {
this._hideHoverIndication(avatar);
this._targetElem = newTargetElem;
this._showHoverIndication(avatar);
}
};
/**
* Завершение переноса.
* Алгоритм обработки (переопределить функцию и написать в потомке):
* 1. Получить данные переноса из avatar.getDragInfo()
* 2. Определить, возможен ли перенос на _targetElem (если он есть)
* 3. Вызвать avatar.onDragEnd() или avatar.onDragCancel()
* Если нужно подтвердить перенос запросом на сервер, то avatar.onDragEnd(),
* а затем асинхронно, если сервер вернул ошибку, avatar.onDragCancel()
* При этом аватар должен уметь "откатываться" после onDragEnd.
*
* При любом завершении этого метода нужно (делается ниже):
* снять текущую индикацию переноса
* обнулить this._targetElem
*/
DropTarget.prototype.onDragEnd = function(avatar, event) {
this._hideHoverIndication(avatar);
this._targetElem = null;
};
/**
* Вход аватара в DropTarget
*/
DropTarget.prototype.onDragEnter = function(fromDropTarget, avatar, event) {};
/**
* Выход аватара из DropTarget
*/
DropTarget.prototype.onDragLeave = function(toDropTarget, avatar, event) {
this._hideHoverIndication();
this._targetElem = null;
};

View file

@ -0,0 +1,43 @@
function TreeDragAvatar(dragZone, dragElem) {
DragAvatar.apply(this, arguments);
}
extend(TreeDragAvatar, DragAvatar);
TreeDragAvatar.prototype.initFromEvent = function(downX, downY, event) {
if (event.target.tagName != 'SPAN') return false;
this._dragZoneElem = event.target;
var elem = this._elem = this._dragZoneElem.cloneNode(true);
elem.className = 'avatar';
// создать вспомогательные свойства shiftX/shiftY
var coords = getCoords(this._dragZoneElem);
this._shiftX = downX - coords.left;
this._shiftY = downY - coords.top;
// инициировать начало переноса
document.body.appendChild(elem);
elem.style.zIndex = 9999;
elem.style.position = 'absolute';
return true;
};
/**
* Вспомогательный метод
*/
TreeDragAvatar.prototype._destroy = function() {
this._elem.parentNode.removeChild(this._elem);
};
/**
* При любом исходе переноса элемент-клон больше не нужен
*/
TreeDragAvatar.prototype.onDragCancel = function() {
this._destroy();
};
TreeDragAvatar.prototype.onDragEnd = function() {
this._destroy();
};

View file

@ -0,0 +1,9 @@
function TreeDragZone(elem) {
DragZone.apply(this, arguments);
}
extend(TreeDragZone, DragZone);
TreeDragZone.prototype._makeAvatar = function() {
return new TreeDragAvatar(this, this._elem);
};

View file

@ -0,0 +1,72 @@
function TreeDropTarget(elem) {
TreeDropTarget.parent.constructor.apply(this, arguments);
}
extend(TreeDropTarget, DropTarget);
TreeDropTarget.prototype._showHoverIndication = function() {
this._targetElem && this._targetElem.classList.add('hover');
};
TreeDropTarget.prototype._hideHoverIndication = function() {
this._targetElem && this._targetElem.classList.remove('hover');
};
TreeDropTarget.prototype._getTargetElem = function(avatar, event) {
var target = avatar.getTargetElem();
if (target.tagName != 'SPAN') {
return;
}
// проверить, может быть перенос узла внутрь самого себя или в себя?
var elemToMove = avatar.getDragInfo(event).dragZoneElem.parentNode;
var elem = target;
while (elem) {
if (elem == elemToMove) return; // попытка перенести родителя в потомка
elem = elem.parentNode;
}
return target;
};
TreeDropTarget.prototype.onDragEnd = function(avatar, event) {
if (!this._targetElem) {
// перенос закончился вне подходящей точки приземления
avatar.onDragCancel();
return;
}
this._hideHoverIndication();
// получить информацию об объекте переноса
var avatarInfo = avatar.getDragInfo(event);
avatar.onDragEnd(); // аватар больше не нужен, перенос успешен
// вставить элемент в детей в отсортированном порядке
var elemToMove = avatarInfo.dragZoneElem.parentNode; // <LI>
var title = avatarInfo.dragZoneElem.innerHTML; // переносимый заголовок
// получить контейнер для узлов дерева, соответствующий точке преземления
var ul = this._targetElem.parentNode.getElementsByTagName('UL')[0];
if (!ul) { // нет детей, создадим контейнер
ul = document.createElement('UL');
this._targetElem.parentNode.appendChild(ul);
}
// вставить новый узел в нужное место среди потомков, в алфавитном порядке
var li = null;
for (var i = 0; i < ul.children.length; i++) {
li = ul.children[i];
var childTitle = li.children[0].innerHTML;
if (childTitle > title) {
break;
}
}
ul.insertBefore(elemToMove, li);
this._targetElem = null;
};

View file

@ -0,0 +1,22 @@
#tree span:hover {
cursor: pointer;
font-weight: bold;
}
/* #tree span.hover, для приоритета перед :hover также добавлен подселектор */
#tree span.hover,
#tree span.hover:hover {
color: red;
font-weight: bold;
cursor: pointer;
}
span.avatar {
padding: 1px;
border: 1px dotted black;
background: white;
cursor: pointer;
opacity: 0.6;
filter: progid: DXImageTransform.Microsoft.Alpha(opacity=60);
/* IE opacity */
}

View file

@ -0,0 +1,73 @@
<!DOCTYPE HTML>
<html>
<head>
<meta charset="UTF-8">
<script src="lib.js"></script>
<script src="DragManager.js"></script>
<script src="DragAvatar.js"></script>
<script src="DragZone.js"></script>
<script src="DropTarget.js"></script>
<script src="TreeDragAvatar.js"></script>
<script src="TreeDragZone.js"></script>
<script src="TreeDropTarget.js"></script>
<link rel="stylesheet" type="text/css" href="dragTree.css">
</head>
<body>
Возьмите за любой заголовок и поменяйте ему родителя.
<br> В собственных детей перенести нельзя.
<br> Потомки всегда отсортированы по алфавиту.
<ul id="tree">
<li><span>Древо жизни (сверхмалая часть)</span>
<ul>
<li><span>Грибы</span>
<ul>
<li><span>Древесные</span>
<ul>
<li><span>Чага</span></li>
</ul>
</li>
<li><span>Наземные</span>
<ul>
<li><span>Опята</span></li>
<li><span>Подосиновики</span></li>
</ul>
</li>
</ul>
</li>
<li><span>Животные</span>
<ul>
<li><span>Земноводные</span>
<ul>
<li><span>Лягушки</span></li>
<li><span>Саламандры</span></li>
<li><span>Тритоны</span></li>
</ul>
</li>
<li><span>Млекопитающие</span>
<ul>
<li><span>Коровы</span></li>
<li><span>Ослы</span></li>
<li><span>Собаки</span></li>
<li><span>Тигры</span></li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
</ul>
<script>
var tree = document.getElementById('tree');
new TreeDragZone(tree);
new TreeDropTarget(tree);
</script>
</body>
</html>

View file

@ -0,0 +1,43 @@
function getCoords(elem) {
var box = elem.getBoundingClientRect();
var body = document.body;
var docElem = document.documentElement;
var scrollTop = window.pageYOffset || docElem.scrollTop || body.scrollTop;
var scrollLeft = window.pageXOffset || docElem.scrollLeft || body.scrollLeft;
var clientTop = docElem.clientTop || body.clientTop || 0;
var clientLeft = docElem.clientLeft || body.clientLeft || 0;
var top = box.top + scrollTop - clientTop;
var left = box.left + scrollLeft - clientLeft;
return {
top: Math.round(top),
left: Math.round(left)
};
}
function getElementUnderClientXY(elem, clientX, clientY) {
var display = elem.style.display || '';
elem.style.display = 'none';
var target = document.elementFromPoint(clientX, clientY);
elem.style.display = display;
if (!target || target == document) { // это бывает при выносе за границы окна
target = document.body; // поправить значение, чтобы был именно элемент
}
return target;
}
function extend(Child, Parent) {
function F() {}
F.prototype = Parent.prototype
Child.prototype = new F()
Child.prototype.constructor = Child
Child.parent = Parent.prototype
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

4
10-extra/index.md Normal file
View file

@ -0,0 +1,4 @@
# О всякой всячине
Статьи на разные темы, которые не вошли в другие разделы.