JavaScript: модальное диалоговое окно из HTML-формы (каркас)

Решил задачу «Модальное диалоговое окно с формой» к подразделу 4.4 «Отправка формы: событие и метод submit» второй части учебника по JavaScript.

В задаче требуется написать функцию showPrompt(html, callback), которая выводит в область просмотра браузера модальное диалоговое окно с сообщением html (первый параметр функции showPrompt), текстовым полем ввода и двумя кнопками: «Ok» и «Отмена». Содержимое модального диалогового окна строится с помощью HTML-формы и HTML-элементов на этой HTML-форме. При нажатии клавиши «Enter» в текстовом поле ввода или нажатии кнопки «Ok» должна быть выполнена функция callback (второй параметр функции showPrompt) с параметром, представляющим собой значение из текстового поля ввода. При нажатии клавиши «Esc» или кнопки «Отмена» должна быть выполнена функция callback (второй параметр функции showPrompt) с параметром, представляющим собой значение null.

По сути нам предлагается создать с помощью связки HTML, CSS и JavaScript аналог модального диалогового окна, выдаваемого браузером при запуске функции prompt его интерфейса. Тут подробнее:

https://developer.mozilla.org/en-US/docs/Web/API/Window/prompt
https://learn.javascript.ru/alert-prompt-confirm

Сначала нужно уяснить, что такое «модальное диалоговое окно». С понятием «диалоговое окно» лично мне всё давно уже понятно: это элемент пользовательского интерфейса, окно, в котором происходит диалог между пользователем и программой. Тут подробнее:

https://ru.wikipedia.org/wiki/Диалоговое_окно

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

https://ru.wikipedia.org/wiki/Модальное_окно

Но почему это окно называется модальным? Откуда произошло это слово? Очевидно, что в русский язык оно пришло из английского языка, от слова «modal». В английском языке слово «modal» происходит от слова «mode» (по-русски «режим»). Например, в русском языке существуют понятия «режимный объект» или «режимное предприятие». О чем здесь речь? «Режимным» называют такой объект (или такое предприятие), на котором действуют некие особые правила («режим»), ограничивающие деятельность работников и посетителей по сравнению с обычными, более либеральными, правилами, действующими за границами территории этого объекта (предприятия). Точно также «режимное» («модальное») диалоговое окно ограничивает пользователя в его действиях так, что пользователь не может работать с главным окном, пока не закончит работу с модальным окном. Думаю, по-русски правильнее было бы говорить не «модальное окно», а «режимное окно», но словосочетание «модальное окно» уже является устоявшимся, поэтому буду пользоваться им.

В общем, об этом же коротко сказано и в тексте постановки задачи, цитата:

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


Кроме текста постановки задачи у нас имеется тестовая HTML-страница index.html и файл стилей к ней style.css, код которых можно посмотреть в песочнице. Также есть демонстрационный пример, который можно рассмотреть на отдельной странице и на странице задачи во фрейме.

* * *

В конце текста постановки задачи есть следующее примечание, цитирую:

P.S. HTML/CSS исходного кода к этой задаче содержит форму с фиксированным позиционированием, но вы должны сделать её модальной.


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

Тело тестовой HTML-страницы:
<body>

  <div id="prompt-form-container">
    <form id="prompt-form">
      <div id="prompt-message">Введите что-нибудь...
        <br>Пожалуйста...</div>
      <input name="text" type="text">
      <input type="submit" value="Ok">
      <input type="button" name="cancel" value="Отмена">
    </form>
  </div>

</body>

Диалоговое окно здесь представлено HTML-элементом div с идентификатором prompt-form-container (будем называть его «контейнером»). Внутри этого контейнера содержится HTML-форма с идентификатором prompt-form. Эта HTML-форма содержит требуемые в задаче элементы: 1) сообщение, представленное HTML-элементом div с идентификатором prompt-message; 2) текстовое поле ввода с именем text; 3) две кнопки «Ok» и «Отмена» (кнопка «Ok» имеет тип submit, значит, можно будет использовать событие submit, а кнопка «Отмена» имеет имя cancel).

Внесем дополнения в тело тестовой HTML-страницы (я пометил изменения красным цветом):
<body>

  <!-- содержимое главного окна -->
  <h2>Кликните на кнопку ниже</h2>
  <button id="button">Кликните, чтобы увидеть форму</button>

  <!-- диалоговое окно, дочернее для главного -->
  <div id="prompt-form-container" hidden>
    <form id="prompt-form">
      <div id="prompt-message">Введите что-нибудь...
        <br>Пожалуйста...</div>
      <input name="text" type="text">
      <input type="submit" value="Ok">
      <input type="button" name="cancel" value="Отмена">
    </form>
  </div>

  <script src="myscript.js"></script>

</body>

Заголовок «Кликните на кнопку ниже» и кнопку «Кликните, чтобы увидеть форму» я добавил, посмотрев работу демонстрационного примера от авторов задачи (в код не подглядывал, определил на глаз; там несложно определить, что используется HTML-элемент h2 и какой-то код HTML для создания кнопки [в HTML кнопку можно создать несколькими путями]... я решил взять для этого HTML-элемент button и дать ему идентификатор button).

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

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

В файле myscript.js будет содержаться скрипт на языке JavaScript, который я напишу далее. Этот скрипт заставит работать наше модальное диалоговое окно так, как требуется по условиям задачи.

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

* * *

Превращение вышеописанного каркаса диалогового окна на языке HTML в модальное диалоговое окно выполняется с помощью стилей на языке CSS, описанных в отдельном файле (ссылка на него была приведена выше).

Продолжение тут.

CSS: схлопывание внешних отступов 2

Чуть больше месяца назад я уже писал пост про так называемое «схлопывание внешних отступов» (по-английски «margin collapsing»), которое касается только вертикальных внешних отступов:

CSS: схлопывание внешних отступов

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

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

У меня в задаче попался такой случай. Код на языке HTML в теле HTML-страницы:
<body>
  <h2>Заголовок</h2>
  <button>Кнопка</button>
</body>

Стили к этой HTML-странице на языке CSS:
html,
body {
  width: 100%;
  height: 100%;
  padding: 0;
  margin: 0;
}

В данном случае «схлопывание внешних отступов» (по вертикали) произойдет у HTML-элементов body (родительский HTML-элемент) и h2 (дочерний HTML-элемент).

Как видно из описания стилей, у HTML-элемента body внешний отступ равен нулю. У HTML-элемента h2 есть внешний отступ, определяемый браузером по умолчанию, в моём браузере он равен 0.83em (около 20px для шрифта по умолчанию, установленного в моём браузере).

Получается, что в данном случае весь внешний отступ HTML-элемента h2 выходит за пределы родительского HTML-элемента body из-за «схлопывания внешних отступов».

Из-за этого начало HTML-элемента html и HTML-элемента body расходятся друг с другом на 20px (HTML-элемент html включает внешний отступ HTML-элемента h2, а HTML-элемент body не включает в себя этот внешний отступ).

А, так как HTML-элемент html и HTML-элемент body имеют одинаковые размеры (это установлено вышеописанным стилем), равные области просмотра браузера, то и их нижние концы тоже расходятся друг с другом. В результате HTML-элемент body вылезает снизу за пределы HTML-элемента html на 20px. Из-за чего появляется вертикальная полоса прокрутки, которая в данном случае на самом деле не нужна, так как содержимым HTML-страницы являются всего лишь заголовок и кнопка (они занимают мало места на обычном экране настольного компьютера, если окно браузера развернуто на весь экран).

Чтобы убрать эту ненужную в данном случае вертикальную полосу прокрутки, лучше всего избавиться от «схлопывания внешних отступов». Это можно сделать множеством способов. Я выбрал тот же способ, который уже упоминал в первом посте по этой теме. Внесем исправления в вышеописанный стиль (я выделил дополнение красным цветом):
html,
body {
  width: 100%;
  height: 100%;
  padding: 0;
  margin: 0;
  overflow: auto;
}

Теперь ненужная вертикальная полоса прокрутки исчезла. Но она появится в нужный момент, если содержимое HTML-страницы действительно выйдет за пределы области просмотра браузера. Указанная инструкция не запрещает появление вертикальной полосы прокрутки полностью.

Адаптация 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» браузера.

Настройка веб-сервера из набора IIS, web.config, заголовки HTTP

Я уже писал несколько постов про то, как начал пользоваться (и пользуюсь до сих пор) локальным веб-сервером (как я понимаю, он может быть не только локальным) из набора серверов «IIS»:

1. JavaScript: локальный веб-сервер из набора IIS
2. JavaScript: локальный веб-сервер и кэш браузера
3. JavaScript: локальный веб-сервер и favicon.ico

Напомню, «IIS» (расшифровывается как «Internet Information Services») — это набор серверов для нескольких служб Интернета от компании «Microsoft». Набор серверов «IIS» включен в состав операционной системы «Windows 10 Pro», которая установлена на моём компьютере. Поэтому, собственно, я и стал пользоваться веб-сервером из этого набора. (Актуальная версия «IIS» — 10.0) Тут подробнее:

https://ru.wikipedia.org/wiki/Internet_Information_Services
https://docs.microsoft.com/en-gb/iis (документация на английском)

В предыдущем посте я писал, как начал пользоваться инструментом разработчика «Issues» в браузере «Microsoft Edge» на движке «Chromium».

И этот самый инструмент «Issues» выдал мне по поводу моей тестовой HTML-страницы следующее предупреждение (по-английски «Warning»):

'content-type' header charset value should be 'utf-8'


Вернее, он выдал мне три таких предупреждения — одно по поводу моей HTML-страницы index.html, а еще два — по поводу двух файлов, подключенных к моей HTML-странице — файла style.css с определениями стилей на языке CSS и файла myscript.js со скриптом на языке JavaScript.

О чем идет речь в предупреждении? Обратим внимание на слово «header» (по-русски «заголовок») в тексте предупреждения. Что это за заголовок? Как оказалось, речь тут идет про так называемые «заголовки HTTP»:

https://ru.wikipedia.org/wiki/Заголовки_HTTP
https://developer.mozilla.org/ru/docs/Web/HTTP/Headers

Казалось бы, для HTML-страницы заголовки HTTP можно настраивать с помощью HTML-элемента meta (об этом сказано на вышеуказанной странице википедии). Например, в данном случае на первый взгляд кажется, что нужно вставить в заголовочную часть HTML-страницы следующее:
<meta http-equiv="content-type" content="text/html; charset=utf-8">

Однако, это в моём браузере не работает (кодировку HTML-страницы указывает, но на заголовок HTTP, отсылаемый веб-сервером из набора «IIS», не влияет). Почему? Во-первых, значение "content-type" атрибута http-equiv HTML-элемента meta считается устаревшим (об этом сказано, например, тут), вместо этого для указания кодировки HTML-страницы рекомендуют использовать следующее:
<meta charset="utf-8">

У меня на тестовой HTML-странице уже указан этот короткий вариант, поэтому дополнительное указание HTML-элемента meta с атрибутом http-equiv="content-type" не требуется (будет считаться дублированием короткого варианта и об этом инструмент разработчика «Issues» браузера выдаст отдельное предупреждение).

Во-вторых (что более важно), как я понимаю (а я не специалист в этом вопросе, можно даже сказать, «чайник») веб-сервер из набора «IIS» вообще не учитывает, что там указано в коде HTML-страницы (возможно, веб-сервер можно как-то настроить, чтобы он учитывал HTML-элементы meta в коде отправляемой HTML-страницы, но я про такое не знаю).

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

* * *

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

https://ru.wikipedia.org/wiki/Клиент_—_сервер

Если интересна эта тема, то, чтобы получить начальное представление о работе компьютерных сетей, я рекомендую прочесть первую главу знаменитой книги Эндрю Таненбаума, про которую у меня есть отдельный пост.

В рассматриваемом здесь случае веб-сервер из набора «IIS» и мой браузер (клиент) находятся на одном и том же моём компьютере, поэтому я здесь часто называю веб-сервер локальным веб-сервером. Это удобно для изучения, потому что оба участника сетевого диалога находятся под моим контролем, доступны для управления и экспериментов.

* * *

Итак, как произвести нужную мне настройку веб-сервера из набора «IIS»? Это можно сделать минимум двумя способами: 1) внести исправления напрямую в файл, в котором хранятся настройки веб-сервера; 2) открыть графический интерфейс веб-сервера и внести изменения в настройки веб-сервера через него, в результате чего эти настройки попадут в тот же файл, что при первом способе. Как я понимаю, результат при использовании любого из этих способов будет одинаковый.

Файлы настроек веб-сервера (как я понял) бывают разными: есть файлы настроек, которые действуют на все файлы, выдаваемые веб-сервером, а есть файлы настроек, которые действуют только на часть файлов, выдаваемых веб-сервером (например, только на файлы одного веб-сайта или на файлы, находящиеся в одном каталоге). Здесь я рассмотрю только последний случай, а именно я рассмотрю файл настроек веб-сервера, который называется web.config. Этот файл настроек действует только на файлы каталога, в котором он находится, в том числе на файлы, находящиеся во вложенных в текущий каталогах. Подробнее:

https://docs.microsoft.com/en-us/iis/manage/managing-your-configuration-settings/delegating-configuration-to-webconfig-files

Что из себя представляет этот файл? Это текстовый файл, в котором с помощью разметки на языке XML описаны настройки веб-сервера. Правила написания этой разметки можно узнать в соответствующем справочнике:

https://docs.microsoft.com/en-us/iis/configuration/

Файлы моего локального веб-сайта находятся в следующем каталоге (я про это рассказывал в отдельном посте):
C:\inetpub\wwwroot\

В этом каталоге изначально нет файла web.config, что значит, что на файлы в этом каталоге распространяются общие настройки веб-сервера.

Я поместил в этот каталог файл web.config со следующим содержанием:
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <system.webServer>
        <staticContent>
            <mimeMap fileExtension=".html" mimeType="text/html; charset=utf-8" />
            <mimeMap fileExtension=".css" mimeType="text/css; charset=utf-8" />
            <mimeMap fileExtension=".js" mimeType="application/javascript; charset=utf-8" />
        </staticContent>
    </system.webServer>
</configuration>

Затем открыл в своём браузере адрес, который соответствует указанному выше каталогу:
http://localhost/index.html
и неожиданно получил сообщение об ошибке «HTTP 500.19 — Internal Server Error».

Почему произошла эта ошибка? Дело в том, что в настройках веб-сервера уже есть определения для трех указанных типов файлов (с расширениями .html, .css, .js), а мой файл настроек web.config пытается добавить определения для этих же расширений файлов. Правила описания настроек веб-сервера запрещают определения mimeMap с одинаковыми атрибутами fileExtension.

Я этого не знал, поэтому решил попытаться внести нужные мне настройки через графический интерфейс веб-сервера из набора «IIS». Как открыть этот графический интерфейс? Как обычно, в операционной системе «Windows 10 Pro» это можно сделать кучей способов. Я открываю поиск (либо кликнув по иконке с увеличительным стеклом справа от кнопки «Пуск», либо нажав комбинацию клавиш Win+S) и набираю там «Диспетчер служб IIS», а затем открываю этот самый диспетчер.

В открывшемся окне «Диспетчера служб IIS» слева будет часть окна, озаглавленная «Подключения», и в ней будет отображено дерево подключений. В корне этого дерева у меня находится название моего компьютера. Если корень «развернуть», то там у меня открывается два пункта: «Пулы приложений» и «сайты». Я «развернул» пункт «сайты» и в нем оказался единственный сайт с названием «Default Web Site» (этот сайт, как я понимаю, был создан при включении набора «IIS», которое я описывал в отдельном посте). Это и есть сайт, который соответствует каталогу C:\inetpub\wwwroot\ (по умолчанию).

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

В центральной части окна диспетчера я кликнул мышкой на пункт «Типы MIME». Центральная часть окна диспетчера сменилась на окно выбранной функции. Но возникла ошибка из-за ошибки в написанном мной (см. выше) файле web.config. Я удалил этот файл и снова открыл пункт «Типы MIME». Теперь в центральной части окна диспетчера (теперь уже без ошибки) открылся большой список разнообразных расширений файлов.

Я нашел в этом списке три нужных мне расширения (.html, .css, .js) и добавил к определению каждого из этих расширений в графе «Тип MIME» текст ; charset=utf-8.

После этого я заглянул в каталог моего сайта C:\inetpub\wwwroot\ и увидел, что там появился файл web.config, по-видимому, созданный диспетчером служб «IIS» в ответ на мои вышеописанные действия. Вот содержимое этого файла (я пометил красным цветом отличия от файла, написанного мной ранее):
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <system.webServer>
        <staticContent>
            <remove fileExtension=".js" />
            <remove fileExtension=".css" />
            <remove fileExtension=".html" />
            <mimeMap fileExtension=".html" mimeType="text/html; charset=utf-8" />
            <mimeMap fileExtension=".css" mimeType="text/css; charset=utf-8" />
            <mimeMap fileExtension=".js" mimeType="application/javascript; charset=utf-8" />
        </staticContent>
    </system.webServer>
</configuration>

То есть, если в списке расширений файлов, обрабатываемых веб-сервером, уже есть расширения, настройку обработки которых нужно изменить, то сначала удаляем «старые» определения, а затем добавляем «новые». Таким образом избегаем появления в списке расширений файлов дубликатов.

После этого я снова открыл в браузере адрес http://localhost/index.html и теперь HTML-страница загрузилась без ошибок сервера.

Обсуждаемое предупреждение инструмента разработчика «Issues» исчезло.

Кстати, полученные браузером от веб-сервера заголовки HTTP можно просмотреть в инструменте разработчика «Network», кликнув мышкой на название конкретного полученного от веб-сервера файла в списке полученных файлов. Я там посмотрел и увидел, что в полученных от веб-сервера заголовках HTTP присутствуют изменения, которые я настроил вышеописанным файлом web.config.

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

https://webhint.io/docs/user-guide/hints/hint-content-type/

Инструмент разработчика Issues в браузере

Я пользуюсь браузером «Microsoft Edge» на движке «Chromium». В нем есть инструменты разработчика (всего их несколько десятков), которые по-английски называются «DevTools» и вызываются клавишей «F12» с клавиатуры.

Некоторыми из этих инструментов я пользуюсь почти ежедневно. Например, это «Elements» (для просмотра DOM-дерева загруженной HTML-страницы), «Console» (консоль разработчика), «Sources» (для просмотра файлов, из которых построена загруженная HTML-страница). Изредка пользуюсь инструментом «Network» (в нем можно посмотреть порядок загрузки файлов, скорость их загрузки, откуда они взяты — из кэша или нет и так далее).

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

https://docs.microsoft.com/en-us/microsoft-edge/devtools-guide-chromium/issues/

Каждое сообщение о проблеме можно «раскрыть» и пройти оттуда по ссылкам на ресурсы интернета, которые занимаются выдачей рекомендаций по поводу кода HTML-страниц.

Один из таких ресурсов — сайт «webhint»:
https://webhint.io

Такие анализаторы кода для разных языков программирования по-русски называют «линтерами». Подробнее о линтерах можно прочитать в википедии (по-английски):
https://en.wikipedia.org/wiki/Lint_(software)

Например, для моей тестовой HTML-страницы «webhint» посоветовал (через инструмент разработчика «Issues») поставить HTML-элемент <meta charset="utf-8"> первым в заголовочной части HTML-страницы (он у меня стоял после HTML-элемента title). Тут подробнее:
https://webhint.io/docs/user-guide/hints/hint-meta-charset-utf-8/

Фрилансер по жизни, Евгений Андриканич

По наводке gnomomamochka смотрю понемногу ютуб-канал «Фрилансер по жизни» Евгения Андриканича (он же — Женя, он же — Жека):

https://www.youtube.com/c/FreelancerLifeStyle



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

Кто такие «фрилансеры»:
https://ru.wikipedia.org/wiki/Фрилансер

Находится в десятке лучших раздела «HTML-верстка» одной из известных бирж фрилансеров. Вот ссылка на его аккаунт там:
https://www.weblancer.net/users/bliznets/

Ведет собственный учебный курс:
https://edu.fls.guru (платная версия)
https://www.youtube.com/playlist?list=PLM6XATa8CAG4F9nAIYNS5oAiPotxwLFIr (бесплатная версия)

Карта ютуб-канала:
https://miro.com/app/board/o9J_lZB3YKI=/

Карта очень ловко сделана, я такого еще не видел.

На «Хабре» есть несколько статей Евгения, вот одна из них, от 4 июня 2019 года, она называется «Фриланс или офис? Ответ фрилансера» (статья написана по мотивам одного из роликов автора с его ютуб-канала):
https://habr.com/ru/post/454776/

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

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*» второй части обсуждаемого учебника.)

Эйфелева башня, парижский флёр

Замечательная картина австралийской художницы Изабеллы Каролевич (Isabella Karolewicz), которая называется «Eiffel Tower, Paris Flair»:

https://bellasartstudioshop.com/collections/cityscapes/products/eiffel-tower-paris-flair



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

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

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

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

Я посмотрел другие картины художницы, что-то нравится, что-то нет.

Спасибо за наводку marss2.

JavaScript: двигаем элемент стрелками клавиатуры

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

Название задачи сначала кажется странным (но вообще задача получилась веселая). Что значит «мышь, управляемая клавиатурой»? Тут путаница: речь идет не о компьютерной мыши (устройстве для взаимодействия с компьютером), а о мыши (мелком животном, грызуне), нарисованной на тестовой HTML-странице. Вот как эта HTML-страница выглядит в моём браузере (картинка):



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

«Мышь» на тестовой HTML-странице представлена HTML-элементом pre с идентификатором mouse, внутри которого средствами ASCII-графики (вики) нарисована мышь.

Задача состоит в том, чтобы написать скрипт на языке JavaScript, который позволит пользователю двигать HTML-элемент (в данном случае — HTML-элемент pre) по экрану компьютера с помощью клавиш со стрелками (вправо, влево, вверх и вниз) на клавиатуре.

На тестовой HTML-странице описаны два стиля на языке CSS для целевого HTML-элемента pre, представляющего мышь:
#mouse {
  display: inline-block;
  cursor: pointer;
  margin: 0;
}

#mouse:focus {
  outline: 1px dashed black;
}

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

«Мышь» на тестовой HTML-странице можно будет (по условиям задачи) двигать клавишами с клавиатуры только после установки фокуса на HTML-элемент pre, представляющий «мышь». При установке на него фокуса этот HTML-элемент будет обведен рамкой (черная прерывистая линия толщиной в 1 пиксель). Это описано с помощью указания outline: 1px dashed black; в соответствующем стиле, приведенном выше.

Код тестовой HTML-страницы можно посмотреть в песочнице. Также есть демонстрационный пример на отдельной странице.

* * *

Приступим к решению задачи. Я открыл тестовую HTML-страницу в своем браузере и попробовал установить фокус на HTML-элемент pre, представляющий мышь. Это у меня не получилось. По умолчанию пользователь не может установить фокус на данный HTML-элемент. Однако, эту возможность можно включить.

По условиям задачи запрещено вносить изменения в код тестовой HTML-страницы на языке HTML или с помощью стилей CSS. Сделаем это в скрипте на языке JavaScript с помощью свойства tabIndex:
let elem = document.getElementById("mouse");
elem.tabIndex = 0;

Теперь на тестовой HTML-странице я могу установить фокус на «мышь» либо кликнув по ней компьютерной мышью, либо с помощью клавиши Tab на клавиатуре. Установка фокуса отображается визуально появлением рамки из черной прерывистой линии толщиной в 1 пиксель, о которой я уже писал выше.

Отмечу два момента. Во-первых, нужно обратить внимание на заглавную букву I в названии свойства tabIndex. Именно так и следует писать в коде, иначе данная инструкция не сработает. А в коде HTML названия свойств можно писать по-разному, как захочется: хоть tabIndex, хоть tabindex и так далее. Названия свойств в HTML регистронезависимы.

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

Дальше я стал думать, как отловить нажатия клавиш на целевом HTML-элементе. Сначала я подумал, что нужно как-то использовать событие focus или событие click, но потом понял, что можно просто использовать событие keydown на нужном HTML-элементе. Нажатия клавиш по умолчанию будут отлавливаться на HTML-элементе, только если на нем установлен фокус.

Дополним код:
let elem = document.getElementById("mouse");
elem.tabIndex = 0;

elem.addEventListener("keydown", function (event) {
    //...
});

В данном случае функция-обработчик события keydown отлавливает нажатие на любую клавишу клавиатуры. Нам же нужно отловить только нажатия на клавиши-стрелки. Исправим это, дополнив код:
let elem = document.getElementById("mouse");
elem.tabIndex = 0;

elem.addEventListener("keydown", function (event) {
    if (event.code != "ArrowRight" && event.code != "ArrowLeft" &&
        event.code != "ArrowUp" && event.code != "ArrowDown") return;

    //...
});

Такой фильтр пройдут только нажатия на четыре нужные клавиши-стрелки. Теперь осталось лишь реализовать движение целевого HTML-элемента, представляющего «мышь». Я решил включить для данного HTML-элемента абсолютное позиционирование. Движение HTML-элемента будет реализовано изменением координат этого HTML-элемента. HTML-элемент будет двигаться в указанную сторону не попиксельно (это слишком медленно), а сразу на свою ширину (влево или вправо) или на свою высоту (вверх или вниз). Дополним код:
let elem = document.getElementById("mouse");
elem.tabIndex = 0;

elem.addEventListener("keydown", function (event) {
    if (event.code != "ArrowRight" && event.code != "ArrowLeft" &&
        event.code != "ArrowUp" && event.code != "ArrowDown") return;

    let rectElem = elem.getBoundingClientRect();
    let x = rectElem.x + pageXOffset,
        y = rectElem.y + pageYOffset;

    if (event.code == "ArrowRight") x += elem.offsetWidth;
    if (event.code == "ArrowLeft")  x -= elem.offsetWidth;
    if (event.code == "ArrowUp")    y -= elem.offsetHeight;
    if (event.code == "ArrowDown")  y += elem.offsetHeight;
    
    elem.style.position = "absolute";
    elem.style.left = x + "px";
    elem.style.top = y + "px";
});

Этот код уже работает так, как требуется в задаче. Установив на «мышь» фокус, можно двигать ее клавишами-стрелками с клавиатуры. Если фокус с «мыши» убрать, ее нельзя будет двигать клавишами-стрелками с клавиатуры.

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

Чтобы избавиться от этого эффекта, я решил запретить прокрутку на тестовой HTML-странице. Дополним код (я отметил изменение красным цветом):
let elem = document.getElementById("mouse");
elem.tabIndex = 0;
document.body.style.overflow = "hidden";

elem.addEventListener("keydown", function (event) {
    if (event.code != "ArrowRight" && event.code != "ArrowLeft" &&
        event.code != "ArrowUp" && event.code != "ArrowDown") return;

    let rectElem = elem.getBoundingClientRect();
    let x = rectElem.x + pageXOffset,
        y = rectElem.y + pageYOffset;

    if (event.code == "ArrowRight") x += elem.offsetWidth;
    if (event.code == "ArrowLeft")  x -= elem.offsetWidth;
    if (event.code == "ArrowUp")    y -= elem.offsetHeight;
    if (event.code == "ArrowDown")  y += elem.offsetHeight;
    
    elem.style.position = "absolute";
    elem.style.left = x + "px";
    elem.style.top = y + "px";
});

Это окончательный вариант скрипта. Теперь «мышь» просто забегает за границу области просмотра и исчезает из области видимости. Но ее можно вернуть обратно.

В прошлом веке, кстати, такими простыми методами и делали игры.

Мяу:
                   /)
          /\___/\ ((
          \`@_@'/  ))
          {_:Y:.}_//
----------{_}^-'{_}----------

Источник ASCII-картинки: https://www.asciiart.eu/animals/cats

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-таблицы.

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