先上效果,带有搜索功能:
前端JS框架使用的是AngularJS.
一、从后台获取树形数据
获取到的树形结构是这个样子的,数组对象:
至于后台如何达到此效果,详情见:
Java递归获取树父节点下的所有树子节点
二、前端JS与html
- js中获取到树形数据
/**
* 文件名:organizationcontroller.js
* 路径:项目名/WebContent/xxx/scripts/controllers/platform/organizationcontroller.js
*/
//获取区域树
$scope.getDivisionTree=function () {
divisionInfoService.getDivisionInfoTree().then(function (data) {
if (data && data.statusCode == 200) {
$scope.divisionTree = [{
code: -1,
name: '全国',
children: data.result
}];
$scope.division = $scope.division || -1;
}
}, function (error) {
tipService.popup.error(error);
});
};
/*页面初始化*/
$scope.getDivisionTree();
- html页面中使用数据
<!--
文件名:organizationform.html
路径:项目名/WebContent/xxx/views/platform/projectsetting/organizationform.html
-->
<label>所在区域</label>
<div>
<search-box search-data="divisionTree" ng-search-model="organization.divisionCode" search-change="divisionChange(item)" search-value-text="code" search-label-name="name" ng-required="true" can-choose-parent="true">
</search-box>
</div>
那么这个<search-box>
标签是什么玩意?
标签里面的参数又是什么意思?是通过什么定义的?请继续往后看,在四、指令详细介绍。
先来简单说一下<search-box>
标签中的各个属性是什么意思:
search-data
表示数据源。在一、从后台获取树形数据中,不是从后台查到了整个树形结构嘛,然后在js中获取到树形数据中,使用$scope.divisionTree
这个变量接收了数据源,因此,html中的search-data
,对应的就是divisionTree
;
ng-search-model
AngularJS的特色之一,数据双向绑定。将当前的数据,与scope中的数据进行绑定。简单来说,只要页面的内容发生变化,那么对应的js中的变量值也就发生变化。在这里,ng-search-model
的值是:organization.divisionCode; 但为什么model是code? 获取到的属性,除了code
,还有id
、shortCode
、name
等等,为什么这里的model偏偏是code
?
search-value-text
那是因为:search-value-text="code"
;
search-label-name
同理,为什么效果图上,显示的是name
? 因为:search-label-name="name"
;
ng-required="true"
必选;
can-choose-parent="true"
可以选择父节点;
search-change="divisionChange(item)"
当值产生变动。
三、树形结构的html与css
<!--
文件名:searchbox.html
路径:项目名/WebContent/xxx/scripts/directives/searchbox/searchbox.html
-->
<div class="search-box-main">
<script type="text/ng-template" id="items_search.html">
<div ui-tree-handle>
<div class="tree-node-content" ng-click="node(this,item,$event)">
<a class="btn handletools expand" data-nodrag ng-click="toggle(this)">
<span class="fa fa-fw" ng-class="{'fa-plus-square-o': collapsed, 'fa-minus-square-o': !collapsed}" ng-show="item.children.length"></span>
</a>
<span class="itemTitle">{{item[ngLabelName]}} </span>
</div>
<ol ui-tree-nodes="options" ng-model="item.children" ng-class="{hidden: collapsed}">
<li ng-repeat="item in item.children | filter:ngText" class="ui-tree-child" ui-tree-node collapsed="true" ng-include="'items_search.html'" ng-show="visible(item)" data-nodrag="true">
</li>
</ol>
</div>
</script>
<div class="search-box-input-context clearfix">
<div class="search-icon search-down"><i class=" glyphicon glyphicon-chevron-down"></i></div>
<div class="search-box-input"><input type="text" readonly class="search-box-input-text" ng-model="ngTempText" name="code" ng-disabled="ngDisable"/></div>
</div>
<div class="search-box-pop">
<div class="search-box-input-context clearfix" ng-show="list&&list.length>0">
<div class="search-icon search-clear"><i class=" glyphicon glyphicon-remove"></i></div>
<div class="search-box-input"><input class="search-box-input-text" ng-model="ngText"/></div>
</div>
<div class="search-tree">
<div class="search-tree-content">
<div ui-tree="options">
<ol ui-tree-nodes data-ng-model="list" id="tree">
<li ng-repeat="item in list | filter:ngText" class="ui-parent" ui-tree-node ng-include="'items_search.html'" ng-show="visible(item)" data-nodrag="true" >
</li>
</ol>
</div>
</div>
</div>
</div>
<div class="error" ng-show="ngRequired&&!ngTempText">
<small class="error" ng-show="ngRequired&&!ngTempText">该项必填</small>
</div>
</div>
/*
文件名:searchbox.css
路径:项目名/WebContent/xxx/scripts/directives/searchbox/searchbox.css
*/
.search-box-main {
position: relative;
}
.search-box-main .search-box-input-context {
overflow: hidden;
height: 30px;
border: 1px solid #ccc;
-webkit-box-shadow: 0 0 0 rgba(0, 0, 0, 0) !important;
-moz-box-shadow: 0 0 0 rgba(0, 0, 0, 0) !important;
box-shadow: 0 0 0 rgba(0, 0, 0, 0) !important;
}
.search-box-main .search-box-input-context .search-icon {
float: right;
20px;
height: 100%;
line-height: 34px;
}
.search-box-main .search-box-input-context .search-icon:hover {
opacity: 0.5;
}
.search-box-main .search-box-input-context .search-box-input {
overflow: hidden;
}
.search-box-main .search-box-input-context .search-box-input .search-box-input-text {
100%;
display: inline-block;
height: 20px;
line-height: 20px;
margin: 5px 0;
padding: 0 0 0 5px;
border: none;
outline: none;
}
.search-box-main.active .search-box-input-context, .search-box-main.focus .search-box-input-context {
outline: 0;
border: 1px solid #66afe9;
-webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 8px rgba(102, 175, 233, .6);
box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 8px rgba(102, 175, 233, .6);
}
.search-box-pop {
display: none;
position: absolute;
100%;
overflow: hidden;
z-index: 999999;
padding-top: 40px;
}
.search-box-main.active .search-box-pop {
display: block;
border: 1px solid #66afe9;
background: #fff;
}
.search-box-main .angular-ui-tree-handle {
border: none;
cursor: pointer;
padding: 0;
}
.search-box-main .angular-ui-tree-handle .handletools.expand {
padding: 0;
position: static;
vertical-align: middle;
}
.search-box-main .angular-ui-tree-handle .itemTitle {
vertical-align: middle;
white-space: nowrap;
}
.search-box-main .search-box-input-context .search-clear .glyphicon {
display: none;
}
.search-box-main .search-box-input-context:hover .search-clear .glyphicon {
display: block;
vertical-align: middle;
line-height: 30px;
text-align: center;
}
.search-box-main.search-disabled .search-box-input-context:hover .search-clear {
display: none;
}
.search-box-main.search-disabled .search-box-input-context .search-icon:hover {
opacity: 1;
}
.search-box-main.search-disabled .search-box-input-context .search-box-input .search-box-input-text {
background-color: #eee;
}
.search-box-main.search-disabled .search-box-input-context {
background: #EEEEEE;
}
.search-box-main .angular-ui-tree-empty {
background: #66AFE9;
border: none;
min-height: 15px;
}
.search-box-pop > .search-box-input-context {
margin: 5px;
position: absolute;
top: 0;
left: 0;
z-index: 2;
}
.search-box-pop > .search-tree {
height: 100%;
overflow: auto;
}
.search-box-pop > .search-tree .search-tree-content li {
word-break: keep-all;
word-wrap: normal;
}
.search-box-pop .tree-node-content {
white-space: nowrap;
text-align: left;
}
.search-box-pop .angular-ui-tree-handle {
overflow: visible;
}
#dataCentersTree .search-box-height {
height: 200px;
}
#dataCentersTree #options {
overflow-y: auto;
height: 195px;
}
#dataCentersTree .search-box-pop {
padding-top: 5px;
}
#dataCentersTree .retract {
padding-left: 20px;
}
#dataCentersTree .handletools {
padding: 2px 4px;
}
四、指令
/**
* 文件名:searchbox.js
* 路径:项目名/WebContent/xxx/scripts/directives/searchbox/searchbox.js
*/
/*
* <search-box ng-search-model="inspectionKnowledgeBase.eqTypeCode" search-change="fn()" search-value-text="typeCode" search-label-name="typeName" search-data="service.equipmentTypes" name="code" required></search-box>
* search-data:当前数据源;
* search-label-name:当前下拉列表显示名称(在数据源中值的名称),默认情况下为name;
* search-value-text:当前下拉列表返回的值的名称(在数据源中值的名称),默认情况下为空;
* ng-search-model:数据绑定对象,当search-value-text为空时,返回当前model在数据源中的对象;
* can-choose-parent="true":是否可以点击父节点,默认为false。
* ng-required='true';是否必填;
* search-change="fn()";内容去选择后的回调
* */
cooleadCloud.directive('searchBox', ['$templateCache', '$http', '$compile', '$window', '$rootScope', '$timeout', '$safeapply', '$cookieStore',
function ($templateCache, $http, $compile, $window, $rootScope, $timeout, $safeapply, $cookieStore) {
return {
restrict: 'AE',
templateUrl: 'scripts/directives/searchbox/searchbox.html',
transclude: true,
scope: {
ngSearchData: "=searchData",
ngSearchModel: "=ngSearchModel",
ngLabelName: "@searchLabelName",
ngValueText: "@searchValueText",
ngCanChooseParent: "=canChooseParent",
ngChange: "&searchChange",
ngRequired: "=ngRequired",
ngDisable: "=ngDisabled",
ngEmpty: "=ngNotEmpty"
},
replace: false,
link: function ($scope, element, attrs) {
$scope.ngTempText = "";
$scope.ngEmpty = false;
$scope.isChoosed = true;
$scope.ngLabelName = $scope.ngLabelName || 'name';
var valText = angular.copy($scope.ngValueText), tempText = angular.copy($scope.ngLabelName);
var validateName = function (item, name) {
var te = null;
if (item) {
if (item.hasOwnProperty(name)) {
te = item[name];
} else {
te = item;
}
}
return te;
}
var iteatorValue = function (items, nv) {
var item;
if (items) {
for (var i = 0; i < items.length; i++) {
item = items[i];
if (nv == validateName(item, valText)) {
var text = validateName(item, tempText);
$scope.ngTempText = text;
if (text) {
$scope.ngEmpty = true;
}
} else {
if (item && item.hasOwnProperty('children')) {
var childrens = item.children;
if (childrens && childrens.length > 0) {
iteatorValue(childrens, nv);
}
}
}
}
}
return;
}
function filterData(nv) {
var tempList = angular.copy($scope.list);
if (tempList) {
var nItem;
for (var i = 0; i < tempList.length; i++) {
nItem = tempList[i];
if (nv == validateName(nItem, valText)) {
var text = validateName(nItem, tempText);
$scope.ngTempText = text;
if (text) {
$scope.ngEmpty = true;
}
} else {
if (nItem && nItem.hasOwnProperty('children')) {
iteatorValue(nItem.children, nv);
}
}
}
}
}
$scope.$watch("ngText", function (nv, ov) {
if (typeof nv === "undefined") {
return;
}
$("#tree").hide();
var li = $("#tree li");
angular.forEach(li, function (item) {
var span = $(item).find(".itemTitle");
for (var i = 0; i < span.length; i++) {
if ($(span[i]).text().indexOf(nv) !== -1) {
$(item).show();
break;
} else {
$(item).hide();
}
}
});
$("#tree").show();
});
if ($scope.ngSearchData) {
$scope.list = angular.copy($scope.ngSearchData);
}
/**
* add by likw 2016-05-31
* 监听数据源改变。
*/
$scope.$watch("ngSearchData", function (nv, ov) {
$scope.ngTempText = "";
$scope.list = angular.copy(nv);
});
var ngIndex = 0;
$scope.$watch('ngSearchData', function (nV, oV) {
if ((nV && nV.length > 0) && !ngIndex) {
ngIndex++;
$scope.list = angular.copy(nV);
filterData($scope.ngSearchModel);
}
});
var $element = $(element);
var $main = $('.search-box-main', $element);
var $searchIcon = $(".search-down", $element);
var $searchClear = $(".search-clear", $element);
var $searchInput = $(".search-box-input-text", $element);
$scope.$watch('ngSearchModel', function (nV, oV) {
if (!nV) {
$scope.ngTempText = "";
return;
}
if (nV != oV) {
$scope.ngTempText = "";
filterData(nV);
}
});
if ($scope.ngDisable) {
$main.addClass('search-disabled');
}
/*点击清空*/
$searchClear.off('click').on('click', function () {
var closest = $(this).closest('.search-box-main');
if (closest.hasClass('search-disabled')) {
return;
}
$safeapply($scope, function () {
$scope.ngText = '';
});
});
/*点击下拉事件*/
$searchIcon.off('click').on('click', function () {
var closest = $(this).closest('.search-box-main');
if (closest.hasClass('search-disabled')) {
return;
}
$main.addClass('active');
var $searchPop = closest.children('.search-box-pop').eq(0);
var $treeCon = $searchPop.children('.search-tree').eq(0);
var h = $treeCon.outerHeight(true);
if (h > 237) {
$searchPop.height(200);
}
});
$element.isSetHeight = false;
$searchInput.off('click').on('click', function () {
var closest = $(this).closest('.search-box-main');
if (closest.hasClass('search-disabled')) {
return;
}
$safeapply($scope, function () {
$scope.ngText = '';
});
$main.addClass('active');
var $searchPop = closest.children('.search-box-pop').eq(0);
var $treeCon = $searchPop.children('.search-tree').eq(0);
var h = $treeCon.outerHeight(true);
if (h > 237) {
$searchPop.height(200);
$element.isSetHeight = true;
}
});
/*非当前区域点击事件*/
$('body').on('click', function (e) {
var $target = $(e.target);
var $pTarget = $target.closest('.search-box-main');
if (!$pTarget[0]) {
$main.removeClass('active');
}
})
/*树状结构 开始*/
$scope.visible = function (item) {
return !($scope.query && $scope.query.length > 0
&& item.title.indexOf($scope.query) == -1);
};
$scope.toggle = function (scope) {
scope.toggle();
};
$scope.node = function (scope, item, $event) {
var isChoose = false;
$scope.isChoosed = true;
if ($scope.ngCanChooseParent) {
isChoose = true;
} else {
isChoose = !scope.hasChild()
}
if (isChoose && !angular.element($event.target).hasClass("fa")) {
$scope.ngSearchModel = validateName(item, valText);
$scope.ngTempText = validateName(item, tempText);
$main.removeClass('active');
$scope.ngChange({
item: item
});
$scope.isChoosed = false;
if ($scope.ngTempText) {
$scope.ngEmpty = true;
}
//是项目,项目树修改时,设置到cookies
if (item.shortCode && item.fullCode && item.divisionCode) {
$cookieStore.put('defaultProject', item);
}
}
var $target = $($event.target).closest(".expand");
if (!$element.isSetHeight && !isChoose && !scope.collapsed && $target && $target[0]) {
var $searchPop = $target.closest('.search-box-pop').eq(0);
var $treeCon = $searchPop.children('.search-tree').eq(0);
setTimeout(function () {
var h = $treeCon.outerHeight(true);
if (h > 237) {
$searchPop.height(200);
$element.isSetHeight = true;
}
}, 500);
}
};
/*树状结构 结束*/
}
};
}]);
最后,别忘了页面中引入相关的js和css.