概述
最近,有客戶向我們請求開發一個前端下拉控件,需求是顯示了一個列表,其中包含可由用戶單獨選擇的項目控件,該控件將在下拉列表中顯示多選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
onload = 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控件擴展爲託管其他類型的元素,同時創建自己的自定義控件。