accordion.js 51 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148
  1. /*!
  2. * Accordion Menu Plugin v1.0
  3. * Copyright (c) 2020 Iven Wong
  4. * Released under the MIT license
  5. */
  6. (function (global, factory) {
  7. typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(require('jquery')) :
  8. typeof define === 'function' && define.amd ? define(['jquery'], factory) :
  9. (global = global || self, global.Accordion = factory(global.jQuery))
  10. })(this, function ($) {
  11. "use strict";
  12. //#region 辅助方法
  13. /**
  14. * 对象合并
  15. * @param {object} target -目标对象,其他对象的成员属性将被附加到该对象上,合并后返回该对象
  16. * @param {object} source -提供属性合并的供体对象
  17. * @param {boolean} override -如果和合并对象有相同属性,选择是否覆盖,默认true覆盖
  18. * @returns {object} 合并后对象
  19. */
  20. function extend(target, source, override) {
  21. override = typeof override === "undefined" ? true : toBool(override);
  22. for (var key in source) {
  23. if (target.hasOwnProperty(key) && !override) continue;
  24. target[key] = source[key];
  25. }
  26. return target;
  27. }
  28. /**
  29. * 转换为布尔值
  30. * 对于0、-0、null、undefined、NaN、false、""、”false“字符串(不区分大小写)都转为false处理
  31. * @param {object} value -要转换的目标
  32. * @returns {boolean} 返回布尔值
  33. */
  34. function toBool(value) {
  35. return (typeof value === "string" && value.toLowerCase() === "false") ? false : Boolean(value);
  36. }
  37. /**
  38. * 解析一个字符串,并返回一个浮点数
  39. * @param {string} num
  40. * @returns {number}
  41. */
  42. function parseNum(num) {
  43. var n = parseFloat(num);
  44. return isNaN(n) ? 0 : n;
  45. }
  46. /**
  47. * 转为对象
  48. * @param {string|object} target -转换目标
  49. * @param {object} defaultVal -默认返回
  50. * @returns {object||null} -返回对象
  51. */
  52. function parseObj(target, defaultVal) {
  53. if (target && typeof target === 'object') {
  54. return target
  55. }
  56. else if (target && typeof target === 'string') {
  57. try {
  58. return JSON.parse(target);
  59. } catch (e) {
  60. console.error(e.message)
  61. }
  62. } else {
  63. return defaultVal || null;
  64. }
  65. }
  66. /**
  67. * 复制json对象
  68. * @param {object} obj -元对象
  69. * @returns {object} 返回复制后的对象
  70. */
  71. function copyObj(obj) {
  72. return JSON.parse(JSON.stringify(typeof obj==='undefined'?null:obj));
  73. }
  74. /**
  75. * 创建XMLHttpRequest对象
  76. * @returns {XMLHttpRequest|null}
  77. */
  78. function createRequest() {
  79. if (window.XMLHttpRequest) {
  80. //DOM 2浏览器
  81. return new XMLHttpRequest();
  82. }
  83. else if (window.ActiveXObject) {
  84. // IE浏览器
  85. var versions = ["Msxml2.XMLHTTP.6.0", "Msxml2.XMLHTTP.3.0", "Msxml2.XMLHTTP", "Microsoft.XMLHTTP"];
  86. for (var i = 0; i < versions.length; i++) {
  87. try {
  88. return new ActiveXObject(versions[i]);
  89. } catch (e) {
  90. //console.error("Your browser does not support "+versions[i]);
  91. }
  92. }
  93. }
  94. return null;
  95. }
  96. /**
  97. * 获取梯度渐变颜色组
  98. * @param {string} sColor -开始渐变色(HEX十六进制颜色码)
  99. * @param {string} eColor -结束渐变色(HEX十六进制颜色码)
  100. * @param {number} step -开始至结束颜色过渡段数
  101. * @returns {Array}
  102. */
  103. function gradientColors(sColor, eColor, step) {
  104. var startRGB = getRgbColor(sColor);//转换为rgb数组模式
  105. var startR = startRGB[0];
  106. var startG = startRGB[1];
  107. var startB = startRGB[2];
  108. var endRGB = getRgbColor(eColor);
  109. var endR = endRGB[0];
  110. var endG = endRGB[1];
  111. var endB = endRGB[2];
  112. var sR = (endR - startR) / step;//总差值
  113. var sG = (endG - startG) / step;
  114. var sB = (endB - startB) / step;
  115. var colorArr = [];
  116. for (var i = 0; i < step; i++) {
  117. //计算每一步的hex值
  118. var hex = getHexColor('rgb(' + parseInt((sR * i + startR)) + ',' + parseInt((sG * i + startG)) + ',' + parseInt((sB * i + startB)) + ')');
  119. colorArr.push(hex);
  120. }
  121. return colorArr;
  122. /**
  123. * 将hex表示方式颜色转换为rgb表示方式颜色(这里返回rgb数组模式)
  124. * @param {string} color -HEX十六进制颜色码
  125. * @returns {array}
  126. */
  127. function getRgbColor(color) {
  128. var reg = /^#([0-9a-fA-f]{3}|[0-9a-fA-f]{6})$/;
  129. var color = color.toLowerCase();
  130. if (color && reg.test(color)) {
  131. if (color.length === 4) {
  132. var sColorNew = "#";
  133. for (var i = 1; i < 4; i += 1) {
  134. sColorNew += color.slice(i, i + 1).concat(color.slice(i, i + 1));
  135. }
  136. color = sColorNew;
  137. }
  138. //处理六位的颜色值
  139. var sColorChange = [];
  140. for (var i = 1; i < 7; i += 2) {
  141. sColorChange.push(parseInt("0x" + color.slice(i, i + 2)));
  142. }
  143. return sColorChange;
  144. } else {
  145. return color;
  146. }
  147. }
  148. /**
  149. * 将rgb表示方式转换为hex表示方式
  150. * @param {string} rgb -rgb颜色
  151. * @returns {string}
  152. */
  153. function getHexColor(rgb) {
  154. var _this = rgb;
  155. var reg = /^#([0-9a-fA-f]{3}|[0-9a-fA-f]{6})$/;
  156. if (/^(rgb|RGB)/.test(_this)) {
  157. var aColor = _this.replace(/(?:(|)|rgb|RGB)*/g, "").split(",");
  158. var strHex = "#";
  159. for (var i = 0; i < aColor.length; i++) {
  160. var hex = Number(aColor[i]).toString(16);
  161. hex = hex < 10 ? 0 + '' + hex : hex;// 保证每个rgb的值为2位
  162. if (hex === "0") {
  163. hex += hex;
  164. }
  165. strHex += hex;
  166. }
  167. if (strHex.length !== 7) {
  168. strHex = _this;
  169. }
  170. return strHex;
  171. } else if (reg.test(_this)) {
  172. var aNum = _this.replace(/#/, "").split("");
  173. if (aNum.length === 6) {
  174. return _this;
  175. } else if (aNum.length === 3) {
  176. var numHex = "#";
  177. for (var i = 0; i < aNum.length; i += 1) {
  178. numHex += (aNum[i] + aNum[i]);
  179. }
  180. return numHex;
  181. }
  182. } else {
  183. return _this;
  184. }
  185. }
  186. }
  187. /**********数组排序************/
  188. /**
  189. * 对树状对象数组进行排序
  190. * @param {array} treeData -树状对象数组
  191. * @param {string} childrenField -子节点数组名称
  192. * @param {string} sortField -排序字段名称
  193. * @param {string} sortMode -排序方式asc/desc,默认asc
  194. * @returns {array} 返回排序后数组,改变原素组
  195. */
  196. function sortTreeArr(treeData, childrenField, sortField, sortMode) {
  197. sortMode = sortMode || 'asc';
  198. if (!sortField) return treeData;
  199. var n = 1;
  200. if (sortMode && sortMode.toLowerCase() == "desc") n = -1;
  201. for (var i = 0; i < treeData.length; i++) {
  202. if (treeData[i][childrenField])
  203. sortTreeArr(treeData[i][childrenField], childrenField, sortField, sortMode)
  204. }
  205. sortArr(treeData, sortMode, sortField);
  206. return treeData;
  207. }
  208. /**
  209. * 数组排序
  210. * @param {array} arr 目标数组
  211. * @param {string} sortMode 排序方式,asc升序,desc降序
  212. * @param {string} sortField 排序字段,没有则为空
  213. * @returns {array} 返回排序后数组,改变原素组
  214. */
  215. function sortArr(arr, sortMode, sortField) {
  216. sortField = sortField || '';
  217. var n = 1;
  218. if (sortMode && sortMode.toLowerCase() == "desc") n = -1;
  219. arr.sort(function (a, b) {
  220. var a1, b1;
  221. a1 = sortField ? a[sortField] : a;
  222. b1 = sortField ? b[sortField] : b;
  223. if (a1 > b1) {
  224. return 1 * n;
  225. }
  226. else if (a1 < b1) {
  227. return -1 * n;
  228. } else {
  229. return 0 * n;
  230. }
  231. });
  232. return arr;
  233. }
  234. //#endregion
  235. //#region HTMLElement节点元素
  236. /**
  237. * 向指定元素添加绑定事件句柄
  238. * @param {object} ele -要绑定事件的dom对象目标
  239. * @param {string} event -要指定的事件名称。注意:不要使用"on"前缀
  240. * @param {function} fn -指定要事件触发时执行的函数。注意:若fn为匿名函数则无法通过removeEventListener方法移除该事件
  241. * @param {boolean} useCapture -布尔值,指定事件是否在捕获或冒泡阶段执行;true-事件句柄在捕获阶段执行,false(默认)-事件句柄在冒泡阶段执行
  242. */
  243. function addEvent(ele, event, fn, useCapture) {
  244. //useCapture undefined即默认false
  245. useCapture = toBool(useCapture);
  246. ele.addEventListener ? ele.addEventListener(event, fn, useCapture) : (ele.attachEvent ? ele.attachEvent("on" + event, fn, useCapture) : ele["on" + event] = fn);
  247. }
  248. /**
  249. * 移除指定元素绑定的事件句柄(必须addEventListener方法添加的事件句柄)
  250. * @param {object} ele -要绑定事件的dom对象目标
  251. * @param {string} event -要移除的事件名称。注意:不要使用"on"前缀
  252. * @param {function} fn -指定要移除的函数。注意:若addEventListener使用的fn为匿名函数则无法通过removeEventListener方法移除该事件
  253. * @param {boolean} useCapture -布尔值,指定移除事件句柄的阶段;true-在捕获阶段移除事件句柄,false(默认)-在冒泡阶段移除事件句柄
  254. */
  255. function removeEvent(ele, event, fn, useCapture) {
  256. useCapture = toBool(useCapture);
  257. ele.removeEventListener ? ele.removeEventListener(event, fn, useCapture) : ele.detachEvent("on" + event, fn, useCapture);
  258. }
  259. /**
  260. * 阻止特定事件的默认行为
  261. * @param {object} event -事件对象
  262. */
  263. function preventDefaultEvent(event) {
  264. event = event || window.event;//兼容IE
  265. //preventDefault W3C标准技术;returnValue 兼容IE
  266. event.preventDefault ? event.preventDefault() : event.returnValue = false;
  267. //用于处理使用对象属性注册的处理程序
  268. return false;
  269. }
  270. /**
  271. * 阻止事件(捕获和冒泡阶段)传播
  272. * @param {object} event -事件对象
  273. */
  274. function stopPropagationEvent(event) {
  275. event = event || window.event;
  276. //阻止捕获和冒泡阶段当前事件的进一步传播(IE是不支持事件捕获)
  277. //stopPropagation W3C标准;cancelBubble 兼容IE
  278. event.stopPropagation ? event.stopPropagation() : event.cancelBubble = true;
  279. }
  280. /**
  281. * 在指定的元素节点上存取数据,返回设置值
  282. * @param {HTMLElement} el -dom节点对象
  283. * @param {string} key -可选。String类型 指定的键名字符串
  284. * @param {object} value -可选。 Object类型 需要存储的任意类型的数据
  285. * @returns {HTMLElement|object} 存数据时返回当前dom节点对象,取数据时则返回之前存的数据
  286. * @description HTMLElement类型 要存储数据的DOM对象。参数key,value都不为空则存数据,否则为取数据。都为空时取所有存储的数据
  287. */
  288. function elData(el, key, value) {
  289. var _dataname = '_elData', ktype = typeof(key), vtype = typeof(value);
  290. key = ktype === 'string' ? key.trim() : key;
  291. //set
  292. if (ktype !== 'undefined' && vtype !== 'undefined') {
  293. if (key === null || ktype === 'number' || ktype === 'boolean') {
  294. return el
  295. }
  296. if (!(_dataname in el)) {
  297. el[_dataname] = {}
  298. }
  299. el[_dataname][key] = value;
  300. return el
  301. }
  302. //get
  303. if (ktype === 'undefined' && vtype === 'undefined') {
  304. return el[_dataname] || {}
  305. }
  306. if (ktype !== 'undefined' && vtype === 'undefined') {
  307. return (_dataname in el && key in el[_dataname]) ? el[_dataname][key] : undefined
  308. }
  309. }
  310. /*
  311. /!**
  312. * 移除之前通过elData()法绑定的数据,返回当前dom节点
  313. * @param {HTMLElement} el -dom节点对象
  314. * @param {string} key -可选,规定要移除的数据的名称。如果没有规定名称,该方法将从被选元素中移除所有已存储的数据。
  315. * @returns {HTMLElement} 返回当前dom元素节点
  316. *!/
  317. function delElData(el, key) {
  318. var type = typeof(key), _dataname = '_elData';
  319. key = type === 'string' ? key.trim() : key;
  320. if (key === null || type === 'number' || type === 'boolean') {
  321. return el
  322. }
  323. if (type === 'undefined') {//remove all
  324. if (_dataname in el) delete el[_dataname]
  325. } else {
  326. if (_dataname in el && key in el[_dataname]) delete el[_dataname][key]
  327. }
  328. return el
  329. }
  330. */
  331. //#region slide平缓滑动动画效果
  332. /**
  333. * 获取dom元素的CSS属性的值
  334. * @param {HTMLElement} el -dom元素
  335. * @param {string} prop -css属性名
  336. * @returns {string}
  337. */
  338. function getStyle(el, prop) {
  339. return window.getComputedStyle ? getComputedStyle(el, null)[prop] : el.currentStyle[prop]
  340. }
  341. /**slide滑动收展节点元素,不考虑’border-box:box-sizing‘这种情况(可能卡顿收展不能平滑过渡)**/
  342. /**
  343. * 记录绑定一些与高度相关的css信息到节点元素上即便后面元素的css样式有变化也可以从中取得其原始值
  344. * @param {HTMLElement} el -dom元素
  345. */
  346. function markCss(el) {
  347. if (!elData(el, 'slide')) {
  348. elData(el, 'slide', true);
  349. elData(el, 'cssText', el.style.cssText);
  350. elData(el, 'borderTopWidth', getStyle(el, 'border-top-width'));
  351. elData(el, 'borderBottomWidth', getStyle(el, 'border-bottom-width'));
  352. elData(el, 'paddingTop', getStyle(el, 'padding-top'));
  353. elData(el, 'paddingBottom', getStyle(el, 'padding-bottom'));
  354. elData(el, 'height', getStyle(el, 'height'))
  355. }
  356. if (elData(el, 'height') === 'auto') {
  357. el.setAttribute('hidden', true);
  358. var c = el.style.cssText;
  359. el.style.cssText = 'display:block';
  360. elData(el, 'height', getStyle(el, 'height'));
  361. el.style.cssText = c;
  362. el.removeAttribute('hidden');
  363. }
  364. }
  365. /**
  366. * 获取当前状态中与元素高度相关的css样式值
  367. * @param {HTMLElement} el -dom节点对象
  368. * @returns {{bt: string, bb: string, pt: string, pb: string, h: string}}
  369. */
  370. function nowH(el) {
  371. return {
  372. bt: getStyle(el, 'border-top-width'),
  373. bb: getStyle(el, 'border-bottom-width'),
  374. pt: getStyle(el, 'padding-top'),
  375. pb: getStyle(el, 'padding-bottom'),
  376. h: getStyle(el, 'height'),
  377. }
  378. }
  379. /**
  380. * 获取最初始未有更改过的block状态中与元素高度相关的css样式值
  381. * @param {HTMLElement} el -dom节点对象
  382. * @returns {{css: (HTMLElement|Object), bt: (HTMLElement|Object), bb: (HTMLElement|Object), pt: (HTMLElement|Object), pb: (HTMLElement|Object), h: (HTMLElement|Object)}}
  383. */
  384. function endH(el) {
  385. return {
  386. css: elData(el, 'cssText'),
  387. bt: elData(el, 'borderTopWidth'),
  388. bb: elData(el, 'borderBottomWidth'),
  389. pt: elData(el, 'paddingTop'),
  390. pb: elData(el, 'paddingBottom'),
  391. h: elData(el, 'height'),
  392. }
  393. }
  394. /**
  395. * 以滑动方式隐藏节点
  396. * @param {HTMLElement} el -dom节点对象
  397. * @param {number} millisecond -滑动速度(完成滑动所需毫秒时间),默认值300
  398. */
  399. function slideUp(el, millisecond) {
  400. markCss(el);
  401. elData(el, 'slideToggle', 'slideup');
  402. var slide = Symbol('slide').toString(),
  403. now = nowH(el),
  404. end = endH(el),
  405. // bt = parseNum(end.bt),
  406. // bb = parseNum(end.bb),
  407. // pt = parseNum(end.pt),
  408. // pb = parseNum(end.pb),
  409. // h = parseNum(end.h),
  410. //total = h + pt + pb + bt + bb,
  411. //sum = total - (parseNum(now.bt) + parseNum(now.bb) + parseNum(now.pt) + parseNum(now.pb) + parseNum(now.h)),
  412. bt = parseNum(now.bt),
  413. bb = parseNum(now.bb),
  414. pt = parseNum(now.pt),
  415. pb = parseNum(now.pb) ,
  416. h = parseNum(now.h),
  417. total=(parseNum(now.bt) + parseNum(now.bb) + parseNum(now.pt) + parseNum(now.pb) + parseNum(now.h)),
  418. sum=0,
  419. finish = false,
  420. speed = (millisecond ? total / millisecond : total / 300) * 5;
  421. el.style.cssText = el.style.cssText + 'overflow:hidden;';
  422. clearInterval(el[slide]);
  423. el[slide] = setInterval(function () {
  424. if (finish) {
  425. clearInterval(el[slide]);
  426. el.style.cssText = end.css + 'display:none';
  427. if (slide in el) {
  428. delete el[slide]
  429. }
  430. } else {
  431. sum += speed;
  432. if (bb - sum > 0) {
  433. el.style.borderBottomWidth = bb - sum + 'px'
  434. } else {
  435. el.style.borderBottomWidth = 0 + 'px';
  436. if (bb + pb - sum > 0) {
  437. el.style.paddingBottom = bb + pb - sum + 'px';
  438. } else {
  439. el.style.paddingBottom = 0 + 'px';
  440. if (bb + pb + h - sum > 0) {
  441. el.style.height = bb + pb + h - sum + 'px';
  442. } else {
  443. el.style.height = 0 + 'px';
  444. if (bb + pb + h + pt - sum > 0) {
  445. el.style.paddingTop = bb + pb + h + pt - sum + 'px';
  446. } else {
  447. el.style.paddingTop = 0 + 'px';
  448. if (bb + pb + h + pt + bt - sum > 0) {
  449. el.style.borderTopWidth = bb + pb + h + pt + bt - sum + 'px';
  450. } else {
  451. el.style.borderTopWidth = 0 + 'px';
  452. finish = true;
  453. }
  454. }
  455. }
  456. }
  457. }
  458. }
  459. }, 5);//间隔时间不要过小或过大,否则最终花费时间会与设定的完成时间误差较大,且设置间隔过大会卡顿没有平缓过度效果
  460. }
  461. /**
  462. * 以滑动方式显示节点
  463. * @param {HTMLElement} el -dom节点对象
  464. * @param {number} millisecond 滑动速度(完成滑动所需毫秒时间),默认值300
  465. */
  466. function slideDown(el, millisecond) {
  467. markCss(el);
  468. elData(el, 'slideToggle', 'slidedown');
  469. var slide = Symbol('slide').toString(),
  470. now = nowH(el),
  471. end = endH(el),
  472. bt = parseNum(end.bt),
  473. bb = parseNum(end.bb),
  474. pt = parseNum(end.pt),
  475. pb = parseNum(end.pb),
  476. h = parseNum(end.h),
  477. total = h + pt + pb + bt + bb,
  478. finish = false,
  479. speed = (millisecond ? total / millisecond : total / 300) * 5,
  480. sum = 0;
  481. if (getStyle(el, 'display') === 'none') {
  482. el.style.cssText = end.css + 'overflow:hidden;height:0;display:block;border-top-width:0;border-bottom-width:0;padding-top:0;padding-bottom:0;';
  483. } else {
  484. el.style.cssText = el.style.cssText + 'overflow:hidden';
  485. sum = parseNum(now.bt) + parseNum(now.bb) + parseNum(now.pt) + parseNum(now.pb) + parseNum(now.h);
  486. }
  487. clearInterval(el[slide]);
  488. el[slide] = setInterval(function () {
  489. if (finish) {
  490. clearInterval(el[slide]);
  491. el.style.cssText = end.css + 'display:block';
  492. if (slide in el) {
  493. delete el[slide]
  494. }
  495. } else {
  496. sum += speed;
  497. if (bt - sum > 0) {
  498. el.style.borderTopWidth = sum + 'px';
  499. } else {
  500. el.style.borderTopWidth = bt + 'px';
  501. if (bt + pt - sum > 0) {
  502. el.style.paddingTop = sum - bt + 'px';
  503. } else {
  504. el.style.paddingTop = pt + 'px';
  505. if (bt + pt + h - sum > 0) {
  506. el.style.height = sum - bt - pt + 'px';
  507. } else {
  508. el.style.height = h + 'px';
  509. if (bt + pt + h + pb - sum > 0) {
  510. el.style.paddingBottom = sum - bt - pt - h + 'px';
  511. } else {
  512. el.style.paddingBottom = pb + 'px';
  513. if (bt + pt + h + pb + bb - sum > 0) {
  514. el.style.borderBottomWidth = sum - bt - pt - h - pb + 'px';
  515. } else {
  516. el.style.borderBottomWidth = bb + 'px';
  517. finish = true;
  518. }
  519. }
  520. }
  521. }
  522. }
  523. }
  524. }, 5);
  525. }
  526. /*
  527. /!**
  528. * dom元素以滑动方式在显示隐藏状态之间切换
  529. * @param {HTMLElement} el -dom节点对象
  530. * @param {number} millisecond 滑动速度(完成滑动所需毫秒时间),默认值300
  531. *!/
  532. function slideToggle(el, millisecond) {
  533. getStyle(el, 'display') === 'none'
  534. || elData(el, 'slideToggle') === 'slideup'
  535. ? slideDown(el, millisecond)
  536. : slideUp(el, millisecond);
  537. }
  538. */
  539. //#endregion
  540. //#endregion
  541. //#region 手风琴菜单
  542. /**
  543. * 手风琴菜单
  544. * @param {string|HTMLElement} el -容器元素的CSS选择器字符串或html对象
  545. * @param {object} options -配置项,也可从标签属性设置获取
  546. */
  547. function Accordion(el, options) {
  548. if (!(this instanceof Accordion)) {
  549. return new Accordion(el, options)
  550. }
  551. this.menu = (typeof $ !== "undefined" && el instanceof jQuery) && el.length > 0
  552. ? el[0]
  553. : typeof (el) === "string"
  554. ? document.querySelector(el)
  555. : (((typeof HTMLElement === 'object')
  556. ? (el instanceof HTMLElement)
  557. : (el && typeof el === 'object' && el.nodeType === 1 && typeof el.nodeName === 'string'))
  558. ? el : null);
  559. //初始配置项
  560. this.options = {};
  561. this.menu.classList.add("accordion");
  562. this.init(options);
  563. }
  564. Accordion.prototype = {
  565. constructor: Accordion,
  566. /**
  567. * 初始化菜单
  568. * @param {object} options -配置项
  569. * @returns {Accordion}
  570. */
  571. init: function (options) {
  572. //若菜单节点中已绑定数据,直接从中取
  573. var lastOpts = elData(this.menu, 'lastOpts'),
  574. menu = this.menu;
  575. //初始化配置值,可从菜单已绑定数据或标签属性中或传入配置项中或取
  576. this.options = lastOpts || {
  577. idField: menu.getAttribute("idField") || "id",//字段名
  578. parentField: menu.getAttribute("parentField") || "pid",//父节点字段名
  579. nameField: menu.getAttribute("nameField") || "name",//节点显示文本
  580. iconField: menu.getAttribute("iconField") || "",//节点图标字段,如字体图标类
  581. sortName: menu.getAttribute("sortName") || "",//节点排序的字段名称
  582. sortOrder: menu.getAttribute("sortOrder") || "asc",//节点排序方式asc/desc
  583. childrenField: menu.getAttribute("childrenField") || "children",//子节点字段名
  584. url: menu.getAttribute("url") || "",//url加载数据初始化菜单。优先以传参data数组数据初始化菜单,若不传参则以url方式加载初始化
  585. ajaxType: menu.getAttribute('ajaxType') || 'get',//请求类型,默认get
  586. ajaxData: menu.getAttribute('ajaxData') || null,//请求参数数据
  587. asTreeData: menu.getAttribute('asTreeData') ? toBool(menu.getAttribute('asTreeData')) : true,//菜单数组数据是否以树状数组展示
  588. data: menu.getAttribute('data') || null,//初始化菜单的数据,url和data共存时优先使用data
  589. indentStep: menu.getAttribute("indentStep") || 1,//菜单层级缩进数值(单位em)
  590. startColor: menu.getAttribute("startColor") || '#18626b',//菜单开始背景色(HEX十六进制颜色码)
  591. endColor: menu.getAttribute("endColor") || '#2fb9ca',//菜单最终背景色(HEX十六进制颜色码)
  592. colorCount: menu.getAttribute("colorCount") || 5,//开始至结束每层级菜单背景色过渡段数
  593. speed: menu.getAttribute("speed") || 300,//滑动速度。菜单完成滑动展开/收缩所用时间(ms)
  594. onnodeclick: eval(menu.getAttribute("onnodeclick")) || null,//菜单节点点击fn(node,sender,ele,e)
  595. onnodemouseenter: eval(menu.getAttribute("onnodemouseenter")) || null,//鼠标进入节点fn(node,sender,ele,e)
  596. onnodemouseleave: eval(menu.getAttribute("onnodemouseleave")) || null,//鼠标离开节点fn(node,sender,ele,e)
  597. onmenuready: eval(menu.getAttribute("onmenuready")) || null//菜单加载渲染完后fn(sender)
  598. };
  599. this.options = extend(this.options, options || {}, true);
  600. var opts = this.options,
  601. _this = this,
  602. colorList = gradientColors(opts.startColor, opts.endColor, opts.colorCount);
  603. setOpts();
  604. render();
  605. bindEvent();
  606. return this;
  607. //#region init
  608. function setOpts() {
  609. opts.ajaxData = parseObj(opts.ajaxData);
  610. opts.data = parseObj(opts.data);
  611. if (!opts.data) {
  612. urlGetData()
  613. }
  614. opts.data = getFmtData(opts.asTreeData);
  615. if (opts.asTreeData) {
  616. elData(menu, 'tree', opts.data);
  617. elData(menu, 'list', getFmtData(!opts.asTreeData))
  618. } else {
  619. elData(menu, 'list', opts.data);
  620. elData(menu, 'tree', getFmtData(!opts.asTreeData))
  621. }
  622. if (!lastOpts) {
  623. //清除标签自定义属性
  624. for (var k in opts) {
  625. menu.removeAttribute(k);
  626. }
  627. }
  628. //将配置项数据绑定到菜单可下次获取
  629. elData(menu, 'lastOpts', opts);
  630. }
  631. /**
  632. * 根据url初始化菜单的数据
  633. */
  634. function urlGetData() {
  635. var url = _this.options.url,
  636. type = _this.options.ajaxType,
  637. json = _this.options.ajaxData;
  638. if (url) {
  639. var xhr = createRequest();
  640. xhr.onreadystatechange = function () {
  641. if (xhr.readyState === 4 && xhr.status === 200) {
  642. _this.options.data = typeof xhr.responseText === 'string' ? JSON.parse(xhr.responseText) : xhr.responseText;
  643. }
  644. };
  645. if (type && type.toLowerCase() === 'post') {
  646. xhr.open('post', url, false);
  647. xhr.setRequestHeader("Content-type", "application/json;charset=utf-8");
  648. xhr.send(JSON.stringify(json));
  649. } else {
  650. if (json) {
  651. var prms = '';
  652. for (var k in json) {
  653. prms += '&' + k + '=' + json[k];
  654. }
  655. url += url.indexOf('?') !== -1 ? prms : prms.replace('&', '?');
  656. }
  657. xhr.open('get', url, false);
  658. xhr.send(null);
  659. }
  660. }
  661. else {
  662. _this.options.data = []
  663. }
  664. }
  665. /**
  666. * 渲染生成菜单
  667. */
  668. function render() {
  669. _this.menu.innerHTML = '';
  670. createUl(_this.getData(true), _this.menu, null, 1);
  671. // 处理菜单层级缩进
  672. menuIndent();
  673. //菜单渲染完回调函数
  674. opts.onmenuready && eval(opts.onmenuready) && eval(opts.onmenuready)(_this);
  675. }
  676. /**
  677. * 创建ul菜单块
  678. * @param {array} data -必需。生成菜单的数据数组
  679. * @param {HTMLElement} box -必需。当前创建菜单块的容器节点
  680. * @param {string|number|object} pid -可选。上级菜单id
  681. * @param {number} lv -当前菜单层级数
  682. */
  683. function createUl(data, box, pid, lv) {
  684. var ul = document.createElement('ul');
  685. data.forEach(function (item) {
  686. var li = createLi(item);
  687. //赋值当前节点数据,并将该数据绑定到该节点标签中
  688. var sender = {
  689. node: copyObj(item),//当前项
  690. level: lv,
  691. isLeaf: false//是否叶子节点
  692. };
  693. //删除该节点的子节点数组数据,只保留自身数据
  694. if (opts.childrenField in sender.node) delete sender.node[opts.childrenField];
  695. sender.node[opts.parentField] = pid;
  696. //设置每层级菜单背景色
  697. li.querySelector("a.menuitem").style.background = lv < colorList.length + 1 ? colorList[lv - 1] : colorList[colorList.length - 1];
  698. //绑定数据到标签中
  699. elData(li.querySelector(":scope>.menuitem"), 'sender', sender);
  700. if (item[opts.childrenField] && item[opts.childrenField].length > 0) {
  701. li.querySelector('a').classList.add('submenu');
  702. //创建子菜单
  703. var subLv = lv + 1;
  704. createUl(item[opts.childrenField], li, item[opts.idField], subLv);
  705. } else {
  706. sender.isLeaf = true;
  707. }
  708. ul.appendChild(li);
  709. });
  710. box.appendChild(ul);
  711. }
  712. /**
  713. * 创建li菜单节点
  714. * @param {object} item -菜单节点数据
  715. * @returns {HTMLLIElement}
  716. */
  717. function createLi(item) {
  718. var iconClass = opts.iconField ? (item[opts.iconField] ? item[opts.iconField] : 'noicon') : "";
  719. var li = document.createElement('li');
  720. var a = document.createElement('a');
  721. var i = document.createElement('i');
  722. var txt = document.createTextNode(item[opts.nameField]);
  723. a.setAttribute('class', 'menuitem');
  724. a.setAttribute('data-id', typeof item[opts.idField] === 'string' ? item[opts.idField].trim() : item[opts.idField]);
  725. iconClass && i.setAttribute('class', iconClass);
  726. a.appendChild(i);
  727. a.appendChild(txt);
  728. li.appendChild(a);
  729. return li;
  730. }
  731. /**
  732. * 处理菜单层级文本缩进
  733. */
  734. function menuIndent() {
  735. var ul = _this.menu.querySelector('.accordion>ul'),
  736. indent = 0,
  737. step = parseFloat(opts.indentStep);
  738. step = isNaN(step) ? 1 : step;
  739. nodeIndent(ul, indent);
  740. function nodeIndent(ul, indent) {
  741. var uls = ul.querySelectorAll(':scope>li>ul');//':scope'若有兼容问题,请使用下面注释方法setItem?
  742. indent = parseFloat(indent) + step;
  743. uls.forEach(function (item) {
  744. var a = item.querySelectorAll(':scope>li>a');
  745. a.forEach(function (m) {
  746. m.style.paddingLeft = indent + 'em';
  747. });
  748. nodeIndent(item, indent);
  749. })
  750. }
  751. /*
  752. function nodeIndent(ul, indent) {
  753. indent = parseFloat(indent) + step;
  754. var c = ul.children;
  755. for (var i = 0; i < c.length; i++) {
  756. if (c[i].tagName === 'LI') {
  757. var s = c[i].children;
  758. for (var j = 0; j < s.length; j++) {
  759. if (s[j].tagName === 'UL') {
  760. var l = s[j].children;
  761. for (var k = 0; k < l.length; k++) {
  762. if (l[k].tagName === 'LI') {
  763. var a = l[k].children;
  764. for (var m = 0; m < a.length; m++) {
  765. if (a[m].tagName === 'A') {
  766. a[m].style.paddingLeft = indent + 'em';
  767. }
  768. }
  769. }
  770. }
  771. nodeIndent(s[j], indent);
  772. }
  773. }
  774. }
  775. }
  776. }
  777. */
  778. }
  779. /**
  780. * 菜单节点绑定事件
  781. */
  782. function bindEvent() {
  783. _this.menu.querySelectorAll('a.menuitem').forEach(function (item) {
  784. // //解绑hover和click事件
  785. removeEvent(item, 'click', clickFn, false);
  786. removeEvent(item, 'mouseenter', enterFn, false);
  787. removeEvent(item, 'mouseleave', leaveFn, false);
  788. //绑定hover和click事件
  789. addEvent(item, 'click', clickFn, false);
  790. addEvent(item, 'mouseenter', enterFn, false);
  791. addEvent(item, 'mouseleave', leaveFn, false);
  792. });
  793. //绑定事件 event
  794. /**
  795. * 鼠标在节点上
  796. * @param e
  797. */
  798. function enterFn(e) {
  799. this.setAttribute('title', this.innerText);
  800. /**
  801. * 鼠标进入菜单节点事件回调函数
  802. *@param {object} sender 菜单控件对象及节点信息
  803. *@param {object} e event对象
  804. * */
  805. opts.onnodemouseenter && eval(opts.onnodemouseenter)(copyObj(elData(this, 'sender')),_this,this, e);
  806. preventDefaultEvent(e);
  807. stopPropagationEvent(e);
  808. }
  809. /**
  810. * 鼠标离开节点
  811. * @param e
  812. */
  813. function leaveFn(e) {
  814. this.removeAttribute('title');
  815. /**
  816. * 鼠标离开菜单节点事件回调函数
  817. *@param {object} sender 菜单控件对象及节点信息
  818. *@param {object} e event对象
  819. * */
  820. opts.onnodemouseleave && eval(opts.onnodemouseleave)(copyObj(elData(this, 'sender')),_this,this,e);
  821. preventDefaultEvent(e);
  822. stopPropagationEvent(e);
  823. }
  824. //或用js原生方法animate()实现滑动动画
  825. /**
  826. * 点击节点滑动收展菜单
  827. * @param e
  828. */
  829. function clickFn(e) {
  830. var speed = _this.options.speed, _self = this;
  831. if (this.classList.contains('submenu')) {//有子菜单,则展开或折叠
  832. if (this.classList.contains('iconopen')) {
  833. this.parentNode.querySelectorAll('.iconopen,ul').forEach(function (o) {
  834. o.tagName === 'A' ? o.classList.remove('iconopen') :getStyle(o,'display')!=='none'?slideUp(o, speed):'';
  835. });
  836. } else {
  837. this.classList.add('iconopen');
  838. // 若':scope'伪选择器有兼容性问题可使用下面注释代码代替
  839. this.parentNode.parentNode.querySelectorAll(':scope>li>a').forEach(function (item) {
  840. if (item !== _self) {
  841. item.parentNode.querySelectorAll('.iconopen,ul').forEach(function (o) {
  842. o.tagName === 'A' ? o.classList.remove('iconopen') :getStyle(o,'display')!=='none'?slideUp(o, speed):'';
  843. });
  844. } else {
  845. var sub = item.parentNode.querySelector('ul');
  846. sub && slideDown(sub, speed);
  847. }
  848. });
  849. /*
  850. var lis = this.parentNode.parentNode.children;
  851. for (var i = 0; i < lis.length; i++) {
  852. if (lis[i] !== this.parentNode) {
  853. lis[i].querySelectorAll('.iconopen,ul').forEach(function (o) {
  854. o.tagName === 'A' ? o.classList.remove('iconopen') : slideUp(o, speed)
  855. });
  856. } else {
  857. var subUl = lis[i].children;
  858. for (var k = 0; k < subUl.length; k++) {
  859. if (subUl[k].tagName === 'UL') {
  860. slideDown(subUl[k], speed);
  861. }
  862. }
  863. }
  864. }
  865. */
  866. }
  867. }
  868. _this.menu.querySelectorAll('a.menuitem').forEach(function (item) {
  869. item.classList.remove('activeitem');
  870. });
  871. this.classList.add('activeitem');
  872. /**
  873. * 鼠标进入菜单节点事件回调函数
  874. *@param {object} sender -菜单控件对象及节点信息
  875. *@param {object} e -事件对象
  876. * */
  877. opts.onnodeclick && eval(opts.onnodeclick)(copyObj(elData(this, 'sender')),_this,this, e);
  878. preventDefaultEvent(e);
  879. stopPropagationEvent(e);
  880. }
  881. /*
  882. /!**
  883. * 点击节点收展菜单(无平缓滑动效果)
  884. * @param e
  885. *!/
  886. function clickFn(e) {
  887. if (this.classList.contains('submenu')) {//有子菜单,则展开或折叠
  888. if (this.classList.contains('iconopen')) {
  889. this.parentNode.querySelectorAll('.iconopen,ul').forEach(function(o){
  890. o.tagName==='A'?o.classList.remove('iconopen'):o.classList.remove('itemshow');
  891. });
  892. } else {
  893. this.classList.add('iconopen');
  894. var s = this.parentNode.parentNode.children;
  895. for (var i = 0; i < s.length; i++) {
  896. if (s[i] !== this.parentNode) {
  897. s[i].querySelectorAll('.iconopen,ul').forEach(function (o) {
  898. o.tagName==='A'?o.classList.remove('iconopen'):o.classList.remove('itemshow');
  899. });
  900. }else{
  901. for(var k=0;k<s[i].children.length;k++){
  902. if(s[i].children[k].tagName==='UL'){
  903. s[i].children[k].classList.add('itemshow');
  904. }
  905. }
  906. }
  907. }
  908. }
  909. }
  910. _this.menu.querySelectorAll('a.menuitem').forEach(function (item) {
  911. item.classList.remove('activeitem');
  912. });
  913. this.classList.add('activeitem');
  914. /!**
  915. * 鼠标进入菜单节点事件回调函数
  916. *@param {object} sender -菜单控件对象及节点信息
  917. *@param {object} e -事件对象
  918. * *!/
  919. opts.onnodeclick && eval(opts.onnodeclick)(copyObj(elData(this, 'sender')),_this,this,e);
  920. preventDefaultEvent(e);
  921. stopPropagationEvent(e);
  922. }
  923. */
  924. }
  925. /**
  926. * 获取指定结构的菜单数据数组
  927. * @param {boolean} asTree -数组是否以树状结构数组展示
  928. * @returns {Array}
  929. */
  930. function getFmtData(asTree) {
  931. var data = copyObj(opts.data), arr = [];
  932. asTree = typeof asTree === 'undefined' ? toBool(opts.asTreeData) : toBool(asTree);
  933. if (asTree) {
  934. for (var i = 0; i < data.length; i++) {
  935. //是否叶子节点,从叶子节点开始到跟节点逐步构建TreeData格式数据
  936. var isLeaf = true;
  937. for (var k = 0; k < data.length; k++) {
  938. //节点id是其它节点pid,不是叶子节点
  939. if (data[i][opts.idField] === data[k][opts.parentField]) {
  940. isLeaf = false
  941. }
  942. }
  943. //如果是叶子节点,将叶子节点加到父节点的children数组内
  944. if (isLeaf) {
  945. for (var j = 0; j < data.length; j++) {
  946. if (data[j][opts.idField] === data[i][opts.parentField]) {
  947. //若children属性不存在或不是数组,则默认设置为空数组
  948. if (!opts.childrenField in data[j] || !Array.isArray(data[j][opts.childrenField])) data[j][opts.childrenField] = [];
  949. //删除其pid属性
  950. if (opts.parentField in data[i]) delete data[i][opts.parentField];
  951. data[j][opts.childrenField].push(data[i]);
  952. //添加到父项后删除原数组该项,并重新遍历数组
  953. data.splice(i, 1);
  954. i = -1;//重置i值,++后重0重新开始循环
  955. break;
  956. }
  957. }
  958. }
  959. }
  960. //删除第一维数组各项的pid属性
  961. for (var i = 0; i < data.length; i++) {
  962. if (opts.parentField in data[i]) delete data[i][opts.parentField];
  963. }
  964. //将数组先排序在返回数组
  965. return sortTreeArr(data, opts.childrenField, opts.sortName, opts.sortOrder);
  966. } else {
  967. toArr(data, arr, null);
  968. return sortArr(arr, opts.sortOrder, opts.sortName)
  969. }
  970. function toArr(data, arr, pid) {
  971. for (var i = 0; i < data.length; i++) {
  972. //赋值对象值并进行修改而不影响被复制对象的值
  973. var obj = copyObj(data[i]);
  974. obj[opts.parentField] = opts.parentField in data[i] ? data[i][opts.parentField] : pid;
  975. //删除子节点数组属性
  976. if (opts.childrenField in obj) delete obj[opts.childrenField];
  977. arr.push(obj);
  978. //对有子节点数组属性的节点递归
  979. if (opts.childrenField in data[i]) toArr(data[i][opts.childrenField], arr, data[i][opts.idField]);
  980. }
  981. }
  982. }
  983. //#endregion;
  984. },
  985. /**
  986. * 获取菜单数据数组
  987. * @param {boolean} asTree -可选。数组是否以树状结构数组展示,默认取决初始化配置属性asTreeData
  988. * @returns {array}
  989. */
  990. getData: function (asTree) {
  991. asTree = typeof asTree === 'undefined' ? toBool(this.options.asTreeData) : toBool(asTree);
  992. return copyObj(asTree ? elData(this.menu, 'tree') : elData(this.menu, 'list'))
  993. },
  994. /**
  995. * 根据节点id获取节点
  996. * @param {*} id -要获取节点的id
  997. * @returns {object}
  998. */
  999. getNode: function (id) {
  1000. var node = this.menu.querySelector("[data-id='" + id + "']");
  1001. return copyObj(node ? elData(node, "sender").node : null)
  1002. },
  1003. /**
  1004. * 获取目标节点的父节点
  1005. * @param {object} node -目标节点
  1006. * @returns {object}
  1007. */
  1008. getParentNode: function (node) {
  1009. return this.getNode(node[this.options.parentField])
  1010. },
  1011. /**
  1012. * 获取当前选中的节点
  1013. * @returns {object}
  1014. */
  1015. getSelectNode: function () {
  1016. return copyObj(elData(this.menu.querySelector("a.activeitem"), "sender").node)
  1017. },
  1018. /**
  1019. * 获取目标节点的子节点
  1020. * @param {object} node -目标节点
  1021. * @param {boolean} asTree -可选。数组是否以树状结构数组展示,默认取决初始化配置属性asTreeData
  1022. * @param {boolean} deep -可选。是否获取该节点下所有子孙节点,默认false
  1023. * @returns {array}
  1024. */
  1025. getChildNodes: function (node, asTree, deep) {
  1026. asTree = typeof asTree === 'undefined' ? toBool(this.options.asTreeData) : toBool(asTree);
  1027. deep = toBool(deep);
  1028. var opts = this.options,
  1029. data = copyObj(this.getData(asTree)),
  1030. arr = [];
  1031. if (asTree) {
  1032. getTree(data);
  1033. } else {
  1034. getList(node[opts.idField]);
  1035. }
  1036. //list children
  1037. function getList(pid) {
  1038. for (var i = 0; i < data.length; i++) {
  1039. if (data[i][opts.parentField] === pid) {
  1040. arr.push(data[i]);
  1041. deep && getList(data[i][opts.idField]);
  1042. data.splice(i, 1);
  1043. i = 0;
  1044. }
  1045. }
  1046. return arr
  1047. }
  1048. //tree children
  1049. function getTree(data) {
  1050. data.forEach(function (item) {
  1051. if (item[opts.idField] === node[opts.idField]) {
  1052. if (!(opts.childrenField in item) || item[opts.childrenField].length === 0) {
  1053. arr = [];
  1054. return arr
  1055. } else {
  1056. arr = item[opts.childrenField];
  1057. if (!deep) {
  1058. arr.forEach(function (obj) {
  1059. if (opts.childrenField in item) {
  1060. delete obj[opts.childrenField]
  1061. }
  1062. });
  1063. }
  1064. return arr
  1065. }
  1066. }
  1067. else {
  1068. if ((opts.childrenField in item) && item[opts.childrenField].length > 0) {
  1069. getTree(item[opts.childrenField]);
  1070. }
  1071. }
  1072. });
  1073. }
  1074. return arr
  1075. }
  1076. };
  1077. //#endregion
  1078. //jQuery
  1079. if (typeof $ !== "undefined") {
  1080. $.fn.accordion = function () {
  1081. var data = this.removeData("accordion"),
  1082. options = $.extend(true, {}, $.fn.accordion.data, arguments[0]);
  1083. data = new Accordion(this, options);
  1084. this.data("accordion", data);
  1085. return $.extend(true, this, data)
  1086. };
  1087. $.fn.accordion.constructor = Accordion
  1088. }
  1089. return Accordion
  1090. });