Category: образование

Category was added automatically. Read all entries about "образование".

JavaScript: TextDecoder и TextEncoder, UTF-8

Прочел подраздел 2.2 «TextDecoder и TextEncoder» третьей части учебника по JavaScript.

В большинстве случаев авторы данного учебника расписывают свои уроки довольно подробно и понятно. Однако, иногда они становятся чересчур лаконичны. И тут такой случай. Два фрагмента кода из учебника:

let uint8Array = new Uint8Array([72, 101, 108, 108, 111]);

alert( new TextDecoder().decode(uint8Array) ); // Hello

let uint8Array = new Uint8Array([228, 189, 160, 229, 165, 189]);

alert( new TextDecoder().decode(uint8Array) ); // 你好

Во-первых, следовало бы отметить, что «你好» — это знаменитое «ни хао», то есть «привет» по-китайски или «hello» по-английски. Без этой ремарки лично мне было не очень понятно, зачем в этом примере эти два китайских иероглифа. Ну и следовало бы отметить, что этих иероглифов тут именно два, это важно. С первого взгляда для нашего, русского, глаза непонятно, сколько в этой надписи символов.

Во-вторых, следовало бы немного пояснить за кодировку. Я, конечно, уже немного ориентируюсь в Юникоде и знаю про существование одного из его форматов UTF-8 (для встроенных объектов TextDecoder и TextEncoder этот формат является форматом по умолчанию). Однако, подробно я разбирался с Юникодом только на примере формата UTF-16LE (тут), для него в операционной системе «Windows» в большинстве случаев всё достаточно просто: все символы представляются двумя байтами.

Для формата UTF-8, однако же, всё не так просто. Легко читать только первые 128 кодов Юникода, которые совпадают с ASCII и содержат все символы латинского алфавита. В UTF-8 они кодируются одним байтом. Поэтому в вышеприведенном примере фраза «Hello» кодируется и декодируется так легко: [72, 101, 108, 108, 111] это и есть «H», «e», «l», «l», «o» (каждый из этих кодов меньше 128).

Но для остальных кодов Юникода в формате UTF-8 используется переменное число байтов: от двух до четырех. Для вышеприведенных китайских иероглифов в формате UTF-8 используется по три байта:

228, 189, 160 — это «你»
229, 165, 189 — это «好»

Перед прочтением этой статьи учебника я этого не знал. Пришлось порыться в интернете:

https://ru.wikipedia.org/wiki/UTF-8

Как при декодировании строки в формате UTF-8 понять, сколько байтов представляет очередной символ? Для этого считывается первый байт из буфера и анализируются его старшие биты (нам это делать не нужно, это делает встроенный в JavaScript объект, но важно понимание процесса). В нашем примере берем первый байт из буфера:

22810 = 111001002

Если старший бит (самый левый) равен нулю (не наш случай), то этот байт является однобайтным представлением символа. Если старший бит равен единице, переходим к соседнему биту справа. Если этот бит равен нулю (не наш случай), значит данный байт является одним из многобайтового представления символа, но не первым. И так далее. Вот шаблоны для анализа (иксами обозначены биты, в которых содержится, собственно, код из таблицы Юникода):

0xxxxxxx — байт однобайтового представления символа;
10xxxxxx — байт многобайтового представления символа (не первый);
110xxxxx — первый байт двухбайтового представления символа;
1110xxxx — первый байт трехбайтового представления символа;
11110xxx — первый байт четырехбайтового представления символа.

В нашем случае подходит шаблон 1110xxxx, следовательно, это первый байт трехбайтового представления символа. Далее движок считывает три байта: 228, 189, 160 и определяет по таблице Юникода, что это символ «你». И так далее.

Вот моя любимая таблица Юникода в интернете:
https://unicode-table.com/ru/

Вот для примера страница символа-иероглифа «你» на этом сайте (там приведены десятичные, шестнадцатиричные и двоичные коды этого символа в разных форматах Юникода):
https://unicode-table.com/ru/4F60/

Этот символ содержится в разделе таблицы Юникода, который называется «Унифицированные идеограммы ККЯ — расширение F» (по-английски «CJK Unified Ideographs Extension F»). «Идеограммами» названы иероглифы. Аббревиатура «ККЯ» расшифровывается как «Китайский, Корейский, Японский» (на самом деле, там еще есть и вьетнамские иероглифы, так что должно быть «ККЯВ»). Тут подробнее:
https://ru.wikipedia.org/wiki/Унифицированные_идеограммы_ККЯ_—_расширение_F

Учебник по JavaScript, ч.3: Фреймы и окна, кликджекинг

Прочел первый раздел «Фреймы и окна» третьей части («Тематические разделы») учебника по JavaScript.

https://learn.javascript.ru

Часть 3. Тематические разделы (в т.ч. 66 подразделов)

Разделы:

1. Фреймы и окна (3 подраздела)

1.1 Открытие окон и методы window
1.2 Общение между окнами
1.3 Атака типа clickjacking

Отделение первой части учебника, в принципе, понятно. Там изучался, собственно, сам язык JavaScript. Смысл разделения оставшихся статей учебника на вторую и третью части менее очевиден. Нельзя сказать, что статьи третьей части учебника менее важны, чем статьи второй части. Так что граница между ними, скорее, косметическая.

Задач к этому разделу нет, зато есть множество примеров кода.

Рассказано, как из JavaScript работать с окнами в браузере. Под «окнами» здесь подразумеваются как «всплывающие окна» (по-английски «pop-up») и вкладки браузера, так и содержимое фреймов. Для реализации последних раньше использовали HTML-элементы frame и frameset, но на сегодня их использование не рекомендуется, вместо них предлагается использовать HTML-элемент iframe. Рассказано о механизме блокировки всплывающих окон браузера и о том, как он работает.

В подразделе 1.2 рассказано, как наладить общение между окнами с помощью языка JavaScript и о том, как при этом обеспечивается безопасность с помощью политики одинакового источника (по-английски «same-origin policy»).

В подразделе 1.3 рассказано про способ обмана пользователей, который называется кликджекингом (по-английски «clickjacking»).

https://ru.wikipedia.org/wiki/Кликджекинг
https://en.wikipedia.org/wiki/Clickjacking

Почему об этом способе обмана вообще рассказывается в учебнике по языку JavaScript и почему о нем рассказывается именно в этом месте учебника? Потому что этот способ легко реализовать с помощью скрипта на языке JavaScript вкупе с кодом на языках HTML и CSS на HTML-странице. При этом может использоваться HTML-элемент iframe. В подразделе 1.3 рассказано о методах противодействия этому способу обмана. Чтобы разобраться в том, как работает этот способ обмана и в методах противодействия, нужно понимание работы с окнами в языке JavaScript.

Как работает этот способ обмана? Злоумышленник заманивает пользователя на свою HTML-страницу. На этой странице есть некий элемент интерфейса, на который предлагается нажать, чтобы что-то получить (например, кнопка для скачивания какого-либо файла и тому подобное). Пользователь нажимает мышкой («кликает») на этот элемент интерфейса. Но он не знает, что над этим элементом интерфейса растянут невидимый HTML-элемент iframe, в котором загружена другая HTML-страница.

Получается, под щелчок мышкой пользователя («клик») незаметно подкладывается другая HTML-страница («джекинг»). Эта другая HTML-страница может быть, к примеру, HTML-страницей в социальной сети, в которой данный пользователь зарегистрирован (или HTML-страницей банка, в котором данный пользователь имеет счет). Злоумышленник может незаметно подставить под щелчок мышкой пользователя кнопку лайка какого-то поста в социальной сети (это еще не так страшно) или кнопку отправки платежа в каком-нибудь интернет-магазине (это уже серьезнее).

Почему этот метод обмана срабатывает, если злоумышленник не имеет на руках ни логина, ни пароля пользователя? Дело в том, что многие пользователи, покидая какой-либо интернет-сервис, не выполняют выхода из него (например, просто закрывают HTML-страницу интернет-сервиса, при этом остаются залогинеными). При таком раскладе злоумышленнику достаточно незаметно подставить пользователю кнопку того интернет-сервиса, из которого пользователь не совершил выхода (разлогина).

Как злоумышленник угадывает, что данный пользователь вообще имеет аккаунт на определенном интернет-сервисе и что он не разлогинился на этом интернет-сервисе? Злоумышленник не угадывает, а просто «работает по площадям». Например, злоумышленник знает, что огромное количество пользователей имеет аккаунт, к примеру, на фейсбуке и не разлогинивается оттуда. Он заманивает всех, кого удастся, на свою HTML-страницу и подставляет с помощью кликджекинга, к примеру, кнопку лайка поста, который он хочет продвинуть в этой социальной сети. Те, у кого нет аккаунта в фейсбуке, или те, кто разлогинился при выходе из фейсбука, могут попасться на уловку и совершить нажатие мышкой, но это ничего не принесет злоумышленнику, обман не сработает. А вот для тех, у кого есть аккаунт в фейсбуке и кто не разлогинился на момент попадания в ловушку, обман сработает.

Естественно, большинство крупных интернет-сервисов типа фейсбука, твиттера и так далее, на сегодня уже защищены от кликджекинга. В подразделе 1.3 рассказано, какие есть способы защиты и как они работают.

Учебник по JavaScript, ч.2: наблюдатель, выделение, событийный цикл

Прочел шестой раздел «Разное» второй части («Браузер: документ, события, интерфейсы») учебника по JavaScript.

https://learn.javascript.ru

Часть 2. Браузер: документ, события, интерфейсы (в т.ч. 32 подраздела)

Разделы:

6. Разное (3 подраздела)

6.1 MutationObserver: наблюдатель за изменениями
6.2 Selection и Range
6.3 Событийный цикл: микрозадачи и макрозадачи

Этот раздел завершает вторую часть учебника. Сюда, как я понимаю, попали темы, которые не получилось вставить в другие разделы. Но попадание в этот раздел, однако, не делает затронутые темы второстепенными. Здесь рассмотрены достаточно важные вопросы.

В подразделе 6.1 описывается встроенный объект MutationObserver, с помощью которого можно следить за изменениями в любом из узлов DOM-дерева. С помощью параметров можно настроить то, за какими конкретно изменениями в указанном узле следует следить. После получения сигнала об изменениях мы можем обработать эти изменения (или сделать что-то другое) так, как нам будет нужно.

В подразделе 6.2 подробно (с большими примерами кода) описана работа с выделениями на HTML-странице (а также с выделениями внутри элементов HTML-форм). Вообще на HTML-странице можно выделять фрагменты текста, картинки и разные HTML-элементы. Именно об этих «выделениях» идет речь. По-английски «выделение» звучит как «selection».

В подразделе 6.3 рассказано про то, как браузер работает с разными задачами, которые ему приходится выполнять. Все задачи попадают в очередь, из которой их извлекает браузер для обработки. Задачи бывают разного типа: макрозадачи, микрозадачи, задача «рендеринга» (отрисовка HTML-страницы при отображении в окне браузера). Рассказано, как и в каком порядке браузер выполняет разные типы задач. По-моему, очень интересная тема. Для лучшего понимания, думаю, следует перечитать хотя бы подразделы 11.2 «Промисы» и 11.7 «Микрозадачи» из первой части учебника, а еще лучше — перечитать весь раздел 11 «Промисы, async/await» первой части учебника.

Больших и интересных задач к этому разделу не завезли, есть только маленькая задачка-вопрос к подразделу 6.3. Зато много больших примеров кода, которые можно скопировать себе в редактор и погонять в браузере для лучшего усвоения.

Учебник по JavaScript, ч.2: Загрузка документа и ресурсов

Прочел пятый раздел «Загрузка документа и ресурсов» второй части («Браузер: документ, события, интерфейсы») учебника по JavaScript.

https://learn.javascript.ru

Часть 2. Браузер: документ, события, интерфейсы (в т.ч. 32 подраздела)

Разделы:

5. Загрузка документа и ресурсов (3 подраздела)

5.1 Страница: DOMContentLoaded, load, beforeunload, unload
5.2 Скрипты: async, defer
5.3 Загрузка ресурсов: onload и onerror

Информация из этих подразделов нужна была мне при чтении учебника гораздо раньше.

Например, при изучении модулей в первой части учебника:
JavaScript: Политика одинакового источника и CORS

А также при начале изучения второй части учебника:
JavaScript: порядок выполнения скриптов и модулей на HTML-странице

Думаю, авторам учебника в связи с этим следует как-то немного перестроить порядок подразделов полностью или частично.

К этому разделу учебника есть только одна задача. Я разобрал ее решение в двух постах:
1. JavaScript: предзагрузка изображений
2. JavaScript: предзагрузка изображений, упрощаю код

JavaScript: предзагрузка изображений

Решил задачу «Загрузите изображения с колбэком» к подразделу 5.3 «Загрузка ресурсов: onload и onerror» второй части учебника по JavaScript.

Задача несложная, но сразу в нее сложно въехать, нужно сначала разобраться с тестовой HTML-страницей, которая прилагается к задаче (ее код можно посмотреть в песочнице).

От нас требуется написать функцию preloadImages(sources, callback). Первый параметр sources — это массив строк. Каждая строка в этом массиве является адресом изображения. Второй параметр callback — это функция, которую требуется запустить после того, как все изображения, адреса которых указаны в массиве sources, будут загружены браузером.

Для чего может понадобиться такая функция? Предположим, у нас есть много картинок, которые будут вставляться на HTML-страницу с помощью HTML-элемента img по мере прокрутки (скроллирования) HTML-страницы, например, вниз. Каждый HTML-элемент img будет вставляться на HTML-страницу быстро, а вот загрузка изображения не будет происходить мгновенно. Чтобы избавиться от этого «торможения», можно «предзагрузить» картинки, а при вставке HTML-элемента img пользователь не будет ждать загрузки, картинка будет показана мгновенно. Предзагрузка следующих картинок может происходить в то время, пока пользователь разглядывает первые картинки в галерее на HTML-странице.

Первый вопрос, который у меня возник: где хранятся предзагруженные браузером картинки? Оказалось (об этом сказано и в текущем подразделе учебника, и в тексте постановки задачи), они хранятся в кэше браузера. То есть наша функция должна будет «предзагрузить» картинки в кэш браузера.

Второй вопрос: как в скрипте на языке JavaScript загрузить картинку в кэш? В тексте постановки задачи это показано на примере:
let img = document.createElement("img");
img.src = "my.jpg";

Именно (и исключительно) для HTML-элемента img загрузка картинки в кэш браузера начинается после того, как мы помещаем адрес картинки в свойство src. (Об этом сказано и в текущем подразделе учебника, и в тексте постановки задачи.) Для других HTML-элементов загрузка ресурса начинается только после добавления HTML-элемента на HTML-страницу.

Третий вопрос: как работает тест для нашей будущей функции, подготовленный авторами задачи на тестовой HTML-странице?

1) Сначала создается массив sources адресов картинок (в этом тесте картинок всего три, но может быть любое другое количество):
let sources = [
    "https://en.js.cx/images-load/1.jpg",
    "https://en.js.cx/images-load/2.jpg",
    "https://en.js.cx/images-load/3.jpg"
];
Конкретно эти три картинки действительно существуют по указанным адресам, я проверил.

2) Затем каждый из этих адресов модифицируется так, чтобы при каждом запуске теста адрес формально получался новый (с точки зрения кэша), но с точки зрения загрузки изображения браузером оставался тем же самым. Я уже рассматривал этот прием ранее в отдельном посте. Этот прием нужен для предотвращения кэширования файлов картинок при первой загрузке тестовой HTML-страницы браузером (это помешало бы тестированию нашей будущей функции).
for (let i = 0; i < sources.length; i++) {
    sources[i] += "?" + Math.random();
}

3) И, наконец, в качестве функции callback для нашей будущей функции создается функция testLoaded, которая моделирует создание HTML-элементов img на некой HTML-странице, для которых требуется предзагрузка изображений, обеспечиваемая нашей будущей функцией.
function testLoaded() {
    let widthSum = 0;
    for (let i = 0; i < sources.length; i++) {
        let img = document.createElement("img");
        img.src = sources[i];
        widthSum += img.width;
    }
    alert(widthSum);
}

4) Производится запуск нашей будущей функции с созданными выше параметрами:
preloadImages(sources, testLoaded);

Как работает этот тест? Если наша будущая функция выполнит предзагрузку в кэш указанных трех картинок правильно, то при моделировании использования этих картинок в вышеописанном третьем пункте тестовый скрипт сможет получить правильную ненулевую ширину каждой картинки (если картинка существует по указанному адресу). Ширина каждой картинки составляет 100 пикселей. Таким образом, при правильной работе нашей будущей функции тестовый скрипт выведет на экран сообщение (с помощью функции alert) с числом 300 (сумма ширин всех трех картинок).

Если же какая-либо одна, или какие-либо две, или все три картинки из заданных трех не будут предзагружены, тестовый скрипт выведет на экран соответственно либо число 200, либо число 100, либо число 0.

* * *

Приступим к решению задачи. Сначала напишем заготовку для функции preloadImages и просто запустим из нее функцию callback, заданную вторым параметром. Посмотрим, что произойдет. Пишем код:
function preloadImages(sources, callback) {
    callback();
}

Эту заготовку вставляем в начало скрипта тестовой HTML-страницы. При загрузке этой HTML-страницы тест выдаёт число 0. Так и должно быть. Ведь мы пока не выполнили никакой предзагрузки изображений.

Далее я решил попробовать предзагрузить первое из трех заданных изображений. Меняем код:
function preloadImages(sources, callback) {
    let img = document.createElement("img");
    img.src = sources[0];

    callback();
}

Однако, тест снова выдал число 0, то есть все три изображения не были предзагружены. Почему не получилось предзагрузить первое из трех изображений? Дело в том, что тестирование (запуск функции callback) в этом коде начинается сразу после запуска предзагрузки первого изображения. Таким образом, первое изображение просто не успевает предзагрузиться.

Очевидно, что именно в этом месте следует начать использовать знания, полученные в текущем подразделе обсуждаемого учебника: нам нужно запустить функцию callback после полной предзагрузки изображения. Меняем код:
function preloadImages(sources, callback) {
    let img = document.createElement("img");
    img.src = sources[0];

    img.addEventListener("load", callback);
}

Для этой редакции функции preloadImages тест уже выдаёт число 100. Это значит, что одно из трех заданных изображений было предзагружено удачно. Теперь следует масштабировать код на все заданные изображения, то есть в данном случае нужен цикл. Меняем код:
function preloadImages(sources, callback) {
    let imgs = [],   // массив HTML-элементов img для предзагрузки картинок
        loaded = []; // массив HTML-элементов img с загруженной картинкой
    
    // цикл выполнения предзагрузки заданных картинок
    for (let i = 0; i < sources.length; i++) {

        // запуск предзагрузки очередной картинки
        let img = document.createElement("img");
        img.src = sources[i];

        // после окончания предзагрузки картинки поместить ее в массив загруженных,
        // и проверить, если это была последняя из заданных картинок, то запустить
        // функцию callback
        img.addEventListener("load", function () {
            loaded.push(this);
            if (loaded.length == sources.length) callback();
        });

        imgs.push(img);
    }
}

При такой редакции кода тест выдаёт требуемое число 300. То есть все три заданные изображения были удачно предзагружены.

Однако, по условиям задачи требуется учесть еще случай, когда одну (или несколько) из заданных картинок не удастся предзагрузить из-за какой-либо ошибки при попытке предзагрузки. Например, если какая-либо картинка из заданных будет отсутствовать по указанному адресу.

Я решил дополнить тестовые данные и добавил в массив sources четвертый адрес картинки. Я специально взял адрес, по которому указанная картинка отсутствует:
let sources = [
    "https://en.js.cx/images-load/1.jpg",
    "https://en.js.cx/images-load/2.jpg",
    "https://en.js.cx/images-load/3.jpg",
    "https://en.js.cx/images-load/4.jpg"  // этой картинки не существует
];

После запуска нашего кода с этими тестовыми данными тестовая HTML-страница не выдала вообще никакого числа. Почему это произошло? Наш код попытался предзагрузить четвертое изображение и получил ошибку, которую можно рассмотреть в консоли разработчика в браузере (ошибка 404). После получения этой ошибки браузер прервал работу нашего скрипта, поэтому в итоге тест не выдал вообще никакого числа. Но наша задача — заставить скрипт работать даже при таких ошибках. Чтобы это сделать, используем событие error. Меняем код:
function preloadImages(sources, callback) {
    let imgs = [],   // массив HTML-элементов img для предзагрузки картинок
        loaded = []; // массив HTML-элементов img с загруженной картинкой
    
    // цикл выполнения предзагрузки заданных картинок
    for (let i = 0; i < sources.length; i++) {

        // запуск предзагрузки очередной картинки
        let img = document.createElement("img");
        img.src = sources[i];

        // после окончания предзагрузки картинки поместить ее в массив загруженных,
        // и проверить, если это была последняя из заданных картинок, то запустить
        // функцию callback
        img.addEventListener("load", onLoad);
        img.addEventListener("error", onLoad);
        function onLoad() {
            loaded.push(this);
            if (loaded.length == sources.length) callback();
        }

        imgs.push(img);
    }
}

При такой редакции кода и вышеуказанных тестовых данных (заданы четыре картинки, одна из которых не существует) тест выдаёт число 300. Так и должно быть: три картинки, которые существуют, были предзагружены, а четвертая картинка, которая не существует, естественно, не была предзагружена, так как и не могла быть предзагружена изначально, при всём желании. Зато тест сработал, несмотря на ошибку, случившуюся при попытке предзагрузить одну из картинок. Теперь наш код не «сломался», хоть и произошла ошибка.

Сообщение об ошибке, кстати, как и в предыдущий раз, можно посмотреть в консоли разработчика в браузере. Этих ошибок теперь будет две одинаковых: одна при попытке предзагрузки несуществующего изображения, а вторая — при попытке загрузки несуществующего изображения (загрузка изображений, ведь, моделируется в тесте).

Этот код является решением задачи, но он меня не устраивает по нескольким причинам, которые я опишу в следующем посте.

Как решать задачи: понимание

Есть текст постановки задачи.

Прежде, чем начать работу по решению задачи, ее нужно понять, понять текст постановки задачи. При решении задачи есть два этапа: 1) понимание; 2) создание решения.

В школе и университете меня не научили решать незнакомые виды задач. Как там учат? Обычно сначала учитель показывает несколько определенных видов задач и объясняет, как их решать. После этого на экзаменах даются только те виды задач, которые были объяснены на уроках (лекциях). Закончив учебу, многие люди впадают в ступор, когда им попадаются задачи незнакомого вида. Люди не могут их решить, не знают, с чего начать. Со временем, конечно, это проходит, ведь на конкретном месте работы человек тоже решает задачи нескольких определенных видов. Изучив эти виды задач, можно спокойно работать.

Что такое «понимание»? На первый взгляд, вопрос кажется смешным. Разве может быть непонятно, что такое «понятно»? Мне нравится определение из википедии: «понимание — это операция мышления, связанная с усвоением нового содержания, включением его в систему устоявшихся идей и представлений».

Как добиться понимания, что для этого нужно делать? Сегодня ещё пока не существует никакой идеальной инструкции, четкого и короткого пути к пониманию. Мозг человека изучен плохо, ему не получится дать команду «пойми!», человек — это не робот. Тогда что же делать, чтобы понять? Хоть не существует команды «пойми!», зато есть разнообразные приемы, которые постепенно подталкивают мозг к моменту «озарения», после чего и приходит понимание.

Что это за приемы, помогающие добиться понимания? Я напишу тут только о трех приемах, но, конечно, существуют и другие.

1) Повторение: читаем текст постановки задачи несколько раз. Как говорится, «повторенье — мать ученья». По мере продвижения решения к тексту постановки задачи необходимо возвращаться и возвращаться. Из-за того, что человек не является роботом, он при чтении постоянно непроизвольно отвлекается на разные вещи, упуская целые куски текста. При повторных прочтениях он отвлекается в разных местах, понимание от разных прочтений наслаивается друг на друга, постепенно закрывая «дыры» непонимания.

2) Детализация: если после неоднократного перечитывания текста постановки задачи понимание не приходит, разбиваем текст постановки задачи сначала на большие смысловые части и пытаемся эти части понять по отдельности. Если понимания не приходит, разбиваем каждую часть на более мелкие части и так далее. Часто человек не может понять текст постановки задачи целиком потому, что он не понимает только некоторые части этого целого: отдельные незнакомые, впервые встреченные, понятия и термины. Отдельные непонятные вещи следует выписать и изучить, найти информацию по ним. Когда будет достигнуто понимание отдельных понятий и терминов, можно вернуться к перечитыванию всего текста целиком.

3) Переписывание: если не сработали предыдущие два способа, закрываем текст постановки задачи и пытаемся переписать его своими словами, как бы объясняя задачу кому-то другому. При этом не следует стараться передать текст постановки задачи в точности. Наоборот, сначала пишем о том, что именно нам в задаче кажется главным, делаем свои пояснения, пометки и расшифровки. По ходу стоит записывать в виде вопросов то, что особенно непонятно.

На этом пока закончу. Возможно, когда-нибудь я продолжу эту тему постом о том, как, собственно, создавать решения задач.

Адаптация HTML-страницы для мобильника 2

Начало:
1. Инструмент разработчика Issues в браузере
2. Настройка веб-сервера из набора IIS, web.config, заголовки HTTP

Продолжаю разбирать замечания инструмента разработчика «Issues» браузера (у меня — «Microsoft Edge» на движке «Chromium») к моей тестовой HTML-странице.

Замечание, которое помечено как «ошибка» (по-английски «Error»):

A 'viewport' meta element was not specified.


Мне эта тема уже знакома, я три года назад писал пост по этому поводу, в котором разбирался, как говорится, «на пальцах»:

Адаптация HTML-страницы для мобильника

Там я подходил к этому вопросу, ориентируясь на руководства от компании «Google», касающиеся оптимизации сайта для мобильных устройств, онлайн-проверки оптимизации сайта под мобильник, адаптивного дизайна. Те ссылки и сейчас остаются актуальными.

Инструмент разработчика «Issues» браузера посылает читать несколько в другом направлении:

https://webhint.io/docs/user-guide/hints/hint-meta-viewport/
(Статья «Correct Viewport»)

https://developer.mozilla.org/en-US/docs/Web/HTML/Viewport_meta_tag
(Статья «Using the viewport meta tag to control layout on mobile browsers»)

Посмотреть, как HTML-страница будет выглядеть на мобильном устройстве, можно в любом браузере на движке «Chromium», открыв нужную HTML-страницу и вызвав для нее панель инструментов разработчика (F12). Там в левом верхнем углу есть кнопка (вторая слева), которая называется «Toggle device emulation». С ее помощью можно переключиться на эмуляцию мобильного устройства и обратно, в режим настольного компьютера.

Как я и описывал три года назад, без определения соответствующего HTML-элемента meta с атрибутом name="viewport" HTML-страница, которая хорошо выглядит на дисплее настольного компьютера, на экране мобильника будет показана так, что текст HTML-страницы будет выглядеть очень мелко и его почти невозможно будет разобрать. Начинать решать эту проблему все известные мне источники предлагают с вставки следующего HTML-элемента в заголовочную часть HTML-страницы:
<meta name="viewport" content="width=device-width, initial-scale=1.0">

Этот метод подойдет для многих HTML-страниц. Для более сложных случаев нужно читать вышеупомянутые статьи и разбираться с адаптивным дизайном (википедия).

Вставка этого HTML-элемента избавляет от вышеупомянутого замечания инструмента разработчика «Issues» браузера.

JavaScript: задача про депозитный калькулятор

Решил задачу «Депозитный калькулятор» к подразделу 4.3 «События: change, input, cut, copy, paste» второй части учебника по JavaScript.

Текст постановки задачи гласит, цитата:

Создайте интерфейс, позволяющий ввести сумму банковского вклада и процент, а затем рассчитать, какая это будет сумма через заданный промежуток времени.


На самом деле, такая постановка неверно передает то, что нужно сделать. Интерфейс не нужно создавать, так как он уже создан на заданной тестовой HTML-странице, код которой можно посмотреть в песочнице.

Работа этого интерфейса в тексте постановки задачи не объясняется, нужно смотреть код тестовой HTML-страницы на языке HTML и анализировать работу демонстрационного примера, который можно посмотреть на странице задачи.

Интерфейс депозитного калькулятора на тестовой HTML-странице состоит из двух частей: 1) HTML-форма, в элементы которой пользователь может вводить данные (сумму первоначального депозита, срок вклада и годовую процентную ставку) и 2) HTML-таблица из трех строк и двух столбцов для визуализации введенных пользователем данных.

HTML-форма для ввода данных тоже содержит HTML-таблицу, с помощью которой элементы HTML-формы позиционируются на HTML-странице. Эта HTML-таблица тоже состоит из трех строк и двух столбцов. Элементов HTML-формы для ввода данных три: 1) HTML-элемент input с названием money и типом number для ввода суммы первоначального депозита; 2) HTML-элемент select с названием months и восемью опциями, одну из которых должен выбрать пользователь, для ввода срока вклада (3 месяца, 6 месяцев, 12 месяцев, 18 месяцев и так далее); 3) HTML-элемент input с названием interest и типом number для ввода годовой процентной ставки.

HTML-таблица для визуализации данных в первой строке содержит заголовки столбцов («Было:» и «Станет:»), во второй строке содержит две ячейки th с идентификаторами money-before и money-after (в которые от нас требуется вписать сумму первоначального депозита и итоговую сумму депозита с учетом срока вклада и годовой процентной ставки), в третьей строке содержит две ячейки td с красным и зеленым цветом фона.

Последние две ячейки представляют два столбца в этакой доморощенной диаграмме: красный столбец представляет сумму первоначального депозита, а зеленый столбец — итоговую сумму депозита. Отличие этих двух столбцов по высоте показывает пользователю визуально разницу между первоначальным депозитом и суммой, которую он получит по окончании срока вклада, включая набежавшие проценты. Высота красного столбца задана в 100 пикселей и ее изменение не предполагается. От нас требуется рассчитать и установить высоту зеленого столбца, для этого у этого столбца определен идентификатор height-after.

В тексте постановки задачи для расчета итоговой суммы депозита с учетом набежавших процентов дана формула на языке JavaScript, которую требуется использовать, чтобы программист не запутался:
// initial: начальная сумма денег
// interest: проценты, например, 0.05 означает 5% в год
// years: сколько лет ждать
let result = Math.round(initial * (1 + interest * years));

С одной стороны, эта формула должна быть известна любому школьнику, который изучил понятие процента, и можно было бы удивиться, зачем ее привели. С другой стороны, из этой формулы видно, что в данном случае предполагается, что набежавшие за год проценты не прибавляются к так называемому «телу депозита» (как говорят банковские работники), и процент в конце каждого расчетного периода считается от суммы первоначального депозита.

У меня в браузере этот интерфейс депозитного калькулятора выглядит так (картинка):



Тонкая серая линия слева и сверху — это граница области просмотра браузера. Я ее оставил на картинке, чтобы можно было понять, где относительно границы области просмотра расположено содержимое тестовой HTML-страницы.

* * *

Приступим к решению задачи. При загрузке тестовой HTML-страницы в HTML-форме депозитного калькулятора уже есть значения по умолчанию (они указаны в коде тестовой HTML-страницы). Диаграмма в начале эти значения пока не отображает (там не отображены сумма начального депозита и итоговая сумма депозита с набежавшими процентами, а также не отображен зеленый столбец, потому что его высота по умолчанию равна нулю).

Очевидно, что нам нужно создать функцию, которая будет переносить данные депозитного калькулятора в диаграмму. Почему это должна быть отдельная функция? Потому что ее код нужно будет сразу выполнить при загрузке тестовой HTML-страницы (для значений по умолчанию), а затем ее код потребуется выполнять каждый раз при изменении данных в элементах HTML-формы депозитного калькулятора. Понятно, что для такого использования код удобно оформить отдельной функцией. Пишем код:
let form = document.forms.calculator;

function dataVisualization() {
    //...
}

form.money.addEventListener("input", dataVisualization);
form.months.addEventListener("input", dataVisualization);   // можно и при событии change
form.interest.addEventListener("input", dataVisualization);

dataVisualization();

dataVisualization — это наша функция для переноса (визуализации) данных из HTML-формы депозитного калькулятора в диаграмму. Она запускается сразу же при открытии тестовой HTML-страницы в браузере (последняя строка кода) и вешается на событие input, которое «выстрелит» (по-английски «fired») при изменении значения конкретного элемента HTML-формы, то есть функцию вешаем в качестве функции-обработчика на указанное событие каждого из трех элементов HTML-формы депозитного калькулятора.

Если в HTML-элементах input для суммы первоначального взноса (с именем money) и для годовой процентной ставки (с именем interest) обязательно следует использовать событие input, то для HTML-элемента select для срока вклада (с именем months) можно использовать хоть событие input, хоть событие change (подробнее про это можно почитать в обсуждаемом подразделе учебника). Это следует из требования в тексте постановки задачи о том, что любое изменение данных должно обрабатываться (то есть отражаться в диаграмме) немедленно.

Как будет работать наша функция? Она должна получить данные из HTML-формы депозитного калькулятора, выполнить вычисление итоговой суммы депозита с набежавшими процентами по заданной формуле и отобразить результат в диаграмме. Дополним код:

let form = document.forms.calculator;

function dataVisualization() {
    // получим данные
    let initial = +form.money.value;          // преобразуем в число
    if (!initial) return;
    let interest = form.interest.value / 100; // преобразуем в число
    if (!interest) return;
    let years = form.months.value / 12;       // преобразуем в число
    if (!years) return;
    
    // произведем вычисление
    let result = Math.round(initial * (1 + interest * years));
    
    // отобразим результат
    window["money-before"].innerHTML = initial;
    window["money-after"].innerHTML = result;
    window["height-after"].style.height =
        Math.round(100 * (1 + interest * years)) + "px";
}

form.money.addEventListener("input", dataVisualization);
form.months.addEventListener("input", dataVisualization);   // можно и при событии change
form.interest.addEventListener("input", dataVisualization);

dataVisualization();

Это окончательный вариант скрипта, он у меня работает (и это решение очень похоже на решение авторов задачи). Но я хочу еще отметить несколько моментов, о которые спотыкался, пока писал этот код.

Несмотря на то, что HTML-элементы input в данном случае имеют тип number в коде на языке HTML, это вовсе не означает, что свойство form.money.value и свойство form.months.value должны возвратить число (а я так думал). На самом деле, они возвращают строку (подробнее).

В нашем случае это можно проигнорировать, так как при выполнении вычисления по указанным выше формулам эти значения попадают на операции умножения и деления, которые автоматически преобразовывают строковые операнды в числовые. Но на случай, если данный код будет как-то изменяться, я преобразовал form.money.value в число с помощью унарной операции + и пометил переменные, которые должны быть числами, комментарием «преобразуем в число». (В языках программирования с типизацией, кстати, это делается более изящно: просто указываешь тип переменной.)

Написать инструкции if (!initial) return; и две другие подобные я сам не догадался, дописал уже после того, как посмотрел решение от авторов задачи. По идее, логично прописать эти инструкции, так как каждый элемент из трех элементов HTML-формы депозитного калькулятора является обязательным для получения правильного результата (в коде тестовой HTML-страницы, кстати, это отмечено указанием атрибута required для этих HTML-элементов).

С идентификаторами money-before, money-after и height-after получилось забавно. Когда я пишу полностью сам свои скрипты и идентификаторы для скриптов, мне и в голову не приходит использовать в идентификаторах дефисы, потому что у меня уже в подкорке забито, что дефисы в идентификаторах использовать нельзя, но явно (сознательно) я этого не помню. Так что некоторое время тупил в ошибки скрипта. На самом деле, такие идентификаторы можно использовать, но не прямо, а, к примеру, указанным выше способом, через глобальный объект window. (Про то, что дефис не разрешен в идентификаторах, было рассказано в подразделе 2.4 «Переменные» первой части обсуждаемого учебника. Про то, как можно обратиться к HTML-элементу с идентификатором, содержащим дефис, было рассказано в подразделе 1.4 «Поиск: getElement*, querySelector*» второй части обсуждаемого учебника.)

JavaScript: редактирование TD по клику

Решил задачу «Редактирование TD по клику» к подразделу 4.2 «Фокусировка: focus/blur» второй части учебника по JavaScript.

В общем, эта задача является расширением задачи с редактируемым HTML-элементом div (которой я посветил один из предыдущих постов) на ячейки HTML-таблицы. Только теперь вместо одного HTML-элемента div у нас множество ячеек td HTML-таблицы и требуется сделать эти ячейки редактируемыми. Окончание редактирования (по условиям задачи) теперь будет происходить не при потере фокуса, а при нажатии на кнопки «OK» и «Отмена», которые наш скрипт должен будет создать при начале редактирования ячейки HTML-таблицы и удалить после нажатия на одну из этих кнопок.

На странице задачи можно потрогать руками демонстрационный пример. Это поможет понять, как должен работать будущий скрипт. Исходные файлы тестовой HTML-страницы, как обычно, заданы в песочнице. Всего этих файлов четыре: index.html (тестовая HTML-страница) и подключенные к ней bagua.css, my.css и script.js.

На тестовой HTML-странице есть HTML-таблица, состоящая из четырех строк (HTML-элементов tr). Первая строка содержит заголовок таблицы (один HTML-элемент th), а три следующие строки состоят каждая из трех ячеек (HTML-элементов td). То есть всего целевых ячеек td в HTML-таблице девять штук. Стили, нужные для оформления этой HTML-таблицы, описаны в файле bagua.css. («Багуа» — это понятие из китайской философии, на русский язык переводится как «восемь триграмм». Кому интересно, можно посмотреть статью википедии по этой теме. А для решения задачи это не имеет значения.) Вот как тестовая HTML-страница выглядит у меня в браузере (картинка):



Я уменьшил изображение. Тонкая серая линия слева и сверху — это край окна браузера. Я оставил эту линию на картинке, чтобы было понятно, где относительно области просмотра расположено содержимое тестовой HTML-страницы.

А вот так та же самая тестовая HTML-страница выглядит у меня в браузере после того, как я кликнул мышью на центральную ячейку HTML-таблицы и эта ячейка перешла в режим редактирования (картинка):



На этой картинке видно, как скрипт добавил под ячейкой, находящейся в режиме редактирования, кнопки «OK» и «Отмена».

Для решения задачи нам предложено написать свой скрипт в файле script.js. А если нам понадобится описать свои собственные стили на языке CSS, это следует сделать в файле my.css. В принципе, тут показано, как два человека (или две команды разработчиков) могут работать параллельно над одной и той же HTML-страницей, помещая свою работу в отдельные файлы.

* * *

Приступим к решению задачи. Очевидно, что тут удобно воспользоваться делегированием событий, чтобы написать одну функцию-обработчик, которая будет вызываться при клике мышью на любую из ячеек HTML-таблицы. Повесим функцию-обработчик на соответствующее событие HTML-таблицы и отфильтруем только те клики мышью, которые произойдут внутри ячеек td HTML-таблицы:
let table = document.getElementById("bagua-table");

table.addEventListener("click", function (event) {
    let td = event.target.closest("td");
    if (!td) return;

    //...
});

Здесь переменная table содержит ссылку на объект целевой HTML-таблицы (HTML-элемента table с идентификатором bagua-table) на тестовой HTML-странице. Внутри ячейки HTML-таблицы пользователь может кликнуть мышью на вложенный в ячейку HTML-элемент, поэтому используется метод closest, чтобы пройти по DOM-дереву HTML-страницы вверх и получить нужный HTML-элемент td. Если пользователь кликнет мышью внутри HTML-таблицы не на содержимое ячейки td (например, это может быть заголовок HTML-таблицы, ячейка th), то обработка прерывается с помощью инструкции return.

Перепишем в наш код создание HTML-элемента textarea и временную замену им ячейки td целевой HTML-таблицы из решения задачи про редактируемый div, которое я описывал в одном из предыдущих постов. Так мы реализуем переход ячейки td HTML-таблицы в режим редактирования. Дополним код:
let table = document.getElementById("bagua-table");

table.addEventListener("click", function (event) {
    let td = event.target.closest("td");
    if (!td) return;

    // создать HTML-элемент textarea
    let textarea = document.createElement("textarea");
    textarea.value = td.innerHTML;                  // «содержимое»
    textarea.classList.add("edit");                 // CSS-класс
    textarea.style.width = td.offsetWidth + "px";   // размеры
    textarea.style.height = td.offsetHeight + "px";
    // скрыть из разметки HTML-элемент td
    td.style.display = "none";
    // вставить HTML-элемент textarea после HTML-элемента td
    td.after(textarea);
    // устанавим фокус на HTML-элемент textarea, чтобы сразу набирать текст
    textarea.focus();

    //...
});

Результат работы этого кода уже можно наблюдать на тестовой HTML-странице: при клике мышью на ячейку HTML-таблицы она входит в «режим редактирования».

Какие тут отличия от решения задачи про редактируемый div? В вышеприведенном коде я устанавливаю размеры HTML-элемента textarea так, чтобы они совпали с размерами подменяемой ячейки td HTML-таблицы. Чтобы получить размеры подменяемой ячейки td, их нужно получать в тот момент, когда ячейка td видима в браузере. Поэтому пришлось инструкцию td.style.display = "none"; передвинуть после определения размеров.

CSS-класс .edit для HTML-элемента textarea пока не описан. Нужен ли он?

На данный момент при клике мышкой на ячейку td HTML-страницы ее заменяет HTML-элемент textarea с вроде бы теми же размерами. Однако, полного совпадения не получается, HTML-элемент textarea слегка (на несколько пикселей) отодвигает соседние ячейки и по ширине, и по высоте (я специально тестировал работу кода на центральной ячейке HTML-таблицы).

Одна из причин такого поведения в том, что в вышеприведенном коде размеры создаваемого HTML-элемента textarea определяются через CSS-стиль, а по умолчанию подразумевается, что это размеры (ширина и высота) содержимого HTML-элемента. Но у HTML-элемента, ведь, еще есть внутренние отступы padding, ширина границы border и внешние отступы margin сверх размеров содержимого! Справиться с этой разницей я решил в описании CSS-класса .edit в файле my.css:
.edit {
  box-sizing: border-box; /* включим border и padding в размеры элемента */
  /* margin: 0; */        /* у textarea по умолчанию margin равен нулю */
}

Указание box-sizing: border-box; включает ширину границы и размеры внутренних отступов в размеры HTML-элемента. Размеры внешних отступов для HTML-элемента textarea в моём браузере по умолчанию равны нулю, но на всякий случай можно вписать в этом стиле указание margin: 0; (для других браузеров, в которых, возможно, по умолчанию для HTML-элемента textarea внешний отступ не равен нулю).

Теперь в моём браузере HTML-элемент textarea уже не раздвигает соседние ячейки HTML-таблицы по ширине при клике мышью на ячейку HTML-таблицы, но всё еще сдвигает снизу (то есть размер HTML-элемента textarea всё еще превышает нужный размер по высоте). Тут дело в том, что HTML-элемент textarea по умолчанию имеет тип отображения inline-block, а для строчных (inline) HTML-элементов браузер выделяет дополнительное место снизу HTML-элемента для элементов букв, «свисающих» ниже базовой линии строки (я это разбирал очень подробно в посте с разбором задачи про карусель). Чтобы убрать это дополнительное место, дополним CSS-класс .edit:
.edit {
  box-sizing: border-box; /* включим border и padding в размеры элемента */
  /* margin: 0; */        /* у textarea по умолчанию margin равен нулю */
  display: block;         /* уберем промежуток снизу для «свисающих» букв */
}

После внесения этих изменений в стиль HTML-элемента textarea в моём браузере он уже ничего не раздвигает, а точно по размерам садится на своё место. Однако, пользователь еще может изменить размеры HTML-элемента textarea, потянув мышкой за правый нижний угол этого HTML-элемента. Чтобы запретить эту возможность, внесем еще одно, последнее, дополнение в CSS-класс .edit:
.edit {
  box-sizing: border-box; /* включим border и padding в размеры элемента */
  /* margin: 0; */        /* у textarea по умолчанию margin равен нулю */
  display: block;         /* уберем промежуток снизу для «свисающих» букв */
  resize: none;           /* отлючим пользователю возможность менять размеры */
}

Вернемся к коду на языке JavaScript. Нам осталось создать две кнопки и поместить их под редактируемой ячейкой HTML-таблицы, а также написать код функций-обработчиков для каждой из кнопок. Хоть это и самая большая часть кода, но она и самая простая, потому что подобных задач в обсуждаемом учебнике было уже немало.

Сначала я написал создание каждой кнопки по отдельности и функции-обработчики для каждой из кнопок тоже по отдельности. Но потом я заметил, что код создания каждой кнопки похож друг на друга, и подумал, что будет логично создать одну функцию, чтобы не было дублирующего кода. То же самое касается и функций-обработчиков для кнопок: эти функции похожи друг на друга, поэтому пишем только одну функцию-обработчик, обслуживающую обе кнопки. Дополним код:
let table = document.getElementById("bagua-table");

table.addEventListener("click", function (event) {
    let td = event.target.closest("td");
    if (!td) return;

    // создать HTML-элемент textarea
    let textarea = document.createElement("textarea");
    textarea.value = td.innerHTML;                  // «содержимое»
    textarea.classList.add("edit");                 // CSS-класс
    textarea.style.width = td.offsetWidth + "px";   // размеры
    textarea.style.height = td.offsetHeight + "px";
    // скрыть из разметки HTML-элемент td
    td.style.display = "none";
    // вставить HTML-элемент textarea после HTML-элемента td
    td.after(textarea);
    // устанавим фокус на HTML-элемент textarea, чтобы сразу набирать текст
    textarea.focus();

    // получим координаты textarea (относительно области просмотра)
    let rect = textarea.getBoundingClientRect();
    
    // создать кнопки и вставить их на HTML-страницу
    let buttonOK = createButton("OK",
        rect.x + pageXOffset,
        rect.y + pageYOffset + textarea.offsetHeight);
    let buttonCL = createButton("Отмена",
        rect.x + pageXOffset + buttonOK.offsetWidth,
        rect.y + pageYOffset + textarea.offsetHeight);
    function createButton(title, x, y) {
        let button = document.createElement("button");
        button.innerHTML = title;
        button.classList.add("button"); // CSS-класс
        button.style.left = x + "px";
        button.style.top = y + "px";
        document.body.append(button);   // вставим на HTML-страницу
        return button;
    }
    
    // при нажатии на кнопки
    buttonOK.addEventListener("click", () => action("OK"));
    buttonCL.addEventListener("click", () => action("Отмена"));
    function action(what) {
        if (what == "OK")              // обновить содержимое td
            td.innerHTML = textarea.value;
        textarea.remove();             // удалить HTML-элемент textarea
        buttonOK.remove();             // удалить кнопки
        buttonCL.remove();
        td.style.display = "";         // убрать стиль, скрывавший HTML-элемент td
    }
});

Кнопки размещаем «поверх» HTML-страницы. Для этого включим для кнопок абсолютное позиционирование с помощью описания CSS-класса .button в файле my.css:
.button {
  position: absolute;
}

Приведенный выше код уже выполняет все действия, которые требовались от нашего скрипта по условиям задачи, кроме одного: данный код позволяет одновременно ввести в режим редактирования сразу несколько ячеек HTML-таблицы. По условиям задачи скрипт не должен позволять такую ситуацию: пока редактируется одна ячейка HTML-таблицы, пользователь не должен иметь возможности начать редактировать другую ячейку HTML-таблицы. Для выполнения этого условия внесем небольшие изменения в код (я не буду приводить код полностью, чтобы не увеличивать размер поста без нужды, ограничимся фрагментами кода; изменения я пометил красным цветом):
let table = document.getElementById("bagua-table");

let td;

table.addEventListener("click", function (event) {
    // если уже есть нажатый td, ничего не делать
    if (td) return;
    
    /* let */ td = event.target.closest("td");
    if (!td) return;

    // создать HTML-элемент textarea
    //...

    //...

    // при нажатии на кнопки
    buttonOK.addEventListener("click", () => action("OK"));
    buttonCL.addEventListener("click", () => action("Отмена"));
    function action(what) {
        if (what == "OK")              // обновить содержимое td
            td.innerHTML = textarea.value;
        textarea.remove();             // удалить HTML-элемент textarea
        buttonOK.remove();             // удалить кнопки
        buttonCL.remove();
        td.style.display = "";         // убрать стиль, скрывавший HTML-элемент td
        td = "";                       // разрешить нажимать на другие td
    }
});

Изменения внесены в четырех местах. Что тут сделано? Переменная td становится глобальной. Если эта переменная определена (то есть какая-то ячейка HTML-таблицы уже редактируется), то клики мышкой по ячейкам HTML-таблицы игнорируются. В противном случае (переменная td не определена, а, значит, никакая ячейка HTML-таблицы не редактируется) начинается редактирование по клику на ячейку HTML-таблицы.

Это окончательная версия скрипта.