Привет!
Я работаю в области web-разработки и на днях у меня появилась интересная задача – необходимо было создать сложную форму регистрации, на которой будет расположено двадцать два (22!) текстовых поля и один большой список с чекбоксами.
Я всегда руководствовался принципом, что большое количество полей на форме регистрации очень неприятно для пользователей, даже если они будут получать материальные бонусы по завершению. Поэтому я всегда старался сделать форму максимально простой, максимум в 4 поля, если это было возможно со стороны бизнеса (заказчика). И даже пренебрегал полем с капчей, использовав вместо нее скрытую js-капчу, или вовсе отказывался от нее. Но все попытки донести это заказчику были безуспешны.
Под хабракатом я попытаюсь создать максимально универсальное решение подобных задач
Первая мысль была о том, что пропали все полимеры, но еще немного подумав стало очевидно, что многие поля можно разбить по группам, а некоторые так вообще удалить. В итоге у меня получилось 4 формы:
- «регистрационные данные» (логин, пароль, емейл, телефон)
- «персональные данные» (дата рождения, пол, инициалы)
- «место работы» (сфера, должность, оклад :))
- «интересные ленты» (список чекбоксов)
- «завершение регистрации», вывести все введенные данные
Теперь это надо как-то оформить для пользователей, исключив возникновение маниакального импульса и немедленного закрытия окна от вида всех этих полей и форм. Лучше всего сделать заполнение полей поэтапно, форма за формой. И еще желательно сделать roadmap (карту), которая наглядно покажет, что предстоит заполнить, на каком этапе я сейчас и сколько мне еще осталось.
Погуглив я не нашел ничего подходящего и начал подумывать как приспособить готовый слайдер изображений (вместо изображений использовать div с полями). Но мне очень хотелось рисовать roadmap с ползунком, который по мере заполнения форм будет двигаться вперед, пока не дойдет до конца. И еще неплохо бы предусмотреть нелинейное движение ползунка — чтобы можно было перепрыгивать пункты, если совсем не хочется заполнять какую-то форму.
После экспериментов с разными слайдерами я полностью отказался от этой идеи – решил создать свой собственный, новенький и чудесный велосипедик. Пусть он будет с квадратными колесами, т.к. опыт разработки плагинов нулевой, зато будет на 100% удовлетворять задаче, не содержать ничего лишнего и, самое главное, возможно понадобиться еще кому-то.
Итак, обертка для плагина, пусть он будет называться Roadmap.
(function ($) {
$.fn.Roadmap = function (){
};
})(jQuery);
Отлично. Теперь добавим возможность настройки некоторых опций плагина (перед его инициализацией), предусмотрим несколько публичных методов и добавим замыкание, чтобы мы могли использовать функции jQuery для нашего объекта.
(function ($) {
$.Roadmap = $.Roadmap || {};
$.extend($.Roadmap, {
extend: function (methods) {
$.extend($.fn.Roadmap, methods);
$.fn.extend(methods);
}
});
//объявляем возможные настройки плагина
$.fn.Roadmap = function (options) {
var options = $.extend({
onInit: null,
allowJump: true,
voyagerSpeed: 300,
voyagerPosition: 0,
checkpoints: [],
onloadEvent: null,
ckeckpointNext: null,
checkpointPrev: null,
width: 400,
}, options);
//добавляем публичные методы
//используем $.Roadmap.extend, в самом верху
$.Roadmap.extend({
CurrentPosition: function () {
},
MoveNext: function () {
},
MovePrev: function () {
}
});
//замыкание, дает возможность использовать методыобъекты jQuery, например $.animate()
return this.each(function () {
//код плагина
});
};
})(jQuery);
Готово. Но код плагина как-то теряется, лучше вынести его в отдельное место, так будет более читабельно и не забыть описать публичные функции. И чтобы было совсем читабельно, все в спойлер.
(function ($) {
$.Roadmap = $.Roadmap || {};
$.extend($.Roadmap, {
extend: function (methods) {
$.extend($.fn.Roadmap, methods);
$.fn.extend(methods);
}
});
$.fn.Roadmap = function (options) {
var options = $.extend({
onInit: null, //подразумевается функция, вызов при инициализации
allowJump: true, //возможность нелинейно заполнять формы, т.е перепрыгивать
voyagerSpeed: 300, //скорость анимации ползунка при переходах между checkpoint
voyagerPosition: 0, //начальная позиция ползунка
checkpoints: [], //массив чекпоинтов и их callbacks
ckeckpointNext: null, //callback при прокрутке вперед (срабатывает при MoveNext())
checkpointPrev: null, //анаогично верхнему
width: 400, //ширина плагина в пикселях
}, options);
var $roadmap = $("<div>").addClass("roadmap"),
$voyager = $("<div>").addClass("voyager"),
voyagerPosition = options.voyagerPosition,
voyagerOffset = -1;
//private функции (она всего одна, но всеже)
var methods = {
determineWidth: function ($obj) {
var r = /[px|em]{2,}/g,
w = 0;
w += parseInt($obj.css("padding-left").replace(r, ""));
w += parseInt($obj.css("padding-right").replace(r, ""));
w += parseInt($obj.css("border-left-width").replace(r, ""));
w += parseInt($obj.css("border-right-width").replace(r, ""));
w += parseInt($obj.css("margin-left").replace(r, ""));
w += parseInt($obj.css("margin-right").replace(r, ""));
return w;
}
};
//основной код, инициализация плагина
var make = function () {
$(this)
.css("width", options.width)
.css("display", "none");
var $mark = $("<div>").addClass("mark"),
$map = $("<div>").addClass("map"),
$checkpoint = {};
$(options.checkpoints).each(function (i, o) {
$checkpoint = $("<div>").addClass("checkpoint");
$checkpoint
.append($("<div>"))
.click(function (e) {
if ((options.allowJump ||
!e.originalEvent) &&
$(e.target).closest(".voyager").length == 0) {
var ts = $(this),
tsOffset = ts.offset(),
rmOffset = $roadmap.offset();
voyagerPosition = i;
if (voyagerOffset < 0) {
voyagerOffset = $voyager.offset().left;
$voyager.css("left", voyagerOffset);
}
$voyager.animate({ left: (voyagerOffset + tsOffset.left - rmOffset.left - parseInt($map.css("padding-left"))) }, 400);
$("div.mark")
.find("div.marklabel").removeClass("active").end()
.find("div.marklabel:eq(" + i + ")").addClass("active");
if (o.hndl != null &&
typeof (o.hndl) === "function") {
o.hndl(voyagerPosition);
}
}
});
$map.append($checkpoint);
if (i < options.checkpoints.length - 1) {
$map.append($("<div>").addClass("road"))
$mark
.append($("<div>").addClass("marklabel").html(o.text))
.append($("<div>").addClass("road"));
}
else {
$map.append($("<div>").addClass("clear"));
$mark.append($("<div>").addClass("marklabel").html(o.text))
}
});
$roadmap
.append($map)
.append($mark);
$(this).append($roadmap);
var roadLength = 0,
checkpointsTotalLength = 0;
checkpointsTotalLength += methods.determineWidth($checkpoint.find("div"));
checkpointsTotalLength += methods.determineWidth($checkpoint);
roadLength = Math.floor((options.width - checkpointsTotalLength * 4) / 3);
roadLength -= methods.determineWidth($map);
roadLength -= methods.determineWidth($(".road", $map));
$map
.find(".road").width(roadLength).end()
.find(".checkpoint").eq(voyagerPosition).prepend($voyager).end();
$mark
.find(".marklabel:eq(" + voyagerPosition + ")").addClass("active").end()
.find(".marklabel").width(checkpointsTotalLength).end()
.find(".road").width(roadLength).end();
if (options.onInit != null &&
typeof (options.onInit) === "function") {
options.onInit();
}
$(this).show();
$map.find(".checkpoint").eq(voyagerPosition).trigger("click");
};
//public функции, о которых я говорил выше
$.Roadmap.extend({
CurrentPosition: function () {
return voyagerPosition;
},
MoveNext: function () {
if (voyagerPosition + 1 < options.checkpoints.length) {
++voyagerPosition;
$("div.roadmap .checkpoint:eq(" + voyagerPosition + ")").trigger("click");
if (typeof (options.ckeckpointNext) === "function") {
options.ckeckpointNext(voyagerPosition);
}
}
},
MovePrev: function () {
if (voyagerPosition - 1 >= 0) {
--voyagerPosition;
$("div.roadmap .checkpoint:eq(" + voyagerPosition + ")").trigger("click");
if (typeof (options.ckeckpointPrev) === "function") {
options.ckeckpointPrev(voyagerPosition);
}
}
}
});
return this.each(make);
};
})(jQuery);//Публичные методы
$.Roadmap.extend({
CurrentPosition: function () {
return voyagerPosition;
},
MoveNext: function () {
if (voyagerPosition + 1 < options.checkpoints.length) {
++voyagerPosition;
$("div.roadmap .checkpoint:eq(" + voyagerPosition + ")").trigger("click");
if (typeof (options.ckeckpointNext) === "function") {
options.ckeckpointNext(voyagerPosition);
}
}
},
MovePrev: function () {
if (voyagerPosition - 1 >= 0) {
--voyagerPosition;
$("div.roadmap .checkpoint:eq(" + voyagerPosition + ")").trigger("click");
if (typeof (options.ckeckpointPrev) === "function") {
options.ckeckpointPrev(voyagerPosition);
}
}
}
});
Осталось вставить его на страницу, например так
$("#rmp").Roadmap({
checkpoints:
[{ text: "Учетные данные", },
{ text: "Персональные данные}]
});
Плагин доступен на github
Демо можно увидеть здесь
PS
Для передвижения $voyager по карте я использовал $.animate(), отказавшись от использования CSS3. Причиной послужило странное поведение конструкции $.css("-webkit-transform", «translateX(100)») в одной из последних версий Chromium (28.0.1482.0 (194616)), хотя в следующим релизе все работало.
PSPS
Плагин, на текущий момент, не готов к промышленной эксплуатации и нуждается в доделке. Ошибки позиционирования в разных браузерах, плавная смена форм, использование шаблонизатора в массиве checkpoints, ajax подтягивание форм и т.д… Но уже не в этом году, улетаю в отпуск )
Всех с наступающим 2014!
Автор: pushthebutton