JPickList - 一个基于 JQuery 的简单 PickList 插件
JPickList 是一个简单的 JQuery 插件,用于显示选择项的 Picklist。它还可以用于以 picklist 格式显示分层数据。
1. 引言
JPickList 是一个基于 JQuery 的 Picklist 插件。它可以用来显示项目选择列表(例如教科书的索引)。 JPickList 也可用于分层数据的分层显示。
下面是我们示例中的一个 Picklist 的样子。
在附带的源代码 zip 文件中,有两个 picklist - 第一个是一个常见的按字母顺序排列的(类似教科书索引)的城市列表(上方截图)。第二个 picklist 是一个示例分层数据显示(CalendarPicker)。此外,还包含了两个 JPickList javascript 文件(jquery.jpicklist.js(JQuery 插件版本)和 JPickList.js(简化版本,便于理解))。
2. 背景
在很多情况下,索引对于快速查找和选择我们所需的信息(例如在教科书中)都非常有用。此外,尽管有自动完成功能,但有时我们希望为我们的 Web 应用程序用户提供一些初始提示来开始使用 - 或提供一个选择他们所需信息的工具。我们试图通过 JPicklist 来解决这个问题,如上图所示。
此工具的另一个好用途是分层数据的分层显示。附带源代码的第二个示例显示了一个示例分层数据 - 索引(顶部菜单)首先显示月份,然后显示相应月份中的日期,最后在内容面板中显示当天的时段,这些都可以选择。由于索引(顶部菜单)是动态渲染的,我们可以根据层次结构动态更新顶部菜单来呈现分层数据。(如果只想显示内容而不是选择列表 - 我们可以简单地覆盖内容模板。然后我们可以绕过 setupCheckboxesJPickList 函数,因为我们只想显示内容而不是复选框。)
依赖:-
- 此插件是用 Javascript 编写的,需要 JQuery 和 Handlebar.js。
- 为了显示弹出窗口,我使用了 JQuery UI dialog。 然而,这个功能完全独立于 JPicklist 插件。JPicklist 插件默认只是将 Picklist 构建到一个 div 中并将其隐藏。然后我们可以以任何我们想要的方式渲染这个 Div(例如,我们只需将其渲染在页面上的某个区域/部分,它仍然会工作)。 在我们的示例代码中,我将 div 启动在一个模态对话框中。
- 我在这里使用了 .Net,但它不一定需要 .Net 服务器端。只要我们获得 Model 中的 JSON 格式数据,插件就应该可以工作。前端与 .Net 没有依赖关系。
如果我们愿意,可以简单地显示/隐藏页面某个区域的 div。要测试该功能,只需在页面 HTML 中取消注释 //$('#JPickListDiv').show(); 和 $('#JPickListDiv').hide(); 代码,并注释掉 .dialog("....") 代码,然后就可以看到它工作了。
3. 使用代码
3.1 配置视图/HTML:-
应包含以下 javascript 和 css:-
- 1. JQuery(例如,<script src="/Scripts/jquery-1.10.2.min.js"></script>)
- 2. Handlebar(<script src="/Scripts/handlebars.js"></script>)
- 3. jquery.JPicklist.js 或 JPicklist.js(<script src="/Scripts/jquery.jpicklist.js"></script>)
- 4. JPickList.css( <link href="/Content/JPickList.css" rel="stylesheet" />)
我使用 JQuery UI 将 picklist 渲染到对话框中。如前所述,将 div 渲染到弹出窗口或页面某个部分完全取决于我们的偏好。我们可以改用 jQModal 或任何其他插件来显示弹出窗口。下面是我用来显示弹出窗口的 JQuery UI 脚本。
- 5. JQuery UI javascript(<script src="/Scripts/jquery-ui.js"></script>)
- 6. JQuery UI Css(<link href="/Content/jquery-ui.min.css" rel="stylesheet" />,<link href="/Content/jquery-ui.theme.css" rel="stylesheet" /> 等)
我使用了 Visual Studio 环境提供的默认 Bootstrap CSS 来渲染页面。(这只是为了方便,我们当然可以使用自己的 CSS 来布局页面。)
JPickList 主要包含在两个文件中:“JPickList.css”和“jquery.jpicklis.js”(或“JPickList.js”,如果我们愿意)。JPickList.css 包含 JPicklist 容器的所有样式。jquery.jpicklist.js(或 JPicklist.js)是包含 JPicklist 实现的 Javascript。我们没有压缩 JS 文件。我们可以通过 .Net 优化或 grunt 来实现。在这里,我们保持简单。我们可以使用许多在线工具来压缩它。
3.1.a 配置 Jquery 插件
Jquery 插件版本与常规 jquery 插件的工作方式非常相似。我们通过像这样调用它来将其附加到任何元素。
$('#JPickListDiv').JPickList({ postUrl:'/Home/AllCitiList', onItemsAdded: addItemToPage, onCancel: handleOnCancel });
3.1.b 简化插件
在这里,我们更加明确。我们需要提供我们的 JPickListDiv 的 ID(以便 JQuery 可以选择它)以及其他参数。我包含此版本是为了帮助理解插件。一旦这个版本工作正常 - 我们就将其转换为上面的 JQuery 插件版本。
JPickList.init({ JPicklistDivSelector: '#JTimeSlotsPickListDiv', postUrl: '/Home/CalendarPicker', onItemsAdded: addItemToPage2, onCancel: handleOnCancel2 });
3.1.c 启动/显示 JPicklist
上面配置的 JPicklist 将在我们的页面 DOM 中准备好,但它将是隐藏的。我需要做的就是显示 JPicklist。如我们在示例中所述,我们像下面这样将 div 启动在一个 JQuery 对话框中:-
$(_jPiclListSelector).dialog({ modal: true, height: 400, width: 520 }).show();
或者我可以直接调用 div 的 show() 方法,它也能工作:-
$('#JPickListDiv').show();
4. JPicklist 的工作原理
下面是一个基本的序列图,解释了插件为特定 DIV 配置 JPicklist 所执行的活动顺序。
让我们按照上面图表中提到的顺序来看一下 JPicklist 的每个函数。
4.1 我们的 ViewModel(我们从服务器发送和接收的 JSON 格式)
我们从服务器接收的插件 ViewModel(或 Model)(以 JSON 格式)如下。我在这里使用的是 .Net,但它不一定需要 .Net 服务器端,它可以是任何后端 Java/PHP/Python 等,只要我们以以下格式获取 JSON 即可。
这是我们从服务器接收的数据的模型。
// JPickerListViewModel
{
Alphabet: "a",
IsFirstLoad: false,
ItemsCount: 565,
ItemsList: [{Key:AAH, Value:Aachen, IsSelected:false, DisplayText:Aachen},…],
PagingKeysList: [a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u, v, w, x, y, z],
SelectedKeys: "["HBL","BFJ"]"
}
ItemsList 中每个显示项的模型如下:-
//JPickerListItems
{Key:AAH,
Value:Aachen,
IsSelected:false,
DisplayText:Aachen}
非常直接,我们有当前的字母。IsFirstLoad 是一个可选的布尔参数,它在首次加载列表时设置。Selectedkeys 是一个字符串列表,它是在我们的 picklist 中选择的键的集合。然后我们有 ItemsList,我们的 JPicklist 在 Content DIV 中显示供我们选择项目。JPickListItem 的模型如上所示。PickListItem 非常像一个简单的 KeyValue 字典,带有一些额外的属性。DisplayText 是可选的,如果某些情况下我们的显示文本与 Value 不同。
所以下面是我们填充、序列化为 JSON 并从服务器每次请求返回的相应类(我们在示例中使用 .Net)。
//Each of our Item is modeled as below
public class JPickerListItems
{
public string Key { get; set; }
public string Value { get; set; }
public bool IsSelected { get; set; }
public string DisplayText { get; set; }
}
//Our viewmodel for Post requests is as below
public class JPickerListViewModel
{
public List<string> PagingKeysList { get; set; }
public List<JPickerListItems> ItemsList { get; set; }
public string Alphabet { get; set; }
public bool IsFirstLoad { get; set; }
public int ItemsCount { get; set; }
public string SelectedKeys { get; set; }
//this is just a helper property which I used to convert from string form to List of string.
//Not necessarily need to be included.
public List<string> _SelectedKeys {
get
{
if (!string.IsNullOrWhiteSpace(SelectedKeys))
{
return (SelectedKeys.Split(new char[] { ',' }).Select(x => x.Trim(new char[] { '"', ' ', '\\' })).ToList());
}
else
{
return (null);
}
}
}
}
其余的服务器端代码只是根据请求填充上述数据模型并返回。在第一个示例中,我们只是从文本文件中提取列表并根据请求进行过滤。在第二个示例中,我们只是用简单的代码后端构造了假数据。
我在这里使用 .Net 只是为了方便。该插件显然应该独立于 .Net 平台工作。服务器端可以运行 PHP/Java/Python/Node.js 或任何其他环境。只要我们获得上述格式的 JSON,我们就准备好了。
4.2 JPicklist 函数(或简化版本中的 init 函数)
一旦我们调用插件,第一个工作空间就是根(JPickList)函数。由于 Javascript 中的一切都是对象 - 我们的插件也是如此。因此,对于我们想要创建的每个独特的插件实例,我们都会创建一个本地的“settings”副本供使用。这个本地副本是通过将“options”(在初始化时提供给我们的插件)与本地“_defaults”合并而创建的。然后,我们将此本地“settings”副本传递给我们的插件其余配置以供使用。下面是我们帮助实现此目的的 JQuery 辅助函数:-
var settings = $.extend({}, this._defaults, options);
上面的 Jquery 辅助函数有助于安全地创建合并后的本地设置副本。我们不希望每次在页面中实例化 JPicklist 时都处理相同的设置对象副本,因为那样的话,一个 JPicklist 的数据和处理程序将与其他 JPicklist 的数据、处理程序重叠。上面的 extend 函数为我们创建了一个安全的“settings”副本供使用。
$.fn.JPickList = function(options) {
var _defaults = {
currentItems: [],
onItemsAdded: function (currentitems) {/*to be overridden */ /*console.log(currentitems);*/ },
onItemsChanged: function (currentitems) {/*to be overridden */ /*console.log(currentitems);*/ },
onCancel: function (currentitems) {/*to be overridden */ /* console.log(currentitems);*/ },
clearOnCancel:false,
JPicklistDivSelector: '#JPickListDiv',
postUrl: ''};
//If No currentItems were passed in so lets re-initialize the currentItems
if (!options.currentItems) {
options.currentItems = [];
}
var settings = $.extend({}, this._defaults, options);
var $t = $(this);
settings.JPicklistDivSelector = $t;
$.fn.JPickList.setupJPickList(settings);
$.fn.JPickList.buildJPickList(settings, {});
$.fn.JPickList.setupJPickListButtons(settings);
//Setting up html is complete. Lets hide the div now
$(settings.JPicklistDivSelector).hide();
};
4.3 设置 JPicklist HTML(setupJPickList)
我们首先构建 JPicklist 的 HTML 框架。下面是我们用来创建 HTML 的函数。这是非常简单的 HTML。有一个“jqmAlphabetPanelDiv”(顶部菜单),然后是内容 Div(JPickListItemsDiv),然后是包含两个按钮“Cancel”和“Add Items”(JPickListButtons)的 div。然后我们有一个隐藏字段(hdnSelectedItemVals)来存储每个 JPicklist 实例内的选定键。
$.fn.JPickList.setupJPickList = function (settings) {
//Sets up the HTML of the JPickList DIV
var jPicklistTemplate = '<div class="jqmAlphabetPanelDiv"></div>';
jPicklistTemplate += '<div class="JPickListItemsDiv"></div>';
jPicklistTemplate += '<div class="JPickListButtons">';
jPicklistTemplate += '<a href="javascript:void(0)" class="cancel"><span>Cancel</span></a>';
jPicklistTemplate += '<a href="javascript:void(0)" class="addItems"><span>Add Items</span></a>';
jPicklistTemplate += '</div>';
jPicklistTemplate += "<input type=\"hidden\" value='' class=\"hdnSelectedItemVals\" />";
$(settings.JPicklistDivSelector).html(jPicklistTemplate);
};
4.4 构建 JPicklist 顶部菜单和内容(buildJPickList)*
接下来我们调用这个函数(buildJPickList) - 这里是与服务器通信发生的地方。我们通过构建符合上述 JSON 数据模型格式的 ViewModel 来发出 AJAX 请求到服务器。
从服务器收到响应后,该函数将调用下面的两个重要函数,首先配置顶部菜单(setupJPickListAlphaLinks),然后配置内容复选框(setupCheckboxesJPickList)。
$.fn.JPickList.buildJPickList = function (settings, postdata) {
//Fetches (AJAX) the Alphabet and Content to render jPicklist
$.ajax({
url: settings.postUrl,
type: "POST",
data: postdata,
success: function (data) {
// console.log('build contents');
// console.log(settings);
//Setting up the alphabet links table for the picklist
var alphaTmplt = '<ul class="tblJPickListAlphabets">{{#.}}<li><a href="{{.}}" class="JPickListAlphaLink">{{.}}</a></li>{{/.}}</ul>';
var templ = Handlebars.compile(alphaTmplt);
var alphHtml = templ(data.PagingKeysList);
$(settings.JPicklistDivSelector).children('.jqmAlphabetPanelDiv:first').html(alphHtml);
$.fn.JPickList.setupJPickListAlphaLinks(settings);
//Setting up the Picklist contents now
var templHtml = '<ul class="picklistItems">{{#ItemsList}}<li><label class="normaltextblue"><input type="checkbox" class="chkJPickList" {{#IsSelected}}checked{{/IsSelected}} value="{{Key}}"/> {{DisplayText}}</label></li>{{/ItemsList}}</ul>';
var template = Handlebars.compile(templHtml);
var htmlstr = template(data);
$(settings.JPicklistDivSelector).children('.JPickListItemsDiv:first').html(htmlstr);
$.fn.JPickList.setupCheckboxesJPickList(settings);
},
error: function (xhr, ajaxOptions, thrownError) {
//what to do in error
$(settings.JPicklistDivSelector).children('.JPickListItemsDiv:first').html('Something went wrong. Please retry again! Error Details:'+JSON.stringify(xhr));
},
timeout: 7000//timeout of the ajax call
});
};
4.4.1 设置顶部菜单超链接(setupJPickListAlphaLinks)
此函数设置顶部菜单链接。请注意,我们希望确保只向超链接附加一个“click”事件处理程序 - 这样每次点击只会触发一个 buildJPickList。否则,它可能会触发多次 AJAX 请求,并且我们会有另一个处理程序绑定到点击事件。这就是为什么我们解绑 'click'(off('click', '.JPickListAlphaLink'))然后重新绑定(on('click', '.JPickListAlphaLink', function(....)) - 以确保我们只有一个处理程序。下面是我们绑定顶部菜单链接的代码。
$.fn.JPickList.setupJPickListAlphaLinks = function (settings) {
//Sets up the click behavior of Alphalinks (top menu)
$(settings.JPicklistDivSelector).off('click', '.JPickListAlphaLink').on('click', '.JPickListAlphaLink', function (event) {
event.preventDefault();
var _letter = $(this);
//_alphabets.removeClass("active");
_letter.addClass("active");
var ur = $(this).attr('href');
var selItms = eval($(settings.JPicklistDivSelector).children('.hdnSelectedItemVals:first').val());
var pdata = { Alphabet: ur, SelectedKeys: JSON.stringify(selItms) };
$.fn.JPickList.buildJPickList(settings, pdata);
return false;
});
};
4.4.2 设置内容 Div 中的复选框(setupCheckboxesJPickList)
此函数设置内容 Div 中的复选框。我们正在做的是:
- 对于每个选中的项目,我们将项目推入一个数组。
- 对于每个取消选中的项目,我们将其从中弹出数组。
- 我们将其保存到隐藏字段(hdnSelectedItemVals)。
- 然后我们调用 (onItemsChanged) 函数(如果提供了)。
除此之外 - 在加载时,我们只是根据现有键来选中和取消选中复选框。
$.fn.JPickList.setupCheckboxesJPickList = function (settings) {
//Sets up the Checkboxes click behavior in the content Div
if ($(settings.JPicklistDivSelector).children('.hdnSelectedItemVals:first').val()) {
settings.currentItems = JSON.parse($(settings.JPicklistDivSelector).children('.hdnSelectedItemVals:first').val());
}
//fixing the display of checkboxes on load of PickList
$(settings.JPicklistDivSelector).children('.chkJPickList').each(function () {
if ((settings.currentItems) && (settings.currentItems.length > 0)) {
var _val = $(this).val();
for (var tmp = 0; tmp < settings.currentItems.length; tmp++) {
if (_val === settings.currentItems[tmp]) {
this.setAttribute("checked", "");
this.checked = true;
//console.log('checked: ['+_val+']');
}
}
}
});
//attach the click event
$(settings.JPicklistDivSelector).off('click','.chkJPickList').on('click','.chkJPickList', function (ev) {
var _val = $(this).val();
if (this.checked) {
if ($.inArray(_val, settings.currentItems) == -1) {
settings.currentItems.push(_val);
}
$($(settings.JPicklistDivSelector).children('.hdnSelectedItemVals:first')).val(JSON.stringify(settings.currentItems));
}
else {
var itmidx = $.inArray(_val, settings.currentItems);
if ($.inArray(_val, settings.currentItems) > -1) {
settings.currentItems.splice(itmidx, 1);
$($(settings.JPicklistDivSelector).children('.hdnSelectedItemVals:first')).val(JSON.stringify(settings.currentItems));
}
}
if (settings.onItemsChanged) {
settings.onItemsChanged(settings.currentItems);
}
});
};
4.5 配置 JPicklist 操作按钮“添加项目和取消”(setupJPickListButtons)
此函数仅附加在初始化 JPicklist 时在“options”中提供的相应处理程序。调用以下两个处理程序。
- “.addItems”链接按钮调用“settings.onItemsAdded”,
- “.cancel”链接按钮调用“settings.onCancel”。
下面是绑定这些按钮(被装饰为按钮的超链接)的点击事件的代码:-
$.fn.JPickList.setupJPickListButtons = function (settings) {
//Sets up the buttons "Cancel" and "Add Items" inside the JPickList
$(settings.JPicklistDivSelector).off('click', ' .JPickListButtons > .addItems').on('click', ' .JPickListButtons > .addItems', function () {
//add selected item to jcrossList
settings.onItemsAdded(settings.currentItems);
//$.fn.JPickList.clearValues(settings);
});
$(settings.JPicklistDivSelector).off("click", " .JPickListButtons > .cancel").on("click", " .JPickListButtons > .cancel", function () {
//cancel the selections and close the window
if (settings.clearOnCancel) {
$.fn.JPickList.clearValues(settings);
}
settings.onCancel(settings.currentItems);
});
};
4.6 服务器端
(我在这里使用 .Net 只是为了方便。服务器端可以是任何语言 Java/PHP/Python/Node.js,只要我们以上述数据模型中概述的 JSON 格式获取数据 - JPicklist 插件就应该可以工作。)
我们有两个 ViewModel 类(如上所述)。 为了填充城市列表,我从文本文件中读取。
private JPickerListViewModel LoadCities(JPickerListViewModel model)
{
model.ItemsList = new List<JPicklist.Controllers.JPickerListItems>();
model.ItemsCount = 0;
Dictionary<string, string> cd = JPicklist.Models.AirportCodes.Instance().ReadAllCodes();
var fltcd = cd.Where(item=> item.Value.Trim().StartsWith(model.Alphabet,StringComparison.InvariantCultureIgnoreCase));
foreach (var item in fltcd)
{
var itmToAdd = new JPickerListItems() { Key = item.Key, Value = item.Value, DisplayText = item.Value, IsSelected = false };
if (((model._SelectedKeys != null) && (model._SelectedKeys.Count > 0))&&(model._SelectedKeys.Contains(itmToAdd.Key)))
{
itmToAdd.IsSelected = true;
}
model.ItemsList.Add(itmToAdd);
model.ItemsCount++;
}
return model;
}
为了填充日历,我只是手动创建它。
private JPickerListViewModel LoadTimeSlots(JPickerListViewModel model)
{
model.ItemsList = new List<JPicklist.Controllers.JPickerListItems>();
model.ItemsCount = 0;
//when no month selected show the Months and return
//also provide a back button to go back to months display
if ((string.IsNullOrWhiteSpace(model.Alphabet)) || (model.Alphabet.Equals("back", StringComparison.InvariantCultureIgnoreCase)))
{
model.PagingKeysList = months.Select(x => x).ToList();
return model;
}//Else if we found a calendar month in the Alphabet
else if(months.Where(x=>x.Equals(model.Alphabet,StringComparison.InvariantCultureIgnoreCase)).Count()>0)
{
int numDays= daysinmonth[Array.IndexOf(months,model.Alphabet)];
model.PagingKeysList = new List<string>();
for (int tmp = 1; tmp <= numDays; tmp++)
{
model.PagingKeysList.Add(model.Alphabet+"-"+tmp.ToString());
}
model.PagingKeysList.Add("back");
}
else
{
model.PagingKeysList = new List<string>();
model.PagingKeysList.Add("back");
var itmlist = timeslots.Select(y => new JPickerListItems() { Key = model.Alphabet + "-" + y, Value = model.Alphabet + "-" + y, DisplayText = y, IsSelected = ((model._SelectedKeys != null) && (model._SelectedKeys.Count > 0) && (model._SelectedKeys.Contains(model.Alphabet+"-"+y))) }).ToList();
foreach (var item in itmlist)
{
model.ItemsList.Add(item);
}
}
return model;
}
5. 历史记录
2014 年 11 月 13 日:首次发布。