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

Редактирование таблицы MySQL в браузере

Заголовок, конечно, пугающий, но там (здесь), в общем, ничего необычного: редактирование таблицы MySQL происходит всё-таки с помощью серверного скрипта PHP. К тому же пользователи вообще очень часто изменяют содержимое таблиц MySQL (только не знают об этом) – например, при добавлении объявлений на доску irkutsk.ir2.ru, или при общении на любом форуме. Мы подчёркиваем, выносим на передний план слова «редактирование таблицы», потому что это именно наглядное редактирование: пользователь получает в браузер всю таблицу MySQL (точнее, данные sql-запроса) сразу, как лист Excel, и может напрямую (как бы) редактировать любую видимую ячейку (тоже как в Excel'е или других электронных таблицах).

Где может найти применение редактирование MySQL таблицы практически напрямую? В первую очередь, у администраторов интернет-ресурсов: чтобы почистить ту же доску объявлений или форум. Могут решаться и пользовательские задачи при управлении большим количеством данных, хранимых на веб-сервере. В Деловой неделе, например, в таблицах MySQL хранятся телефоны и другая информация, необходимая сотрудникам; и когда эта информация меняется, сотрудники должны как-то редактировать данные таблиц; и это делается именно в виде «редактирования напрямую», потому что так проще и привычнее, чем открытие новой страницы с отдельной формой для изменения одной цифры в телефоне.

Постановка задачи

Задача распадается на две части: 1) сделать доступным для редактирования содержимое выбранной ячейки; 2) отправить изменённое содержимое на сервер, не перегружая страницу (так как она может оказаться довольно большой). При решении первой части задачи первой мыслью любого разработчика, конечно, будет: «А почему нельзя всю таблицу сразу вывести в виде элементов input и textarea»? В первом приближении, наверное, можно. Но будут свои сложности: а) возникнет вопрос, надо ли делать весь текст внутри редактируемых элементов видимым без прокрутки (и если надо, то как?); б) наш любимый Firefox очень не любит большого количества редактируемых элементов на одной странице (начинает заметно тормозить и вообще нервничать). Поэтому по первой части мы приняли решение выводить всю таблицу «как есть», обычным текстом (точнее, HTML), и делать доступным для редактирования текст данной ячейки по требованию (сигналу) пользователя.

При выборе такой стратегии («динамического» доступа к тексту) тоже возможны два решения: 1а) заменять содержимое ячейки на элемент textarea и помещать текст в этот элемент прямо внутри ячейки; 1б) создавать «всплывающий» элемент textarea где-то рядом, в непосредственной близости от ячейки. Мы выбрали второй вариант, так как он более универсальный – удобнее отображать целиком (по возможности, без прокрутки) весь редактируемый текст и элементы управления; их надо как минимум два: кнопка «Сохранить» и кнопка «Закрыть» (а в идеале ещё больше). Внутри ячейки делать доступным текст, конечно, очень соблазнительно. «Закрыть» (отказаться от редактирования) в этом случае можно, например, по «естественной» клавише Escape. Но как сэмулировать кнопку «Сохранить»? Enter не пойдёт, так как может понадобиться просто для разрыва строки в тексте. Ctrl+Enter очевидно далеко не для всех пользователей (это надо учить), да и может быть зачем-то нужно браузеру (и он не позволит перехватить это сочетание клавиш). В общем, всё равно надо будет кнопку делать (хотя бы для потери фокуса, при котором тоже можно сохранять текст). Хотя в будущем мы обязательно что-нибудь придумаем – больно уж привелкательна идея.

При решении задачи 2) – пообщаться с сервером, не перегружая страницу, – на ум сразу приходит супер-пупер-Ajax, с которым так много носятся некоторые восторженные разработчики. Тот самый супер-пупер-Ajax, который на самом деле никому на ..р не нужен, потому что его вполне может заменить старый кондовый iframe. Когда-то перед съёмками фильма «И корабль плывёт» какие-то ср...е англичане предложили Феллини работать на реальном корабле. «Скоко, скоко?» – переспросил Феллини, и послал англичан туда же, куда мы послали Ajax. А в конце фильма наглядно показал, как настоящий мастер может снять фильм без «реального» корабля.

Мы бы, конечно, без раздумий выбрали для решения наш любимый scriptRequest(), но он может общаться с сервером только методом GET (а значит, не может отправлять текст большого размера). Поэтому, чтобы не перегружать большую страницу, мы решили открывать для редактирования ячейки новое окно (с ма-аленькой страницей), прямо посередине старого окна. То есть всё-таки iframe. Разработчики любят Ajax, в частности, за то, что там можно отслеживать стадии выполнения фонового http-запроса, и если запрос неуспешный, сообщать об этом пользователю (или выполнять какие-то другие действия). Мы покажем, что при использовании iframe всё это можно делать легко.

Покажем наглядно, на странице Учебный пример редактирования таблицы MySQL в браузере. Всплывающее на этой странице окно для редактирования – savecell.php, а необходимые для работы Javascript-функции – в файле tableedit.js. Описание работы скриптов PHP на сервере выходит за рамки этой статьи (за небольшим исключением – о генерации возвращаемого сервером Javascript кода упомянуть придётся).

Принципиальная схема редактирования таблицы в браузере

  • Таблица выдаётся клиенту (браузеру) на странице tableedit.htm. Особенностей таблицы две: 1) верхняя строка должна содержать наименования полей отображённой таблицы MySQL; 2) в первой колонке таблицы должны быть идентификаторы строк таблицы MySQL (и они должны быть в данном случае первичными или уникальными ключами).
  • Вверху страницы находится меню, переключающее режимы работы: режим редактирования, режим DOM-инспектора (опциональный пункт) и обычный режим.
  • При включённом режиме редактирования событию click назначается пользовательская функция showframe().
  • При щелчке по ячейке таблицы, предназначенной для редактирования, (и при удержанных клавишах Ctrl+Alt) функция showframe() отображает на экране форму редактирования ячейки в отдельном документе, находящемся в элементе iframe. Функция также копирует в основное поле формы текст выбранной ячейки и в дополнительные поля номер редактируемой строки и наименование редактируемого поля MySQL-таблицы.
  • После изменения текста форма обычным путём (по нажатию кнопки типа submit) отправляется на сервер. Форма находится внутри страницы savecell.php (а страница – внутри элемента iframe); форма отправляется на ту же страницу (action=""); страница, естественно, перегружается; но не большая страница со всей таблицей, а маленькая страница с текстом одной ячейки внутри iframe – ради этого весь огород.
  • После перезагрузки документа внутри iframe обновлённая страница savecell.php передаёт через команды Javascript некоторую информацию главному окну: 1) в случае неуспешного выполнения запроса SQL по обновлению MySQL-таблицы (данные таблицы не изменились): ничего не передаёт; 2) в случае успешного изменения данных: меняет на экране текст (innerHTML) редактируемой ячейки и скрывает элемент iframe.
  • Для закрытия окна редактирования ячейки по желанию пользователя есть 1) кнопка "x" и 2) перехват нажатия клавиши Esc.
  • Если к серверу пропал доступ (или в ответе вернулась другая HTTP-ошибка), об этом сообщит пользователю браузер прямо внутри окна iframe (ничего программировать специально не надо, чтобы пользователь увидел проблему).

В нашем случае к странице tableedit.htm присоединён ещё сортировщик HTML таблиц. Всего используется 4 файла Javascript (в сумме 11Кб): ir2.js (общая библиотека функций), dom-javascript.js, tabsort.js, tableedit.js.

Доступ к документу в окне iframe

Первая проблема – двойственность элемента iframe: нам нужны его свойства как элемента, являющегося частью страницы, частью дерева DOM (назовём это face), и нужно содержимое HTML-документа, вложенного в этот iframe (назовём back). Нам как-то не удалось сразу получить доступ к этим двум сторонам медали. Аналогичную проблему нам приходилось решать на доске объявлений irkutsk.ir2.ru (там целых три фрейма на одной странице). Там мы, недолго думая, прибегли к перебору в цикле свойств window.frames[i], и проблема, в общем, оказалась решена для всех доступных нам браузеров. Проблема iframe-back. Но проблема face (управления внешним видом фреймов) осталась нерешённой (на доске объявлений она решалась статически – через файлы CSS).

То есть, если мы находим фрейм по формуле myframe=window.frames[0], мы не можем потом в сценарии Javascript использовать выражения вида myframe.style.display="none". Нужно всё-таки получать доступ к фрейму через editframe=document.getElementById("edit"). Но после этого возникнет сложность с доступом к back – документу, вложенному во фрейм. Использовав наш DOM-инспектор (для этого пришлось подключить его к странице и сделать форму выбора режима) и пощёлкав клавишами Ctrl+Alt+I в разных браузерах, мы обнаружили у элемента iframe свойства contentDocument и contentWindow (а в некоторых браузерах оба эти свойства). Свойство contentWindow встречалось чаще всего, поэтому его поставили на первое место. Получилась формула:

Последнее свойство фрейма (document) не встретилось нам ни в одном браузере, где не было бы уже contentWindow. Но, после проверки наших гипотез в Гугле, мы решили эту конструкцию на всякий случай добавить тоже (возможно, она нужна для старых версий какого-нибудь ИЕ).

Положение «всплывающего» iframe на странице

Мы отчасти усложнили себе жизнь, отказавшись от вложения iframe в редактируемую ячейку таблицы. Теперь его надо как-то очень точно позиционировать, чтобы он по возможности не заслонял ячейку и сам был виден весь. Да ещё размер его желательно сделать как раз по содержимому. То есть размер элемента iframe должен быть таким, чтобы его документ был виден без прокрутки (но не больше). После того как мы получили элемент iframe (editframe) и внешний документ, привязанный к этому элементу (editdoc), вычислить размеры оказалось несложно. Только надо, чтобы в момент вычислений, элемент был виден на странице (его свойство style.display не должно быть "none"):

В первый момент отображения на странице элемент body в нашем вложенном документе может отображаться не весь, а с прокруткой, поэтому для получения всей ширины body (и видимой, и невидимой), используем свойство scrollWidth. Так же и с высотой. Идею parseInt (как и много других полезных мелочей) мы подсмотрели на сайте javascript.ru. Очень разумная предосторожность (вдруг scrollWidth в каком-нибудь браузере окажется равным "450px").

Вариантов, куда помещать всплывающую форму для редактирования, было несколько. После непродолжительной внутренней дискуссии было решено отображать её немного левее и немного выше (ниже) ячейки, по которой щёлкнул пользователь. Насчёт «немного левее» всё оказалось достаточно просто: editframe.style.left=obj.offsetLeft, где obj – редактируемая ячейка. Мы знаем, что редактируемая таблица занимает всё окно, за исключением небольших полей. Вот на величину левого поля как раз и будет сдвинута всплывающая форма («немного левее»). С высотой оказалось сложнее: пришлось вычислять по цепочке offsetHeight для ячейки и всех её offsetParent. И, как уже не раз бывало, пришлось уничтожить придуманную нами функцию после обнаружения её лучшего варианта на странице http://javascript.ru/ui/offset. Вот какие функции надо включать в топ-10! Ну, мы и включаем их в свою common-библиотеку ir2.js.

После выяснения left-top (позиции от начала документа) редактируемой ячейки и высоты (offsetHeight) и зная высоту формы редактирования (вложенной в iframe), можно принимать решение, выше или ниже ячейки показывать форму. Это уже арифметика. Для неё, правда, понадобится (чуть не забыли!) ещё расстояние до верхнего края видимой части (а не всего) документа. Тут, чтобы не мудрствовать долго, мы использовали не край элемента (ячейки), а место, куда щёлкнула мышь. Потому что оно (это свойство), одинаково во всех известных нам браузерах – это свойство события event.clientY.

Общая схема нахождения места фрейма получилась такая:

  1. Измеряем расстояние от места щелчка до верхнего края окна.
  2. Если туда по высоте влезает наш iframe с формой редактирования, помещаем его на 20px выше верхнего края ячейки.
  3. Если не влезает, помещаем его на 20px ниже нижнего края ячейки.

А помещать нужно, используя расстояние не от края окна, а от верхней границы всего документа (неважно, видна она или нет); как раз для этого и приходится вычислять абсолютную высоту элемента на странице. Полностью код нашей функции showframe() находится в файле tableedit.js.

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

Некоторые особенности SQL запросов

Форма редактирования всегда отправляет данные на сервер при нажатии кнопки submit (у нас это "Сохранить значение"). И на сервере PHP скрипт составляет и отправляет в базу SQL запрос. Что-то вроде «заменить текст поля f в строке r на присланный пользователем»: update `$table` set `$field`='$text' where `id`='$id'. Запрос считается (у нас) успешным, если данные в таблице были изменены. Но сервер MySQL не будет менять данные в таблице, если:

  • пользователь отправил на сервер текст без изменений;
  • пользователь изменил только регистр букв;
  • пользователь изменил только тэги HTML.

Во всех этих случаях обновления не произойдёт, и форма редактирования останется висеть на странице. Последний пункт, впрочем, к MySQL-серверу отношения не имеет (его выполняет PHP). Регистр букв – дело настраиваемое (в принципе, на SQL-сервере можно менять запросом только регистр букв), администраторы БД могут менять этот пункт по своему усмотрению.

D.M., admin

Комментарии

genrih 27.12.2013 00:07:43

Данный пример почему – то не корректно работает в хроме. А именно, не отрисовывается рамка и не всплывает окно редактирования

лесник 08.05.2011 20:06:15

Vik, добавил в архив http://ir2.ru/static/tableedit.zip таблицу betontest (структуру и начальные данные), а также файл configbase.php, в котором находится функция cacheh(). Пароль для подключения к своей бд выкладывать не буду (вы должны сами установить подключение к СВОЕЙ бд в своём файле config.php).

Вообще эта статья больше не о PHP, а о том, как организовать интерфейс в браузере для редактирования таблиц в БД...

Vik 08.05.2011 04:00:45

И еще, в этом файле (savecell.php) идет речь вначале, что необходим "config.php"; а его нигде нету... – и где происходит соединение с БД? Чтобы опробовать метод нужно создать БД "betontest" с 4-мя полями? – и их нужно заполнить или нет? (они же есть в таблице, может опишите еще какие атрибуты у полей таблицы должны быть?)

Материал не полный ((

Vik 08.05.2011 03:45:22

А я сохранил файл: savecell.php

и выдает ошибку при обработке:

Fatal error: Call to undefined function cacheh() in C:\Program Files\xampp\htdocs\222\savecell.php on line 52

В чем проблема? ((

лесник 03.02.2011 20:40:44

Кирилл, вот исходный текст (php) учебной таблицы: http://ir2.ru/static/tableedit.zip

Кирилл 03.02.2011 15:16:57

Доброго время суток!А нельзя исходники учебной таблицы выложить? а то не сильно понятно как работает.

лесник 21.04.2010 23:56:54

Не, так не будет работать в этой версии. Опять изменил ссылку для получения кода: http://ir2.ru/static/savecell.php?code=1

лесник 21.04.2010 23:16:17

гм, действительно. Исправил. Теперь при прямом просмотре открывается код файла: http://ir2.ru/static/savecell.php

В следующей статье tableedit2.aspx сразу правильно было сделано (там другой скрипт – http://ir2.ru/static/savecell2.php)

Аркадий 21.04.2010 11:57:10

Доброго время суток! У меня вопрос, а где можно посмотреть пхп для обращение к БД? или к вашему примеру нужно писать свои пхп? Скрипт который вы выложили savecell.php при скачивание такой: <!DOCTYPE HTML PUBLIC '-//W3C//DTD HTML 4.0 Transitional//EN'> <meta http-equiv='Content-Type' content='text/html; charset=windows-1251'> <link rel='stylesheet' type='text/css' href='static/http://ir2.ru/static/ir2.css'> <button id='closebutton' onclick='hidTemp()'> х </button> <table><tr><td style='font-size:.75em;'><dir class='msg'></dir> <form class='edit' method='post' action=''> <p>Строка (id): <input name='idrow' id='idrow' value='0' size='4' readonly='readonly'>, поле: <input name='field' id='field' value='' readonly='readonly'> </p><p><textarea name='text' id='text' rows='5' cols='70'></textarea> </p><p class='r'>Таблица: `access`. <input style='margin-left:2em;' type='submit' value='Сохранить значение'> </p></form> <script type='text/javascript' src='http://ir2.ru/static/ir2.js'></script> <script type='text/javascript'> document.onkeyup=cancel; </script>