dropdown.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516
  1. /**
  2. * @Name ${name}
  3. * @Author: ${author}
  4. * @License: ${license}
  5. * @Version: ${version}
  6. */
  7. layui.define(['jquery', 'laytpl'], function (exports){
  8. "use strict";
  9. var $ = layui.jquery || layui.$,
  10. laytpl = layui.laytpl,
  11. $body = $(window.document.body),
  12. // === 内部事件模块, 由于layui不让同一事件注册多次监听了,故此处自己实现。 ===>
  13. EVENT = {
  14. DROPDOWN_SHOW: "a"
  15. },
  16. // 内部事件。目前仅仅有一个打开下拉框事件。此事件不暴露给开发者,仅作为内部使用。
  17. INNER_EVENT = {},
  18. // 监听指定事件。
  19. onEvent = function (event, cb) {
  20. var evnts = INNER_EVENT[event] || [];
  21. evnts.push(cb);
  22. INNER_EVENT[event] = evnts;
  23. },
  24. // 发出事件
  25. makeEvent = function(event, param) {
  26. var evnts = INNER_EVENT[event] || [];
  27. $.each(evnts, function (index, value) {
  28. value(param);
  29. });
  30. },
  31. // <=== 内部事件模块 ===
  32. // 允许通过为 window 设置 MICROANSWER_DROPDOWAN 变量来改变本组件的注册名。
  33. // 以避免将来 layui 官方加入下拉控件与本控件重名时,可让本控件依然能正常运行
  34. // 在另一个名称上。
  35. MOD_NAME = window.MICROANSWER_DROPDOWAN || "dropdown",
  36. // 小箭头模板
  37. MENUS_POINTER_TEMPLATE = "{{# if (d.arrow){ }}<div class='dropdown-pointer'></div>{{# } }}",
  38. MENUS_TEMPLATE_START = "<div tabindex='0' " +
  39. "class='layui-anim layui-anim-upbit dropdown-root' " + MOD_NAME + "-id='{{d.downid}}' " +
  40. "style='z-index: {{d.zIndex}}'>" +
  41. MENUS_POINTER_TEMPLATE +
  42. "<div class='dropdown-content' " +
  43. "style='margin: {{d.gap}}px {{d.gap}}px;" +
  44. "background-color: {{d.backgroundColor}};" +
  45. "min-width: {{d.minWidth}}px;" +
  46. "min-height: {{d.minHeight}}px;" +
  47. "max-height: {{d.maxHeight}}px;" +
  48. "overflow: auto;'>",
  49. MENUS_TEMPLATE_END = "</div></div>",
  50. // 菜单项目模板。
  51. MENUS_TEMPLATE =
  52. MENUS_TEMPLATE_START +
  53. "{{# layui.each(d.menus, function(index, item){ }}" +
  54. "{{# if ('hr' === item) { }}" +
  55. "<hr>" +
  56. "{{# } else if (item.header) { }}" +
  57. "{{# if (item.withLine) { }}" +
  58. "<fieldset class=\"layui-elem-field layui-field-title menu-header\" style=\"margin-left:0;margin-bottom: 0;margin-right: 0\">" +
  59. "<legend>{{item.header}}</legend>" +
  60. "</fieldset>" +
  61. "{{# } else { }}" +
  62. "<div class='menu-header' style='text-align: {{item.align||\'left\'}}'>{{item.header}}</div>" +
  63. "{{# } }}" +
  64. "{{# } else { }}" +
  65. "<div class='menu-item'>" +
  66. "<a href='javascript:;' lay-event='{{item.event}}'>" +
  67. "{{# if (item.layIcon){ }}" +
  68. "<i class='layui-icon {{item.layIcon}}'></i>&nbsp;" +
  69. "{{# } }}" +
  70. "<span>{{item.txt}}</span>" +
  71. "</a>" +
  72. "</div>" +
  73. "{{# } }}" +
  74. "{{# }); }}" +
  75. MENUS_TEMPLATE_END,
  76. // 默认配置。
  77. DEFAULT_OPTION = {
  78. // 打开下拉框的触发方式
  79. // 可填: click, hover
  80. showBy: 'click',
  81. // 左对齐。可选值: left, center, right
  82. align: "left",
  83. // 最小宽度
  84. minWidth: 80,
  85. minHeight: 10,
  86. // 最大高度
  87. maxHeight: 300,
  88. zIndex: 891,
  89. // 下拉框和触发按钮的间隙
  90. gap: 8,
  91. // 隐藏事件
  92. onHide: function ($dom, $down) {},
  93. // 显示事件
  94. onShow: function ($dom, $down) {},
  95. // 滚动界面的时候,如果下拉框是显示的,则将隐藏,如果值为 follow 则不会隐藏。
  96. scrollBehavior: "follow",
  97. // 下拉内容背景颜色
  98. backgroundColor: "#FFF",
  99. // 默认css地址,允许通过配置指定其它地址
  100. cssLink: "https://cdn.jsdelivr.net/gh/microanswer/layui_dropdown@${version}/dist/dropdown.css",
  101. // 初始化完成后是否立即显示下拉框。
  102. immed: false,
  103. // 是否显示小箭头
  104. arrow: true,
  105. },
  106. /**
  107. * 下拉菜单本体定义类。
  108. *
  109. * @Param $dom 可以是这些内容: jquery对象、选择器。
  110. */
  111. Dropdown = function($dom, option) {
  112. /*
  113. * 在实现逻辑中使用了一些字段挂载在 Dropdown 上,这里统一做一个介绍:
  114. * this.$dom: 表示触发器的jquery对象。
  115. * this.$down: 表示下拉框下拉部分的jquery对象。在init方法里初始化。
  116. * this.option: 表示选项配置。
  117. * this.opened: 表示下拉框是否展开。
  118. * this.fcd: 表示当前是否处于有焦点状态。
  119. * this.mic: 表示鼠标是否在组件范围内, 鼠标在 触发器+下拉框 里即算是在组件范围内。mic = mouseInComponent
  120. * */
  121. if (typeof $dom === "string") {$dom = $($dom);}
  122. this.$dom = $dom;
  123. this.option = $.extend({
  124. downid: String(Math.random()).split('.')[1],
  125. filter: $dom.attr("lay-filter")
  126. }, DEFAULT_OPTION, option);
  127. if (this.option.gap > 20) {
  128. this.option.gap = 20;
  129. }
  130. this.init();
  131. };
  132. // 加载css,使外部不需要手动引入css。允许通过设置 window.dropdown_cssLink 来修改默认css地址。
  133. //layui.link(window[MOD_NAME+"_cssLink"] || DEFAULT_OPTION.cssLink, function () {/*ignore*/}, MOD_NAME + "_css");
  134. // 初始化下拉菜单。
  135. Dropdown.prototype.init = function () {
  136. var _this = this;
  137. if (_this.option.menus && _this.option.menus.length > 0) {
  138. laytpl(MENUS_TEMPLATE).render(_this.option, function (html) {
  139. _this.$down = $(html);
  140. _this.$dom.after(_this.$down);
  141. _this.initSize();
  142. _this.initEvent();
  143. _this.onSuccess();
  144. });
  145. } else if (_this.option.template) {
  146. var templateId;
  147. if (_this.option.template.indexOf("#") === -1) {
  148. templateId = "#" + _this.option.template;
  149. } else {
  150. templateId = _this.option.template;
  151. }
  152. var data = $.extend($.extend({}, _this.option), _this.option.data || {});
  153. laytpl(MENUS_TEMPLATE_START + $(templateId).html() + MENUS_TEMPLATE_END).render(data, function (html) {
  154. _this.$down = $(html);
  155. _this.$dom.after(_this.$down);
  156. _this.initSize();
  157. _this.initEvent();
  158. _this.onSuccess();
  159. });
  160. } else {
  161. layui.hint().error("下拉框目前即没配置菜单项,也没配置下拉模板。[#" + (_this.$dom.attr("id")||"") + ",filter="+_this.option.filter + "]");
  162. }
  163. };
  164. Dropdown.prototype.initSize = function () {
  165. this.$down.find(".dropdown-pointer").css("width", this.option.gap * 2);
  166. this.$down.find(".dropdown-pointer").css("height", this.option.gap * 2);
  167. };
  168. // 初始化位置信息
  169. Dropdown.prototype.initPosition = function() {
  170. var btnOffset = this.$dom.offset();
  171. var btnHeight = this.$dom.outerHeight();
  172. var btnWidth = this.$dom.outerWidth();
  173. var btnLeft = btnOffset.left;
  174. var btnTop = btnOffset.top - window.pageYOffset;
  175. var downHeight = this.$down.outerHeight();
  176. var downWidth = this.$down.outerWidth();
  177. var downLeft;
  178. var downTop;
  179. var pointerLeft; // 箭头左边偏移量
  180. var pointerTop; // 箭头右边偏移量
  181. if (this.option.align === 'right') {
  182. downLeft = (btnLeft + btnWidth) - downWidth + this.option.gap;
  183. pointerLeft = - (Math.min(downWidth - (this.option.gap * 2), btnWidth) / 2);
  184. } else if (this.option.align === 'center') {
  185. downLeft = btnLeft + ((btnWidth - downWidth) / 2);
  186. pointerLeft = (downWidth - (this.option.gap * 2)) / 2;
  187. } else {
  188. downLeft = btnLeft - this.option.gap;
  189. pointerLeft = Math.min(downWidth - (this.option.gap * 2), btnWidth) / 2;
  190. }
  191. downTop = btnHeight + btnTop;// + this.option.gap;
  192. var pt = this.$arrowDom || (this.$arrowDom = this.$down.find(".dropdown-pointer"));
  193. // var pointerHeigt = Math.pow(this.option.gap, 2) / Math.sqrt(Math.pow(this.option.gap, 2)*2);
  194. pointerTop = -this.option.gap;
  195. if (pointerLeft > 0) {
  196. pt.css("left", pointerLeft);
  197. pt.css("right", "unset");
  198. } else {
  199. pt.css("left", "unset");
  200. pt.css("right", (-1 * pointerLeft));
  201. }
  202. // 检测是否超出浏览器边缘
  203. if (downLeft + downWidth >= window.innerWidth) {
  204. downLeft = window.innerWidth - downWidth + this.option.gap;
  205. }
  206. if (downTop + downHeight >= window.innerHeight) {
  207. downTop = btnTop - downHeight;// - this.option.gap;
  208. pointerTop = downHeight - (this.option.gap); //(pointerHeigt * 2) - 1;
  209. pt.css("top", pointerTop).addClass("bottom");
  210. } else {
  211. pt.css("top", pointerTop).removeClass("bottom");
  212. }
  213. this.$down.css("left", downLeft);
  214. this.$down.css("top", downTop);
  215. };
  216. // 显示下拉内容
  217. Dropdown.prototype.show = function () {
  218. var _this = this;
  219. _this.initPosition();
  220. _this.opening = true; // 引入这个字段用于确保在动画过程中鼠标移除组件区域时不会隐藏下拉框。
  221. // 使用settimeout原因:
  222. // 如果 这个show方法在某个点击事件里面调用,那么立即调用focus方法的话是不会生效的。
  223. // 为了稳妥起见,延时100毫秒,再使下拉框获取焦点。从而在其失去焦点时能够自动隐藏。
  224. setTimeout(function () {
  225. _this.$down.focus();
  226. }, 100);
  227. _this.$down.addClass("layui-show");
  228. _this.opened = true;
  229. // 发出通知,告诉其他dropdown,我打开了,你们自己看情况办事!
  230. makeEvent(EVENT.DROPDOWN_SHOW, _this);
  231. // 调起回调。
  232. _this.option.onShow && _this.option.onShow(_this.$dom, _this.$down);
  233. };
  234. // 隐藏下拉内容
  235. Dropdown.prototype.hide = function () {
  236. this.fcd = false;
  237. this.$down.removeClass("layui-show");
  238. this.opened = false;
  239. this.option.onHide && this.option.onHide(this.$dom, this.$down);
  240. };
  241. // 当可以条件允许隐藏时,进行隐藏。
  242. // 条件:鼠标在下拉框范围外、下拉框和触发按钮都没有焦点
  243. Dropdown.prototype.hideWhenCan = function () {
  244. if (this.mic) {
  245. return;
  246. }
  247. if (this.opening) {
  248. return;
  249. }
  250. if (this.fcd) {
  251. return;
  252. }
  253. this.hide();
  254. };
  255. // 显示/隐藏下拉内容
  256. Dropdown.prototype.toggle = function () {
  257. if (this.opened) {
  258. this.hide();
  259. } else {
  260. this.show();
  261. }
  262. };
  263. Dropdown.prototype.onSuccess = function () {
  264. // 调起回调。
  265. this.option.success && this.option.success(this.$down);
  266. // 如果配置了立即显示,这里进行显示。
  267. if (this.option.immed) {
  268. this.show();
  269. }
  270. };
  271. // 滚动界面时此方法会执行
  272. Dropdown.prototype._onScroll = function() {
  273. var _this = this;
  274. // 如果下拉框不是展开状态,不用执行这些逻辑。
  275. // OHHHHHH! md才发现,当界面上出现很多下拉框(很少情况),这个判断真的极大提高了性能,避免了无用的执行。
  276. // 。使页面滚动不卡顿了,尤其是在ie里。
  277. if (!_this.opened) {
  278. return;
  279. }
  280. if (this.option.scrollBehavior === 'follow') {
  281. setTimeout(function () {
  282. _this.initPosition();
  283. }, 1);
  284. } else {
  285. this.hide();
  286. }
  287. };
  288. // 初始化事件。
  289. Dropdown.prototype.initEvent = function () {
  290. var _this = this;
  291. // 全局仅允许同时开启一个下拉菜单。所以这里注册一个监听。
  292. // 如果打开的下拉菜单不是我本身,则我应该隐藏自己。
  293. onEvent(EVENT.DROPDOWN_SHOW, function (dropdown) {
  294. if (dropdown !== _this) {
  295. _this.hide();
  296. }
  297. });
  298. _this.$dom.mouseenter(function () {
  299. _this.mic = true;
  300. if (_this.option.showBy === 'hover') {
  301. _this.fcd = true;
  302. _this.$down.focus();
  303. _this.show();
  304. }
  305. });
  306. _this.$dom.mouseleave(function () {
  307. _this.mic = false;
  308. });
  309. _this.$down.mouseenter(function () {
  310. _this.mic = true;
  311. _this.$down.focus();
  312. });
  313. _this.$down.mouseleave(function () {
  314. _this.mic = false;
  315. });
  316. if (_this.option.showBy === 'click') {
  317. _this.$dom.on("click", function () {
  318. _this.fcd = true;
  319. _this.toggle();
  320. });
  321. }
  322. /* 现通过失焦来保证下拉框隐藏,就不用这块了 $body.on("click", function () {
  323. if (!_this.mic) {
  324. _this.fcd = false;
  325. _this.hideWhenCan();
  326. }
  327. });*/
  328. $(window).on("scroll", function () {_this._onScroll();});
  329. _this.$dom.parents().on("scroll", function () {_this._onScroll();});
  330. $(window).on("resize", function () {
  331. if (!_this.opened) {
  332. return;
  333. }
  334. _this.initPosition();
  335. });
  336. _this.$dom.on("blur", function () {
  337. _this.fcd = false;
  338. _this.hideWhenCan();
  339. });
  340. _this.$down.on("blur", function () {
  341. _this.fcd = false;
  342. _this.hideWhenCan();
  343. });
  344. // 当下拉框获取焦点时,必然下拉框显示了,这时 吧 opening 设置false
  345. _this.$down.on("focus", function () {
  346. _this.opening = false;
  347. });
  348. // 点击下拉菜单里的条目事件
  349. if (_this.option.menus) {
  350. var $md = $("[" + MOD_NAME + "-id='" + _this.option.downid + "']");
  351. $md.on("click", "a", function () {
  352. var event = ($(this).attr('lay-event') || '').trim();
  353. if (event) {
  354. layui.event.call(this, MOD_NAME, MOD_NAME + '(' + _this.option.filter + ')', event);
  355. _this.hide();
  356. } else {
  357. layui.hint().error("菜单条目[" + this.outerHTML + "]未设置event。");
  358. }
  359. });
  360. }
  361. };
  362. // 监听事件方法
  363. function onFilter(layFitler, cb) {
  364. layui.onevent(MOD_NAME, MOD_NAME + "(" + layFitler + ")", function (event) {
  365. cb && cb(event);
  366. });
  367. }
  368. // 全局初始化方法。
  369. function suite(sector, option) {
  370. // 初始化页面上已有的下拉控件。
  371. $(sector || "[lay-"+ MOD_NAME +"]").each(function () {
  372. var $this = $(this);
  373. var attrOption = new Function('return '+ ($this.attr("lay-" + MOD_NAME) || "{}"))();
  374. $this.removeAttr("lay-" + MOD_NAME); // 移除节点上的这个标签,因为它很长,不利于调试。
  375. var dp = $this.data(MOD_NAME) || new Dropdown($this, $.extend({}, attrOption, option || {}));
  376. $this.data(MOD_NAME, dp);
  377. });
  378. }
  379. // 执行一次,立马让界面上的dropdown乖乖听话。
  380. suite();
  381. exports(MOD_NAME, {
  382. /**
  383. * 方便手动对界面上的按钮进行初始化
  384. */
  385. suite: suite,
  386. /**
  387. * 监听menu菜单点击事件
  388. */
  389. onFilter: onFilter,
  390. /**
  391. * 传入选择器,将其对应的下拉框隐藏。
  392. * 这个方法常常用代码调用。它不被设计为某个按钮点击后执行这个方法。
  393. * 因为下拉框的隐藏会在失去focus时自动隐藏,无论点击哪个按钮都会使
  394. * 下拉框失去focus而隐藏,此方法调用也没意义了。
  395. * @param {String} sector
  396. */
  397. hide: function (sector) {
  398. // 隐藏指定下拉框。
  399. $(sector).each(function () {
  400. var $this = $(this);
  401. var dp = $this.data(MOD_NAME);
  402. if (dp) {
  403. dp.hide();
  404. }
  405. });
  406. },
  407. /**
  408. * 传入选择器,将其对应的下拉框显示。
  409. *
  410. * 注意:如果选择器对应的dom没有进行下拉初始化,则此方法会进行初始化。此时会用到参数option,你可以
  411. * 通过第二个参数传入。但是通常建议传入的选择器对应的dom是经过了下拉框初始化的。
  412. * @param sector
  413. * @param option
  414. */
  415. show: function (sector, option) {
  416. // 显示指定下拉框。
  417. $(sector).each(function () {
  418. var $this = $(this);
  419. var dp = $this.data(MOD_NAME);
  420. if (dp) {
  421. dp.show();
  422. } else {
  423. layui.hint().error("警告:尝试在选择器【" + sector + "】上进行下拉框show操作,但此选择器对应的dom并没有初始化下拉框。");
  424. // 尝试在一个没有初始化下拉框的dom上调用show方法,这里立即进行初始化。
  425. option = option || {};
  426. // 立即显示。
  427. option.immed = true;
  428. suite(sector, option);
  429. }
  430. });
  431. },
  432. version: "${version}"
  433. });
  434. });