概述
最近,有客户向我们请求开发一个前端下拉控件,需求是显示了一个列表,其中包含可由用户单独选择的项目控件,该控件将在下拉列表中显示多选TreeView(树形图)。
如今WijmoJS已经实现了该控件——DropDownTree,本文将主要介绍如何创建自定义DropDownTree控件以及其源代码。
DropDownTree 控件源代码
HTML
<div class="container"> <h1> DropDownTree Control </h1> <p> The <b>DropDownTree</b> control is similar to a <b>MultiSelect</b>, but it hosts a <b>TreeView</b> in the drop-down instead of a <b>ListBox</b>.</p> <p> The <b>DropDownTree</b>'s object model is also similar to the <b>MultiSelect</b>'s: you can listen to the <b>checkedItemsChanged</b> event and get/set the selection using the <b>checkedItems</b> property:</p> <h3> Drop-Down-Tree </h3> <input id="ddTree" placeholder="multi tree"> <h3> Multi-Select </h3> <input id="multiSelect" placeholder="multi select"></div>
JavaScript
= function() { // create the DropDownTree var ddTree = new wijmo.input.DropDownTree('#ddTree', { displayMemberPath: 'header', childItemsPath: 'items', showCheckboxes: true, itemsSource: getTreeData(), checkedItemsChanged: function (s, e) { console.log('dropDownTree.checkedItemsChanged:'); s.checkedItems.forEach(function (item, index) { console.log(index, item[s.displayMemberPath]) }) } }); // create the MultiSelect var multiSelect = new wijmo.input.MultiSelect('#multiSelect', { itemsSource: 'Austria,Belgium,Chile,Denmark'.split(','), checkedItemsChanged: function (s, e) { console.log('multiSelect.checkedItemsChanged:'); s.checkedItems.forEach(function (item, index) { console.log(index, item) }) } }); // get the tree data function getTreeData() { return [ { header: 'Electronics', img: 'resources/electronics.png', items: [ { header: 'Trimmers/Shavers' }, { header: 'Tablets' }, { header: 'Phones', img: 'resources/phones.png', items: [ { header: 'Apple' }, { header: 'Motorola', newItem: true }, { header: 'Nokia' }, { header: 'Samsung' } ]}, { header: 'Speakers', newItem: true }, { header: 'Monitors' } ]}, { header: 'Toys', img: 'resources/toys.png', items: [ { header: 'Shopkins' }, { header: 'Train Sets' }, { header: 'Science Kit', newItem: true }, { header: 'Play-Doh' }, { header: 'Crayola' } ]}, { header: 'Home', img: 'resources/home.png', items: [ { header: 'Coffeee Maker' }, { header: 'Breadmaker', newItem: true }, { header: 'Solar Panel', newItem: true }, { header: 'Work Table' }, { header: 'Propane Grill' } ]} ]; } } // DropDownTree: transpiled TypeScript var __extends = (this && this.__extends) || (function () { var extendStatics = Object.setPrototypeOf || ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; }; return function (d, b) { extendStatics(d, b); function __() { this.constructor = d; } d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); }; })(); var wijmo; (function (wijmo) { var input; (function (input) { var DropDownTree = /** @class */ (function (_super) { __extends(DropDownTree, _super); /** * Initializes a new instance of the @see:DropDownTree class. * * @param element The DOM element that hosts the control, or a CSS selector for the host element (e.g. '#theCtrl'). * @param options The JavaScript object containing initialization data for the control. */ function DropDownTree(element, options) { var _this = _super.call(this, element) || this; _this._maxHdrItems = 2; _this._hdrFmt = wijmo.culture.MultiSelect.itemsSelected; /** * Occurs when the value of the @see:checkedItems property changes. */ _this.checkedItemsChanged = new wijmo.Event(); wijmo.addClass(_this.hostElement, 'wj-dropdowntree'); // make header element read-only _this._tbx.readOnly = true; // toggle drop-down when clicking on the header element // (and not on a containing label element) _this.addEventListener(_this.inputElement, 'click', function (e) { if (document.elementFromPoint(e.clientX, e.clientY) == _this.inputElement) { _this.isDroppedDown = !_this.isDroppedDown; } }); // update header now, when the itemsSource changes, and when items are selected _this._updateHeader(); var tree = _this._tree; tree.checkedItemsChanged.addHandler(function () { _this._updateHeader(); _this.onCheckedItemsChanged(); }); tree.selectedItemChanged.addHandler(function () { if (!tree.showCheckboxes) { _this._updateHeader(); _this.onCheckedItemsChanged(); } }); tree.loadedItems.addHandler(function () { _this._updateHeader(); }); // close tree on enter/escape tree.addEventListener(tree.hostElement, 'keydown', function (e) { switch (e.keyCode) { case wijmo.Key.Enter: case wijmo.Key.Escape: _this.isDroppedDown = false; break; } }); // initialize control options _this.initialize(options); return _this; } Object.defineProperty(DropDownTree.prototype, "treeView", { /** * Gets the @see:TreeView control shown in the drop-down. */ get: function () { return this._tree; }, enumerable: true, configurable: true }); Object.defineProperty(DropDownTree.prototype, "itemsSource", { /** * Gets or sets the array that contains the @see:TreeView items. * * @see:TreeView #see:itemsSource arrays usually have a hierarchical * structure with items that contain child items. There is no fixed * limit to the depth of the items. * * For details, see the @see:TreeView.itemsSource property. */ get: function () { return this._tree.itemsSource; }, set: function (value) { this._tree.itemsSource = value; }, enumerable: true, configurable: true }); Object.defineProperty(DropDownTree.prototype, "displayMemberPath", { /** * Gets or sets the name of the property (or properties) to use as * the visual representation of the nodes. * * The default value for this property is the string 'header'. * * For details, see the @see:TreeView.displayMemberPath property. */ get: function () { return this._tree.displayMemberPath; }, set: function (value) { this._tree.displayMemberPath = value; }, enumerable: true, configurable: true }); Object.defineProperty(DropDownTree.prototype, "childItemsPath", { /** * Gets or sets the name of the property (or properties) that contains * the child items for each node. * * The default value for this property is the string 'items'. * * For details, see the @see:TreeView.childItemsPath property. */ get: function () { return this._tree.childItemsPath; }, set: function (value) { this._tree.childItemsPath = value; }, enumerable: true, configurable: true }); Object.defineProperty(DropDownTree.prototype, "showCheckboxes", { /** * Gets or sets a value that determines whether the @see:TreeView should * add checkboxes to nodes and manage their state. * * For details, see the @see:TreeView.showCheckboxes property. */ get: function () { return this._tree.showCheckboxes; }, set: function (value) { this._tree.showCheckboxes = value; }, enumerable: true, configurable: true }); Object.defineProperty(DropDownTree.prototype, "checkedItems", { /** * Gets or sets an array containing the items that are currently checked. */ get: function () { var tree = this._tree; if (tree.showCheckboxes) { return tree.checkedItems; } else { return tree.selectedItem ? [tree.selectedItem] : []; } }, set: function (value) { var tree = this._tree; if (tree.showCheckboxes) { tree.checkedItems = wijmo.asArray(value); } else { tree.selectedItem = wijmo.isArray(value) ? value[0] : value; } }, enumerable: true, configurable: true }); Object.defineProperty(DropDownTree.prototype, "maxHeaderItems", { /** * Gets or sets the maximum number of items to display on the control header. * * If no items are selected, the header displays the text specified by the * @see:placeholder property. * * If the number of selected items is smaller than or equal to the value of the * @see:maxHeaderItems property, the selected items are shown in the header. * * If the number of selected items is greater than @see:maxHeaderItems, the * header displays the selected item count instead. */ get: function () { return this._maxHdrItems; }, set: function (value) { if (this._maxHdrItems != value) { this._maxHdrItems = wijmo.asNumber(value); this._updateHeader(); } }, enumerable: true, configurable: true }); Object.defineProperty(DropDownTree.prototype, "headerFormat", { /** * Gets or sets the format string used to create the header content * when the control has more than @see:maxHeaderItems items checked. * * The format string may contain the '{count}' replacement string * which gets replaced with the number of items currently checked. * The default value for this property in the English culture is * '{count:n0} items selected'. */ get: function () { return this._hdrFmt; }, set: function (value) { if (value != this._hdrFmt) { this._hdrFmt = wijmo.asString(value); this._updateHeader(); } }, enumerable: true, configurable: true }); Object.defineProperty(DropDownTree.prototype, "headerFormatter", { /** * Gets or sets a function that gets the HTML in the control header. * * By default, the control header content is determined based on the * @see:placeholder, @see:maxHeaderItems, and on the current selection. * * You may customize the header content by specifying a function that * returns a custom string based on whatever criteria your application * requires. */ get: function () { return this._hdrFormatter; }, set: function (value) { if (value != this._hdrFormatter) { this._hdrFormatter = wijmo.asFunction(value); this._updateHeader(); } }, enumerable: true, configurable: true }); /** * Raises the @see:checkedItemsChanged event. */ DropDownTree.prototype.onCheckedItemsChanged = function (e) { this.checkedItemsChanged.raise(this, e); }; //** overrides // switch focus to the tree when the drop-down opens DropDownTree.prototype.onIsDroppedDownChanged = function (e) { if (this.containsFocus() && this.isDroppedDown) { this._tree.focus(); } _super.prototype.onIsDroppedDownChanged.call(this, e); }; // create the drop-down element DropDownTree.prototype._createDropDown = function () { // create child TreeView control var lbHost = wijmo.createElement('<div style="width:100%;border:none"></div>', this._dropDown); this._tree = new wijmo.nav.TreeView(lbHost, { showCheckboxes: true, }); // let base class do its thing _super.prototype._createDropDown.call(this); }; Object.defineProperty(DropDownTree.prototype, "isReadOnly", { // override since our input is always read-only get: function () { return this._readOnly; }, set: function (value) { this._readOnly = wijmo.asBoolean(value); wijmo.toggleClass(this.hostElement, 'wj-state-readonly', this.isReadOnly); }, enumerable: true, configurable: true }); // update header when refreshing DropDownTree.prototype.refresh = function (fullUpdate) { if (fullUpdate === void 0) { fullUpdate = true; } _super.prototype.refresh.call(this, fullUpdate); this._updateHeader(); }; //** implementation // update the value of the control header DropDownTree.prototype._updateHeader = function () { // get selected items var items = this.checkedItems; // update the header if (wijmo.isFunction(this._hdrFormatter)) { this.inputElement.value = this._hdrFormatter(); } else { var hdr = ''; if (items.length > 0) { if (items.length <= this._maxHdrItems) { if (this.displayMemberPath) { var binding_1 = new wijmo.Binding(this.displayMemberPath); items = items.map(function (item) { return binding_1.getValue(item); }); } hdr = items.join(', '); } else { hdr = wijmo.format(this.headerFormat, { count: items.length }); } } this.inputElement.value = hdr; } // update wj-state attributes this._updateState(); }; return DropDownTree; }(input.DropDown)); input.DropDownTree = DropDownTree; })(input = wijmo.input || (wijmo.input = {})); })(wijmo || (wijmo = {})); //# sourceMappingURL=DropDownTree.js.map
CSS
body {margin-bottom: 24pt; }
控件准备就绪后,它将如下所示:
本控件使用两个独立的WijmoJS模块:输入和导航。所需的步骤与开发MultiSelect控件时所采用的步骤相同:
选择基类
在这种场景下,我们可以将DropDown控件进行扩展,该控件包含使用下拉按钮实现输入元素所需的所有逻辑以及可用于托管任何控件的通用下拉列表。 DropDown控件用作ComboBox,InputColor和InputDate控件的基类。
定义对象模型
因为DropDownTree控件在其下拉列表中托管TreeView,所以我们决定直接从DropDownTree公开TreeView控件的主要属性:
TreeView获取对下拉列表中显示的TreeView控件的引用。
ItemsSource获取或设置对用于填充TreeView的对象数组的引用。
DisplayMemberPath获取或设置用作项目可视化表示的属性名称(默认为“header”)。
ChildItemsPath获取或设置包含数据源中每个项的子项的属性的名称(默认为“items”)。
ShowCheckboxes获取或设置一个值,该值确定控件是否应为每个项添加复选框,以便用户可以选择多个项(默认为true)。
我们还添加了一些额外的属性和事件来定义当前选择以及它在控制头中的表示方式。这些属性镜像MultiSelect控件中的相应属性:
CheckedItems获取或设置包含当前所选项目的数组。
CheckedItemsChanged是CheckedItems属性值更改时发生的事件。
MaxHeaderItems是控件头中显示的最大选定项数。
当控件具有超过*maxHeaderItems项目选项时,headerFormat获取或设置用于创建标题内容的格式字符串。
HeaderFormatter获取或设置一个函数,该函数获取控件头中显示的文本。 这将覆盖maxHeaderItems和headerFormat属性的设置。
实现控件
我们首先将控件声明为基类的扩展:
namespace wijmo.input { export class DropDownTree extends DropDown { } }
“extendsDropDown”语句确保我们的控件继承基本DropDown类的所有功能,包括属性,事件,方法和所有内部/私有成员。
创建树视图
接下来,我们覆盖DropDown类中的_createDropDown方法,以创建将在下拉列表中显示的TreeView控件。
除了创建TreeView之外,我们还会覆盖onIsDroppedDownChanged方法,以便在下拉列表打开且控件具有焦点时将焦点转移到树。 这允许用户使用键盘导航树。 他们可以通过键入内容来搜索项目,通过按空格键来检查项目,或使用光标键导航树。
namespace wijmo.input { export class DropDownTree extends DropDown { private _tree: wijmo.nav.TreeView; // create the drop-down element protected _createDropDown() { // create child TreeView control let lbHost = document.createElement('div'); setCss(lbHost, { width: '100%', border: 'none' }); this._tree = new wijmo.nav.TreeView(lbHost, { showCheckboxes: true }); } // switch focus to the tree when the drop-down opens onIsDroppedDownChanged(e?: EventArgs) { if (this.containsFocus() && this.isDroppedDown) { this._tree.focus(); } super.onIsDroppedDownChanged(e); } } }
公开TreeView及其属性
下一步是添加公开TreeView及其主要属性:
namespace wijmo.input { export class DropDownTree extends DropDown { private _tree: wijmo.nav.TreeView; get treeView(): wijmo.nav.TreeView { return this._tree; } get itemsSource(): any[] { return this._tree.itemsSource; } set itemsSource(value: any[]) { this._tree.itemsSource = value; } // same for displayMemberPath, childItemsPath, // and showCheckboxes // create the drop-down element protected _createDropDown() {…} } }
这些属性只是获取或设置包含的TreeView上的相应属性的快捷方式。 因此,它们非常简单,我们甚至不启用类型检查,因为TreeView将为我们处理。
CheckedItems属性
控件的主要属性是CheckedItems,它用来表示用户当前已获取和自定义的数组。 我们可以用它实现上面那样的传递属性,也可以实现多选和单选功能。比如想实现其单选功能时,我们需要检查ShowCheckboxes属性的值并使用树的checkedItems或selectedItem属性。
除了CheckedItems属性,我们还实现了checkedItemsChanged事件及其伴随方法onCheckedItemsChanged。 这是WijmoJS事件的标准模式。 每个事件X都有一个相应的onX方法,负责触发事件。
namespace wijmo.input { export class DropDownTree extends DropDown { private _tree: wijmo.nav.TreeView; // TreeView pass-through properties… get checkedItems(): any[] { let tree = this._tree; if (tree.showCheckboxes) { return tree.checkedItems; } else { return tree.selectedItem ? [tree.selectedItem] : []; } } set checkedItems(value: any[]) { let tree = this._tree; if (tree.showCheckboxes) { tree.checkedItems = asArray(value); } else { tree.selectedItem = isArray(value) ? value[0] : value; } } readonly checkedItemsChanged = new Event(); onCheckedItemsChanged(e?: EventArgs) { this.checkedItemsChanged.raise(this, e); } // create the drop-down element protected _createDropDown() {…} }
请注意,即使在单个选择的情况下,checkedItems属性也会返回一个数组(该数组为空或包含单个元素)。
更新控件头
这里不会重点讨论maxHeaderItems,headerFormat或headerFormatter属性的实现方式,因为它们很简单。我们需要将目光聚焦在_updateHeader函数的逻辑中,该函数使用这些属性,并在其值或选择更改时自动调用以更新控件头:
namespace wijmo.input { export class DropDownTree extends DropDown { private _tree: wijmo.nav.TreeView; // TreeView pass-through properties… // checketItems property… private _updateHeader() { let items = this.checkedItems; if (isFunction(this._hdrFormatter)) { this.inputElement.value = this._hdrFormatter(); } else { let hdr = ''; if (items.length > 0) { if (items.length <= this._maxHdrItems) { if (this.displayMemberPath) { let dmp = this.displayMemberPath, binding = new Binding(dmp); items = items.map((item) => { return binding.getValue(item); }); } hdr = items.join(', '); } else { hdr = format(this.headerFormat, { count: items.length }); } } this.inputElement.value = hdr; } } // create the drop-down element protected _createDropDown() {…} } }
构造函数
到此为止,我们几乎已经完成了控件架构。最后一步是实现构造函数,该构造函数将部件与事件侦听器连接,并调用initialize方法以使用options参数中的用户提供的值初始化属性和事件处理程序:
namespace wijmo.input { export class DropDownTree extends DropDown { private _tree: wijmo.nav.TreeView; private _readOnly: boolean; private _maxHdrItems = 2; private _hdrFmt = wijmo.culture.MultiSelect.itemsSelected; private _hdrFormatter: Function; constructor(element: HTMLElement, options?: any) { super(element); addClass(this.hostElement, 'wj-dropdowntree'); // make header element read-only this._tbx.readOnly = true; // update header now, when the itemsSource changes, // and when items are selected this._updateHeader(); let tree = this._tree; tree.checkedItemsChanged.addHandler(() => { this._updateHeader(); this.onCheckedItemsChanged(); }); tree.selectedItemChanged.addHandler(() => { if (!tree.showCheckboxes) { this._updateHeader(); this.onCheckedItemsChanged(); } }); tree.loadedItems.addHandler(() => { this._updateHeader(); }); // initialize control options this.initialize(options); } // TreeView pass-through properties… // checketItems property… // _updateHeader implementation… // _createDropDown implementation… } }
测试控件
现在控件已准备好,我们可以测试它,并检查它是否按照我们想要的方式运行。
运行DropDownTree 控件源代码,单击下拉按钮以打开TreeView。 打开后,单击几个项目以选择它们,并注意控件头的更新方式:
我们由衷希望DropDownTree控件对您产生帮助。更重要的是,我们希望您现在可以放心地将DropDown控件扩展为托管其他类型的元素,同时创建自己的自定义控件。
WijmoJS:灵活高效的前端开发工具包,可快速搭建企业 Web 应用程序