javascript

Плагин jQuery — jdDialog. Принцип «транзитных вызовов»

  • среда, 28 июня 2017 г. в 03:16:28
https://habrahabr.ru/post/331770/
  • JavaScript


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


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

Поэтому принято решение выполнить косметическую операцию.

Итак, сформулированы следующие требования:

  • реализовывать на 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). Параметр присваивается кнопке перед каждым повторным вызовом и удаляется сразу же после обработки повторного вызова. Ориентируясь на данную метку, удаляем все-данные «транзитной сессии» если:

  1. было обработано-последнее окно в функции,
  2. в одном из окон была выбрана отмена

Проблема №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.