ilyachalov (ilyachalov) wrote,
ilyachalov
ilyachalov

Categories:

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

Это окончательная версия скрипта.
Tags: Образование, Программирование
Subscribe

Recent Posts from This Journal

  • Post a new comment

    Error

    Anonymous comments are disabled in this journal

    default userpic

    Your IP address will be recorded 

  • 0 comments