Постановка задачи
В процессе доработки существующей административной страницы на «самописном» движке возникла необходимость замены грубых стандартных модальных диалоговых окон на окна вписывающиеся в дизайн сайта. Переписывать административную часть никто не позволит, да и нет в этом никакой необходимости. Основное условие — быстрая интеграция в действующий код.
Поэтому принято решение выполнить косметическую операцию.
Итак, сформулированы следующие требования:
- реализовывать на jQuery 1.9,
- вызов аналогично стандартным окнам для быстрой замены кода,
- вложенность диалоговых окон 2-3 уровня,
- заменить диалоговые окна типов confirm и alert.
Первым делом обратился к поиску в Google. Имеющиеся разработки мне не подошли, т.к. хотелось по максимум сохранить синтаксис вызова…
if(confirm('') ) {...}
или предлагали дописывать достаточно объёмные фрагменты кода в виде дополнительные функций, описывающих что именно будет происходить после того или иного выбора в окне (например Dialog UI).
В процессе разбора задачи выявил основные проблемы:
- точка вызова генерирует диалоговое окно из функции,
- возврат после выбора элемента управления должен осуществляться на следующую строку после точки вызова,
- при этом функция, генерирующая html-код диалогового окна уже завершила выполнение.
Главный вопрос — как вернуться на то место в коде, которое уже проскочил?
Задача стала выглядит следующим образом:
- Остановить выполнение функции.
- Сформировать диалоговое окно.
- Дождаться выбора пользователя (при этом неизвестно когда это произойдёт).
- Обработать вызов пользователя.
- Продолжить выполнение с точки вызова.
Реализовать такое на jQuery, по крайней мере для меня, выглядит довольно затруднительной задачей.
Принцип решения
В качестве решения были опробованы функции обратного вызова или таймеры для перехвата момента выбора.
Наилучший результат по данной задаче я получил немного изменив промежуточные условия задачи и реализовав следующий принцип:
- при генерации диалогового окна выполнение прерываем,
- после выбора в диалоговом окне запускаем функцию заново,
- а в точке вызова диалогового окна если выбор в текущей «сессии» уже был сделан,
- проходим условие «транзитом» и возвращаем выбор в скрипт.
Таким образом формирование каждого диалогового окна — это одна итерация, одно звено «транзитной сессии». Обработчик вызывается-дважды — первый раз генерируется само окно, второй раз проходит транзитом до следующего условия.
Однако если диалоговых окон в пределах одной вызывающей функции несколько, т.е. они имеют вложенность, формируется целая «транзитная цепочка». В каждой итерации — по 2 вызова функции. И с каждым новым диалоговом окне в последовательности количество вызовов функции-обработчика удваивается. Не думаю, что когда-либо потребуется вкладывать десятки окон, поэтому накладные расходы ресурсов браузера клиента расцениваю как минимальные.
Напоминает рекурсию, но отличается тем, что:
- в одной итерации функция запускается дважды,
- с каждой итерацией не выполняется «копия» функции, а происходит как-бы пошаговое проталкивание, как бы «накатывается снежный ком», пока условия функции не выполнятся полностью.
Результаты выбора удобно сохранять в привязке к элементу DOM, инициировавшему вызов диалога в виде атрибутов data-, нестандартных атрибутов или в виде именованных данных с помощью функции .data().
Данному принципу присвоил рабочее название «транзитно-диалоговых» или «транзитных» вызовов.
В моём примере реализовано в виде плагина jQuery.
Код плагина с примером вызовов выложен здесь.
По мере разработки столкнулся со следующими проблемами:
Проблема №1)
Т.к. диалоговые окна могут быть вложенными, придётся сохранять состояние каждого окна. Для этого необходимо ввести идентификатор окна.
Для решения данной пробелмы в вызове диалогового окна в качестве параметра ввёл id окна. Они должны быть уникальные в пределах одной вызывающей функции. Для разработчика это неудобство, но генерировать id автоматически, используя например хэш входных параметров рискованно, т.к. теоретически в транзитной цепочке могут быть абсолютно одинаковые вызовы (в том числе с одинаковыми текстами). Кроме того окна создаются динамически — для создания id при генерации окна надёжный признак пока не нашёл.
Ответы сохраняются для каждой кнопки-инициализатора диалога, так что мы получаем некое «транзитное пространство имён», благодаря чему можем в каждой функции использовать повторяющиеся id окон. Я использую 1,2, и так далее.
Проблема №2)
Необходимо отличать реальный клик по элементу управления от транзитного. Это нужно с целью запускать всю цепочку транзитных вызовов заново.
Решение:
Для этой цели введён флаг (у меня jdReclick). Параметр присваивается кнопке перед каждым повторным вызовом и удаляется сразу же после обработки повторного вызова. Ориентируясь на данную метку, удаляем все-данные «транзитной сессии» если:
- было обработано-последнее окно в функции,
- в одном из окон была выбрана отмена
Проблема №3)
Как отличить последнее это окно в вызывающей функции или нет. Если окно последнее, мы имеем право удалить все данные «транзитной сессии» чтобы при повторном нажатии на кнопку алгоритм запускался заново.
Препятствия:
- В вызывающем скрипте нет возможности заглянуть за точку запуска и посмотреть есть ли там ещё диалоговые вызовы.
- Если используется ветвление, в разных условиях может быть разное кол-во окон.
Варианты решения:
- Регистрировать диалоговые окна в начале скрипта и передавать данный реестр в обрабатывающий скрипт.
- В каждом вызове передавать метку является ли окно финальным.
- После обработки последнего окна отдельным вызовом запускать очистку «транзитной сессии». Во всех случаях имеются дополнительные параметры, которые нужно помнить и не перепутать, это также является некоторым неудобством. Я совместил метку и id окна, зарезервировав 0 в качестве флага отмены. Если заранее неизвестно будет ли запущено ещё одно окно в транзитной цепочке, т.к. это зависит от выбора пользователя, в условии где окон больше не будет, просто прописываем принудительную очистку «транзитной сессии».
Теперь детально о реализации в моём примере
Событие на элементе запускает функцию-инициатор «транзитно-диалоговой» цепочки:
$('#test').click(function() { ...
Собственно запуск диалогового окна выглядит так:
$(this).jdDialogs('confirm',1,['Текст?','Заголовок'],fncname)
Для привязки данных к элементу, необходимо передать в плагин селектор this,
в атрибутах передаём:
1 — тип окна (имя метода плагина),
2 — id окна
3 — текстовые параметры окна
4 — функция обратного вызова
Обработка результатов можно реализовать несколькими способами:
if(! $(this).jdDialogs('confirm',1,['Текст?','Заголовок']) ) return;
if( $(this).jdDialogs('confirm',1,['Текст?','Заголовок']) ) {
...
}
switch( $(this).jdDialogs('confirm',1,['Текст?','Заголовок']) ) {
case 1: ...;
default: return;
}
Если после вызова Alert есть выполняющийся кода, придётся использовать return, если нет — return можно опустить.
$(this).jdDialogs('alert',0,['Сделано!','Project'])
if(! $(this).jdDialogs('alert',0,['Сделано!',project]) ) return;
alert('Код выполнен');
В плагине предусмотрены стандартные методы confirm, alert, их краткие алиасы cnf, al для сокращения записи. Можно дописать собственные вызовы.
Все вызовы запускают универсальный метод jdDialog, в котором:
- распознаётся клик клиента или повторный «транзитный» вызов
- для «транзитного» вызовы возвращается сохранённое значение выбора
- если окно запускается впервые — запускается генерация самого окна jdGetWin
- генерируется id элемента управления если не было указано — метод jdCheckId
В данном методе можно изменить/дописать новые условия case для формирования своего набора кнопок, а также в return вывести отдельный отличный от остальных шаблон.
Клик на кнопки обрабатывают привязанные события. Для alert предложено 2 варианта закрывающей кнопки — jdClose0 с отменой и jdClose1 — с подтверждением. Какую выставить настраивается в jdGetWin в switch case.
Событие переадресовывается на метод jdSetAnswer. В методе распознаётся id текущего окна и id элемента управления-инициатора запуска диалогового окна. Зная id кнопки, можем сохранить результат выбора с ключом по id окна в «транзитную сессию».
$(id).data(fname,value);
Далее уничтожаем окно с помощью .detach() с анимационным эффектом например fadeIn 10
$('.jdModalBg').detach().fadeIn(10,function() {
В функции обратного вызова проверяем: если отмена — сбрасываем «транзитную сессию». В этом методе если при вызове диалогового окна 4-м параметром была передано имя функции, функция вызывается.
if(!!fncdo) window[fncdo]();
Затем запускается транзитный вызов. Передаём ID элемента управления — инициатора для повторного клика по нему. Т.е. эмулируется клик по элементу управлению — инициатору диалога.
methods.jdReclick(id);
В моём примере довольно просто дописать произвольные конструкции с вызовом и обработкой окон.
Пример реализации трёх-кнопочного окна
1. В вызове в data добавляем ещё 2 параметра: надписи на двух кнопках вместо «Ок».
$(this).jdDialogs('confirm2bttn',0,['Мы на перепутье','Действие шаг 3','Идти налево','Идти направо'])
Использование массива с текстами позволяет гибко управлять количеством параметров — здесь нужно просто дописать ещё два параметра в массив.
2. Подключаем вызов:
confirm2bttn : function(fid,data,fname) {
return methods.jdDialog('Confirm2bttn',fid,data,$(this),fname);
}
3. Подключаем обработку вызова. Сам шаблон оставляем старый, меняем только кнопки:
case 'Confirm2bttn':
var bttntext1 = data[2];
var bttntext2 = data[3];
jdBttns = '<button class="jdOk jdOk1">'+bttntext1+'</button>'+
'<button class="jdOk jdOk2">'+bttntext2+'</button>'+
'<button class="jdCancel">Отмена</button>';
clClass = 'jdClose0';
break;
4. Добавляем событие на кнопку Ok2 чтобы различать нажатие кнопок — транзитный вызов при нажатии на .jdOk2 теперь будет возвращать значение 2:
.on('click','.jdOk2', function() {
methods.jdSetAnswer(2,$(this));
})
5. Возвращаемся в скрипт-инициатор и прописываем условия для разных кнопок:
switch($(this).jdDialogs('confirm2bttn',0,['Мы на перепутье','Действие шаг 3','Идти налево','Идти направо'])) {
case 0: return;
case 1:
alert('Идём налево');
break;
case 2:
alert('Идём направо');
break;
default:
6. Ну и можно присвоить элементам нового окна новый стиль, например сделать зелёным с жёлтым текстом. Как-то так:
.jdDialogConfirm2bttn {
min-width:380px;
max-width:450px;
}
.jdDialogConfirm2bttn .jdText {
min-height:60px;
}
.jdDialogConfirm2bttn .jdHeader{
background-color: hsl(115,63%,15%);
color:#F0C800;
}
.jdDialogConfirm2bttn .jdHeader .jdClose{
background-color: hsl(114,58%,22%);
color:#F5DA50;
}
Предполагаю, что использование принципа «транзитных вызовов» предоставляет способ решения проблем, связанных с ожиданием действий от клиента. При этом достаточно использовать библиотеку jQuery с предлагающимся расширением. Представленный полностью функциональный плагин разрабатывался для использования с библиотекой jQuery версии 1.9, работает также с наиболее свежей на момент написания статьи версией 3.2.1.
Автор: drtropin