cascader.js 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525
  1. /**
  2. * 级联选择器模块
  3. * date:2019-07-27 License By http://easyweb.vip
  4. */
  5. layui.define(function (exports) {
  6. var $ = layui.jquery;
  7. layui.link(layui.cache.base + 'cascader/cascader.css');
  8. var onVisibleChangeList = []; // 所有展开和关闭回调
  9. var cascader = {
  10. /* 初始化 */
  11. render: function (param) {
  12. // 默认参数
  13. var defaultOptions = {
  14. renderFormat: function (labels, values) {
  15. return labels.join(' / ');
  16. },
  17. clearable: true,
  18. clearAllActive: false,
  19. disabled: false,
  20. trigger: 'click',
  21. changeOnSelect: false,
  22. filterable: false,
  23. notFoundText: '没有匹配数据'
  24. };
  25. param = $.extend(defaultOptions, param);
  26. var elem = param.elem; // 目标元素
  27. var mDataList = param.data; // 数据
  28. var renderFormat = param.renderFormat; // 选择后用于展示的函数
  29. var clearable = param.clearable; // 是否支持清除
  30. var clearAllActive = param.clearAllActive; // 清除所有选中
  31. var disabled = param.disabled; // 是否禁用
  32. var trigger = param.trigger; // 次级菜单触发方式
  33. var changeOnSelect = param.changeOnSelect; // 是否点击每项选项值都改变
  34. var reqData = param.reqData; // 自定义获取数据的方法
  35. var filterable = param.filterable; // 是否开启搜索
  36. var notFoundText = param.notFoundText; // 搜索列表为空时显示的内容
  37. var reqSearch = param.reqSearch; // 自定义搜索的方法
  38. var onChange = param.onChange; // 数据选择完成的回调
  39. var onVisibleChange = param.onVisibleChange; // 展开和关闭弹窗时触发
  40. var itemHeight = param.itemHeight; // 下拉列表每一项高度
  41. var isFirst = true;
  42. // 如果渲染过重新渲染
  43. var $elem = $(elem);
  44. if ($elem.next().hasClass('ew-cascader-group')) {
  45. $elem.next().remove();
  46. for (var i = 0; i < onVisibleChangeList.length; i++) {
  47. if (elem == onVisibleChangeList[i].elem) {
  48. onVisibleChangeList.splice(i, 1);
  49. break;
  50. }
  51. }
  52. }
  53. onVisibleChangeList.push({elem: elem, onVisibleChange: onVisibleChange});
  54. $elem.addClass('ew-cascader-hide');
  55. var htmlStr = '<div class="ew-cascader-group">';
  56. htmlStr += ' <div class="ew-cascader-input-group">';
  57. htmlStr += ' <input class="layui-input ew-cascader-input" readonly/>';
  58. htmlStr += ' <input class="layui-input ew-cascader-input-search"/>';
  59. htmlStr += ' <i class="layui-icon layui-icon-triangle-d ew-icon-arrow"></i>';
  60. htmlStr += ' <i class="layui-icon layui-icon-loading-1 layui-anim layui-anim-rotate layui-anim-loop ew-icon-loading"></i>';
  61. htmlStr += ' <i class="layui-icon layui-icon-close-fill ew-icon-clear"></i>';
  62. htmlStr += ' </div>';
  63. htmlStr += ' <div class="ew-cascader-dropdown layui-anim layui-anim-upbit"></div>';
  64. htmlStr += ' <div class="ew-cascader-search-list"></div>';
  65. htmlStr += ' </div>';
  66. $elem.after(htmlStr);
  67. var $cascader = $elem.next();
  68. var $inputGroup = $cascader.children('.ew-cascader-input-group');
  69. var $input = $inputGroup.children('.ew-cascader-input');
  70. var $inputSearch = $inputGroup.children('.ew-cascader-input-search');
  71. var $dropdown = $cascader.children('.ew-cascader-dropdown');
  72. var $search = $cascader.children('.ew-cascader-search-list');
  73. $input.attr('placeholder', $elem.attr('placeholder'));
  74. disabled && $input.addClass('layui-disabled');
  75. // 构建渲染后的实例
  76. var _instance = {
  77. data: mDataList,
  78. /* 展开 */
  79. open: function () {
  80. if ($cascader.hasClass('ew-cascader-open')) {
  81. return;
  82. }
  83. cascader.hideAll();
  84. $cascader.addClass('ew-cascader-open');
  85. cascader.checkWidthPosition($dropdown); // 溢出屏幕判断
  86. cascader.checkHeightPosition($dropdown); // 溢出屏幕判断
  87. onVisibleChange && onVisibleChange(true); // 展开回调
  88. if (filterable) { // 如果开启搜索功能让输入框获取焦点
  89. $inputGroup.addClass('show-search');
  90. $inputSearch.focus();
  91. }
  92. },
  93. /* 关闭 */
  94. hide: function () {
  95. if (!$cascader.hasClass('ew-cascader-open')) {
  96. return;
  97. }
  98. $cascader.removeClass('ew-cascader-open');
  99. $cascader.removeClass('dropdown-show-top');
  100. $cascader.removeClass('dropdown-show-left');
  101. cascader.hideAllSearch();
  102. onVisibleChange && onVisibleChange(false); // 关闭回调
  103. },
  104. /* 移除加载中的状态*/
  105. removeLoading: function () {
  106. $cascader.removeClass('show-loading');
  107. $dropdown.find('.ew-cascader-dropdown-list-item').removeClass('show-loading');
  108. },
  109. /* 设置禁用状态 */
  110. setDisabled: function (dis) {
  111. disabled = dis;
  112. if (dis) {
  113. $input.addClass('layui-disabled');
  114. _instance.hide();
  115. } else {
  116. $input.removeClass('layui-disabled');
  117. }
  118. },
  119. /* 获取值*/
  120. getValue: function () {
  121. return $elem.val();
  122. },
  123. /* 获取label */
  124. getLabel: function () {
  125. return $input.val();
  126. },
  127. /* 设置值*/
  128. setValue: function (value) {
  129. if (value == undefined || value == null || !value.toString()) {
  130. $input.val('');
  131. $elem.val('');
  132. if (clearAllActive || changeOnSelect) { // 清除所有
  133. $dropdown.children('.ew-cascader-dropdown-list').not(':first').remove();
  134. $dropdown.find('.ew-cascader-dropdown-list-item').removeClass('active');
  135. cascader.checkWidthPosition($dropdown); // 溢出屏幕判断
  136. } else { // 仅清除最后一项
  137. $dropdown.find('.ew-cascader-dropdown-list-item.is-last').removeClass('active');
  138. }
  139. $inputGroup.removeClass('show-clear');
  140. return;
  141. }
  142. value = value.toString().split(',');
  143. var labels = [];
  144. // 通过递归控制异步加载回显默认值对应label时的请求顺序
  145. function doReqData(tValues, data, i, values, callback) {
  146. if (!tValues && data) {
  147. tValues = [];
  148. setData(data);
  149. } else if (tValues && data && data.children) {
  150. setData(data.children);
  151. } else { // 数据不存在时才去请求数据
  152. $cascader.addClass('show-loading');
  153. reqData(tValues, function (dataList) {
  154. if (tValues) {
  155. data.children = dataList;
  156. } else {
  157. mDataList = dataList;
  158. tValues = [];
  159. }
  160. setData(dataList);
  161. }, data);
  162. }
  163. function setData(dataList) {
  164. for (var j = 0; j < dataList.length; j++) {
  165. if (dataList[j].value == values[i]) {
  166. labels[i] = dataList[j].label;
  167. tValues[i] = dataList[j].value;
  168. if (i < values.length - 1) {
  169. doReqData(tValues, dataList[j], i + 1, values, callback);
  170. } else {
  171. callback();
  172. }
  173. break;
  174. }
  175. }
  176. }
  177. }
  178. doReqData(undefined, mDataList, 0, value, function () {
  179. $cascader.removeClass('show-loading');
  180. $input.val(renderFormat(labels, value));
  181. $elem.val(value.join(','));
  182. });
  183. }
  184. };
  185. // 回显初始值
  186. _instance.setValue($elem.val());
  187. // 点击展开/关闭下拉列表
  188. $inputGroup.off('click').on('click', function (e) {
  189. // 判断是否是禁用状态
  190. if ($input.hasClass('layui-disabled')) {
  191. return;
  192. }
  193. // 判断是否是加载中状态
  194. if ($cascader.hasClass('show-loading')) {
  195. return;
  196. }
  197. // 关闭
  198. if ($cascader.hasClass('ew-cascader-open')) {
  199. if (!filterable) { // 是否开启搜索功能
  200. _instance.hide();
  201. }
  202. return;
  203. }
  204. // 展开
  205. if (isFirst) { // 第一次展开渲染第一列数据
  206. if (mDataList) {
  207. isFirst = false;
  208. renderList($dropdown, mDataList, undefined, itemHeight);
  209. initLabel();
  210. _instance.open();
  211. } else { // 异步方式
  212. $cascader.addClass('show-loading');
  213. reqData(undefined, function (dataList) {
  214. isFirst = false;
  215. mDataList = dataList;
  216. renderList($dropdown, dataList, undefined, itemHeight);
  217. $cascader.removeClass('show-loading');
  218. _instance.open();
  219. }, undefined);
  220. }
  221. } else {
  222. initLabel();
  223. _instance.open();
  224. }
  225. // 回显上次选中项
  226. function initLabel() {
  227. var value = $elem.val().toString();
  228. if (value) {
  229. value = value.split(',');
  230. for (var i = 0; i < value.length; i++) {
  231. var $item = $dropdown.children('.ew-cascader-dropdown-list').eq(i).children('.ew-cascader-dropdown-list-item[data-value="' + value[i] + '"]');
  232. if (i == value.length - 1) {
  233. $item.addClass('active');
  234. } else {
  235. $item.trigger('click');
  236. }
  237. }
  238. } else {
  239. _instance.setValue();
  240. }
  241. }
  242. });
  243. $inputGroup.children('.ew-icon-arrow').off('click').on('click', function (e) {
  244. if ($cascader.hasClass('ew-cascader-open')) {
  245. _instance.hide();
  246. e.stopPropagation();
  247. }
  248. });
  249. // 点击渲染次级列表
  250. $dropdown.off('click').on('click', '.ew-cascader-dropdown-list-item', function () {
  251. var $this = $(this);
  252. // 防止重复点击
  253. if ($this.hasClass('active')) {
  254. if ($this.hasClass('is-last')) {
  255. _instance.hide();
  256. }
  257. return;
  258. }
  259. // 判断是否是禁用状态
  260. if ($this.hasClass('ew-cascader-disabled')) {
  261. return;
  262. }
  263. // 判断是否是加载中状态
  264. if ($this.parent().parent().find('.ew-cascader-dropdown-list-item').hasClass('show-loading')) {
  265. return;
  266. }
  267. var index = $this.data('index').toString();
  268. var indexList = index.split('-');
  269. var data = mDataList[parseInt(indexList[0])], values = [data.value], labels = [data.label];
  270. for (var i = 1; i < indexList.length; i++) {
  271. data = data.children[parseInt(indexList[i])];
  272. values[i] = data.value;
  273. labels[i] = data.label;
  274. }
  275. if (data.haveChildren) { // 非最后一项
  276. if (data.children) { // 数据方式或已经异步加载直接渲染
  277. $this.parent().nextAll().remove();
  278. cascader.checkWidthPosition($dropdown); // 检查是否溢出屏幕
  279. activeThis();
  280. renderList($dropdown, data.children, index, itemHeight);
  281. } else { // 异步方式先请求数据再渲染
  282. $this.addClass('show-loading');
  283. reqData(values, function (dataList) {
  284. data.children = dataList;
  285. $this.parent().nextAll().remove();
  286. cascader.checkWidthPosition($dropdown); // 检查是否溢出屏幕
  287. activeThis();
  288. renderList($dropdown, dataList, index, itemHeight);
  289. $this.removeClass('show-loading');
  290. }, data);
  291. }
  292. // 点击非最后一项也触发选中
  293. if (changeOnSelect) {
  294. activeThis();
  295. doChange();
  296. }
  297. } else { // 最后一项
  298. $this.parent().nextAll().remove();
  299. activeThis();
  300. doChange();
  301. _instance.hide(); // 选中后关闭
  302. }
  303. /* 选中当前 */
  304. function activeThis() {
  305. $this.parent().children('.ew-cascader-dropdown-list-item').removeClass('active');
  306. $this.addClass('active');
  307. }
  308. /* 触发选中回调 */
  309. function doChange() {
  310. $input.val(renderFormat(labels, values)); // 赋值label
  311. $elem.val(values.join(',')); // 赋值value
  312. $elem.removeClass('layui-form-danger'); // 移除表单验证
  313. onChange && onChange(values, data); // 选中回调
  314. }
  315. });
  316. // hover方式触发
  317. if (trigger == 'hover') {
  318. $dropdown.off('mouseenter').on('mouseenter', '.ew-cascader-dropdown-list-item', function () {
  319. if (!$(this).hasClass('is-last')) {
  320. $(this).trigger('click');
  321. }
  322. });
  323. }
  324. // 开启清除功能
  325. if (clearable) {
  326. $inputGroup.off('mouseenter').on('mouseenter', function () {
  327. if ($elem.val().toString()) {
  328. $(this).addClass('show-clear');
  329. }
  330. });
  331. $inputGroup.off('mouseleave').on('mouseleave', function () {
  332. $(this).removeClass('show-clear');
  333. });
  334. // 点击清除
  335. $inputGroup.children('.ew-icon-clear').off('click').on('click', function (e) {
  336. e.stopPropagation();
  337. _instance.setValue();
  338. });
  339. }
  340. // 开启搜索功能
  341. if (filterable) {
  342. $inputSearch.off('input').on('input', function () {
  343. var value = $(this).val();
  344. if (!value) {
  345. $cascader.removeClass('show-search-list');
  346. $inputGroup.removeClass('have-value');
  347. return;
  348. }
  349. $inputGroup.addClass('have-value');
  350. if (reqSearch) { // 异步搜索
  351. reqSearch(value, function (rsList) {
  352. // 渲染搜索结果
  353. renderSearchList($search, rsList, notFoundText);
  354. $cascader.addClass('show-search-list');
  355. }, mDataList);
  356. } else { // 前端搜索
  357. var allList = [], rsList = [];
  358. // 把树形list转一维list
  359. function toAllList(arr, label, value, disabled) {
  360. for (var i = 0; i < arr.length; i++) {
  361. var item = arr[i];
  362. item.__label = label ? label + ' / ' + item.label : item.label;
  363. item.__value = value ? value + ',' + item.value : item.value;
  364. item.__disabled = item.disabled ? item.disabled : disabled;
  365. if (item.children && item.children.length) {
  366. toAllList(item.children, item.__label, item.__value, item.__disabled);
  367. delete item.__label;
  368. delete item.__value;
  369. } else {
  370. allList.push({
  371. label: item.__label,
  372. value: item.__value,
  373. disabled: item.__disabled
  374. });
  375. }
  376. }
  377. }
  378. toAllList(mDataList);
  379. // 过滤数据
  380. for (var i = 0; i < allList.length; i++) {
  381. var item = allList[i];
  382. if (item.label.indexOf(value) > -1) {
  383. item.label = item.label.replace(new RegExp(value, 'g'), '<span class="search-keyword">' + value + '</span>');
  384. rsList.push(item);
  385. }
  386. }
  387. // 渲染搜索结果
  388. renderSearchList($search, rsList, notFoundText);
  389. $cascader.addClass('show-search-list');
  390. }
  391. });
  392. $search.off('click').on('click', '.ew-cascader-search-list-item', function (e) {
  393. e.stopPropagation();
  394. if ($(this).hasClass('ew-cascader-disabled')) { // 是否禁用
  395. return;
  396. }
  397. _instance.hide();
  398. _instance.setValue($(this).data('value'));
  399. });
  400. }
  401. return _instance;
  402. },
  403. /** 关闭所有 */
  404. hideAll: function () {
  405. cascader.hideAllSearch();
  406. for (var i = 0; i < onVisibleChangeList.length; i++) {
  407. var elem = onVisibleChangeList[i].elem;
  408. var onVisibleChange = onVisibleChangeList[i].onVisibleChange;
  409. var $cascader = $(elem).next();
  410. if ($cascader.hasClass('ew-cascader-open')) {
  411. $cascader.removeClass('ew-cascader-open');
  412. $cascader.removeClass('dropdown-show-top');
  413. $cascader.removeClass('dropdown-show-left');
  414. onVisibleChange && onVisibleChange(false);
  415. }
  416. }
  417. },
  418. /** 关闭所有搜索面板 */
  419. hideAllSearch: function () {
  420. $('.ew-cascader-input-group').removeClass('show-search');
  421. $('.ew-cascader-group').removeClass('show-search-list');
  422. $('.ew-cascader-input-group').removeClass('have-value');
  423. $('.ew-cascader-input-search').val('');
  424. },
  425. /* 获取浏览器高度 */
  426. getPageHeight: function () {
  427. return document.documentElement.clientHeight || document.body.clientHeight;
  428. },
  429. /* 获取浏览器宽度 */
  430. getPageWidth: function () {
  431. return document.documentElement.clientWidth || document.body.clientWidth;
  432. },
  433. /* 检查宽度是否溢出屏幕 */
  434. checkWidthPosition: function ($dropdown) {
  435. if ($dropdown.offset().left + $dropdown.outerWidth() > cascader.getPageWidth()) {
  436. $dropdown.parent().addClass('dropdown-show-left');
  437. } else {
  438. $dropdown.parent().removeClass('dropdown-show-left');
  439. }
  440. },
  441. /* 检查高度是否溢出屏幕 */
  442. checkHeightPosition: function ($dropdown) {
  443. if ($dropdown.offset().top + $dropdown.outerHeight() > cascader.getPageHeight()) {
  444. $dropdown.parent().addClass('dropdown-show-top');
  445. if ($dropdown.offset().top < 0) {
  446. $dropdown.parent().removeClass('dropdown-show-top');
  447. }
  448. }
  449. }
  450. };
  451. /** 渲染列表 */
  452. var renderList = function ($dropdown, dataList, pIndex, itemHeight) {
  453. var style = itemHeight ? ' style="height:' + itemHeight + ';"' : '';
  454. var htmlStr = '<div class="ew-cascader-dropdown-list" ' + style + '>';
  455. for (var i = 0; i < dataList.length; i++) {
  456. var item = dataList[i];
  457. var index = pIndex == undefined ? i : (pIndex + '-' + i);
  458. if (item.haveChildren == undefined) {
  459. item.haveChildren = item.children ? true : false;
  460. }
  461. var className = item.haveChildren ? '' : ' is-last'; // 是否是叶子节点
  462. item.disabled && (className += ' ew-cascader-disabled'); // 是否禁用
  463. htmlStr += ' <div class="ew-cascader-dropdown-list-item' + className + '" data-index="' + index + '" data-value="' + item.value + '">' + item.label + '<i class="layui-icon layui-icon-right ew-icon-right"></i><i class="layui-icon layui-icon-loading-1 layui-anim layui-anim-rotate layui-anim-loop ew-icon-loading"></i></div>';
  464. }
  465. htmlStr += ' </div>';
  466. $dropdown.append(htmlStr);
  467. cascader.checkWidthPosition($dropdown); // 检查是否溢出屏幕
  468. };
  469. /** 渲染搜索列表 */
  470. var renderSearchList = function ($search, dataList, notFoundText) {
  471. var htmlStr = '';
  472. if (dataList.length == 0) {
  473. htmlStr = '<div class="ew-cascader-search-list-empty">' + notFoundText + '</div>';
  474. } else {
  475. for (var i = 0; i < dataList.length; i++) {
  476. var item = dataList[i];
  477. var className = item.disabled ? ' ew-cascader-disabled' : ''; // 是否禁用
  478. htmlStr += '<div class="ew-cascader-search-list-item' + className + '" data-value="' + item.value + '">' + item.label + '</div>';
  479. }
  480. }
  481. $search.html(htmlStr);
  482. };
  483. // 点击空白区域关闭下拉列表
  484. $(document).off('click.cascader').on('click.cascader', function (e) {
  485. try {
  486. var classNames = e.target.className.split(' ');
  487. var cascaders = ['ew-cascader-group', 'ew-cascader-input', 'ew-icon-arrow', 'ew-cascader-dropdown', 'ew-cascader-dropdown-list', 'ew-cascader-dropdown-list-item', 'ew-icon-right', 'ew-cascader-input-search', 'ew-cascader-search-list', 'ew-cascader-search-list-item'];
  488. for (var i in classNames) {
  489. for (var j in cascaders) {
  490. if (classNames[i] == cascaders[j]) {
  491. return;
  492. }
  493. }
  494. }
  495. } catch (e) {
  496. }
  497. cascader.hideAll();
  498. });
  499. exports('cascader', cascader);
  500. });