material

AJAX-редактирование материалов без перезагрузки

Теги: Статьи, Веб-разработка

Всем привет! Сегодня пришла идея написать здесь небольшую статью по некоему нововведению, реализованному мною недавно на одном своем сайте. Функция очень интересная и, я думаю, для многих может оказаться полезной. Итак, задача перед нами стоит следующая: реализовать Ajax-метод редактирования сообщений на форуме под управлением Seblod, работающий без перезагрузки страницы.

Сразу внесу несколько уточнений:

  • Самое основное. В примере описан метод решения задачи с использованием Seblod (CCK Joomla), но это не значит, что он применим только здесь. С Seblod статью связывает только описание возможности генерации нужной нам разметки. Если вы найдете способ сгенерировать необходимую разметку самостоятельно, с используемыми вами средствами, то сможете применить этот метод где пожелаете.
  • Пример использует несколько функций WYSIWYG-редактора CKEditor, являющегося, думаю, самым популярным из бесплатных редакторов, но это не значит, что код привязан именно к CKEditor. Вы можете взять любой редактор, работающий с Inline-редактированием, вам потребуются только функции встраивания и удаления редактора, а также, получения из него и установки в него текста.
  • В данном примере описывается метод редактирования сообщений на форуме под управлением Seblod, но это не значит, что форум - его единственное применение. Точно так же вы можете редактировать комментарии или любые другие материалы. К слову, на сайте, где этот метод был применен, в дальнейшем, также, планируется его расширение, как минимум, до комментариев.
  • Наверняка найдется тот, кто сможет усовершенствовать мой метод, указать мне на его недочеты или предложить свой - я буду рад любому такому совету.

Небольшое введение

Задуманная задача довольно сложна в реализации - для ее успешного решения требуются знания JavaScript (на уровне jQuery), методов работы с Ajax и серверного языка PHP. Но, разумеется, для прохождения урока по шагам этими знаниями можно пренебречь. Зато что вы получите в итоге?

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

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

Объяснять здесь основы JS, PHP, Ajax,... я, конечно, не буду. Вкратце можно сказать, что Ajax - есть метод взаимодействия с PHP-скриптом посредством кода на JavaScript. То есть, взаимодействие клиента (браузера) с сервером. Например, считать данные из базы с помощью чистого JavaScript у вас, конечно, не получится, но вы легко сможете сделать это внутри PHP-скрипта, вызываемого при помощи Ajax и затем получить эти данные в качестве результата работы скрипта, например, в виде Json-объекта.

Итак, приступим... План работы следующий:

  • Создание необходимой нам HTML-разметки
  • JavaScript-код функций, генерирующих динамическую разметку и делающих AJAX-запросы
  • PHP-код вызываемого скрипта на сервере

Предварительная разметка

Для начала, подготовим нашу разметку к последующему вызову JS-функции редактирования. Для этого нам нужна кнопка «Изменить», при нажатии на которую мы будем подключать к телу сообщения WYSIWYG-редактор, а также, убирать стандартные кнопки («Изменить», «Удалить») и ставить вместо них динамические («Сохранить», «Отменить»).

Давайте условимся, что разметка нашего сообщения на форуме будет следующей:

<div>
...<div id = "message_179" class = "message_body">
......Тело сообщения
...</div>
...<div class = "message_buttons">
......<span class = "button button_ajax" type_ajax = "0">Изменить</span>
...</div>
</div>

Теперь немного разъясню, что к чему. У нас есть два основных блока: один, содержащий тело сообщения и второй, содержащий кнопки, управления этим сообщением. Блок тела сообщения ('message_body') имеет идентификатор, хранящий в себе номер этого сообщения в базе данных (записи с форума хранятся в отдельной таблице). Если вы используете Seblod, то этот идентификатор вы должны поставить при генерации списка (List & Search) в Seblod, в разделе 'Item', при помощи поля Core42. Вот один из возможных вариантов кода этого поля, генерирующего такую разметку:

$database = JFactory::getDBO();
$query = 'SELECT text FROM #__table WHERE id = ' . $value;
$database->setQuery($query);
$array = $database->loadRowList();
$text = $array[0][0];

$text = trim(preg_replace('~<p>(?-i:\\s++| )*</p>~i', ' ', $text));
$value = '<div id = "message_' . $value . '" class = "message_body"> ' . $text . '</div>'

Считываем из базы данных текст сообщения, обрабатываем его и создаем разметку. Вернемся же к нашему примеру. Наибольшую важность здесь имеет код кнопки редактирования. Класс «button_ajax» в будущем скажет нашему JS-скрипту, что эта кнопка обеспечивает взаимодействие с PHP-скриптом на сервере. Забегая вперед, скажу, что таких взаимодействий может быть великое множество, например, у меня на сайте их уже около 13.

Каждое взаимодействие имеет свой порядковый номер, здесь за это отвечает параметр «type_ajax». По этому номеру PHP-скрипт узнает, какие действия и над чем ему нужно выполнить.

Как я уже говорил раньше, в одном PHP-скрипте мы можем описать великое множество взаимодействий. Думаю, это самая сложная часть планирования структуры сайта - создать некую систему взаимодействий, работающую при помощи ограниченного набора параметров. В нашем случае он всего один - «type_ajax», в моем же этих параметров целых 3, но с их помощью я могу реализовать любое необходимое мне действие от оценки статьи до удаления или редактирования сообщений и даже блокировки пользователей, выдачи им награды и штрафов.

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

Основная функция, выполняющая Ajax-запрос

Приведу здесь еще одно необычайно важное уточнение. До этого момента мы думали, что описываемая функция на JavaScript будет выполнять только одно дейстие - 'Редактировать сообщение'. На самом деле, действия здесь два...

Что значит 'Редактировать сообщение'? Во-первых, это значит, создать поле для его редактирования и кнопки 'Сохранить' и 'Отменить'. Во-вторых, это значит описать действие скрипта при нажатии на одну из этих кнопок. Все эти действия будет выполнять одна и та же функция, так что, при ее разборе будьте крайне внимательны. Итак...

Объявим функцию, реагирующую на нажатие нашей кнопки:

function fAjaxButtonClick() {
//Объявление констант и переменных
...const MESSAGE_EDIT = 0;
...const MESSAGE_ENDEDIT = 1;
...var vAjaxType = jQuery(this).attr('type_ajax');
...var vBtnType = jQuery(this).attr('type_btn');//А вот и наш второй параметр
//Выполнение подготовительных действий в зависимости от типа запроса
...if (vAjaxType == MESSAGE_EDIT) fSetEditingHTML(jQuery(this));
...if (vAjaxType == MESSAGE_ENDEDIT) {
......var vTextArea = fGetEditingArea(jQuery(this));
......var vMessageText = CKEDITOR.instances[vTextArea.attr('id')].getData();
......var vMessageId = fStrToInt(vTextArea.attr('id'));
...}
...var aAjaxResult = [];
//Вызов Ajax-запроса к PHP-скрипту
...jQuery.ajax({
......url: location.origin + {Полный путь к файлу PHP-скрипта},
......async: false,
......type: 'POST',
......data: {'id_message': vMessageId, 'type_ajax': vAjaxType,
.........'type_btn': vBtnType, 'text': vMessageText},
......dataType: 'json',
......success: function(data) {aAjaxResult = data;}
...});
//Обработка результата AJAX-запроса
...if (!aAjaxResult[0]) fCallAlert('Невозможно выполнить', aAjaxResult[1]);
...if (vAjaxType == MESSAGE_ENDEDIT) {
......fSetEndEditingHTML(jQuery(this), aAjaxResult[2]);
......fCallAlert('Операция завершена', aAjaxResult[1], 0, 0, false);
...}
}

Вот такая у нас получилась функция. Давайте разберем ее по-порядку.

Сначала мы объявляем константы для облегчения понимания кода скрипта: так как тип Ajax-взаимодействия у нас задан в виде числовой переменной, введением подобных констант мы обеспечиваем некую синхнонизацию нашего JS-скрипта с PHP-скриптом на сервере. Номера типов взаимодействий в обоих скриптах будут совпадать.

Затем объявляем начальные переменные. Это vAjaxType - тот самый тип взаимодействия и vBtnType - обещанный мною ранее второй параметр, необходимый для определения нажатой кнопки («Сохранить» или «Отменить»). Оба параметра берутся при помощи метода attr из HTML-атрибутов нажатой кнопки. Как вы уже, вероятно, догадались, в первой части алгоритма («Генерация динамической разметки») значение vAjaxType будет равно 0, а vBtnType будет отсутствовать. Во второй же части, вызванной нажатием на одну из динамических кнопок, мы получим vAjaxType равной единице и vBtnType равной типу нажатой кнопки (0 или 1).

Далее, в зависимости от типа запроса, мы выполняем некие подготовительные действия. Если функция была вызвана кнопкой «Изменить», то мы вызываем функцию fSetEditingHTML(), генерирующую динамическую разметку (ее описание будет приведено ниже). Если же выполнение функции будет ответом на нажатие кнопки «Сохранить» или «Отменить» (динамическая разметка), то действия выполняются другие. Во-первых, это функция fGetEditingArea(), возвращающая jQuery-объект элемента, содержащего текст сообщения. Ну а далее - получение текста сообщения при помощи метода CKEditor и его идентификатора. Функция fStrToInt() здесь преобразует запись вида message_179 в число 179.

Затем мы объявляем массив, в который будет записан результат Ajax-запроса и делаем этот самый запрос. Здесь, думаю, стоит объяснить лишь причину выбора метода POST для отправки запроса. Дело в том, что параметры, передающиеся при помощи метода GET, записываются в заголовок сообщения и, следовательно, их размер ограничен. А так как мы должны передать PHP-скрипту тело сообщения, велика вероятность того, что оно просто туда не поместится. Поэтому и выбран метод POST - параметры, передающиеся с его помощью, записываются в тело сообщения и, насколько я знаю, практически не ограничены по размеру.

Тип данных для AJAX-запроса - Json, а результат его выполнения записывается в массив aAjaxResult.

Ну и в самом конце мы обрабатываем полученные данные. Здесь, думаю, важно сказать о структуре массива aAjaxResult. В первый его элемент я записываю результат выполнения функции (выполнено удачно или с ошибкой). Второй элемент содержит текст сообщения, объясняющего причину неудачи или текст сообщения об успешно завершенной операции. Третий же элемент содержит в себе некоторый дополнительный параметр, использующийся в зависимости от потребностей функции. Отсчет ведется с нуля.

То есть, здесь мы, во-первых, проверяем результат выполнения скрипта. Если он отрицателен, то выводим сообщение об ошибке. Если положителен, то вызываем функцию удаления динамической разметки и выводим сообщение об успехе операции. Сообщение выводится при помощи функции fCallAlert(), куда мы передаем заголовок окна, текст сообщения, размеры окна и флаг, отвечающий за необходимость перезагрузки страницы после его закрытия. Думаю, эту функцию описывать здесь не буду по причине ее объема и недостаточного соответствия нашей теме. Вы можете вызвать вместо нее, например, alert('...').

Вот и все, основная работа над JS-скриптом завершена, теперь будем смотреть дополнительные функции.

Допольнительные функции

//Получение элемента, содержащего тело сообщения
function fGetEditingArea(vButton) {
...return vButton.parent('.message_buttons').siblings('.message_body');
}
//Преобразование строки вида 'message_179' в число '179'
function fStrToInt(vStr) {
...return Number(vStr.replace(/\D+/g,''));
}
//Генерация HTML-кода при нажатии на кнопку 'Редактировать' и включение редактора
function fSetEditingHTML(vButton) {
...if (jQuery('.cke_editable').length > 0) return false;
...//Предварительные расчеты
...var vTextArea = fGetEditingArea(vButton);
...var vID = fStrToInt(vTextArea.attr('id'));
...//Динамические HTML-элементы
...btnSave = jQuery('<span class="button button_ajax delme" type_ajax = "1" type_btn="0">Сохранить</span>');
...btnCancel = jQuery('<span class="button button_ajax delme" type_ajax = "1" type_btn="1">Отменить</span>');
...btnSave.bind('click', fAjaxButtonClick);
...btnCancel.bind('click', fAjaxButtonClick);
...//Остальные действия
...vButton.parent('.message_buttons').find('.button').hide();
...vButton.parent('.message_buttons').append(btnSave, btnCancel);
...vTextArea.attr('contenteditable', 'true');
...vTextArea.addClass('editing_message');
...CKEDITOR.inline(vTextArea.attr('id'), {customConfig: 'имя_конфигурации.js'});
}
//Отключение редактора при отмене или сохранении редактирования
function fSetEndEditingHTML(vButton, vText) {
...var vTextArea = fGetEditingArea(vButton);
...var vButtons = vButton.parent('.message_buttons');
...//Установка текста и отключение редактора
...if (vText != '') CKEDITOR.instances[vTextArea.attr('id')].setData(vText);
...CKEDITOR.instances[vTextArea.attr('id')].destroy();
...vTextArea.removeAttr('contenteditable').removeClass('editing_message');
...//Обратная замена кнопок
...vButtons.find('.delme').remove();    
...vButtons.find('.button').show();
}

Итак, давайте посмотрим, что делают эти функции.

fGetEditingArea()

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

fSetEditingHTML()

Здесь мы, во-первых, проверяем возможность включения редактора. В моем случае стоит length > 1, так как на странице темы присутствует форма добавления сообщения. Итак, первая строка проверяет возможность включения режима редактирования для сообщения. Это возможно только в том случае, если больше на странице ни одно сообщение не редактируется. Затем мы получаем объект тела сообщения (vTextArea) и его идентификатор (vID).

Далее задаются динамические кнопки «Сохранить» и «Отменить». В них type_ajax - тип запроса к серверу и type_btn - тип самой кнопки. Следующие после этого строки назначают на событие onClick кнопок функцию, созданную нами ранее.

В разделе «Остальные действия» мы скрываем основные кнопки нашего сообщения и добавляем динамические (append). Затем включаем стандартную в HTML5 возможность inline-редактирования блока (contenteditable) и подключаем к нему CKEditor. Да, еще добавляем блоку, содержащему тело сообщения, класс editing_message - этот класс позволит нам стилизовать блок редактируемого сообщения, например, в виде текстового поля, так как мы используем inline-редактирование вместо элемента textarea.

fSetEndEditingHTML()

Не нашел ничего лучше, чем назвать эту функцию так. =) Она вызывается при нажатии кнопок «Сохранить» или «Отменить». Здесь все просто, и во многом повторяются действия из предыдущей функции. Мы устанавливаем в качестве сообщения текст, возвращенный нам сервером, уничтожаем объект редактора, удаляем параметр contenteditable и класс editing_message, чтобы поле сообщения приняло свой обычный вид. Затем убираем динамические кнопки, помеченные классом delme и включаем стандартные кнопки сообщения.

Важно отметить еще условие if (vText != ''). Здесь я использую немного нестандартный подход: пустая строка будет возвращена сервером в том случае, если мы нажали на кнопку «Сохранить». Дело в том, что при сохранении сообщения мы уже имеем на странице его текст, и нам не надо ничего менять. При нажатии же кнопки «Отменить» нам необходимо восстановить старую версию сообщения, поэтому, в данном случае сервер возвращает нам неизмененный текст сообщения, считанный из базы данных.

Связывание функции JS с HTML-разметкой

И, наконец, последнее, о чем нам нужно подумать, - назначение функции fAjaxButtonClick() на onClick нашей кнопки «Изменить». Делается это одной короткой строкой:

jQuery('.button_ajax').bind('click', fAjaxButtonClick);

PHP-код скрипта, реагирующего на Ajax-запрос

Итак, мы переходим к финальной части нашей статьи - написанию PHP-кода скрипта, вызываемого нашим Ajax-запросом. Но прежде хотел бы сделать пару замечаний по поводу организации взаимодействий клиента с сервером.

Дело в том, что это очень небезопасно. Как вы видите, мы вынуждены передавать серверу данные, записанные прямо в HTML-разметке страницы, и пользователь, изменив эти данные, может напрямую повлиять на работу нашего скрипта. Например, если приtype_ajax равном2 сервер будет, скажем, банить пользователя, то, заменив это значение у кнопки «Отменить», злой человек вполне может кого-нибудь забанить. Даже администратора. =) Поэтому, вот некоторые правила организации безопасного взаимодействия с сервером:

Всегда проверять на сервере принятые данные.

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

Мы не можем простыми средствами запретить ему это сделать, зато легко можем проверить, чье сообщение он редактирует, и если у него недостаточно прав для этого, просто не выполнять никаких действий. Максимум, чего он сможет добиться, - редактирование своего же сообщения в другом разделе. Что ж, если он, и правда, хочет идти этим путем - удачи ему. Возможно, для его скорости соединения указать посторонний идентификатор будет проще, чем переходить на страницу с конкретным сообщением. =)

Никогда не передавать ненужных данных.

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

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

Итак, перейдем к серверной части. Важно заметить, что конкретных функций у нас здесь не будет (кроме внутренних, вызываемых самим скриптом), а их роль будут выполнять проверки на тип производимого действия (type_ajax).

<?php
//Подготовительные действия. Активация системных скриптов
...define('_JEXEC', 1);
...define('DS', DIRECTORY_SEPARATOR);
...define('JPATH_BASE', $_SERVER['DOCUMENT_ROOT'] . DS . '');
...require_once($_SERVER['DOCUMENT_ROOT'] . '/includes/defines.php');
...require_once($_SERVER['DOCUMENT_ROOT'] . '/includes/framework.php');
...$mainframe = JFactory::getApplication('site');
...$input = JFactory::getApplication()->input;
...$database = JFactory::getDBO();
...$user = JFactory::getUser();
//Считывание переданных параметров
...$id_message = (int) $input->get('id_message', '', post);
...$type_ajax = (int) $input->get('type_ajax', '', post);
...$type_btn = (int) $input->get('type_btn', '', post);
...$text = $input->get('text', '', RAW);
...$text = $database->quote($text);
//Объявление констант
...$ENDEDIT_MESSAGE = 1;
...$SUCCESS_SAVED = 'Ваш материал успешно сохранен.';
//Прерывание работы скрипта
...function func_returnValue($success, $text = '', $new = '') {
......$result = array($success, $text, $new);
......echo json_encode($result);
......exit;
...}
//Тело скрипта
...if ($type_ajax == $ENDEDIT_MESSAGE) {
...//Действия, если произошла отмена редактирования
......if ($type_btn == 1) {
.........$query = 'SELECT text FROM #__table WHERE id = ' . $id_message;
.........$database->setQuery($query);
.........$array = $database->loadRowList();
.........func_returnValue(true, '', $array[0][0]);
......}
...//Действия, если редактирование было сохранено
......if ($type_btn == 0) {
......//Получение данных о сообщении
.........$query = 'SELECT author FROM #__table WHERE id = ' . $id_message;
.........$database->setQuery($query);
.........$array = $database->loadRowList();
.........$id_author = $array[0][0];
......//Проверка на возможность редактирования
.........if (($id_author != $user->id) && (!func_isAdmin($user->id)))
............func_returnValue(false);
......//Сохранение материала
.........$query = 'UPDATE #__table SET text = ' . $text . ' '
......................'WHERE id = ' . $id_message;
.........$database->setQuery($query);
.........$database->query();
......//Возвращение значения
.........func_returnValue(true, $SUCCESS_SAVED);
......}
...}
?>

Сначала к скрипту подключаются необходимые библиотеки и создаются некоторые системные переменные: для работы с POST-параметрами, базой данных и для получения информации о текущем пользователе (текущий - тот, что вызвал скрипт). С помощью объекта input мы получаем параметры, переданные серверу с клиента (браузера). Числовые параметры должны приводиться к числовому виду при помощи (int). Обратите внимание, что при считывании текста сообщения мы указываем RAW вместо post, в иных же случаях скрипт обрежет HTML-теги в тексте сообщения (пока не могу дать разъяснений по этому воводу). О преимуществах объекта input перед обычными глобальными массивами GET и POST вам лучше самим почитать в интернете.

Кстати, заметьте, что текст сообщения после считывания обрабатывается методом quote, о нем тоже полезно было бы почитать.

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

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

Затем идет обработка действий при нажатии кнопки «Сохранить». Как я уже писал выше, изначально здесь мы обязаны провести ряд проверок, запрещающих злоумышленнику неправомерный доступ к функциям сайта. Получаем ID автора материала и проверяем, совпадает ли он с ID текущего пользователя или является ли текущий пользователь администратором. Если условия не выполнены, то прерываем работу скрипта. Иначе - обновляем текст сообщения в базе данных и тоже прерываем его работу.

Заключение

Вот, в общем, и все. Описанный мною метод может быть использован не только в связке Joomla + Seblod, а также, не только для редактирования сообщений на форуме, но и в любых других случаях. Важно понять его основную идею, тогда сможете реализовать и другие взаимодействия, по своим потребностям.

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

Спойлер: «Устаревшая информация» Открыть спойлер

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

login: test_seblod; password: test_seblod

Переходите в тему и редактируйте, сколько пожелаете.

Если возникнут вопросы по статье, задавайте их личным сообщением мне в Контакте.

Ну и напоследок: учите JavaScript, PHP и практикуйтесь в их использовании. Joomla - это хорошо, Seblod - тоже. Но ровно настолько, насколько Joomla имеет преимущества перед блокнотом, и Seblod имеет преимущества перед чистой CMS Joomla - ровно настолько связка Joomla + Seblod + JS/PHP имеет преимущество перед чистыми Joomla + Seblod. =)

автор: SaiLight работа над проектом: 2015 год
выложено уже 20 проектов из 124 созданных команда Perfect Light, 2017 год