1
0

luci.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511
  1. (function(window, document, undefined) {
  2. var modalDiv = null,
  3. tooltipDiv = null,
  4. tooltipTimeout = null,
  5. dummyElem = null,
  6. domParser = null;
  7. LuCI.prototype = {
  8. /* URL construction helpers */
  9. path: function(prefix, parts) {
  10. var url = [ prefix || '' ];
  11. for (var i = 0; i < parts.length; i++)
  12. if (/^(?:[a-zA-Z0-9_.%,;-]+\/)*[a-zA-Z0-9_.%,;-]+$/.test(parts[i]))
  13. url.push('/', parts[i]);
  14. if (url.length === 1)
  15. url.push('/');
  16. return url.join('');
  17. },
  18. url: function() {
  19. return this.path(this.env.scriptname, arguments);
  20. },
  21. resource: function() {
  22. return this.path(this.env.resource, arguments);
  23. },
  24. location: function() {
  25. return this.path(this.env.scriptname, this.env.requestpath);
  26. },
  27. /* HTTP resource fetching */
  28. get: function(url, args, cb) {
  29. return this.poll(0, url, args, cb, false);
  30. },
  31. post: function(url, args, cb) {
  32. return this.poll(0, url, args, cb, true);
  33. },
  34. poll: function(interval, url, args, cb, post) {
  35. var data = post ? { token: this.env.token } : null;
  36. if (!/^(?:\/|\S+:\/\/)/.test(url))
  37. url = this.url(url);
  38. if (typeof(args) === 'object' && args !== null) {
  39. data = data || {};
  40. for (var key in args)
  41. if (args.hasOwnProperty(key))
  42. switch (typeof(args[key])) {
  43. case 'string':
  44. case 'number':
  45. case 'boolean':
  46. data[key] = args[key];
  47. break;
  48. case 'object':
  49. data[key] = JSON.stringify(args[key]);
  50. break;
  51. }
  52. }
  53. if (interval > 0)
  54. return XHR.poll(interval, url, data, cb, post);
  55. else if (post)
  56. return XHR.post(url, data, cb);
  57. else
  58. return XHR.get(url, data, cb);
  59. },
  60. stop: function(entry) { XHR.stop(entry) },
  61. halt: function() { XHR.halt() },
  62. run: function() { XHR.run() },
  63. /* Modal dialog */
  64. showModal: function(title, children) {
  65. var dlg = modalDiv.firstElementChild;
  66. dlg.setAttribute('class', 'modal');
  67. this.dom.content(dlg, this.dom.create('h4', {}, title));
  68. this.dom.append(dlg, children);
  69. document.body.classList.add('modal-overlay-active');
  70. return dlg;
  71. },
  72. hideModal: function() {
  73. document.body.classList.remove('modal-overlay-active');
  74. },
  75. /* Tooltip */
  76. showTooltip: function(ev) {
  77. var target = findParent(ev.target, '[data-tooltip]');
  78. if (!target)
  79. return;
  80. if (tooltipTimeout !== null) {
  81. window.clearTimeout(tooltipTimeout);
  82. tooltipTimeout = null;
  83. }
  84. var rect = target.getBoundingClientRect(),
  85. x = rect.left + window.pageXOffset,
  86. y = rect.top + rect.height + window.pageYOffset;
  87. tooltipDiv.className = 'cbi-tooltip';
  88. tooltipDiv.innerHTML = '▲ ';
  89. tooltipDiv.firstChild.data += target.getAttribute('data-tooltip');
  90. if (target.hasAttribute('data-tooltip-style'))
  91. tooltipDiv.classList.add(target.getAttribute('data-tooltip-style'));
  92. if ((y + tooltipDiv.offsetHeight) > (window.innerHeight + window.pageYOffset)) {
  93. y -= (tooltipDiv.offsetHeight + target.offsetHeight);
  94. tooltipDiv.firstChild.data = '▼ ' + tooltipDiv.firstChild.data.substr(2);
  95. }
  96. tooltipDiv.style.top = y + 'px';
  97. tooltipDiv.style.left = x + 'px';
  98. tooltipDiv.style.opacity = 1;
  99. tooltipDiv.dispatchEvent(new CustomEvent('tooltip-open', {
  100. bubbles: true,
  101. detail: { target: target }
  102. }));
  103. },
  104. hideTooltip: function(ev) {
  105. if (ev.target === tooltipDiv || ev.relatedTarget === tooltipDiv ||
  106. tooltipDiv.contains(ev.target) || tooltipDiv.contains(ev.relatedTarget))
  107. return;
  108. if (tooltipTimeout !== null) {
  109. window.clearTimeout(tooltipTimeout);
  110. tooltipTimeout = null;
  111. }
  112. tooltipDiv.style.opacity = 0;
  113. tooltipTimeout = window.setTimeout(function() { tooltipDiv.removeAttribute('style'); }, 250);
  114. tooltipDiv.dispatchEvent(new CustomEvent('tooltip-close', { bubbles: true }));
  115. },
  116. /* Widget helper */
  117. itemlist: function(node, items, separators) {
  118. var children = [];
  119. if (!Array.isArray(separators))
  120. separators = [ separators || E('br') ];
  121. for (var i = 0; i < items.length; i += 2) {
  122. if (items[i+1] !== null && items[i+1] !== undefined) {
  123. var sep = separators[(i/2) % separators.length],
  124. cld = [];
  125. children.push(E('span', { class: 'nowrap' }, [
  126. items[i] ? E('strong', items[i] + ': ') : '',
  127. items[i+1]
  128. ]));
  129. if ((i+2) < items.length)
  130. children.push(this.dom.elem(sep) ? sep.cloneNode(true) : sep);
  131. }
  132. }
  133. this.dom.content(node, children);
  134. return node;
  135. }
  136. };
  137. /* Tabs */
  138. LuCI.prototype.tabs = {
  139. init: function() {
  140. var groups = [], prevGroup = null, currGroup = null;
  141. document.querySelectorAll('[data-tab]').forEach(function(tab) {
  142. var parent = tab.parentNode;
  143. if (!parent.hasAttribute('data-tab-group'))
  144. parent.setAttribute('data-tab-group', groups.length);
  145. currGroup = +parent.getAttribute('data-tab-group');
  146. if (currGroup !== prevGroup) {
  147. prevGroup = currGroup;
  148. if (!groups[currGroup])
  149. groups[currGroup] = [];
  150. }
  151. groups[currGroup].push(tab);
  152. });
  153. for (var i = 0; i < groups.length; i++)
  154. this.initTabGroup(groups[i]);
  155. document.addEventListener('dependency-update', this.updateTabs.bind(this));
  156. this.updateTabs();
  157. if (!groups.length)
  158. this.setActiveTabId(-1, -1);
  159. },
  160. initTabGroup: function(panes) {
  161. if (!Array.isArray(panes) || panes.length === 0)
  162. return;
  163. var menu = E('ul', { 'class': 'cbi-tabmenu' }),
  164. group = panes[0].parentNode,
  165. groupId = +group.getAttribute('data-tab-group'),
  166. selected = null;
  167. for (var i = 0, pane; pane = panes[i]; i++) {
  168. var name = pane.getAttribute('data-tab'),
  169. title = pane.getAttribute('data-tab-title'),
  170. active = pane.getAttribute('data-tab-active') === 'true';
  171. menu.appendChild(E('li', {
  172. 'class': active ? 'cbi-tab' : 'cbi-tab-disabled',
  173. 'data-tab': name
  174. }, E('a', {
  175. 'href': '#',
  176. 'click': this.switchTab.bind(this)
  177. }, title)));
  178. if (active)
  179. selected = i;
  180. }
  181. group.parentNode.insertBefore(menu, group);
  182. if (selected === null) {
  183. selected = this.getActiveTabId(groupId);
  184. if (selected < 0 || selected >= panes.length)
  185. selected = 0;
  186. menu.childNodes[selected].classList.add('cbi-tab');
  187. menu.childNodes[selected].classList.remove('cbi-tab-disabled');
  188. panes[selected].setAttribute('data-tab-active', 'true');
  189. this.setActiveTabId(groupId, selected);
  190. }
  191. },
  192. getActiveTabState: function() {
  193. var page = document.body.getAttribute('data-page');
  194. try {
  195. var val = JSON.parse(window.sessionStorage.getItem('tab'));
  196. if (val.page === page && Array.isArray(val.groups))
  197. return val;
  198. }
  199. catch(e) {}
  200. window.sessionStorage.removeItem('tab');
  201. return { page: page, groups: [] };
  202. },
  203. getActiveTabId: function(groupId) {
  204. return +this.getActiveTabState().groups[groupId] || 0;
  205. },
  206. setActiveTabId: function(groupId, tabIndex) {
  207. try {
  208. var state = this.getActiveTabState();
  209. state.groups[groupId] = tabIndex;
  210. window.sessionStorage.setItem('tab', JSON.stringify(state));
  211. }
  212. catch (e) { return false; }
  213. return true;
  214. },
  215. updateTabs: function(ev) {
  216. document.querySelectorAll('[data-tab-title]').forEach(function(pane) {
  217. var menu = pane.parentNode.previousElementSibling,
  218. tab = menu.querySelector('[data-tab="%s"]'.format(pane.getAttribute('data-tab'))),
  219. n_errors = pane.querySelectorAll('.cbi-input-invalid').length;
  220. if (!pane.firstElementChild) {
  221. tab.style.display = 'none';
  222. tab.classList.remove('flash');
  223. }
  224. else if (tab.style.display === 'none') {
  225. tab.style.display = '';
  226. requestAnimationFrame(function() { tab.classList.add('flash') });
  227. }
  228. if (n_errors) {
  229. tab.setAttribute('data-errors', n_errors);
  230. tab.setAttribute('data-tooltip', _('%d invalid field(s)').format(n_errors));
  231. tab.setAttribute('data-tooltip-style', 'error');
  232. }
  233. else {
  234. tab.removeAttribute('data-errors');
  235. tab.removeAttribute('data-tooltip');
  236. }
  237. });
  238. },
  239. switchTab: function(ev) {
  240. var tab = ev.target.parentNode,
  241. name = tab.getAttribute('data-tab'),
  242. menu = tab.parentNode,
  243. group = menu.nextElementSibling,
  244. groupId = +group.getAttribute('data-tab-group'),
  245. index = 0;
  246. ev.preventDefault();
  247. if (!tab.classList.contains('cbi-tab-disabled'))
  248. return;
  249. menu.querySelectorAll('[data-tab]').forEach(function(tab) {
  250. tab.classList.remove('cbi-tab');
  251. tab.classList.remove('cbi-tab-disabled');
  252. tab.classList.add(
  253. tab.getAttribute('data-tab') === name ? 'cbi-tab' : 'cbi-tab-disabled');
  254. });
  255. group.childNodes.forEach(function(pane) {
  256. if (L.dom.matches(pane, '[data-tab]')) {
  257. if (pane.getAttribute('data-tab') === name) {
  258. pane.setAttribute('data-tab-active', 'true');
  259. L.tabs.setActiveTabId(groupId, index);
  260. }
  261. else {
  262. pane.setAttribute('data-tab-active', 'false');
  263. }
  264. index++;
  265. }
  266. });
  267. }
  268. };
  269. /* DOM manipulation */
  270. LuCI.prototype.dom = {
  271. elem: function(e) {
  272. return (typeof(e) === 'object' && e !== null && 'nodeType' in e);
  273. },
  274. parse: function(s) {
  275. var elem;
  276. try {
  277. domParser = domParser || new DOMParser();
  278. elem = domParser.parseFromString(s, 'text/html').body.firstChild;
  279. }
  280. catch(e) {}
  281. if (!elem) {
  282. try {
  283. dummyElem = dummyElem || document.createElement('div');
  284. dummyElem.innerHTML = s;
  285. elem = dummyElem.firstChild;
  286. }
  287. catch (e) {}
  288. }
  289. return elem || null;
  290. },
  291. matches: function(node, selector) {
  292. var m = this.elem(node) ? node.matches || node.msMatchesSelector : null;
  293. return m ? m.call(node, selector) : false;
  294. },
  295. parent: function(node, selector) {
  296. if (this.elem(node) && node.closest)
  297. return node.closest(selector);
  298. while (this.elem(node))
  299. if (this.matches(node, selector))
  300. return node;
  301. else
  302. node = node.parentNode;
  303. return null;
  304. },
  305. append: function(node, children) {
  306. if (!this.elem(node))
  307. return null;
  308. if (Array.isArray(children)) {
  309. for (var i = 0; i < children.length; i++)
  310. if (this.elem(children[i]))
  311. node.appendChild(children[i]);
  312. else if (children !== null && children !== undefined)
  313. node.appendChild(document.createTextNode('' + children[i]));
  314. return node.lastChild;
  315. }
  316. else if (typeof(children) === 'function') {
  317. return this.append(node, children(node));
  318. }
  319. else if (this.elem(children)) {
  320. return node.appendChild(children);
  321. }
  322. else if (children !== null && children !== undefined) {
  323. node.innerHTML = '' + children;
  324. return node.lastChild;
  325. }
  326. return null;
  327. },
  328. content: function(node, children) {
  329. if (!this.elem(node))
  330. return null;
  331. while (node.firstChild)
  332. node.removeChild(node.firstChild);
  333. return this.append(node, children);
  334. },
  335. attr: function(node, key, val) {
  336. if (!this.elem(node))
  337. return null;
  338. var attr = null;
  339. if (typeof(key) === 'object' && key !== null)
  340. attr = key;
  341. else if (typeof(key) === 'string')
  342. attr = {}, attr[key] = val;
  343. for (key in attr) {
  344. if (!attr.hasOwnProperty(key) || attr[key] === null || attr[key] === undefined)
  345. continue;
  346. switch (typeof(attr[key])) {
  347. case 'function':
  348. node.addEventListener(key, attr[key]);
  349. break;
  350. case 'object':
  351. node.setAttribute(key, JSON.stringify(attr[key]));
  352. break;
  353. default:
  354. node.setAttribute(key, attr[key]);
  355. }
  356. }
  357. },
  358. create: function() {
  359. var html = arguments[0],
  360. attr = (arguments[1] instanceof Object && !Array.isArray(arguments[1])) ? arguments[1] : null,
  361. data = attr ? arguments[2] : arguments[1],
  362. elem;
  363. if (this.elem(html))
  364. elem = html;
  365. else if (html.charCodeAt(0) === 60)
  366. elem = this.parse(html);
  367. else
  368. elem = document.createElement(html);
  369. if (!elem)
  370. return null;
  371. this.attr(elem, attr);
  372. this.append(elem, data);
  373. return elem;
  374. }
  375. };
  376. /* Setup */
  377. LuCI.prototype.setupDOM = function(ev) {
  378. this.tabs.init();
  379. };
  380. function LuCI(env) {
  381. this.env = env;
  382. modalDiv = document.body.appendChild(
  383. this.dom.create('div', { id: 'modal_overlay' },
  384. this.dom.create('div', { class: 'modal', role: 'dialog', 'aria-modal': true })));
  385. tooltipDiv = document.body.appendChild(this.dom.create('div', { class: 'cbi-tooltip' }));
  386. document.addEventListener('mouseover', this.showTooltip.bind(this), true);
  387. document.addEventListener('mouseout', this.hideTooltip.bind(this), true);
  388. document.addEventListener('focus', this.showTooltip.bind(this), true);
  389. document.addEventListener('blur', this.hideTooltip.bind(this), true);
  390. document.addEventListener('DOMContentLoaded', this.setupDOM.bind(this));
  391. }
  392. window.LuCI = LuCI;
  393. })(window, document);