dropdown.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527
  1. /**
  2. @Name:dropdown 下拉菜单组件
  3. @License:MIT
  4. */
  5. layui.define(['jquery', 'laytpl', 'lay'], function(exports){
  6. "use strict";
  7. var $ = layui.$
  8. ,laytpl = layui.laytpl
  9. ,hint = layui.hint()
  10. ,device = layui.device()
  11. ,clickOrMousedown = (device.mobile ? 'click' : 'mousedown')
  12. //模块名
  13. ,MOD_NAME = 'dropdown'
  14. ,MOD_INDEX = 'layui_'+ MOD_NAME +'_index' //模块索引名
  15. //外部接口
  16. ,dropdown = {
  17. config: {}
  18. ,index: layui[MOD_NAME] ? (layui[MOD_NAME].index + 10000) : 0
  19. //设置全局项
  20. ,set: function(options){
  21. var that = this;
  22. that.config = $.extend({}, that.config, options);
  23. return that;
  24. }
  25. //事件
  26. ,on: function(events, callback){
  27. return layui.onevent.call(this, MOD_NAME, events, callback);
  28. }
  29. }
  30. //操作当前实例
  31. ,thisModule = function(){
  32. var that = this
  33. ,options = that.config
  34. ,id = options.id;
  35. thisModule.that[id] = that; //记录当前实例对象
  36. return {
  37. config: options
  38. //重置实例
  39. ,reload: function(options){
  40. that.reload.call(that, options);
  41. }
  42. }
  43. }
  44. //字符常量
  45. ,STR_ELEM = 'layui-dropdown', STR_HIDE = 'layui-hide', STR_DISABLED = 'layui-disabled', STR_NONE = 'layui-none'
  46. ,STR_ITEM_UP = 'layui-menu-item-up', STR_ITEM_DOWN = 'layui-menu-item-down', STR_MENU_TITLE = 'layui-menu-body-title', STR_ITEM_GROUP = 'layui-menu-item-group', STR_ITEM_PARENT = 'layui-menu-item-parent', STR_ITEM_DIV = 'layui-menu-item-divider', STR_ITEM_CHECKED = 'layui-menu-item-checked', STR_ITEM_CHECKED2 = 'layui-menu-item-checked2', STR_MENU_PANEL = 'layui-menu-body-panel', STR_MENU_PANEL_L = 'layui-menu-body-panel-left'
  47. ,STR_GROUP_TITLE = '.'+ STR_ITEM_GROUP + '>.'+ STR_MENU_TITLE
  48. //构造器
  49. ,Class = function(options){
  50. var that = this;
  51. that.index = ++dropdown.index;
  52. that.config = $.extend({}, that.config, dropdown.config, options);
  53. that.init();
  54. };
  55. //默认配置
  56. Class.prototype.config = {
  57. trigger: 'click' //事件类型
  58. ,content: '' //自定义菜单内容
  59. ,className: '' //自定义样式类名
  60. ,style: '' //设置面板 style 属性
  61. ,show: false //是否初始即显示菜单面板
  62. ,isAllowSpread: true //是否允许菜单组展开收缩
  63. ,isSpreadItem: true //是否初始展开子菜单
  64. ,data: [] //菜单数据结构
  65. ,delay: 300 //延迟关闭的毫秒数,若 trigger 为 hover 时才生效
  66. };
  67. //重载实例
  68. Class.prototype.reload = function(options){
  69. var that = this;
  70. that.config = $.extend({}, that.config, options);
  71. that.init(true);
  72. };
  73. //初始化准备
  74. Class.prototype.init = function(rerender){
  75. var that = this
  76. ,options = that.config
  77. ,elem = options.elem = $(options.elem);
  78. //若 elem 非唯一
  79. if(elem.length > 1){
  80. layui.each(elem, function(){
  81. dropdown.render($.extend({}, options, {
  82. elem: this
  83. }));
  84. });
  85. return that;
  86. }
  87. //若重复执行 render,则视为 reload 处理
  88. if(!rerender && elem[0] && elem.data(MOD_INDEX)){;
  89. var newThat = thisModule.getThis(elem.data(MOD_INDEX));
  90. if(!newThat) return;
  91. return newThat.reload(options);
  92. };
  93. //初始化 id 参数
  94. options.id = ('id' in options) ? options.id : that.index;
  95. if(options.show) that.render(rerender); //初始即显示
  96. that.events(); //事件
  97. };
  98. //渲染
  99. Class.prototype.render = function(rerender){
  100. var that = this
  101. ,options = that.config
  102. ,elemBody = $('body')
  103. //默认菜单内容
  104. ,getDefaultView = function(){
  105. var elemUl = $('<ul class="layui-menu layui-dropdown-menu"></ul>');
  106. if(options.data.length > 0 ){
  107. eachItemView(elemUl, options.data)
  108. } else {
  109. elemUl.html('<li class="layui-menu-item-none">no menu</li>');
  110. }
  111. return elemUl;
  112. }
  113. //遍历菜单项
  114. ,eachItemView = function(views, data){
  115. //var views = [];
  116. layui.each(data, function(index, item){
  117. //是否存在子级
  118. var isChild = item.child && item.child.length > 0
  119. ,isSpreadItem = ('isSpreadItem' in item) ? item.isSpreadItem : options.isSpreadItem
  120. ,title = item.templet
  121. ? laytpl(item.templet).render(item)
  122. : (options.templet ? laytpl(options.templet).render(item) : item.title)
  123. //初始类型
  124. ,type = function(){
  125. if(isChild){
  126. item.type = item.type || 'parent';
  127. }
  128. if(item.type){
  129. return ({
  130. group: 'group'
  131. ,parent: 'parent'
  132. ,'-': '-'
  133. })[item.type] || 'parent';
  134. }
  135. return '';
  136. }();
  137. if(type !== '-' && (!item.title && !item.id && !isChild)) return;
  138. //列表元素
  139. var viewLi = $(['<li'+ function(){
  140. var className = {
  141. group: 'layui-menu-item-group'+ (
  142. options.isAllowSpread ? (
  143. isSpreadItem ? ' layui-menu-item-down' : ' layui-menu-item-up'
  144. ) : ''
  145. )
  146. ,parent: STR_ITEM_PARENT
  147. ,'-': 'layui-menu-item-divider'
  148. };
  149. if(isChild || type){
  150. return ' class="'+ className[type] +'"';
  151. }
  152. return '';
  153. }() +'>'
  154. //标题区
  155. ,function(){
  156. //是否超文本
  157. var viewText = ('href' in item) ? (
  158. '<a href="'+ item.href +'" target="'+ (item.target || '_self') +'">'+ title +'</a>'
  159. ) : title;
  160. //是否存在子级
  161. if(isChild){
  162. return '<div class="'+ STR_MENU_TITLE +'">'+ viewText + function(){
  163. if(type === 'parent'){
  164. return '<i class="layui-icon layui-icon-right"></i>';
  165. } else if(type === 'group' && options.isAllowSpread){
  166. return '<i class="layui-icon layui-icon-'+ (isSpreadItem ? 'up' : 'down') +'"></i>';
  167. } else {
  168. return '';
  169. }
  170. }() +'</div>'
  171. }
  172. return '<div class="'+ STR_MENU_TITLE +'">'+ viewText +'</div>';
  173. }()
  174. ,'</li>'].join(''));
  175. viewLi.data('item', item);
  176. //子级区
  177. if(isChild){
  178. var elemPanel = $('<div class="layui-panel layui-menu-body-panel"></div>')
  179. ,elemUl = $('<ul></ul>');
  180. if(type === 'parent'){
  181. elemPanel.append(eachItemView(elemUl, item.child));
  182. viewLi.append(elemPanel);
  183. } else {
  184. viewLi.append(eachItemView(elemUl, item.child));
  185. }
  186. }
  187. views.append(viewLi);
  188. });
  189. return views;
  190. }
  191. //主模板
  192. ,TPL_MAIN = ['<div class="layui-dropdown layui-border-box layui-panel layui-anim layui-anim-downbit">'
  193. ,'</div>'].join('');
  194. //如果是右键事件,则每次触发事件时,将允许重新渲染
  195. if(options.trigger === 'contextmenu' || lay.isTopElem(options.elem[0])) rerender = true;
  196. //判断是否已经打开了下拉菜单面板
  197. if(!rerender && options.elem.data(MOD_INDEX +'_opened')) return;
  198. //记录模板对象
  199. that.elemView = $(TPL_MAIN);
  200. that.elemView.append(options.content || getDefaultView());
  201. //初始化某些属性
  202. if(options.className) that.elemView.addClass(options.className);
  203. if(options.style) that.elemView.attr('style', options.style);
  204. //记录当前执行的实例索引
  205. dropdown.thisId = options.id;
  206. //插入视图
  207. that.remove(); //移除非当前绑定元素的面板
  208. elemBody.append(that.elemView);
  209. options.elem.data(MOD_INDEX +'_opened', true);
  210. //坐标定位
  211. that.position();
  212. thisModule.prevElem = that.elemView; //记录当前打开的元素,以便在下次关闭
  213. thisModule.prevElem.data('prevElem', options.elem); //将当前绑定的元素,记录在打开元素的 data 对象中
  214. //阻止全局事件
  215. that.elemView.find('.layui-menu').on(clickOrMousedown, function(e){
  216. layui.stope(e);
  217. });
  218. //触发菜单列表事件
  219. that.elemView.find('.layui-menu li').on('click', function(e){
  220. var othis = $(this)
  221. ,data = othis.data('item') || {}
  222. ,isChild = data.child && data.child.length > 0;
  223. if(!isChild && data.type !== '-'){
  224. that.remove();
  225. typeof options.click === 'function' && options.click(data, othis);
  226. }
  227. });
  228. //触发菜单组展开收缩
  229. that.elemView.find(STR_GROUP_TITLE).on('click', function(e){
  230. var othis = $(this)
  231. ,elemGroup = othis.parent()
  232. ,data = elemGroup.data('item') || {}
  233. if(data.type === 'group' && options.isAllowSpread){
  234. thisModule.spread(elemGroup);
  235. }
  236. });
  237. //如果是鼠标移入事件,则鼠标移出时自动关闭
  238. if(options.trigger === 'mouseenter'){
  239. that.elemView.on('mouseenter', function(){
  240. clearTimeout(thisModule.timer);
  241. }).on('mouseleave', function(){
  242. that.delayRemove();
  243. });
  244. }
  245. };
  246. //位置定位
  247. Class.prototype.position = function(obj){
  248. var that = this
  249. ,options = that.config;
  250. lay.position(options.elem[0], that.elemView[0], {
  251. position: options.position
  252. ,e: that.e
  253. ,clickType: options.trigger === 'contextmenu' ? 'right' : null
  254. ,align: options.align || null
  255. });
  256. };
  257. //删除视图
  258. Class.prototype.remove = function(){
  259. var that = this
  260. ,options = that.config
  261. ,elemPrev = thisModule.prevElem;
  262. //若存在已打开的面板元素,则移除
  263. if(elemPrev){
  264. elemPrev.data('prevElem') && (
  265. elemPrev.data('prevElem').data(MOD_INDEX +'_opened', false)
  266. );
  267. elemPrev.remove();
  268. }
  269. };
  270. //延迟删除视图
  271. Class.prototype.delayRemove = function(){
  272. var that = this
  273. ,options = that.config;
  274. clearTimeout(thisModule.timer);
  275. thisModule.timer = setTimeout(function(){
  276. that.remove();
  277. }, options.delay);
  278. };
  279. //事件
  280. Class.prototype.events = function(){
  281. var that = this
  282. ,options = that.config;
  283. //如果传入 hover,则解析为 mouseenter
  284. if(options.trigger === 'hover') options.trigger = 'mouseenter';
  285. //解除上一个事件
  286. if(that.prevElem) that.prevElem.off(options.trigger, that.prevElemCallback);
  287. //记录被绑定的元素及回调
  288. that.prevElem = options.elem;
  289. that.prevElemCallback = function(e){
  290. clearTimeout(thisModule.timer);
  291. that.e = e;
  292. that.render();
  293. e.preventDefault();
  294. //组件打开完毕的时间
  295. typeof options.ready === 'function' && options.ready(that.elemView, options.elem, that.e.target);
  296. };
  297. //触发元素事件
  298. options.elem.on(options.trigger, that.prevElemCallback);
  299. //如果是鼠标移入事件
  300. if(options.trigger === 'mouseenter'){
  301. //直行鼠标移出事件
  302. options.elem.on('mouseleave', function(){
  303. that.delayRemove();
  304. });
  305. }
  306. };
  307. //记录所有实例
  308. thisModule.that = {}; //记录所有实例对象
  309. //获取当前实例对象
  310. thisModule.getThis = function(id){
  311. var that = thisModule.that[id];
  312. if(!that) hint.error(id ? (MOD_NAME +' instance with ID \''+ id +'\' not found') : 'ID argument required');
  313. return that;
  314. };
  315. //设置菜单组展开和收缩状态
  316. thisModule.spread = function(othis){
  317. //菜单组展开和收缩
  318. var elemIcon = othis.children('.'+ STR_MENU_TITLE).find('.layui-icon');
  319. if(othis.hasClass(STR_ITEM_UP)){
  320. othis.removeClass(STR_ITEM_UP).addClass(STR_ITEM_DOWN);
  321. elemIcon.removeClass('layui-icon-down').addClass('layui-icon-up');
  322. } else {
  323. othis.removeClass(STR_ITEM_DOWN).addClass(STR_ITEM_UP);
  324. elemIcon.removeClass('layui-icon-up').addClass('layui-icon-down')
  325. }
  326. };
  327. //全局事件
  328. ;!function(){
  329. var _WIN = $(window)
  330. ,_DOC = $(document);
  331. //自适应定位
  332. _WIN.on('resize', function(){
  333. if(!dropdown.thisId) return;
  334. var that = thisModule.getThis(dropdown.thisId);
  335. if(!that) return;
  336. if(!that.elemView[0] || !$('.'+ STR_ELEM)[0]){
  337. return false;
  338. }
  339. var options = that.config;
  340. if(options.trigger === 'contextmenu'){
  341. that.remove();
  342. } else {
  343. that.position();
  344. }
  345. });
  346. //点击任意处关闭
  347. _DOC.on(clickOrMousedown, function(e){
  348. if(!dropdown.thisId) return;
  349. var that = thisModule.getThis(dropdown.thisId)
  350. if(!that) return;
  351. var options = that.config;
  352. //如果触发的是绑定的元素,或者属于绑定元素的子元素,则不关闭
  353. //满足条件:当前绑定的元素不是 body document,或者不是鼠标右键事件
  354. if(!(lay.isTopElem(options.elem[0]) || options.trigger === 'contextmenu')){
  355. if(
  356. e.target === options.elem[0] ||
  357. options.elem.find(e.target)[0] ||
  358. e.target === that.elemView[0] ||
  359. (that.elemView && that.elemView.find(e.target)[0])
  360. ) return;
  361. }
  362. that.remove();
  363. });
  364. //基础菜单的静态元素事件
  365. var ELEM_LI = '.layui-menu:not(.layui-dropdown-menu) li';
  366. _DOC.on('click', ELEM_LI, function(e){
  367. var othis = $(this)
  368. ,parent = othis.parents('.layui-menu').eq(0)
  369. ,isChild = othis.hasClass(STR_ITEM_GROUP) || othis.hasClass(STR_ITEM_PARENT)
  370. ,filter = parent.attr('lay-filter') || parent.attr('id')
  371. ,options = lay.options(this);
  372. //非触发元素
  373. if(othis.hasClass(STR_ITEM_DIV)) return;
  374. //非菜单组
  375. if(!isChild){
  376. //选中
  377. parent.find('.'+ STR_ITEM_CHECKED).removeClass(STR_ITEM_CHECKED); //清除选中样式
  378. parent.find('.'+ STR_ITEM_CHECKED2).removeClass(STR_ITEM_CHECKED2); //清除父级菜单选中样式
  379. othis.addClass(STR_ITEM_CHECKED); //添加选中样式
  380. othis.parents('.'+ STR_ITEM_PARENT).addClass(STR_ITEM_CHECKED2); //添加父级菜单选中样式
  381. //触发事件
  382. layui.event.call(this, MOD_NAME, 'click('+ filter +')', options);
  383. }
  384. });
  385. //基础菜单的展开收缩事件
  386. _DOC.on('click', (ELEM_LI + STR_GROUP_TITLE), function(e){
  387. var othis = $(this)
  388. ,elemGroup = othis.parents('.'+ STR_ITEM_GROUP +':eq(0)')
  389. ,options = lay.options(elemGroup[0]);
  390. if(('isAllowSpread' in options) ? options.isAllowSpread : true){
  391. thisModule.spread(elemGroup);
  392. };
  393. });
  394. //判断子级菜单是否超出屏幕
  395. var ELEM_LI_PAR = '.layui-menu .'+ STR_ITEM_PARENT
  396. _DOC.on('mouseenter', ELEM_LI_PAR, function(e){
  397. var othis = $(this)
  398. ,elemPanel = othis.find('.'+ STR_MENU_PANEL);
  399. if(!elemPanel[0]) return;
  400. var rect = elemPanel[0].getBoundingClientRect();
  401. //是否超出右侧屏幕
  402. if(rect.right > _WIN.width()){
  403. elemPanel.addClass(STR_MENU_PANEL_L);
  404. //不允许超出左侧屏幕
  405. rect = elemPanel[0].getBoundingClientRect();
  406. if(rect.left < 0){
  407. elemPanel.removeClass(STR_MENU_PANEL_L);
  408. }
  409. }
  410. //是否超出底部屏幕
  411. if(rect.bottom > _WIN.height()){
  412. elemPanel.eq(0).css('margin-top', -(rect.bottom - _WIN.height()));
  413. };
  414. }).on('mouseleave', ELEM_LI_PAR, function(e){
  415. var othis = $(this)
  416. ,elemPanel = othis.children('.'+ STR_MENU_PANEL);
  417. elemPanel.removeClass(STR_MENU_PANEL_L);
  418. elemPanel.css('margin-top', 0);
  419. });
  420. }();
  421. //重载实例
  422. dropdown.reload = function(id, options){
  423. var that = thisModule.getThis(id);
  424. if(!that) return this;
  425. that.reload(options);
  426. return thisModule.call(that);
  427. };
  428. //核心入口
  429. dropdown.render = function(options){
  430. var inst = new Class(options);
  431. return thisModule.call(inst);
  432. };
  433. exports(MOD_NAME, dropdown);
  434. });