docs: update js api docs
[project/luci.git] / docs / jsapi / ui.js.html
1 <!DOCTYPE html>
2 <html>
3 <head>
4 <meta charset="utf-8">
5 <title>Source: ui.js</title>
6
7
8 <script src="scripts/prettify/prettify.js"></script>
9 <script src="scripts/prettify/lang-css.js"></script>
10 <script src="scripts/jquery.min.js"></script>
11 <!--[if lt IE 9]>
12 <script src="//html5shiv.googlecode.com/svn/trunk/html5.js"></script>
13 <![endif]-->
14 <link type="text/css" rel="stylesheet" href="styles/prettify-tomorrow.css">
15 <link type="text/css" rel="stylesheet" href="styles/bootstrap.min.css">
16 <link type="text/css" rel="stylesheet" href="styles/jaguar.css">
17
18
19 <script>
20 var config = {"monospaceLinks":true,"cleverLinks":true,"default":{"outputSourceFiles":true}};
21 </script>
22
23
24
25 </head>
26 <body>
27 <div id="wrap" class="clearfix">
28
29 <div class="navigation">
30 <h3 class="applicationName"><a href="index.html"></a></h3>
31
32 <div class="search">
33 <input id="search" type="text" class="form-control input-sm" placeholder="Search Documentations">
34 </div>
35 <ul class="list">
36
37 <li class="item" data-name="LuCI">
38 <span class="title">
39 <a href="LuCI.html">LuCI</a>
40
41 </span>
42 <ul class="members itemMembers">
43
44 <span class="subtitle">Members</span>
45
46 <li data-name="LuCI#Class"><a href="LuCI.html#Class">Class</a></li>
47
48 <li data-name="LuCI#dom"><a href="LuCI.html#dom">dom</a></li>
49
50 <li data-name="LuCI#env"><a href="LuCI.html#env">env</a></li>
51
52 <li data-name="LuCI#Poll"><a href="LuCI.html#Poll">Poll</a></li>
53
54 <li data-name="LuCI#Request"><a href="LuCI.html#Request">Request</a></li>
55
56 <li data-name="LuCI#view"><a href="LuCI.html#view">view</a></li>
57
58 </ul>
59 <ul class="typedefs itemMembers">
60
61 <span class="subtitle">Typedefs</span>
62
63 <li data-name="LuCI.requestCallbackFn"><a href="LuCI.html#.requestCallbackFn">requestCallbackFn</a></li>
64
65 </ul>
66 <ul class="typedefs itemMembers">
67
68 </ul>
69 <ul class="methods itemMembers">
70
71 <span class="subtitle">Methods</span>
72
73 <li data-name="LuCI#bind"><a href="LuCI.html#bind">bind</a></li>
74
75 <li data-name="LuCI#error"><a href="LuCI.html#error">error</a></li>
76
77 <li data-name="LuCI#get"><a href="LuCI.html#get">get</a></li>
78
79 <li data-name="LuCI#halt"><a href="LuCI.html#halt">halt</a></li>
80
81 <li data-name="LuCI#hasSystemFeature"><a href="LuCI.html#hasSystemFeature">hasSystemFeature</a></li>
82
83 <li data-name="LuCI#isObject"><a href="LuCI.html#isObject">isObject</a></li>
84
85 <li data-name="LuCI#location"><a href="LuCI.html#location">location</a></li>
86
87 <li data-name="LuCI#media"><a href="LuCI.html#media">media</a></li>
88
89 <li data-name="LuCI#path"><a href="LuCI.html#path">path</a></li>
90
91 <li data-name="LuCI#poll"><a href="LuCI.html#poll">poll</a></li>
92
93 <li data-name="LuCI#post"><a href="LuCI.html#post">post</a></li>
94
95 <li data-name="LuCI#raise"><a href="LuCI.html#raise">raise</a></li>
96
97 <li data-name="LuCI#require"><a href="LuCI.html#require">require</a></li>
98
99 <li data-name="LuCI#resolveDefault"><a href="LuCI.html#resolveDefault">resolveDefault</a></li>
100
101 <li data-name="LuCI#resource"><a href="LuCI.html#resource">resource</a></li>
102
103 <li data-name="LuCI#run"><a href="LuCI.html#run">run</a></li>
104
105 <li data-name="LuCI#sortedKeys"><a href="LuCI.html#sortedKeys">sortedKeys</a></li>
106
107 <li data-name="LuCI#stop"><a href="LuCI.html#stop">stop</a></li>
108
109 <li data-name="LuCI#toArray"><a href="LuCI.html#toArray">toArray</a></li>
110
111 <li data-name="LuCI#url"><a href="LuCI.html#url">url</a></li>
112
113 </ul>
114 <ul class="events itemMembers">
115
116 </ul>
117 </li>
118
119 <li class="item" data-name="LuCI.baseclass">
120 <span class="title">
121 <a href="LuCI.baseclass.html">LuCI.baseclass</a>
122
123 </span>
124 <ul class="members itemMembers">
125
126 </ul>
127 <ul class="typedefs itemMembers">
128
129 </ul>
130 <ul class="typedefs itemMembers">
131
132 </ul>
133 <ul class="methods itemMembers">
134
135 <span class="subtitle">Methods</span>
136
137 <li data-name="LuCI.baseclass.extend"><a href="LuCI.baseclass.html#.extend">extend</a></li>
138
139 <li data-name="LuCI.baseclass.instantiate"><a href="LuCI.baseclass.html#.instantiate">instantiate</a></li>
140
141 <li data-name="LuCI.baseclass.isSubclass"><a href="LuCI.baseclass.html#.isSubclass">isSubclass</a></li>
142
143 <li data-name="LuCI.baseclass.singleton"><a href="LuCI.baseclass.html#.singleton">singleton</a></li>
144
145 <li data-name="LuCI.baseclass#super"><a href="LuCI.baseclass.html#super">super</a></li>
146
147 <li data-name="LuCI.baseclass#varargs"><a href="LuCI.baseclass.html#varargs">varargs</a></li>
148
149 </ul>
150 <ul class="events itemMembers">
151
152 </ul>
153 </li>
154
155 <li class="item" data-name="LuCI.dom">
156 <span class="title">
157 <a href="LuCI.dom.html">LuCI.dom</a>
158
159 </span>
160 <ul class="members itemMembers">
161
162 </ul>
163 <ul class="typedefs itemMembers">
164
165 <span class="subtitle">Typedefs</span>
166
167 <li data-name="LuCI.dom~ignoreCallbackFn"><a href="LuCI.dom.html#~ignoreCallbackFn">ignoreCallbackFn</a></li>
168
169 </ul>
170 <ul class="typedefs itemMembers">
171
172 </ul>
173 <ul class="methods itemMembers">
174
175 <span class="subtitle">Methods</span>
176
177 <li data-name="LuCI.dom#append"><a href="LuCI.dom.html#append">append</a></li>
178
179 <li data-name="LuCI.dom#attr"><a href="LuCI.dom.html#attr">attr</a></li>
180
181 <li data-name="LuCI.dom#bindClassInstance"><a href="LuCI.dom.html#bindClassInstance">bindClassInstance</a></li>
182
183 <li data-name="LuCI.dom#callClassMethod"><a href="LuCI.dom.html#callClassMethod">callClassMethod</a></li>
184
185 <li data-name="LuCI.dom#content"><a href="LuCI.dom.html#content">content</a></li>
186
187 <li data-name="LuCI.dom#create"><a href="LuCI.dom.html#create">create</a></li>
188
189 <li data-name="LuCI.dom#data"><a href="LuCI.dom.html#data">data</a></li>
190
191 <li data-name="LuCI.dom#elem"><a href="LuCI.dom.html#elem">elem</a></li>
192
193 <li data-name="LuCI.dom#findClassInstance"><a href="LuCI.dom.html#findClassInstance">findClassInstance</a></li>
194
195 <li data-name="LuCI.dom#isEmpty"><a href="LuCI.dom.html#isEmpty">isEmpty</a></li>
196
197 <li data-name="LuCI.dom#matches"><a href="LuCI.dom.html#matches">matches</a></li>
198
199 <li data-name="LuCI.dom#parent"><a href="LuCI.dom.html#parent">parent</a></li>
200
201 <li data-name="LuCI.dom#parse"><a href="LuCI.dom.html#parse">parse</a></li>
202
203 </ul>
204 <ul class="events itemMembers">
205
206 </ul>
207 </li>
208
209 <li class="item" data-name="LuCI.fs">
210 <span class="title">
211 <a href="LuCI.fs.html">LuCI.fs</a>
212
213 </span>
214 <ul class="members itemMembers">
215
216 </ul>
217 <ul class="typedefs itemMembers">
218
219 <span class="subtitle">Typedefs</span>
220
221 <li data-name="LuCI.fs.FileExecResult"><a href="LuCI.fs.html#.FileExecResult">FileExecResult</a></li>
222
223 <li data-name="LuCI.fs.FileStatEntry"><a href="LuCI.fs.html#.FileStatEntry">FileStatEntry</a></li>
224
225 </ul>
226 <ul class="typedefs itemMembers">
227
228 </ul>
229 <ul class="methods itemMembers">
230
231 <span class="subtitle">Methods</span>
232
233 <li data-name="LuCI.fs#exec"><a href="LuCI.fs.html#exec">exec</a></li>
234
235 <li data-name="LuCI.fs#exec_direct"><a href="LuCI.fs.html#exec_direct">exec_direct</a></li>
236
237 <li data-name="LuCI.fs#lines"><a href="LuCI.fs.html#lines">lines</a></li>
238
239 <li data-name="LuCI.fs#list"><a href="LuCI.fs.html#list">list</a></li>
240
241 <li data-name="LuCI.fs#read"><a href="LuCI.fs.html#read">read</a></li>
242
243 <li data-name="LuCI.fs#read_direct"><a href="LuCI.fs.html#read_direct">read_direct</a></li>
244
245 <li data-name="LuCI.fs#remove"><a href="LuCI.fs.html#remove">remove</a></li>
246
247 <li data-name="LuCI.fs#stat"><a href="LuCI.fs.html#stat">stat</a></li>
248
249 <li data-name="LuCI.fs#trimmed"><a href="LuCI.fs.html#trimmed">trimmed</a></li>
250
251 <li data-name="LuCI.fs#write"><a href="LuCI.fs.html#write">write</a></li>
252
253 </ul>
254 <ul class="events itemMembers">
255
256 </ul>
257 </li>
258
259 <li class="item" data-name="LuCI.headers">
260 <span class="title">
261 <a href="LuCI.headers.html">LuCI.headers</a>
262
263 </span>
264 <ul class="members itemMembers">
265
266 </ul>
267 <ul class="typedefs itemMembers">
268
269 </ul>
270 <ul class="typedefs itemMembers">
271
272 </ul>
273 <ul class="methods itemMembers">
274
275 <span class="subtitle">Methods</span>
276
277 <li data-name="LuCI.headers#get"><a href="LuCI.headers.html#get">get</a></li>
278
279 <li data-name="LuCI.headers#has"><a href="LuCI.headers.html#has">has</a></li>
280
281 </ul>
282 <ul class="events itemMembers">
283
284 </ul>
285 </li>
286
287 <li class="item" data-name="LuCI.network">
288 <span class="title">
289 <a href="LuCI.network.html">LuCI.network</a>
290
291 </span>
292 <ul class="members itemMembers">
293
294 </ul>
295 <ul class="typedefs itemMembers">
296
297 <span class="subtitle">Typedefs</span>
298
299 <li data-name="LuCI.network.SwitchTopology"><a href="LuCI.network.html#.SwitchTopology">SwitchTopology</a></li>
300
301 <li data-name="LuCI.network.WifiEncryption"><a href="LuCI.network.html#.WifiEncryption">WifiEncryption</a></li>
302
303 <li data-name="LuCI.network.WifiPeerEntry"><a href="LuCI.network.html#.WifiPeerEntry">WifiPeerEntry</a></li>
304
305 <li data-name="LuCI.network.WifiRateEntry"><a href="LuCI.network.html#.WifiRateEntry">WifiRateEntry</a></li>
306
307 <li data-name="LuCI.network.WifiScanResult"><a href="LuCI.network.html#.WifiScanResult">WifiScanResult</a></li>
308
309 </ul>
310 <ul class="typedefs itemMembers">
311
312 </ul>
313 <ul class="methods itemMembers">
314
315 <span class="subtitle">Methods</span>
316
317 <li data-name="LuCI.network#addNetwork"><a href="LuCI.network.html#addNetwork">addNetwork</a></li>
318
319 <li data-name="LuCI.network#addWifiNetwork"><a href="LuCI.network.html#addWifiNetwork">addWifiNetwork</a></li>
320
321 <li data-name="LuCI.network#deleteNetwork"><a href="LuCI.network.html#deleteNetwork">deleteNetwork</a></li>
322
323 <li data-name="LuCI.network#deleteWifiNetwork"><a href="LuCI.network.html#deleteWifiNetwork">deleteWifiNetwork</a></li>
324
325 <li data-name="LuCI.network#flushCache"><a href="LuCI.network.html#flushCache">flushCache</a></li>
326
327 <li data-name="LuCI.network#formatWifiEncryption"><a href="LuCI.network.html#formatWifiEncryption">formatWifiEncryption</a></li>
328
329 <li data-name="LuCI.network#getDevice"><a href="LuCI.network.html#getDevice">getDevice</a></li>
330
331 <li data-name="LuCI.network#getDevices"><a href="LuCI.network.html#getDevices">getDevices</a></li>
332
333 <li data-name="LuCI.network#getDSLModemType"><a href="LuCI.network.html#getDSLModemType">getDSLModemType</a></li>
334
335 <li data-name="LuCI.network#getHostHints"><a href="LuCI.network.html#getHostHints">getHostHints</a></li>
336
337 <li data-name="LuCI.network#getIfnameOf"><a href="LuCI.network.html#getIfnameOf">getIfnameOf</a></li>
338
339 <li data-name="LuCI.network#getNetwork"><a href="LuCI.network.html#getNetwork">getNetwork</a></li>
340
341 <li data-name="LuCI.network#getNetworks"><a href="LuCI.network.html#getNetworks">getNetworks</a></li>
342
343 <li data-name="LuCI.network#getProtocol"><a href="LuCI.network.html#getProtocol">getProtocol</a></li>
344
345 <li data-name="LuCI.network#getProtocols"><a href="LuCI.network.html#getProtocols">getProtocols</a></li>
346
347 <li data-name="LuCI.network#getSwitchTopologies"><a href="LuCI.network.html#getSwitchTopologies">getSwitchTopologies</a></li>
348
349 <li data-name="LuCI.network#getWAN6Networks"><a href="LuCI.network.html#getWAN6Networks">getWAN6Networks</a></li>
350
351 <li data-name="LuCI.network#getWANNetworks"><a href="LuCI.network.html#getWANNetworks">getWANNetworks</a></li>
352
353 <li data-name="LuCI.network#getWifiDevice"><a href="LuCI.network.html#getWifiDevice">getWifiDevice</a></li>
354
355 <li data-name="LuCI.network#getWifiDevices"><a href="LuCI.network.html#getWifiDevices">getWifiDevices</a></li>
356
357 <li data-name="LuCI.network#getWifiNetwork"><a href="LuCI.network.html#getWifiNetwork">getWifiNetwork</a></li>
358
359 <li data-name="LuCI.network#getWifiNetworks"><a href="LuCI.network.html#getWifiNetworks">getWifiNetworks</a></li>
360
361 <li data-name="LuCI.network#isIgnoredDevice"><a href="LuCI.network.html#isIgnoredDevice">isIgnoredDevice</a></li>
362
363 <li data-name="LuCI.network#maskToPrefix"><a href="LuCI.network.html#maskToPrefix">maskToPrefix</a></li>
364
365 <li data-name="LuCI.network#prefixToMask"><a href="LuCI.network.html#prefixToMask">prefixToMask</a></li>
366
367 <li data-name="LuCI.network#registerErrorCode"><a href="LuCI.network.html#registerErrorCode">registerErrorCode</a></li>
368
369 <li data-name="LuCI.network#registerPatternVirtual"><a href="LuCI.network.html#registerPatternVirtual">registerPatternVirtual</a></li>
370
371 <li data-name="LuCI.network#registerProtocol"><a href="LuCI.network.html#registerProtocol">registerProtocol</a></li>
372
373 <li data-name="LuCI.network#renameNetwork"><a href="LuCI.network.html#renameNetwork">renameNetwork</a></li>
374
375 </ul>
376 <ul class="events itemMembers">
377
378 </ul>
379 </li>
380
381 <li class="item" data-name="LuCI.network.Device">
382 <span class="title">
383 <a href="LuCI.network.Device.html">LuCI.network.Device</a>
384
385 </span>
386 <ul class="members itemMembers">
387
388 </ul>
389 <ul class="typedefs itemMembers">
390
391 </ul>
392 <ul class="typedefs itemMembers">
393
394 </ul>
395 <ul class="methods itemMembers">
396
397 <span class="subtitle">Methods</span>
398
399 <li data-name="LuCI.network.Device#getBridgeID"><a href="LuCI.network.Device.html#getBridgeID">getBridgeID</a></li>
400
401 <li data-name="LuCI.network.Device#getBridgeSTP"><a href="LuCI.network.Device.html#getBridgeSTP">getBridgeSTP</a></li>
402
403 <li data-name="LuCI.network.Device#getI18n"><a href="LuCI.network.Device.html#getI18n">getI18n</a></li>
404
405 <li data-name="LuCI.network.Device#getIP6Addrs"><a href="LuCI.network.Device.html#getIP6Addrs">getIP6Addrs</a></li>
406
407 <li data-name="LuCI.network.Device#getIPAddrs"><a href="LuCI.network.Device.html#getIPAddrs">getIPAddrs</a></li>
408
409 <li data-name="LuCI.network.Device#getMAC"><a href="LuCI.network.Device.html#getMAC">getMAC</a></li>
410
411 <li data-name="LuCI.network.Device#getMTU"><a href="LuCI.network.Device.html#getMTU">getMTU</a></li>
412
413 <li data-name="LuCI.network.Device#getName"><a href="LuCI.network.Device.html#getName">getName</a></li>
414
415 <li data-name="LuCI.network.Device#getNetwork"><a href="LuCI.network.Device.html#getNetwork">getNetwork</a></li>
416
417 <li data-name="LuCI.network.Device#getNetworks"><a href="LuCI.network.Device.html#getNetworks">getNetworks</a></li>
418
419 <li data-name="LuCI.network.Device#getPorts"><a href="LuCI.network.Device.html#getPorts">getPorts</a></li>
420
421 <li data-name="LuCI.network.Device#getRXBytes"><a href="LuCI.network.Device.html#getRXBytes">getRXBytes</a></li>
422
423 <li data-name="LuCI.network.Device#getRXPackets"><a href="LuCI.network.Device.html#getRXPackets">getRXPackets</a></li>
424
425 <li data-name="LuCI.network.Device#getShortName"><a href="LuCI.network.Device.html#getShortName">getShortName</a></li>
426
427 <li data-name="LuCI.network.Device#getTXBytes"><a href="LuCI.network.Device.html#getTXBytes">getTXBytes</a></li>
428
429 <li data-name="LuCI.network.Device#getTXPackets"><a href="LuCI.network.Device.html#getTXPackets">getTXPackets</a></li>
430
431 <li data-name="LuCI.network.Device#getType"><a href="LuCI.network.Device.html#getType">getType</a></li>
432
433 <li data-name="LuCI.network.Device#getTypeI18n"><a href="LuCI.network.Device.html#getTypeI18n">getTypeI18n</a></li>
434
435 <li data-name="LuCI.network.Device#getWifiNetwork"><a href="LuCI.network.Device.html#getWifiNetwork">getWifiNetwork</a></li>
436
437 <li data-name="LuCI.network.Device#isBridge"><a href="LuCI.network.Device.html#isBridge">isBridge</a></li>
438
439 <li data-name="LuCI.network.Device#isBridgePort"><a href="LuCI.network.Device.html#isBridgePort">isBridgePort</a></li>
440
441 <li data-name="LuCI.network.Device#isUp"><a href="LuCI.network.Device.html#isUp">isUp</a></li>
442
443 </ul>
444 <ul class="events itemMembers">
445
446 </ul>
447 </li>
448
449 <li class="item" data-name="LuCI.network.Hosts">
450 <span class="title">
451 <a href="LuCI.network.Hosts.html">LuCI.network.Hosts</a>
452
453 </span>
454 <ul class="members itemMembers">
455
456 </ul>
457 <ul class="typedefs itemMembers">
458
459 </ul>
460 <ul class="typedefs itemMembers">
461
462 </ul>
463 <ul class="methods itemMembers">
464
465 <span class="subtitle">Methods</span>
466
467 <li data-name="LuCI.network.Hosts#getHostnameByIP6Addr"><a href="LuCI.network.Hosts.html#getHostnameByIP6Addr">getHostnameByIP6Addr</a></li>
468
469 <li data-name="LuCI.network.Hosts#getHostnameByIPAddr"><a href="LuCI.network.Hosts.html#getHostnameByIPAddr">getHostnameByIPAddr</a></li>
470
471 <li data-name="LuCI.network.Hosts#getHostnameByMACAddr"><a href="LuCI.network.Hosts.html#getHostnameByMACAddr">getHostnameByMACAddr</a></li>
472
473 <li data-name="LuCI.network.Hosts#getIP6AddrByMACAddr"><a href="LuCI.network.Hosts.html#getIP6AddrByMACAddr">getIP6AddrByMACAddr</a></li>
474
475 <li data-name="LuCI.network.Hosts#getIPAddrByMACAddr"><a href="LuCI.network.Hosts.html#getIPAddrByMACAddr">getIPAddrByMACAddr</a></li>
476
477 <li data-name="LuCI.network.Hosts#getMACAddrByIP6Addr"><a href="LuCI.network.Hosts.html#getMACAddrByIP6Addr">getMACAddrByIP6Addr</a></li>
478
479 <li data-name="LuCI.network.Hosts#getMACAddrByIPAddr"><a href="LuCI.network.Hosts.html#getMACAddrByIPAddr">getMACAddrByIPAddr</a></li>
480
481 <li data-name="LuCI.network.Hosts#getMACHints"><a href="LuCI.network.Hosts.html#getMACHints">getMACHints</a></li>
482
483 </ul>
484 <ul class="events itemMembers">
485
486 </ul>
487 </li>
488
489 <li class="item" data-name="LuCI.network.Protocol">
490 <span class="title">
491 <a href="LuCI.network.Protocol.html">LuCI.network.Protocol</a>
492
493 </span>
494 <ul class="members itemMembers">
495
496 </ul>
497 <ul class="typedefs itemMembers">
498
499 </ul>
500 <ul class="typedefs itemMembers">
501
502 </ul>
503 <ul class="methods itemMembers">
504
505 <span class="subtitle">Methods</span>
506
507 <li data-name="LuCI.network.Protocol#addDevice"><a href="LuCI.network.Protocol.html#addDevice">addDevice</a></li>
508
509 <li data-name="LuCI.network.Protocol#containsDevice"><a href="LuCI.network.Protocol.html#containsDevice">containsDevice</a></li>
510
511 <li data-name="LuCI.network.Protocol#deleteConfiguration"><a href="LuCI.network.Protocol.html#deleteConfiguration">deleteConfiguration</a></li>
512
513 <li data-name="LuCI.network.Protocol#deleteDevice"><a href="LuCI.network.Protocol.html#deleteDevice">deleteDevice</a></li>
514
515 <li data-name="LuCI.network.Protocol#get"><a href="LuCI.network.Protocol.html#get">get</a></li>
516
517 <li data-name="LuCI.network.Protocol#getDevice"><a href="LuCI.network.Protocol.html#getDevice">getDevice</a></li>
518
519 <li data-name="LuCI.network.Protocol#getDevices"><a href="LuCI.network.Protocol.html#getDevices">getDevices</a></li>
520
521 <li data-name="LuCI.network.Protocol#getDNS6Addrs"><a href="LuCI.network.Protocol.html#getDNS6Addrs">getDNS6Addrs</a></li>
522
523 <li data-name="LuCI.network.Protocol#getDNSAddrs"><a href="LuCI.network.Protocol.html#getDNSAddrs">getDNSAddrs</a></li>
524
525 <li data-name="LuCI.network.Protocol#getErrors"><a href="LuCI.network.Protocol.html#getErrors">getErrors</a></li>
526
527 <li data-name="LuCI.network.Protocol#getExpiry"><a href="LuCI.network.Protocol.html#getExpiry">getExpiry</a></li>
528
529 <li data-name="LuCI.network.Protocol#getGateway6Addr"><a href="LuCI.network.Protocol.html#getGateway6Addr">getGateway6Addr</a></li>
530
531 <li data-name="LuCI.network.Protocol#getGatewayAddr"><a href="LuCI.network.Protocol.html#getGatewayAddr">getGatewayAddr</a></li>
532
533 <li data-name="LuCI.network.Protocol#getI18n"><a href="LuCI.network.Protocol.html#getI18n">getI18n</a></li>
534
535 <li data-name="LuCI.network.Protocol#getIfname"><a href="LuCI.network.Protocol.html#getIfname">getIfname</a></li>
536
537 <li data-name="LuCI.network.Protocol#getIP6Addr"><a href="LuCI.network.Protocol.html#getIP6Addr">getIP6Addr</a></li>
538
539 <li data-name="LuCI.network.Protocol#getIP6Addrs"><a href="LuCI.network.Protocol.html#getIP6Addrs">getIP6Addrs</a></li>
540
541 <li data-name="LuCI.network.Protocol#getIP6Prefix"><a href="LuCI.network.Protocol.html#getIP6Prefix">getIP6Prefix</a></li>
542
543 <li data-name="LuCI.network.Protocol#getIPAddr"><a href="LuCI.network.Protocol.html#getIPAddr">getIPAddr</a></li>
544
545 <li data-name="LuCI.network.Protocol#getIPAddrs"><a href="LuCI.network.Protocol.html#getIPAddrs">getIPAddrs</a></li>
546
547 <li data-name="LuCI.network.Protocol#getL2Device"><a href="LuCI.network.Protocol.html#getL2Device">getL2Device</a></li>
548
549 <li data-name="LuCI.network.Protocol#getL3Device"><a href="LuCI.network.Protocol.html#getL3Device">getL3Device</a></li>
550
551 <li data-name="LuCI.network.Protocol#getMetric"><a href="LuCI.network.Protocol.html#getMetric">getMetric</a></li>
552
553 <li data-name="LuCI.network.Protocol#getName"><a href="LuCI.network.Protocol.html#getName">getName</a></li>
554
555 <li data-name="LuCI.network.Protocol#getNetmask"><a href="LuCI.network.Protocol.html#getNetmask">getNetmask</a></li>
556
557 <li data-name="LuCI.network.Protocol#getOpkgPackage"><a href="LuCI.network.Protocol.html#getOpkgPackage">getOpkgPackage</a></li>
558
559 <li data-name="LuCI.network.Protocol#getProtocol"><a href="LuCI.network.Protocol.html#getProtocol">getProtocol</a></li>
560
561 <li data-name="LuCI.network.Protocol#getType"><a href="LuCI.network.Protocol.html#getType">getType</a></li>
562
563 <li data-name="LuCI.network.Protocol#getUptime"><a href="LuCI.network.Protocol.html#getUptime">getUptime</a></li>
564
565 <li data-name="LuCI.network.Protocol#getZoneName"><a href="LuCI.network.Protocol.html#getZoneName">getZoneName</a></li>
566
567 <li data-name="LuCI.network.Protocol#isAlias"><a href="LuCI.network.Protocol.html#isAlias">isAlias</a></li>
568
569 <li data-name="LuCI.network.Protocol#isBridge"><a href="LuCI.network.Protocol.html#isBridge">isBridge</a></li>
570
571 <li data-name="LuCI.network.Protocol#isDynamic"><a href="LuCI.network.Protocol.html#isDynamic">isDynamic</a></li>
572
573 <li data-name="LuCI.network.Protocol#isEmpty"><a href="LuCI.network.Protocol.html#isEmpty">isEmpty</a></li>
574
575 <li data-name="LuCI.network.Protocol#isFloating"><a href="LuCI.network.Protocol.html#isFloating">isFloating</a></li>
576
577 <li data-name="LuCI.network.Protocol#isInstalled"><a href="LuCI.network.Protocol.html#isInstalled">isInstalled</a></li>
578
579 <li data-name="LuCI.network.Protocol#isUp"><a href="LuCI.network.Protocol.html#isUp">isUp</a></li>
580
581 <li data-name="LuCI.network.Protocol#isVirtual"><a href="LuCI.network.Protocol.html#isVirtual">isVirtual</a></li>
582
583 <li data-name="LuCI.network.Protocol#set"><a href="LuCI.network.Protocol.html#set">set</a></li>
584
585 </ul>
586 <ul class="events itemMembers">
587
588 </ul>
589 </li>
590
591 <li class="item" data-name="LuCI.network.WifiDevice">
592 <span class="title">
593 <a href="LuCI.network.WifiDevice.html">LuCI.network.WifiDevice</a>
594
595 </span>
596 <ul class="members itemMembers">
597
598 </ul>
599 <ul class="typedefs itemMembers">
600
601 </ul>
602 <ul class="typedefs itemMembers">
603
604 </ul>
605 <ul class="methods itemMembers">
606
607 <span class="subtitle">Methods</span>
608
609 <li data-name="LuCI.network.WifiDevice#addWifiNetwork"><a href="LuCI.network.WifiDevice.html#addWifiNetwork">addWifiNetwork</a></li>
610
611 <li data-name="LuCI.network.WifiDevice#deleteWifiNetwork"><a href="LuCI.network.WifiDevice.html#deleteWifiNetwork">deleteWifiNetwork</a></li>
612
613 <li data-name="LuCI.network.WifiDevice#get"><a href="LuCI.network.WifiDevice.html#get">get</a></li>
614
615 <li data-name="LuCI.network.WifiDevice#getHTModes"><a href="LuCI.network.WifiDevice.html#getHTModes">getHTModes</a></li>
616
617 <li data-name="LuCI.network.WifiDevice#getHWModes"><a href="LuCI.network.WifiDevice.html#getHWModes">getHWModes</a></li>
618
619 <li data-name="LuCI.network.WifiDevice#getI18n"><a href="LuCI.network.WifiDevice.html#getI18n">getI18n</a></li>
620
621 <li data-name="LuCI.network.WifiDevice#getName"><a href="LuCI.network.WifiDevice.html#getName">getName</a></li>
622
623 <li data-name="LuCI.network.WifiDevice#getScanList"><a href="LuCI.network.WifiDevice.html#getScanList">getScanList</a></li>
624
625 <li data-name="LuCI.network.WifiDevice#getWifiNetwork"><a href="LuCI.network.WifiDevice.html#getWifiNetwork">getWifiNetwork</a></li>
626
627 <li data-name="LuCI.network.WifiDevice#getWifiNetworks"><a href="LuCI.network.WifiDevice.html#getWifiNetworks">getWifiNetworks</a></li>
628
629 <li data-name="LuCI.network.WifiDevice#isDisabled"><a href="LuCI.network.WifiDevice.html#isDisabled">isDisabled</a></li>
630
631 <li data-name="LuCI.network.WifiDevice#isUp"><a href="LuCI.network.WifiDevice.html#isUp">isUp</a></li>
632
633 <li data-name="LuCI.network.WifiDevice#set"><a href="LuCI.network.WifiDevice.html#set">set</a></li>
634
635 </ul>
636 <ul class="events itemMembers">
637
638 </ul>
639 </li>
640
641 <li class="item" data-name="LuCI.network.WifiNetwork">
642 <span class="title">
643 <a href="LuCI.network.WifiNetwork.html">LuCI.network.WifiNetwork</a>
644
645 </span>
646 <ul class="members itemMembers">
647
648 </ul>
649 <ul class="typedefs itemMembers">
650
651 </ul>
652 <ul class="typedefs itemMembers">
653
654 </ul>
655 <ul class="methods itemMembers">
656
657 <span class="subtitle">Methods</span>
658
659 <li data-name="LuCI.network.WifiNetwork#disconnectClient"><a href="LuCI.network.WifiNetwork.html#disconnectClient">disconnectClient</a></li>
660
661 <li data-name="LuCI.network.WifiNetwork#get"><a href="LuCI.network.WifiNetwork.html#get">get</a></li>
662
663 <li data-name="LuCI.network.WifiNetwork#getActiveBSSID"><a href="LuCI.network.WifiNetwork.html#getActiveBSSID">getActiveBSSID</a></li>
664
665 <li data-name="LuCI.network.WifiNetwork#getActiveEncryption"><a href="LuCI.network.WifiNetwork.html#getActiveEncryption">getActiveEncryption</a></li>
666
667 <li data-name="LuCI.network.WifiNetwork#getActiveMode"><a href="LuCI.network.WifiNetwork.html#getActiveMode">getActiveMode</a></li>
668
669 <li data-name="LuCI.network.WifiNetwork#getActiveModeI18n"><a href="LuCI.network.WifiNetwork.html#getActiveModeI18n">getActiveModeI18n</a></li>
670
671 <li data-name="LuCI.network.WifiNetwork#getActiveSSID"><a href="LuCI.network.WifiNetwork.html#getActiveSSID">getActiveSSID</a></li>
672
673 <li data-name="LuCI.network.WifiNetwork#getAssocList"><a href="LuCI.network.WifiNetwork.html#getAssocList">getAssocList</a></li>
674
675 <li data-name="LuCI.network.WifiNetwork#getBitRate"><a href="LuCI.network.WifiNetwork.html#getBitRate">getBitRate</a></li>
676
677 <li data-name="LuCI.network.WifiNetwork#getBSSID"><a href="LuCI.network.WifiNetwork.html#getBSSID">getBSSID</a></li>
678
679 <li data-name="LuCI.network.WifiNetwork#getChannel"><a href="LuCI.network.WifiNetwork.html#getChannel">getChannel</a></li>
680
681 <li data-name="LuCI.network.WifiNetwork#getCountryCode"><a href="LuCI.network.WifiNetwork.html#getCountryCode">getCountryCode</a></li>
682
683 <li data-name="LuCI.network.WifiNetwork#getDevice"><a href="LuCI.network.WifiNetwork.html#getDevice">getDevice</a></li>
684
685 <li data-name="LuCI.network.WifiNetwork#getFrequency"><a href="LuCI.network.WifiNetwork.html#getFrequency">getFrequency</a></li>
686
687 <li data-name="LuCI.network.WifiNetwork#getI18n"><a href="LuCI.network.WifiNetwork.html#getI18n">getI18n</a></li>
688
689 <li data-name="LuCI.network.WifiNetwork#getID"><a href="LuCI.network.WifiNetwork.html#getID">getID</a></li>
690
691 <li data-name="LuCI.network.WifiNetwork#getIfname"><a href="LuCI.network.WifiNetwork.html#getIfname">getIfname</a></li>
692
693 <li data-name="LuCI.network.WifiNetwork#getMeshID"><a href="LuCI.network.WifiNetwork.html#getMeshID">getMeshID</a></li>
694
695 <li data-name="LuCI.network.WifiNetwork#getMode"><a href="LuCI.network.WifiNetwork.html#getMode">getMode</a></li>
696
697 <li data-name="LuCI.network.WifiNetwork#getName"><a href="LuCI.network.WifiNetwork.html#getName">getName</a></li>
698
699 <li data-name="LuCI.network.WifiNetwork#getNetwork"><a href="LuCI.network.WifiNetwork.html#getNetwork">getNetwork</a></li>
700
701 <li data-name="LuCI.network.WifiNetwork#getNetworkNames"><a href="LuCI.network.WifiNetwork.html#getNetworkNames">getNetworkNames</a></li>
702
703 <li data-name="LuCI.network.WifiNetwork#getNetworks"><a href="LuCI.network.WifiNetwork.html#getNetworks">getNetworks</a></li>
704
705 <li data-name="LuCI.network.WifiNetwork#getNoise"><a href="LuCI.network.WifiNetwork.html#getNoise">getNoise</a></li>
706
707 <li data-name="LuCI.network.WifiNetwork#getShortName"><a href="LuCI.network.WifiNetwork.html#getShortName">getShortName</a></li>
708
709 <li data-name="LuCI.network.WifiNetwork#getSignal"><a href="LuCI.network.WifiNetwork.html#getSignal">getSignal</a></li>
710
711 <li data-name="LuCI.network.WifiNetwork#getSignalLevel"><a href="LuCI.network.WifiNetwork.html#getSignalLevel">getSignalLevel</a></li>
712
713 <li data-name="LuCI.network.WifiNetwork#getSignalPercent"><a href="LuCI.network.WifiNetwork.html#getSignalPercent">getSignalPercent</a></li>
714
715 <li data-name="LuCI.network.WifiNetwork#getSSID"><a href="LuCI.network.WifiNetwork.html#getSSID">getSSID</a></li>
716
717 <li data-name="LuCI.network.WifiNetwork#getTXPower"><a href="LuCI.network.WifiNetwork.html#getTXPower">getTXPower</a></li>
718
719 <li data-name="LuCI.network.WifiNetwork#getTXPowerOffset"><a href="LuCI.network.WifiNetwork.html#getTXPowerOffset">getTXPowerOffset</a></li>
720
721 <li data-name="LuCI.network.WifiNetwork#getWifiDevice"><a href="LuCI.network.WifiNetwork.html#getWifiDevice">getWifiDevice</a></li>
722
723 <li data-name="LuCI.network.WifiNetwork#getWifiDeviceName"><a href="LuCI.network.WifiNetwork.html#getWifiDeviceName">getWifiDeviceName</a></li>
724
725 <li data-name="LuCI.network.WifiNetwork#isClientDisconnectSupported"><a href="LuCI.network.WifiNetwork.html#isClientDisconnectSupported">isClientDisconnectSupported</a></li>
726
727 <li data-name="LuCI.network.WifiNetwork#isDisabled"><a href="LuCI.network.WifiNetwork.html#isDisabled">isDisabled</a></li>
728
729 <li data-name="LuCI.network.WifiNetwork#isUp"><a href="LuCI.network.WifiNetwork.html#isUp">isUp</a></li>
730
731 <li data-name="LuCI.network.WifiNetwork#set"><a href="LuCI.network.WifiNetwork.html#set">set</a></li>
732
733 </ul>
734 <ul class="events itemMembers">
735
736 </ul>
737 </li>
738
739 <li class="item" data-name="LuCI.poll">
740 <span class="title">
741 <a href="LuCI.poll.html">LuCI.poll</a>
742
743 </span>
744 <ul class="members itemMembers">
745
746 </ul>
747 <ul class="typedefs itemMembers">
748
749 </ul>
750 <ul class="typedefs itemMembers">
751
752 </ul>
753 <ul class="methods itemMembers">
754
755 <span class="subtitle">Methods</span>
756
757 <li data-name="LuCI.poll#active"><a href="LuCI.poll.html#active">active</a></li>
758
759 <li data-name="LuCI.poll#add"><a href="LuCI.poll.html#add">add</a></li>
760
761 <li data-name="LuCI.poll#remove"><a href="LuCI.poll.html#remove">remove</a></li>
762
763 <li data-name="LuCI.poll#start"><a href="LuCI.poll.html#start">start</a></li>
764
765 <li data-name="LuCI.poll#stop"><a href="LuCI.poll.html#stop">stop</a></li>
766
767 </ul>
768 <ul class="events itemMembers">
769
770 </ul>
771 </li>
772
773 <li class="item" data-name="LuCI.request">
774 <span class="title">
775 <a href="LuCI.request.html">LuCI.request</a>
776
777 </span>
778 <ul class="members itemMembers">
779
780 </ul>
781 <ul class="typedefs itemMembers">
782
783 <span class="subtitle">Typedefs</span>
784
785 <li data-name="LuCI.request.interceptorFn"><a href="LuCI.request.html#.interceptorFn">interceptorFn</a></li>
786
787 <li data-name="LuCI.request.RequestOptions"><a href="LuCI.request.html#.RequestOptions">RequestOptions</a></li>
788
789 </ul>
790 <ul class="typedefs itemMembers">
791
792 </ul>
793 <ul class="methods itemMembers">
794
795 <span class="subtitle">Methods</span>
796
797 <li data-name="LuCI.request#addInterceptor"><a href="LuCI.request.html#addInterceptor">addInterceptor</a></li>
798
799 <li data-name="LuCI.request#expandURL"><a href="LuCI.request.html#expandURL">expandURL</a></li>
800
801 <li data-name="LuCI.request#get"><a href="LuCI.request.html#get">get</a></li>
802
803 <li data-name="LuCI.request#post"><a href="LuCI.request.html#post">post</a></li>
804
805 <li data-name="LuCI.request#removeInterceptor"><a href="LuCI.request.html#removeInterceptor">removeInterceptor</a></li>
806
807 <li data-name="LuCI.request#request"><a href="LuCI.request.html#request">request</a></li>
808
809 </ul>
810 <ul class="events itemMembers">
811
812 </ul>
813 </li>
814
815 <li class="item" data-name="LuCI.request.poll">
816 <span class="title">
817 <a href="LuCI.request.poll.html">LuCI.request.poll</a>
818
819 </span>
820 <ul class="members itemMembers">
821
822 </ul>
823 <ul class="typedefs itemMembers">
824
825 <span class="subtitle">Typedefs</span>
826
827 <li data-name="LuCI.request.poll~callbackFn"><a href="LuCI.request.poll.html#~callbackFn">callbackFn</a></li>
828
829 </ul>
830 <ul class="typedefs itemMembers">
831
832 </ul>
833 <ul class="methods itemMembers">
834
835 <span class="subtitle">Methods</span>
836
837 <li data-name="LuCI.request.poll#active"><a href="LuCI.request.poll.html#active">active</a></li>
838
839 <li data-name="LuCI.request.poll#add"><a href="LuCI.request.poll.html#add">add</a></li>
840
841 <li data-name="LuCI.request.poll#remove"><a href="LuCI.request.poll.html#remove">remove</a></li>
842
843 <li data-name="LuCI.request.poll#start"><a href="LuCI.request.poll.html#start">start</a></li>
844
845 <li data-name="LuCI.request.poll#stop"><a href="LuCI.request.poll.html#stop">stop</a></li>
846
847 </ul>
848 <ul class="events itemMembers">
849
850 </ul>
851 </li>
852
853 <li class="item" data-name="LuCI.response">
854 <span class="title">
855 <a href="LuCI.response.html">LuCI.response</a>
856
857 </span>
858 <ul class="members itemMembers">
859
860 <span class="subtitle">Members</span>
861
862 <li data-name="LuCI.response#duration"><a href="LuCI.response.html#duration">duration</a></li>
863
864 <li data-name="LuCI.response#headers"><a href="LuCI.response.html#headers">headers</a></li>
865
866 <li data-name="LuCI.response#ok"><a href="LuCI.response.html#ok">ok</a></li>
867
868 <li data-name="LuCI.response#status"><a href="LuCI.response.html#status">status</a></li>
869
870 <li data-name="LuCI.response#statusText"><a href="LuCI.response.html#statusText">statusText</a></li>
871
872 <li data-name="LuCI.response#url"><a href="LuCI.response.html#url">url</a></li>
873
874 </ul>
875 <ul class="typedefs itemMembers">
876
877 </ul>
878 <ul class="typedefs itemMembers">
879
880 </ul>
881 <ul class="methods itemMembers">
882
883 <span class="subtitle">Methods</span>
884
885 <li data-name="LuCI.response#blob"><a href="LuCI.response.html#blob">blob</a></li>
886
887 <li data-name="LuCI.response#clone"><a href="LuCI.response.html#clone">clone</a></li>
888
889 <li data-name="LuCI.response#json"><a href="LuCI.response.html#json">json</a></li>
890
891 <li data-name="LuCI.response#text"><a href="LuCI.response.html#text">text</a></li>
892
893 </ul>
894 <ul class="events itemMembers">
895
896 </ul>
897 </li>
898
899 <li class="item" data-name="LuCI.rpc">
900 <span class="title">
901 <a href="LuCI.rpc.html">LuCI.rpc</a>
902
903 </span>
904 <ul class="members itemMembers">
905
906 </ul>
907 <ul class="typedefs itemMembers">
908
909 <span class="subtitle">Typedefs</span>
910
911 <li data-name="LuCI.rpc.DeclareOptions"><a href="LuCI.rpc.html#.DeclareOptions">DeclareOptions</a></li>
912
913 <li data-name="LuCI.rpc~filterFn"><a href="LuCI.rpc.html#~filterFn">filterFn</a></li>
914
915 <li data-name="LuCI.rpc~interceptorFn"><a href="LuCI.rpc.html#~interceptorFn">interceptorFn</a></li>
916
917 <li data-name="LuCI.rpc~invokeFn"><a href="LuCI.rpc.html#~invokeFn">invokeFn</a></li>
918
919 </ul>
920 <ul class="typedefs itemMembers">
921
922 </ul>
923 <ul class="methods itemMembers">
924
925 <span class="subtitle">Methods</span>
926
927 <li data-name="LuCI.rpc#addInterceptor"><a href="LuCI.rpc.html#addInterceptor">addInterceptor</a></li>
928
929 <li data-name="LuCI.rpc#declare"><a href="LuCI.rpc.html#declare">declare</a></li>
930
931 <li data-name="LuCI.rpc#getBaseURL"><a href="LuCI.rpc.html#getBaseURL">getBaseURL</a></li>
932
933 <li data-name="LuCI.rpc#getSessionID"><a href="LuCI.rpc.html#getSessionID">getSessionID</a></li>
934
935 <li data-name="LuCI.rpc#getStatusText"><a href="LuCI.rpc.html#getStatusText">getStatusText</a></li>
936
937 <li data-name="LuCI.rpc#list"><a href="LuCI.rpc.html#list">list</a></li>
938
939 <li data-name="LuCI.rpc#removeInterceptor"><a href="LuCI.rpc.html#removeInterceptor">removeInterceptor</a></li>
940
941 <li data-name="LuCI.rpc#setBaseURL"><a href="LuCI.rpc.html#setBaseURL">setBaseURL</a></li>
942
943 <li data-name="LuCI.rpc#setSessionID"><a href="LuCI.rpc.html#setSessionID">setSessionID</a></li>
944
945 </ul>
946 <ul class="events itemMembers">
947
948 </ul>
949 </li>
950
951 <li class="item" data-name="LuCI.uci">
952 <span class="title">
953 <a href="LuCI.uci.html">LuCI.uci</a>
954
955 </span>
956 <ul class="members itemMembers">
957
958 </ul>
959 <ul class="typedefs itemMembers">
960
961 <span class="subtitle">Typedefs</span>
962
963 <li data-name="LuCI.uci.ChangeRecord"><a href="LuCI.uci.html#.ChangeRecord">ChangeRecord</a></li>
964
965 <li data-name="LuCI.uci.SectionObject"><a href="LuCI.uci.html#.SectionObject">SectionObject</a></li>
966
967 <li data-name="LuCI.uci~sectionsFn"><a href="LuCI.uci.html#~sectionsFn">sectionsFn</a></li>
968
969 </ul>
970 <ul class="typedefs itemMembers">
971
972 </ul>
973 <ul class="methods itemMembers">
974
975 <span class="subtitle">Methods</span>
976
977 <li data-name="LuCI.uci#add"><a href="LuCI.uci.html#add">add</a></li>
978
979 <li data-name="LuCI.uci#apply"><a href="LuCI.uci.html#apply">apply</a></li>
980
981 <li data-name="LuCI.uci#changes"><a href="LuCI.uci.html#changes">changes</a></li>
982
983 <li data-name="LuCI.uci#createSID"><a href="LuCI.uci.html#createSID">createSID</a></li>
984
985 <li data-name="LuCI.uci#get"><a href="LuCI.uci.html#get">get</a></li>
986
987 <li data-name="LuCI.uci#get_first"><a href="LuCI.uci.html#get_first">get_first</a></li>
988
989 <li data-name="LuCI.uci#load"><a href="LuCI.uci.html#load">load</a></li>
990
991 <li data-name="LuCI.uci#move"><a href="LuCI.uci.html#move">move</a></li>
992
993 <li data-name="LuCI.uci#remove"><a href="LuCI.uci.html#remove">remove</a></li>
994
995 <li data-name="LuCI.uci#resolveSID"><a href="LuCI.uci.html#resolveSID">resolveSID</a></li>
996
997 <li data-name="LuCI.uci#save"><a href="LuCI.uci.html#save">save</a></li>
998
999 <li data-name="LuCI.uci#sections"><a href="LuCI.uci.html#sections">sections</a></li>
1000
1001 <li data-name="LuCI.uci#set"><a href="LuCI.uci.html#set">set</a></li>
1002
1003 <li data-name="LuCI.uci#set_first"><a href="LuCI.uci.html#set_first">set_first</a></li>
1004
1005 <li data-name="LuCI.uci#unload"><a href="LuCI.uci.html#unload">unload</a></li>
1006
1007 <li data-name="LuCI.uci#unset"><a href="LuCI.uci.html#unset">unset</a></li>
1008
1009 <li data-name="LuCI.uci#unset_first"><a href="LuCI.uci.html#unset_first">unset_first</a></li>
1010
1011 </ul>
1012 <ul class="events itemMembers">
1013
1014 </ul>
1015 </li>
1016
1017 <li class="item" data-name="LuCI.ui">
1018 <span class="title">
1019 <a href="LuCI.ui.html">LuCI.ui</a>
1020
1021 </span>
1022 <ul class="members itemMembers">
1023
1024 </ul>
1025 <ul class="typedefs itemMembers">
1026
1027 <span class="subtitle">Typedefs</span>
1028
1029 <li data-name="LuCI.ui.FileUploadReply"><a href="LuCI.ui.html#.FileUploadReply">FileUploadReply</a></li>
1030
1031 </ul>
1032 <ul class="typedefs itemMembers">
1033
1034 </ul>
1035 <ul class="methods itemMembers">
1036
1037 <span class="subtitle">Methods</span>
1038
1039 <li data-name="LuCI.ui#addNotification"><a href="LuCI.ui.html#addNotification">addNotification</a></li>
1040
1041 <li data-name="LuCI.ui#addValidator"><a href="LuCI.ui.html#addValidator">addValidator</a></li>
1042
1043 <li data-name="LuCI.ui#awaitReconnect"><a href="LuCI.ui.html#awaitReconnect">awaitReconnect</a></li>
1044
1045 <li data-name="LuCI.ui#createHandlerFn"><a href="LuCI.ui.html#createHandlerFn">createHandlerFn</a></li>
1046
1047 <li data-name="LuCI.ui#hideIndicator"><a href="LuCI.ui.html#hideIndicator">hideIndicator</a></li>
1048
1049 <li data-name="LuCI.ui#hideModal"><a href="LuCI.ui.html#hideModal">hideModal</a></li>
1050
1051 <li data-name="LuCI.ui#instantiateView"><a href="LuCI.ui.html#instantiateView">instantiateView</a></li>
1052
1053 <li data-name="LuCI.ui#itemlist"><a href="LuCI.ui.html#itemlist">itemlist</a></li>
1054
1055 <li data-name="LuCI.ui#pingDevice"><a href="LuCI.ui.html#pingDevice">pingDevice</a></li>
1056
1057 <li data-name="LuCI.ui#showIndicator"><a href="LuCI.ui.html#showIndicator">showIndicator</a></li>
1058
1059 <li data-name="LuCI.ui#showModal"><a href="LuCI.ui.html#showModal">showModal</a></li>
1060
1061 <li data-name="LuCI.ui#uploadFile"><a href="LuCI.ui.html#uploadFile">uploadFile</a></li>
1062
1063 </ul>
1064 <ul class="events itemMembers">
1065
1066 </ul>
1067 </li>
1068
1069 <li class="item" data-name="LuCI.ui.AbstractElement">
1070 <span class="title">
1071 <a href="LuCI.ui.AbstractElement.html">LuCI.ui.AbstractElement</a>
1072
1073 </span>
1074 <ul class="members itemMembers">
1075
1076 </ul>
1077 <ul class="typedefs itemMembers">
1078
1079 <span class="subtitle">Typedefs</span>
1080
1081 <li data-name="LuCI.ui.AbstractElement.InitOptions"><a href="LuCI.ui.AbstractElement.html#.InitOptions">InitOptions</a></li>
1082
1083 </ul>
1084 <ul class="typedefs itemMembers">
1085
1086 </ul>
1087 <ul class="methods itemMembers">
1088
1089 <span class="subtitle">Methods</span>
1090
1091 <li data-name="LuCI.ui.AbstractElement#getValue"><a href="LuCI.ui.AbstractElement.html#getValue">getValue</a></li>
1092
1093 <li data-name="LuCI.ui.AbstractElement#isValid"><a href="LuCI.ui.AbstractElement.html#isValid">isValid</a></li>
1094
1095 <li data-name="LuCI.ui.AbstractElement#registerEvents"><a href="LuCI.ui.AbstractElement.html#registerEvents">registerEvents</a></li>
1096
1097 <li data-name="LuCI.ui.AbstractElement#render"><a href="LuCI.ui.AbstractElement.html#render">render</a></li>
1098
1099 <li data-name="LuCI.ui.AbstractElement#setChangeEvents"><a href="LuCI.ui.AbstractElement.html#setChangeEvents">setChangeEvents</a></li>
1100
1101 <li data-name="LuCI.ui.AbstractElement#setUpdateEvents"><a href="LuCI.ui.AbstractElement.html#setUpdateEvents">setUpdateEvents</a></li>
1102
1103 <li data-name="LuCI.ui.AbstractElement#setValue"><a href="LuCI.ui.AbstractElement.html#setValue">setValue</a></li>
1104
1105 <li data-name="LuCI.ui.AbstractElement#triggerValidation"><a href="LuCI.ui.AbstractElement.html#triggerValidation">triggerValidation</a></li>
1106
1107 </ul>
1108 <ul class="events itemMembers">
1109
1110 </ul>
1111 </li>
1112
1113 <li class="item" data-name="LuCI.ui.changes">
1114 <span class="title">
1115 <a href="LuCI.ui.changes.html">LuCI.ui.changes</a>
1116
1117 </span>
1118 <ul class="members itemMembers">
1119
1120 </ul>
1121 <ul class="typedefs itemMembers">
1122
1123 </ul>
1124 <ul class="typedefs itemMembers">
1125
1126 </ul>
1127 <ul class="methods itemMembers">
1128
1129 <span class="subtitle">Methods</span>
1130
1131 <li data-name="LuCI.ui.changes#apply"><a href="LuCI.ui.changes.html#apply">apply</a></li>
1132
1133 <li data-name="LuCI.ui.changes#displayChanges"><a href="LuCI.ui.changes.html#displayChanges">displayChanges</a></li>
1134
1135 <li data-name="LuCI.ui.changes#renderChangeIndicator"><a href="LuCI.ui.changes.html#renderChangeIndicator">renderChangeIndicator</a></li>
1136
1137 <li data-name="LuCI.ui.changes#revert"><a href="LuCI.ui.changes.html#revert">revert</a></li>
1138
1139 <li data-name="LuCI.ui.changes#setIndicator"><a href="LuCI.ui.changes.html#setIndicator">setIndicator</a></li>
1140
1141 </ul>
1142 <ul class="events itemMembers">
1143
1144 </ul>
1145 </li>
1146
1147 <li class="item" data-name="LuCI.ui.Checkbox">
1148 <span class="title">
1149 <a href="LuCI.ui.Checkbox.html">LuCI.ui.Checkbox</a>
1150
1151 </span>
1152 <ul class="members itemMembers">
1153
1154 </ul>
1155 <ul class="typedefs itemMembers">
1156
1157 <span class="subtitle">Typedefs</span>
1158
1159 <li data-name="LuCI.ui.Checkbox.InitOptions"><a href="LuCI.ui.Checkbox.html#.InitOptions">InitOptions</a></li>
1160
1161 </ul>
1162 <ul class="typedefs itemMembers">
1163
1164 </ul>
1165 <ul class="methods itemMembers">
1166
1167 <span class="subtitle">Methods</span>
1168
1169 <li data-name="LuCI.ui.Checkbox#getValue"><a href="LuCI.ui.Checkbox.html#getValue">getValue</a></li>
1170
1171 <li data-name="LuCI.ui.Checkbox#isChecked"><a href="LuCI.ui.Checkbox.html#isChecked">isChecked</a></li>
1172
1173 <li data-name="LuCI.ui.Checkbox#isValid"><a href="LuCI.ui.Checkbox.html#isValid">isValid</a></li>
1174
1175 <li data-name="LuCI.ui.Checkbox#registerEvents"><a href="LuCI.ui.Checkbox.html#registerEvents">registerEvents</a></li>
1176
1177 <li data-name="LuCI.ui.Checkbox#render"><a href="LuCI.ui.Checkbox.html#render">render</a></li>
1178
1179 <li data-name="LuCI.ui.Checkbox#setChangeEvents"><a href="LuCI.ui.Checkbox.html#setChangeEvents">setChangeEvents</a></li>
1180
1181 <li data-name="LuCI.ui.Checkbox#setUpdateEvents"><a href="LuCI.ui.Checkbox.html#setUpdateEvents">setUpdateEvents</a></li>
1182
1183 <li data-name="LuCI.ui.Checkbox#setValue"><a href="LuCI.ui.Checkbox.html#setValue">setValue</a></li>
1184
1185 <li data-name="LuCI.ui.Checkbox#triggerValidation"><a href="LuCI.ui.Checkbox.html#triggerValidation">triggerValidation</a></li>
1186
1187 </ul>
1188 <ul class="events itemMembers">
1189
1190 </ul>
1191 </li>
1192
1193 <li class="item" data-name="LuCI.ui.Combobox">
1194 <span class="title">
1195 <a href="LuCI.ui.Combobox.html">LuCI.ui.Combobox</a>
1196
1197 </span>
1198 <ul class="members itemMembers">
1199
1200 </ul>
1201 <ul class="typedefs itemMembers">
1202
1203 <span class="subtitle">Typedefs</span>
1204
1205 <li data-name="LuCI.ui.Combobox.InitOptions"><a href="LuCI.ui.Combobox.html#.InitOptions">InitOptions</a></li>
1206
1207 </ul>
1208 <ul class="typedefs itemMembers">
1209
1210 </ul>
1211 <ul class="methods itemMembers">
1212
1213 <span class="subtitle">Methods</span>
1214
1215 <li data-name="LuCI.ui.Combobox#addChoices"><a href="LuCI.ui.Combobox.html#addChoices">addChoices</a></li>
1216
1217 <li data-name="LuCI.ui.Combobox#clearChoices"><a href="LuCI.ui.Combobox.html#clearChoices">clearChoices</a></li>
1218
1219 <li data-name="LuCI.ui.Combobox#closeAllDropdowns"><a href="LuCI.ui.Combobox.html#closeAllDropdowns">closeAllDropdowns</a></li>
1220
1221 <li data-name="LuCI.ui.Combobox#isValid"><a href="LuCI.ui.Combobox.html#isValid">isValid</a></li>
1222
1223 <li data-name="LuCI.ui.Combobox#registerEvents"><a href="LuCI.ui.Combobox.html#registerEvents">registerEvents</a></li>
1224
1225 <li data-name="LuCI.ui.Combobox#setChangeEvents"><a href="LuCI.ui.Combobox.html#setChangeEvents">setChangeEvents</a></li>
1226
1227 <li data-name="LuCI.ui.Combobox#setUpdateEvents"><a href="LuCI.ui.Combobox.html#setUpdateEvents">setUpdateEvents</a></li>
1228
1229 <li data-name="LuCI.ui.Combobox#triggerValidation"><a href="LuCI.ui.Combobox.html#triggerValidation">triggerValidation</a></li>
1230
1231 </ul>
1232 <ul class="events itemMembers">
1233
1234 </ul>
1235 </li>
1236
1237 <li class="item" data-name="LuCI.ui.ComboButton">
1238 <span class="title">
1239 <a href="LuCI.ui.ComboButton.html">LuCI.ui.ComboButton</a>
1240
1241 </span>
1242 <ul class="members itemMembers">
1243
1244 </ul>
1245 <ul class="typedefs itemMembers">
1246
1247 <span class="subtitle">Typedefs</span>
1248
1249 <li data-name="LuCI.ui.ComboButton.InitOptions"><a href="LuCI.ui.ComboButton.html#.InitOptions">InitOptions</a></li>
1250
1251 </ul>
1252 <ul class="typedefs itemMembers">
1253
1254 </ul>
1255 <ul class="methods itemMembers">
1256
1257 <span class="subtitle">Methods</span>
1258
1259 <li data-name="LuCI.ui.ComboButton#addChoices"><a href="LuCI.ui.ComboButton.html#addChoices">addChoices</a></li>
1260
1261 <li data-name="LuCI.ui.ComboButton#clearChoices"><a href="LuCI.ui.ComboButton.html#clearChoices">clearChoices</a></li>
1262
1263 <li data-name="LuCI.ui.ComboButton#closeAllDropdowns"><a href="LuCI.ui.ComboButton.html#closeAllDropdowns">closeAllDropdowns</a></li>
1264
1265 <li data-name="LuCI.ui.ComboButton#isValid"><a href="LuCI.ui.ComboButton.html#isValid">isValid</a></li>
1266
1267 <li data-name="LuCI.ui.ComboButton#registerEvents"><a href="LuCI.ui.ComboButton.html#registerEvents">registerEvents</a></li>
1268
1269 <li data-name="LuCI.ui.ComboButton#setChangeEvents"><a href="LuCI.ui.ComboButton.html#setChangeEvents">setChangeEvents</a></li>
1270
1271 <li data-name="LuCI.ui.ComboButton#setUpdateEvents"><a href="LuCI.ui.ComboButton.html#setUpdateEvents">setUpdateEvents</a></li>
1272
1273 <li data-name="LuCI.ui.ComboButton#triggerValidation"><a href="LuCI.ui.ComboButton.html#triggerValidation">triggerValidation</a></li>
1274
1275 </ul>
1276 <ul class="events itemMembers">
1277
1278 </ul>
1279 </li>
1280
1281 <li class="item" data-name="LuCI.ui.Dropdown">
1282 <span class="title">
1283 <a href="LuCI.ui.Dropdown.html">LuCI.ui.Dropdown</a>
1284
1285 </span>
1286 <ul class="members itemMembers">
1287
1288 </ul>
1289 <ul class="typedefs itemMembers">
1290
1291 <span class="subtitle">Typedefs</span>
1292
1293 <li data-name="LuCI.ui.Dropdown.InitOptions"><a href="LuCI.ui.Dropdown.html#.InitOptions">InitOptions</a></li>
1294
1295 </ul>
1296 <ul class="typedefs itemMembers">
1297
1298 </ul>
1299 <ul class="methods itemMembers">
1300
1301 <span class="subtitle">Methods</span>
1302
1303 <li data-name="LuCI.ui.Dropdown#addChoices"><a href="LuCI.ui.Dropdown.html#addChoices">addChoices</a></li>
1304
1305 <li data-name="LuCI.ui.Dropdown#clearChoices"><a href="LuCI.ui.Dropdown.html#clearChoices">clearChoices</a></li>
1306
1307 <li data-name="LuCI.ui.Dropdown#closeAllDropdowns"><a href="LuCI.ui.Dropdown.html#closeAllDropdowns">closeAllDropdowns</a></li>
1308
1309 <li data-name="LuCI.ui.Dropdown#getValue"><a href="LuCI.ui.Dropdown.html#getValue">getValue</a></li>
1310
1311 <li data-name="LuCI.ui.Dropdown#isValid"><a href="LuCI.ui.Dropdown.html#isValid">isValid</a></li>
1312
1313 <li data-name="LuCI.ui.Dropdown#registerEvents"><a href="LuCI.ui.Dropdown.html#registerEvents">registerEvents</a></li>
1314
1315 <li data-name="LuCI.ui.Dropdown#render"><a href="LuCI.ui.Dropdown.html#render">render</a></li>
1316
1317 <li data-name="LuCI.ui.Dropdown#setChangeEvents"><a href="LuCI.ui.Dropdown.html#setChangeEvents">setChangeEvents</a></li>
1318
1319 <li data-name="LuCI.ui.Dropdown#setUpdateEvents"><a href="LuCI.ui.Dropdown.html#setUpdateEvents">setUpdateEvents</a></li>
1320
1321 <li data-name="LuCI.ui.Dropdown#setValue"><a href="LuCI.ui.Dropdown.html#setValue">setValue</a></li>
1322
1323 <li data-name="LuCI.ui.Dropdown#triggerValidation"><a href="LuCI.ui.Dropdown.html#triggerValidation">triggerValidation</a></li>
1324
1325 </ul>
1326 <ul class="events itemMembers">
1327
1328 </ul>
1329 </li>
1330
1331 <li class="item" data-name="LuCI.ui.DynamicList">
1332 <span class="title">
1333 <a href="LuCI.ui.DynamicList.html">LuCI.ui.DynamicList</a>
1334
1335 </span>
1336 <ul class="members itemMembers">
1337
1338 </ul>
1339 <ul class="typedefs itemMembers">
1340
1341 <span class="subtitle">Typedefs</span>
1342
1343 <li data-name="LuCI.ui.DynamicList.InitOptions"><a href="LuCI.ui.DynamicList.html#.InitOptions">InitOptions</a></li>
1344
1345 </ul>
1346 <ul class="typedefs itemMembers">
1347
1348 </ul>
1349 <ul class="methods itemMembers">
1350
1351 <span class="subtitle">Methods</span>
1352
1353 <li data-name="LuCI.ui.DynamicList#addChoices"><a href="LuCI.ui.DynamicList.html#addChoices">addChoices</a></li>
1354
1355 <li data-name="LuCI.ui.DynamicList#clearChoices"><a href="LuCI.ui.DynamicList.html#clearChoices">clearChoices</a></li>
1356
1357 <li data-name="LuCI.ui.DynamicList#getValue"><a href="LuCI.ui.DynamicList.html#getValue">getValue</a></li>
1358
1359 <li data-name="LuCI.ui.DynamicList#isValid"><a href="LuCI.ui.DynamicList.html#isValid">isValid</a></li>
1360
1361 <li data-name="LuCI.ui.DynamicList#registerEvents"><a href="LuCI.ui.DynamicList.html#registerEvents">registerEvents</a></li>
1362
1363 <li data-name="LuCI.ui.DynamicList#render"><a href="LuCI.ui.DynamicList.html#render">render</a></li>
1364
1365 <li data-name="LuCI.ui.DynamicList#setChangeEvents"><a href="LuCI.ui.DynamicList.html#setChangeEvents">setChangeEvents</a></li>
1366
1367 <li data-name="LuCI.ui.DynamicList#setUpdateEvents"><a href="LuCI.ui.DynamicList.html#setUpdateEvents">setUpdateEvents</a></li>
1368
1369 <li data-name="LuCI.ui.DynamicList#setValue"><a href="LuCI.ui.DynamicList.html#setValue">setValue</a></li>
1370
1371 <li data-name="LuCI.ui.DynamicList#triggerValidation"><a href="LuCI.ui.DynamicList.html#triggerValidation">triggerValidation</a></li>
1372
1373 </ul>
1374 <ul class="events itemMembers">
1375
1376 </ul>
1377 </li>
1378
1379 <li class="item" data-name="LuCI.ui.FileUpload">
1380 <span class="title">
1381 <a href="LuCI.ui.FileUpload.html">LuCI.ui.FileUpload</a>
1382
1383 </span>
1384 <ul class="members itemMembers">
1385
1386 </ul>
1387 <ul class="typedefs itemMembers">
1388
1389 <span class="subtitle">Typedefs</span>
1390
1391 <li data-name="LuCI.ui.FileUpload.InitOptions"><a href="LuCI.ui.FileUpload.html#.InitOptions">InitOptions</a></li>
1392
1393 </ul>
1394 <ul class="typedefs itemMembers">
1395
1396 </ul>
1397 <ul class="methods itemMembers">
1398
1399 <span class="subtitle">Methods</span>
1400
1401 <li data-name="LuCI.ui.FileUpload#getValue"><a href="LuCI.ui.FileUpload.html#getValue">getValue</a></li>
1402
1403 <li data-name="LuCI.ui.FileUpload#isValid"><a href="LuCI.ui.FileUpload.html#isValid">isValid</a></li>
1404
1405 <li data-name="LuCI.ui.FileUpload#registerEvents"><a href="LuCI.ui.FileUpload.html#registerEvents">registerEvents</a></li>
1406
1407 <li data-name="LuCI.ui.FileUpload#render"><a href="LuCI.ui.FileUpload.html#render">render</a></li>
1408
1409 <li data-name="LuCI.ui.FileUpload#setChangeEvents"><a href="LuCI.ui.FileUpload.html#setChangeEvents">setChangeEvents</a></li>
1410
1411 <li data-name="LuCI.ui.FileUpload#setUpdateEvents"><a href="LuCI.ui.FileUpload.html#setUpdateEvents">setUpdateEvents</a></li>
1412
1413 <li data-name="LuCI.ui.FileUpload#setValue"><a href="LuCI.ui.FileUpload.html#setValue">setValue</a></li>
1414
1415 <li data-name="LuCI.ui.FileUpload#triggerValidation"><a href="LuCI.ui.FileUpload.html#triggerValidation">triggerValidation</a></li>
1416
1417 </ul>
1418 <ul class="events itemMembers">
1419
1420 </ul>
1421 </li>
1422
1423 <li class="item" data-name="LuCI.ui.Hiddenfield">
1424 <span class="title">
1425 <a href="LuCI.ui.Hiddenfield.html">LuCI.ui.Hiddenfield</a>
1426
1427 </span>
1428 <ul class="members itemMembers">
1429
1430 </ul>
1431 <ul class="typedefs itemMembers">
1432
1433 </ul>
1434 <ul class="typedefs itemMembers">
1435
1436 </ul>
1437 <ul class="methods itemMembers">
1438
1439 <span class="subtitle">Methods</span>
1440
1441 <li data-name="LuCI.ui.Hiddenfield#getValue"><a href="LuCI.ui.Hiddenfield.html#getValue">getValue</a></li>
1442
1443 <li data-name="LuCI.ui.Hiddenfield#isValid"><a href="LuCI.ui.Hiddenfield.html#isValid">isValid</a></li>
1444
1445 <li data-name="LuCI.ui.Hiddenfield#registerEvents"><a href="LuCI.ui.Hiddenfield.html#registerEvents">registerEvents</a></li>
1446
1447 <li data-name="LuCI.ui.Hiddenfield#render"><a href="LuCI.ui.Hiddenfield.html#render">render</a></li>
1448
1449 <li data-name="LuCI.ui.Hiddenfield#setChangeEvents"><a href="LuCI.ui.Hiddenfield.html#setChangeEvents">setChangeEvents</a></li>
1450
1451 <li data-name="LuCI.ui.Hiddenfield#setUpdateEvents"><a href="LuCI.ui.Hiddenfield.html#setUpdateEvents">setUpdateEvents</a></li>
1452
1453 <li data-name="LuCI.ui.Hiddenfield#setValue"><a href="LuCI.ui.Hiddenfield.html#setValue">setValue</a></li>
1454
1455 <li data-name="LuCI.ui.Hiddenfield#triggerValidation"><a href="LuCI.ui.Hiddenfield.html#triggerValidation">triggerValidation</a></li>
1456
1457 </ul>
1458 <ul class="events itemMembers">
1459
1460 </ul>
1461 </li>
1462
1463 <li class="item" data-name="LuCI.ui.Select">
1464 <span class="title">
1465 <a href="LuCI.ui.Select.html">LuCI.ui.Select</a>
1466
1467 </span>
1468 <ul class="members itemMembers">
1469
1470 </ul>
1471 <ul class="typedefs itemMembers">
1472
1473 <span class="subtitle">Typedefs</span>
1474
1475 <li data-name="LuCI.ui.Select.InitOptions"><a href="LuCI.ui.Select.html#.InitOptions">InitOptions</a></li>
1476
1477 </ul>
1478 <ul class="typedefs itemMembers">
1479
1480 </ul>
1481 <ul class="methods itemMembers">
1482
1483 <span class="subtitle">Methods</span>
1484
1485 <li data-name="LuCI.ui.Select#getValue"><a href="LuCI.ui.Select.html#getValue">getValue</a></li>
1486
1487 <li data-name="LuCI.ui.Select#isValid"><a href="LuCI.ui.Select.html#isValid">isValid</a></li>
1488
1489 <li data-name="LuCI.ui.Select#registerEvents"><a href="LuCI.ui.Select.html#registerEvents">registerEvents</a></li>
1490
1491 <li data-name="LuCI.ui.Select#render"><a href="LuCI.ui.Select.html#render">render</a></li>
1492
1493 <li data-name="LuCI.ui.Select#setChangeEvents"><a href="LuCI.ui.Select.html#setChangeEvents">setChangeEvents</a></li>
1494
1495 <li data-name="LuCI.ui.Select#setUpdateEvents"><a href="LuCI.ui.Select.html#setUpdateEvents">setUpdateEvents</a></li>
1496
1497 <li data-name="LuCI.ui.Select#setValue"><a href="LuCI.ui.Select.html#setValue">setValue</a></li>
1498
1499 <li data-name="LuCI.ui.Select#triggerValidation"><a href="LuCI.ui.Select.html#triggerValidation">triggerValidation</a></li>
1500
1501 </ul>
1502 <ul class="events itemMembers">
1503
1504 </ul>
1505 </li>
1506
1507 <li class="item" data-name="LuCI.ui.tabs">
1508 <span class="title">
1509 <a href="LuCI.ui.tabs.html">LuCI.ui.tabs</a>
1510
1511 </span>
1512 <ul class="members itemMembers">
1513
1514 </ul>
1515 <ul class="typedefs itemMembers">
1516
1517 </ul>
1518 <ul class="typedefs itemMembers">
1519
1520 </ul>
1521 <ul class="methods itemMembers">
1522
1523 <span class="subtitle">Methods</span>
1524
1525 <li data-name="LuCI.ui.tabs#initTabGroup"><a href="LuCI.ui.tabs.html#initTabGroup">initTabGroup</a></li>
1526
1527 <li data-name="LuCI.ui.tabs#isEmptyPane"><a href="LuCI.ui.tabs.html#isEmptyPane">isEmptyPane</a></li>
1528
1529 </ul>
1530 <ul class="events itemMembers">
1531
1532 </ul>
1533 </li>
1534
1535 <li class="item" data-name="LuCI.ui.Textarea">
1536 <span class="title">
1537 <a href="LuCI.ui.Textarea.html">LuCI.ui.Textarea</a>
1538
1539 </span>
1540 <ul class="members itemMembers">
1541
1542 </ul>
1543 <ul class="typedefs itemMembers">
1544
1545 <span class="subtitle">Typedefs</span>
1546
1547 <li data-name="LuCI.ui.Textarea.InitOptions"><a href="LuCI.ui.Textarea.html#.InitOptions">InitOptions</a></li>
1548
1549 </ul>
1550 <ul class="typedefs itemMembers">
1551
1552 </ul>
1553 <ul class="methods itemMembers">
1554
1555 <span class="subtitle">Methods</span>
1556
1557 <li data-name="LuCI.ui.Textarea#getValue"><a href="LuCI.ui.Textarea.html#getValue">getValue</a></li>
1558
1559 <li data-name="LuCI.ui.Textarea#isValid"><a href="LuCI.ui.Textarea.html#isValid">isValid</a></li>
1560
1561 <li data-name="LuCI.ui.Textarea#registerEvents"><a href="LuCI.ui.Textarea.html#registerEvents">registerEvents</a></li>
1562
1563 <li data-name="LuCI.ui.Textarea#render"><a href="LuCI.ui.Textarea.html#render">render</a></li>
1564
1565 <li data-name="LuCI.ui.Textarea#setChangeEvents"><a href="LuCI.ui.Textarea.html#setChangeEvents">setChangeEvents</a></li>
1566
1567 <li data-name="LuCI.ui.Textarea#setUpdateEvents"><a href="LuCI.ui.Textarea.html#setUpdateEvents">setUpdateEvents</a></li>
1568
1569 <li data-name="LuCI.ui.Textarea#setValue"><a href="LuCI.ui.Textarea.html#setValue">setValue</a></li>
1570
1571 <li data-name="LuCI.ui.Textarea#triggerValidation"><a href="LuCI.ui.Textarea.html#triggerValidation">triggerValidation</a></li>
1572
1573 </ul>
1574 <ul class="events itemMembers">
1575
1576 </ul>
1577 </li>
1578
1579 <li class="item" data-name="LuCI.ui.Textfield">
1580 <span class="title">
1581 <a href="LuCI.ui.Textfield.html">LuCI.ui.Textfield</a>
1582
1583 </span>
1584 <ul class="members itemMembers">
1585
1586 </ul>
1587 <ul class="typedefs itemMembers">
1588
1589 <span class="subtitle">Typedefs</span>
1590
1591 <li data-name="LuCI.ui.Textfield.InitOptions"><a href="LuCI.ui.Textfield.html#.InitOptions">InitOptions</a></li>
1592
1593 </ul>
1594 <ul class="typedefs itemMembers">
1595
1596 </ul>
1597 <ul class="methods itemMembers">
1598
1599 <span class="subtitle">Methods</span>
1600
1601 <li data-name="LuCI.ui.Textfield#getValue"><a href="LuCI.ui.Textfield.html#getValue">getValue</a></li>
1602
1603 <li data-name="LuCI.ui.Textfield#isValid"><a href="LuCI.ui.Textfield.html#isValid">isValid</a></li>
1604
1605 <li data-name="LuCI.ui.Textfield#registerEvents"><a href="LuCI.ui.Textfield.html#registerEvents">registerEvents</a></li>
1606
1607 <li data-name="LuCI.ui.Textfield#render"><a href="LuCI.ui.Textfield.html#render">render</a></li>
1608
1609 <li data-name="LuCI.ui.Textfield#setChangeEvents"><a href="LuCI.ui.Textfield.html#setChangeEvents">setChangeEvents</a></li>
1610
1611 <li data-name="LuCI.ui.Textfield#setUpdateEvents"><a href="LuCI.ui.Textfield.html#setUpdateEvents">setUpdateEvents</a></li>
1612
1613 <li data-name="LuCI.ui.Textfield#setValue"><a href="LuCI.ui.Textfield.html#setValue">setValue</a></li>
1614
1615 <li data-name="LuCI.ui.Textfield#triggerValidation"><a href="LuCI.ui.Textfield.html#triggerValidation">triggerValidation</a></li>
1616
1617 </ul>
1618 <ul class="events itemMembers">
1619
1620 </ul>
1621 </li>
1622
1623 <li class="item" data-name="LuCI.view">
1624 <span class="title">
1625 <a href="LuCI.view.html">LuCI.view</a>
1626
1627 </span>
1628 <ul class="members itemMembers">
1629
1630 </ul>
1631 <ul class="typedefs itemMembers">
1632
1633 </ul>
1634 <ul class="typedefs itemMembers">
1635
1636 </ul>
1637 <ul class="methods itemMembers">
1638
1639 <span class="subtitle">Methods</span>
1640
1641 <li data-name="LuCI.view#addFooter"><a href="LuCI.view.html#addFooter">addFooter</a></li>
1642
1643 <li data-name="LuCI.view#handleReset"><a href="LuCI.view.html#handleReset">handleReset</a></li>
1644
1645 <li data-name="LuCI.view#handleSave"><a href="LuCI.view.html#handleSave">handleSave</a></li>
1646
1647 <li data-name="LuCI.view#handleSaveApply"><a href="LuCI.view.html#handleSaveApply">handleSaveApply</a></li>
1648
1649 <li data-name="LuCI.view#load"><a href="LuCI.view.html#load">load</a></li>
1650
1651 <li data-name="LuCI.view#render"><a href="LuCI.view.html#render">render</a></li>
1652
1653 </ul>
1654 <ul class="events itemMembers">
1655
1656 </ul>
1657 </li>
1658
1659 <li class="item" data-name="LuCI.xhr">
1660 <span class="title">
1661 <a href="LuCI.xhr.html">LuCI.xhr</a>
1662
1663 </span>
1664 <ul class="members itemMembers">
1665
1666 </ul>
1667 <ul class="typedefs itemMembers">
1668
1669 </ul>
1670 <ul class="typedefs itemMembers">
1671
1672 </ul>
1673 <ul class="methods itemMembers">
1674
1675 <span class="subtitle">Methods</span>
1676
1677 <li data-name="LuCI.xhr#abort"><a href="LuCI.xhr.html#abort">abort</a></li>
1678
1679 <li data-name="LuCI.xhr#busy"><a href="LuCI.xhr.html#busy">busy</a></li>
1680
1681 <li data-name="LuCI.xhr#cancel"><a href="LuCI.xhr.html#cancel">cancel</a></li>
1682
1683 <li data-name="LuCI.xhr#get"><a href="LuCI.xhr.html#get">get</a></li>
1684
1685 <li data-name="LuCI.xhr#post"><a href="LuCI.xhr.html#post">post</a></li>
1686
1687 <li data-name="LuCI.xhr#send_form"><a href="LuCI.xhr.html#send_form">send_form</a></li>
1688
1689 </ul>
1690 <ul class="events itemMembers">
1691
1692 </ul>
1693 </li>
1694
1695 </ul>
1696 </div>
1697 <div class="main">
1698 <h1 class="page-title" data-filename="ui.js.html">Source: ui.js</h1>
1699
1700
1701
1702
1703 <section>
1704 <article>
1705 <pre id="source-code" class="prettyprint source "><code>'use strict';
1706 'require validation';
1707 'require baseclass';
1708 'require request';
1709 'require poll';
1710 'require dom';
1711 'require rpc';
1712 'require uci';
1713 'require fs';
1714
1715 var modalDiv = null,
1716 tooltipDiv = null,
1717 indicatorDiv = null,
1718 tooltipTimeout = null;
1719
1720 /**
1721 * @class AbstractElement
1722 * @memberof LuCI.ui
1723 * @hideconstructor
1724 * @classdesc
1725 *
1726 * The `AbstractElement` class serves as abstract base for the different widgets
1727 * implemented by `LuCI.ui`. It provides the common logic for getting and
1728 * setting values, for checking the validity state and for wiring up required
1729 * events.
1730 *
1731 * UI widget instances are usually not supposed to be created by view code
1732 * directly, instead they're implicitely created by `LuCI.form` when
1733 * instantiating CBI forms.
1734 *
1735 * This class is automatically instantiated as part of `LuCI.ui`. To use it
1736 * in views, use `'require ui'` and refer to `ui.AbstractElement`. To import
1737 * it in external JavaScript, use `L.require("ui").then(...)` and access the
1738 * `AbstractElement` property of the class instance value.
1739 */
1740 var UIElement = baseclass.extend(/** @lends LuCI.ui.AbstractElement.prototype */ {
1741 /**
1742 * @typedef {Object} InitOptions
1743 * @memberof LuCI.ui.AbstractElement
1744 *
1745 * @property {string} [id]
1746 * Specifies the widget ID to use. It will be used as HTML `id` attribute
1747 * on the toplevel widget DOM node.
1748 *
1749 * @property {string} [name]
1750 * Specifies the widget name which is set as HTML `name` attribute on the
1751 * corresponding `&lt;input>` element.
1752 *
1753 * @property {boolean} [optional=true]
1754 * Specifies whether the input field allows empty values.
1755 *
1756 * @property {string} [datatype=string]
1757 * An expression describing the input data validation constraints.
1758 * It defaults to `string` which will allow any value.
1759 * See {@link LuCI.validation} for details on the expression format.
1760 *
1761 * @property {function} [validator]
1762 * Specifies a custom validator function which is invoked after the
1763 * standard validation constraints are checked. The function should return
1764 * `true` to accept the given input value. Any other return value type is
1765 * converted to a string and treated as validation error message.
1766 */
1767
1768 /**
1769 * Read the current value of the input widget.
1770 *
1771 * @instance
1772 * @memberof LuCI.ui.AbstractElement
1773 * @returns {string|string[]|null}
1774 * The current value of the input element. For simple inputs like text
1775 * fields or selects, the return value type will be a - possibly empty -
1776 * string. Complex widgets such as `DynamicList` instances may result in
1777 * an array of strings or `null` for unset values.
1778 */
1779 getValue: function() {
1780 if (dom.matches(this.node, 'select') || dom.matches(this.node, 'input'))
1781 return this.node.value;
1782
1783 return null;
1784 },
1785
1786 /**
1787 * Set the current value of the input widget.
1788 *
1789 * @instance
1790 * @memberof LuCI.ui.AbstractElement
1791 * @param {string|string[]|null} value
1792 * The value to set the input element to. For simple inputs like text
1793 * fields or selects, the value should be a - possibly empty - string.
1794 * Complex widgets such as `DynamicList` instances may accept string array
1795 * or `null` values.
1796 */
1797 setValue: function(value) {
1798 if (dom.matches(this.node, 'select') || dom.matches(this.node, 'input'))
1799 this.node.value = value;
1800 },
1801
1802 /**
1803 * Check whether the current input value is valid.
1804 *
1805 * @instance
1806 * @memberof LuCI.ui.AbstractElement
1807 * @returns {boolean}
1808 * Returns `true` if the current input value is valid or `false` if it does
1809 * not meet the validation constraints.
1810 */
1811 isValid: function() {
1812 return (this.validState !== false);
1813 },
1814
1815 /**
1816 * Force validation of the current input value.
1817 *
1818 * Usually input validation is automatically triggered by various DOM events
1819 * bound to the input widget. In some cases it is required though to manually
1820 * trigger validation runs, e.g. when programmatically altering values.
1821 *
1822 * @instance
1823 * @memberof LuCI.ui.AbstractElement
1824 */
1825 triggerValidation: function() {
1826 if (typeof(this.vfunc) != 'function')
1827 return false;
1828
1829 var wasValid = this.isValid();
1830
1831 this.vfunc();
1832
1833 return (wasValid != this.isValid());
1834 },
1835
1836 /**
1837 * Dispatch a custom (synthetic) event in response to received events.
1838 *
1839 * Sets up event handlers on the given target DOM node for the given event
1840 * names that dispatch a custom event of the given type to the widget root
1841 * DOM node.
1842 *
1843 * The primary purpose of this function is to set up a series of custom
1844 * uniform standard events such as `widget-update`, `validation-success`,
1845 * `validation-failure` etc. which are triggered by various different
1846 * widget specific native DOM events.
1847 *
1848 * @instance
1849 * @memberof LuCI.ui.AbstractElement
1850 * @param {Node} targetNode
1851 * Specifies the DOM node on which the native event listeners should be
1852 * registered.
1853 *
1854 * @param {string} synevent
1855 * The name of the custom event to dispatch to the widget root DOM node.
1856 *
1857 * @param {string[]} events
1858 * The native DOM events for which event handlers should be registered.
1859 */
1860 registerEvents: function(targetNode, synevent, events) {
1861 var dispatchFn = L.bind(function(ev) {
1862 this.node.dispatchEvent(new CustomEvent(synevent, { bubbles: true }));
1863 }, this);
1864
1865 for (var i = 0; i &lt; events.length; i++)
1866 targetNode.addEventListener(events[i], dispatchFn);
1867 },
1868
1869 /**
1870 * Setup listeners for native DOM events that may update the widget value.
1871 *
1872 * Sets up event handlers on the given target DOM node for the given event
1873 * names which may cause the input value to update, such as `keyup` or
1874 * `onclick` events. In contrast to change events, such update events will
1875 * trigger input value validation.
1876 *
1877 * @instance
1878 * @memberof LuCI.ui.AbstractElement
1879 * @param {Node} targetNode
1880 * Specifies the DOM node on which the event listeners should be registered.
1881 *
1882 * @param {...string} events
1883 * The DOM events for which event handlers should be registered.
1884 */
1885 setUpdateEvents: function(targetNode /*, ... */) {
1886 var datatype = this.options.datatype,
1887 optional = this.options.hasOwnProperty('optional') ? this.options.optional : true,
1888 validate = this.options.validate,
1889 events = this.varargs(arguments, 1);
1890
1891 this.registerEvents(targetNode, 'widget-update', events);
1892
1893 if (!datatype &amp;&amp; !validate)
1894 return;
1895
1896 this.vfunc = UI.prototype.addValidator.apply(UI.prototype, [
1897 targetNode, datatype || 'string',
1898 optional, validate
1899 ].concat(events));
1900
1901 this.node.addEventListener('validation-success', L.bind(function(ev) {
1902 this.validState = true;
1903 }, this));
1904
1905 this.node.addEventListener('validation-failure', L.bind(function(ev) {
1906 this.validState = false;
1907 }, this));
1908 },
1909
1910 /**
1911 * Setup listeners for native DOM events that may change the widget value.
1912 *
1913 * Sets up event handlers on the given target DOM node for the given event
1914 * names which may cause the input value to change completely, such as
1915 * `change` events in a select menu. In contrast to update events, such
1916 * change events will not trigger input value validation but they may cause
1917 * field dependencies to get re-evaluated and will mark the input widget
1918 * as dirty.
1919 *
1920 * @instance
1921 * @memberof LuCI.ui.AbstractElement
1922 * @param {Node} targetNode
1923 * Specifies the DOM node on which the event listeners should be registered.
1924 *
1925 * @param {...string} events
1926 * The DOM events for which event handlers should be registered.
1927 */
1928 setChangeEvents: function(targetNode /*, ... */) {
1929 var tag_changed = L.bind(function(ev) { this.setAttribute('data-changed', true) }, this.node);
1930
1931 for (var i = 1; i &lt; arguments.length; i++)
1932 targetNode.addEventListener(arguments[i], tag_changed);
1933
1934 this.registerEvents(targetNode, 'widget-change', this.varargs(arguments, 1));
1935 },
1936
1937 /**
1938 * Render the widget, setup event listeners and return resulting markup.
1939 *
1940 * @instance
1941 * @memberof LuCI.ui.AbstractElement
1942 *
1943 * @returns {Node}
1944 * Returns a DOM Node or DocumentFragment containing the rendered
1945 * widget markup.
1946 */
1947 render: function() {}
1948 });
1949
1950 /**
1951 * Instantiate a text input widget.
1952 *
1953 * @constructor Textfield
1954 * @memberof LuCI.ui
1955 * @augments LuCI.ui.AbstractElement
1956 *
1957 * @classdesc
1958 *
1959 * The `Textfield` class implements a standard single line text input field.
1960 *
1961 * UI widget instances are usually not supposed to be created by view code
1962 * directly, instead they're implicitely created by `LuCI.form` when
1963 * instantiating CBI forms.
1964 *
1965 * This class is automatically instantiated as part of `LuCI.ui`. To use it
1966 * in views, use `'require ui'` and refer to `ui.Textfield`. To import it in
1967 * external JavaScript, use `L.require("ui").then(...)` and access the
1968 * `Textfield` property of the class instance value.
1969 *
1970 * @param {string} [value=null]
1971 * The initial input value.
1972 *
1973 * @param {LuCI.ui.Textfield.InitOptions} [options]
1974 * Object describing the widget specific options to initialize the input.
1975 */
1976 var UITextfield = UIElement.extend(/** @lends LuCI.ui.Textfield.prototype */ {
1977 /**
1978 * In addition to the [AbstractElement.InitOptions]{@link LuCI.ui.AbstractElement.InitOptions}
1979 * the following properties are recognized:
1980 *
1981 * @typedef {LuCI.ui.AbstractElement.InitOptions} InitOptions
1982 * @memberof LuCI.ui.Textfield
1983 *
1984 * @property {boolean} [password=false]
1985 * Specifies whether the input should be rendered as concealed password field.
1986 *
1987 * @property {boolean} [readonly=false]
1988 * Specifies whether the input widget should be rendered readonly.
1989 *
1990 * @property {number} [maxlength]
1991 * Specifies the HTML `maxlength` attribute to set on the corresponding
1992 * `&lt;input>` element. Note that this a legacy property that exists for
1993 * compatibility reasons. It is usually better to `maxlength(N)` validation
1994 * expression.
1995 *
1996 * @property {string} [placeholder]
1997 * Specifies the HTML `placeholder` attribute which is displayed when the
1998 * corresponding `&lt;input>` element is empty.
1999 */
2000 __init__: function(value, options) {
2001 this.value = value;
2002 this.options = Object.assign({
2003 optional: true,
2004 password: false
2005 }, options);
2006 },
2007
2008 /** @override */
2009 render: function() {
2010 var frameEl = E('div', { 'id': this.options.id });
2011
2012 if (this.options.password) {
2013 frameEl.classList.add('nowrap');
2014 frameEl.appendChild(E('input', {
2015 'type': 'password',
2016 'style': 'position:absolute; left:-100000px',
2017 'aria-hidden': true,
2018 'tabindex': -1,
2019 'name': this.options.name ? 'password.%s'.format(this.options.name) : null
2020 }));
2021 }
2022
2023 frameEl.appendChild(E('input', {
2024 'id': this.options.id ? 'widget.' + this.options.id : null,
2025 'name': this.options.name,
2026 'type': this.options.password ? 'password' : 'text',
2027 'class': this.options.password ? 'cbi-input-password' : 'cbi-input-text',
2028 'readonly': this.options.readonly ? '' : null,
2029 'maxlength': this.options.maxlength,
2030 'placeholder': this.options.placeholder,
2031 'value': this.value,
2032 }));
2033
2034 if (this.options.password)
2035 frameEl.appendChild(E('button', {
2036 'class': 'cbi-button cbi-button-neutral',
2037 'title': _('Reveal/hide password'),
2038 'aria-label': _('Reveal/hide password'),
2039 'click': function(ev) {
2040 var e = this.previousElementSibling;
2041 e.type = (e.type === 'password') ? 'text' : 'password';
2042 ev.preventDefault();
2043 }
2044 }, '∗'));
2045
2046 return this.bind(frameEl);
2047 },
2048
2049 /** @private */
2050 bind: function(frameEl) {
2051 var inputEl = frameEl.childNodes[+!!this.options.password];
2052
2053 this.node = frameEl;
2054
2055 this.setUpdateEvents(inputEl, 'keyup', 'blur');
2056 this.setChangeEvents(inputEl, 'change');
2057
2058 dom.bindClassInstance(frameEl, this);
2059
2060 return frameEl;
2061 },
2062
2063 /** @override */
2064 getValue: function() {
2065 var inputEl = this.node.childNodes[+!!this.options.password];
2066 return inputEl.value;
2067 },
2068
2069 /** @override */
2070 setValue: function(value) {
2071 var inputEl = this.node.childNodes[+!!this.options.password];
2072 inputEl.value = value;
2073 }
2074 });
2075
2076 /**
2077 * Instantiate a textarea widget.
2078 *
2079 * @constructor Textarea
2080 * @memberof LuCI.ui
2081 * @augments LuCI.ui.AbstractElement
2082 *
2083 * @classdesc
2084 *
2085 * The `Textarea` class implements a multiline text area input field.
2086 *
2087 * UI widget instances are usually not supposed to be created by view code
2088 * directly, instead they're implicitely created by `LuCI.form` when
2089 * instantiating CBI forms.
2090 *
2091 * This class is automatically instantiated as part of `LuCI.ui`. To use it
2092 * in views, use `'require ui'` and refer to `ui.Textarea`. To import it in
2093 * external JavaScript, use `L.require("ui").then(...)` and access the
2094 * `Textarea` property of the class instance value.
2095 *
2096 * @param {string} [value=null]
2097 * The initial input value.
2098 *
2099 * @param {LuCI.ui.Textarea.InitOptions} [options]
2100 * Object describing the widget specific options to initialize the input.
2101 */
2102 var UITextarea = UIElement.extend(/** @lends LuCI.ui.Textarea.prototype */ {
2103 /**
2104 * In addition to the [AbstractElement.InitOptions]{@link LuCI.ui.AbstractElement.InitOptions}
2105 * the following properties are recognized:
2106 *
2107 * @typedef {LuCI.ui.AbstractElement.InitOptions} InitOptions
2108 * @memberof LuCI.ui.Textarea
2109 *
2110 * @property {boolean} [readonly=false]
2111 * Specifies whether the input widget should be rendered readonly.
2112 *
2113 * @property {string} [placeholder]
2114 * Specifies the HTML `placeholder` attribute which is displayed when the
2115 * corresponding `&lt;textarea>` element is empty.
2116 *
2117 * @property {boolean} [monospace=false]
2118 * Specifies whether a monospace font should be forced for the textarea
2119 * contents.
2120 *
2121 * @property {number} [cols]
2122 * Specifies the HTML `cols` attribute to set on the corresponding
2123 * `&lt;textarea>` element.
2124 *
2125 * @property {number} [rows]
2126 * Specifies the HTML `rows` attribute to set on the corresponding
2127 * `&lt;textarea>` element.
2128 *
2129 * @property {boolean} [wrap=false]
2130 * Specifies whether the HTML `wrap` attribute should be set.
2131 */
2132 __init__: function(value, options) {
2133 this.value = value;
2134 this.options = Object.assign({
2135 optional: true,
2136 wrap: false,
2137 cols: null,
2138 rows: null
2139 }, options);
2140 },
2141
2142 /** @override */
2143 render: function() {
2144 var frameEl = E('div', { 'id': this.options.id }),
2145 value = (this.value != null) ? String(this.value) : '';
2146
2147 frameEl.appendChild(E('textarea', {
2148 'id': this.options.id ? 'widget.' + this.options.id : null,
2149 'name': this.options.name,
2150 'class': 'cbi-input-textarea',
2151 'readonly': this.options.readonly ? '' : null,
2152 'placeholder': this.options.placeholder,
2153 'style': !this.options.cols ? 'width:100%' : null,
2154 'cols': this.options.cols,
2155 'rows': this.options.rows,
2156 'wrap': this.options.wrap ? '' : null
2157 }, [ value ]));
2158
2159 if (this.options.monospace)
2160 frameEl.firstElementChild.style.fontFamily = 'monospace';
2161
2162 return this.bind(frameEl);
2163 },
2164
2165 /** @private */
2166 bind: function(frameEl) {
2167 var inputEl = frameEl.firstElementChild;
2168
2169 this.node = frameEl;
2170
2171 this.setUpdateEvents(inputEl, 'keyup', 'blur');
2172 this.setChangeEvents(inputEl, 'change');
2173
2174 dom.bindClassInstance(frameEl, this);
2175
2176 return frameEl;
2177 },
2178
2179 /** @override */
2180 getValue: function() {
2181 return this.node.firstElementChild.value;
2182 },
2183
2184 /** @override */
2185 setValue: function(value) {
2186 this.node.firstElementChild.value = value;
2187 }
2188 });
2189
2190 /**
2191 * Instantiate a checkbox widget.
2192 *
2193 * @constructor Checkbox
2194 * @memberof LuCI.ui
2195 * @augments LuCI.ui.AbstractElement
2196 *
2197 * @classdesc
2198 *
2199 * The `Checkbox` class implements a simple checkbox input field.
2200 *
2201 * UI widget instances are usually not supposed to be created by view code
2202 * directly, instead they're implicitely created by `LuCI.form` when
2203 * instantiating CBI forms.
2204 *
2205 * This class is automatically instantiated as part of `LuCI.ui`. To use it
2206 * in views, use `'require ui'` and refer to `ui.Checkbox`. To import it in
2207 * external JavaScript, use `L.require("ui").then(...)` and access the
2208 * `Checkbox` property of the class instance value.
2209 *
2210 * @param {string} [value=null]
2211 * The initial input value.
2212 *
2213 * @param {LuCI.ui.Checkbox.InitOptions} [options]
2214 * Object describing the widget specific options to initialize the input.
2215 */
2216 var UICheckbox = UIElement.extend(/** @lends LuCI.ui.Checkbox.prototype */ {
2217 /**
2218 * In addition to the [AbstractElement.InitOptions]{@link LuCI.ui.AbstractElement.InitOptions}
2219 * the following properties are recognized:
2220 *
2221 * @typedef {LuCI.ui.AbstractElement.InitOptions} InitOptions
2222 * @memberof LuCI.ui.Checkbox
2223 *
2224 * @property {string} [value_enabled=1]
2225 * Specifies the value corresponding to a checked checkbox.
2226 *
2227 * @property {string} [value_disabled=0]
2228 * Specifies the value corresponding to an unchecked checkbox.
2229 *
2230 * @property {string} [hiddenname]
2231 * Specifies the HTML `name` attribute of the hidden input backing the
2232 * checkbox. This is a legacy property existing for compatibility reasons,
2233 * it is required for HTML based form submissions.
2234 */
2235 __init__: function(value, options) {
2236 this.value = value;
2237 this.options = Object.assign({
2238 value_enabled: '1',
2239 value_disabled: '0'
2240 }, options);
2241 },
2242
2243 /** @override */
2244 render: function() {
2245 var id = 'cb%08x'.format(Math.random() * 0xffffffff);
2246 var frameEl = E('div', {
2247 'id': this.options.id,
2248 'class': 'cbi-checkbox'
2249 });
2250
2251 if (this.options.hiddenname)
2252 frameEl.appendChild(E('input', {
2253 'type': 'hidden',
2254 'name': this.options.hiddenname,
2255 'value': 1
2256 }));
2257
2258 frameEl.appendChild(E('input', {
2259 'id': id,
2260 'name': this.options.name,
2261 'type': 'checkbox',
2262 'value': this.options.value_enabled,
2263 'checked': (this.value == this.options.value_enabled) ? '' : null,
2264 'data-widget-id': this.options.id ? 'widget.' + this.options.id : null
2265 }));
2266
2267 frameEl.appendChild(E('label', { 'for': id }));
2268
2269 return this.bind(frameEl);
2270 },
2271
2272 /** @private */
2273 bind: function(frameEl) {
2274 this.node = frameEl;
2275
2276 this.setUpdateEvents(frameEl.lastElementChild.previousElementSibling, 'click', 'blur');
2277 this.setChangeEvents(frameEl.lastElementChild.previousElementSibling, 'change');
2278
2279 dom.bindClassInstance(frameEl, this);
2280
2281 return frameEl;
2282 },
2283
2284 /**
2285 * Test whether the checkbox is currently checked.
2286 *
2287 * @instance
2288 * @memberof LuCI.ui.Checkbox
2289 * @returns {boolean}
2290 * Returns `true` when the checkbox is currently checked, otherwise `false`.
2291 */
2292 isChecked: function() {
2293 return this.node.lastElementChild.previousElementSibling.checked;
2294 },
2295
2296 /** @override */
2297 getValue: function() {
2298 return this.isChecked()
2299 ? this.options.value_enabled
2300 : this.options.value_disabled;
2301 },
2302
2303 /** @override */
2304 setValue: function(value) {
2305 this.node.lastElementChild.previousElementSibling.checked = (value == this.options.value_enabled);
2306 }
2307 });
2308
2309 /**
2310 * Instantiate a select dropdown or checkbox/radiobutton group.
2311 *
2312 * @constructor Select
2313 * @memberof LuCI.ui
2314 * @augments LuCI.ui.AbstractElement
2315 *
2316 * @classdesc
2317 *
2318 * The `Select` class implements either a traditional HTML `&lt;select>` element
2319 * or a group of checkboxes or radio buttons, depending on whether multiple
2320 * values are enabled or not.
2321 *
2322 * UI widget instances are usually not supposed to be created by view code
2323 * directly, instead they're implicitely created by `LuCI.form` when
2324 * instantiating CBI forms.
2325 *
2326 * This class is automatically instantiated as part of `LuCI.ui`. To use it
2327 * in views, use `'require ui'` and refer to `ui.Select`. To import it in
2328 * external JavaScript, use `L.require("ui").then(...)` and access the
2329 * `Select` property of the class instance value.
2330 *
2331 * @param {string|string[]} [value=null]
2332 * The initial input value(s).
2333 *
2334 * @param {Object&lt;string, string>} choices
2335 * Object containing the selectable choices of the widget. The object keys
2336 * serve as values for the different choices while the values are used as
2337 * choice labels.
2338 *
2339 * @param {LuCI.ui.Select.InitOptions} [options]
2340 * Object describing the widget specific options to initialize the inputs.
2341 */
2342 var UISelect = UIElement.extend(/** @lends LuCI.ui.Select.prototype */ {
2343 /**
2344 * In addition to the [AbstractElement.InitOptions]{@link LuCI.ui.AbstractElement.InitOptions}
2345 * the following properties are recognized:
2346 *
2347 * @typedef {LuCI.ui.AbstractElement.InitOptions} InitOptions
2348 * @memberof LuCI.ui.Select
2349 *
2350 * @property {boolean} [multiple=false]
2351 * Specifies whether multiple choice values may be selected.
2352 *
2353 * @property {string} [widget=select]
2354 * Specifies the kind of widget to render. May be either `select` or
2355 * `individual`. When set to `select` an HTML `&lt;select>` element will be
2356 * used, otherwise a group of checkbox or radio button elements is created,
2357 * depending on the value of the `multiple` option.
2358 *
2359 * @property {string} [orientation=horizontal]
2360 * Specifies whether checkbox / radio button groups should be rendered
2361 * in a `horizontal` or `vertical` manner. Does not apply to the `select`
2362 * widget type.
2363 *
2364 * @property {boolean|string[]} [sort=false]
2365 * Specifies if and how to sort choice values. If set to `true`, the choice
2366 * values will be sorted alphabetically. If set to an array of strings, the
2367 * choice sort order is derived from the array.
2368 *
2369 * @property {number} [size]
2370 * Specifies the HTML `size` attribute to set on the `&lt;select>` element.
2371 * Only applicable to the `select` widget type.
2372 *
2373 * @property {string} [placeholder=-- Please choose --]
2374 * Specifies a placeholder text which is displayed when no choice is
2375 * selected yet. Only applicable to the `select` widget type.
2376 */
2377 __init__: function(value, choices, options) {
2378 if (!L.isObject(choices))
2379 choices = {};
2380
2381 if (!Array.isArray(value))
2382 value = (value != null &amp;&amp; value != '') ? [ value ] : [];
2383
2384 if (!options.multiple &amp;&amp; value.length > 1)
2385 value.length = 1;
2386
2387 this.values = value;
2388 this.choices = choices;
2389 this.options = Object.assign({
2390 multiple: false,
2391 widget: 'select',
2392 orientation: 'horizontal'
2393 }, options);
2394
2395 if (this.choices.hasOwnProperty(''))
2396 this.options.optional = true;
2397 },
2398
2399 /** @override */
2400 render: function() {
2401 var frameEl = E('div', { 'id': this.options.id }),
2402 keys = Object.keys(this.choices);
2403
2404 if (this.options.sort === true)
2405 keys.sort();
2406 else if (Array.isArray(this.options.sort))
2407 keys = this.options.sort;
2408
2409 if (this.options.widget == 'select') {
2410 frameEl.appendChild(E('select', {
2411 'id': this.options.id ? 'widget.' + this.options.id : null,
2412 'name': this.options.name,
2413 'size': this.options.size,
2414 'class': 'cbi-input-select',
2415 'multiple': this.options.multiple ? '' : null
2416 }));
2417
2418 if (this.options.optional)
2419 frameEl.lastChild.appendChild(E('option', {
2420 'value': '',
2421 'selected': (this.values.length == 0 || this.values[0] == '') ? '' : null
2422 }, [ this.choices[''] || this.options.placeholder || _('-- Please choose --') ]));
2423
2424 for (var i = 0; i &lt; keys.length; i++) {
2425 if (keys[i] == null || keys[i] == '')
2426 continue;
2427
2428 frameEl.lastChild.appendChild(E('option', {
2429 'value': keys[i],
2430 'selected': (this.values.indexOf(keys[i]) > -1) ? '' : null
2431 }, [ this.choices[keys[i]] || keys[i] ]));
2432 }
2433 }
2434 else {
2435 var brEl = (this.options.orientation === 'horizontal') ? document.createTextNode(' ') : E('br');
2436
2437 for (var i = 0; i &lt; keys.length; i++) {
2438 frameEl.appendChild(E('label', {}, [
2439 E('input', {
2440 'id': this.options.id ? 'widget.' + this.options.id : null,
2441 'name': this.options.id || this.options.name,
2442 'type': this.options.multiple ? 'checkbox' : 'radio',
2443 'class': this.options.multiple ? 'cbi-input-checkbox' : 'cbi-input-radio',
2444 'value': keys[i],
2445 'checked': (this.values.indexOf(keys[i]) > -1) ? '' : null
2446 }),
2447 this.choices[keys[i]] || keys[i]
2448 ]));
2449
2450 if (i + 1 == this.options.size)
2451 frameEl.appendChild(brEl);
2452 }
2453 }
2454
2455 return this.bind(frameEl);
2456 },
2457
2458 /** @private */
2459 bind: function(frameEl) {
2460 this.node = frameEl;
2461
2462 if (this.options.widget == 'select') {
2463 this.setUpdateEvents(frameEl.firstChild, 'change', 'click', 'blur');
2464 this.setChangeEvents(frameEl.firstChild, 'change');
2465 }
2466 else {
2467 var radioEls = frameEl.querySelectorAll('input[type="radio"]');
2468 for (var i = 0; i &lt; radioEls.length; i++) {
2469 this.setUpdateEvents(radioEls[i], 'change', 'click', 'blur');
2470 this.setChangeEvents(radioEls[i], 'change', 'click', 'blur');
2471 }
2472 }
2473
2474 dom.bindClassInstance(frameEl, this);
2475
2476 return frameEl;
2477 },
2478
2479 /** @override */
2480 getValue: function() {
2481 if (this.options.widget == 'select')
2482 return this.node.firstChild.value;
2483
2484 var radioEls = frameEl.querySelectorAll('input[type="radio"]');
2485 for (var i = 0; i &lt; radioEls.length; i++)
2486 if (radioEls[i].checked)
2487 return radioEls[i].value;
2488
2489 return null;
2490 },
2491
2492 /** @override */
2493 setValue: function(value) {
2494 if (this.options.widget == 'select') {
2495 if (value == null)
2496 value = '';
2497
2498 for (var i = 0; i &lt; this.node.firstChild.options.length; i++)
2499 this.node.firstChild.options[i].selected = (this.node.firstChild.options[i].value == value);
2500
2501 return;
2502 }
2503
2504 var radioEls = frameEl.querySelectorAll('input[type="radio"]');
2505 for (var i = 0; i &lt; radioEls.length; i++)
2506 radioEls[i].checked = (radioEls[i].value == value);
2507 }
2508 });
2509
2510 /**
2511 * Instantiate a rich dropdown choice widget.
2512 *
2513 * @constructor Dropdown
2514 * @memberof LuCI.ui
2515 * @augments LuCI.ui.AbstractElement
2516 *
2517 * @classdesc
2518 *
2519 * The `Dropdown` class implements a rich, stylable dropdown menu which
2520 * supports non-text choice labels.
2521 *
2522 * UI widget instances are usually not supposed to be created by view code
2523 * directly, instead they're implicitely created by `LuCI.form` when
2524 * instantiating CBI forms.
2525 *
2526 * This class is automatically instantiated as part of `LuCI.ui`. To use it
2527 * in views, use `'require ui'` and refer to `ui.Dropdown`. To import it in
2528 * external JavaScript, use `L.require("ui").then(...)` and access the
2529 * `Dropdown` property of the class instance value.
2530 *
2531 * @param {string|string[]} [value=null]
2532 * The initial input value(s).
2533 *
2534 * @param {Object&lt;string, *>} choices
2535 * Object containing the selectable choices of the widget. The object keys
2536 * serve as values for the different choices while the values are used as
2537 * choice labels.
2538 *
2539 * @param {LuCI.ui.Dropdown.InitOptions} [options]
2540 * Object describing the widget specific options to initialize the dropdown.
2541 */
2542 var UIDropdown = UIElement.extend(/** @lends LuCI.ui.Dropdown.prototype */ {
2543 /**
2544 * In addition to the [AbstractElement.InitOptions]{@link LuCI.ui.AbstractElement.InitOptions}
2545 * the following properties are recognized:
2546 *
2547 * @typedef {LuCI.ui.AbstractElement.InitOptions} InitOptions
2548 * @memberof LuCI.ui.Dropdown
2549 *
2550 * @property {boolean} [optional=true]
2551 * Specifies whether the dropdown selection is optional. In contrast to
2552 * other widgets, the `optional` constraint of dropdowns works differently;
2553 * instead of marking the widget invalid on empty values when set to `false`,
2554 * the user is not allowed to deselect all choices.
2555 *
2556 * For single value dropdowns that means that no empty "please select"
2557 * choice is offered and for multi value dropdowns, the last selected choice
2558 * may not be deselected without selecting another choice first.
2559 *
2560 * @property {boolean} [multiple]
2561 * Specifies whether multiple choice values may be selected. It defaults
2562 * to `true` when an array is passed as input value to the constructor.
2563 *
2564 * @property {boolean|string[]} [sort=false]
2565 * Specifies if and how to sort choice values. If set to `true`, the choice
2566 * values will be sorted alphabetically. If set to an array of strings, the
2567 * choice sort order is derived from the array.
2568 *
2569 * @property {string} [select_placeholder=-- Please choose --]
2570 * Specifies a placeholder text which is displayed when no choice is
2571 * selected yet.
2572 *
2573 * @property {string} [custom_placeholder=-- custom --]
2574 * Specifies a placeholder text which is displayed in the text input
2575 * field allowing to enter custom choice values. Only applicable if the
2576 * `create` option is set to `true`.
2577 *
2578 * @property {boolean} [create=false]
2579 * Specifies whether custom choices may be entered into the dropdown
2580 * widget.
2581 *
2582 * @property {string} [create_query=.create-item-input]
2583 * Specifies a CSS selector expression used to find the input element
2584 * which is used to enter custom choice values. This should not normally
2585 * be used except by widgets derived from the Dropdown class.
2586 *
2587 * @property {string} [create_template=script[type="item-template"]]
2588 * Specifies a CSS selector expression used to find an HTML element
2589 * serving as template for newly added custom choice values.
2590 *
2591 * Any `{{value}}` placeholder string within the template elements text
2592 * content will be replaced by the user supplied choice value, the
2593 * resulting string is parsed as HTML and appended to the end of the
2594 * choice list. The template markup may specify one HTML element with a
2595 * `data-label-placeholder` attribute which is replaced by a matching
2596 * label value from the `choices` object or with the user supplied value
2597 * itself in case `choices` contains no matching choice label.
2598 *
2599 * If the template element is not found or if no `create_template` selector
2600 * expression is specified, the default markup for newly created elements is
2601 * `&lt;li data-value="{{value}}">&lt;span data-label-placeholder="true" />&lt;/li>`.
2602 *
2603 * @property {string} [create_markup]
2604 * This property allows specifying the markup for custom choices directly
2605 * instead of referring to a template element through CSS selectors.
2606 *
2607 * Apart from that it works exactly like `create_template`.
2608 *
2609 * @property {number} [display_items=3]
2610 * Specifies the maximum amount of choice labels that should be shown in
2611 * collapsed dropdown state before further selected choices are cut off.
2612 *
2613 * Only applicable when `multiple` is `true`.
2614 *
2615 * @property {number} [dropdown_items=-1]
2616 * Specifies the maximum amount of choices that should be shown when the
2617 * dropdown is open. If the amount of available choices exceeds this number,
2618 * the dropdown area must be scrolled to reach further items.
2619 *
2620 * If set to `-1`, the dropdown menu will attempt to show all choice values
2621 * and only resort to scrolling if the amount of choices exceeds the available
2622 * screen space above and below the dropdown widget.
2623 *
2624 * @property {string} [placeholder]
2625 * This property serves as a shortcut to set both `select_placeholder` and
2626 * `custom_placeholder`. Either of these properties will fallback to
2627 * `placeholder` if not specified.
2628 *
2629 * @property {boolean} [readonly=false]
2630 * Specifies whether the custom choice input field should be rendered
2631 * readonly. Only applicable when `create` is `true`.
2632 *
2633 * @property {number} [maxlength]
2634 * Specifies the HTML `maxlength` attribute to set on the custom choice
2635 * `&lt;input>` element. Note that this a legacy property that exists for
2636 * compatibility reasons. It is usually better to `maxlength(N)` validation
2637 * expression. Only applicable when `create` is `true`.
2638 */
2639 __init__: function(value, choices, options) {
2640 if (typeof(choices) != 'object')
2641 choices = {};
2642
2643 if (!Array.isArray(value))
2644 this.values = (value != null &amp;&amp; value != '') ? [ value ] : [];
2645 else
2646 this.values = value;
2647
2648 this.choices = choices;
2649 this.options = Object.assign({
2650 sort: true,
2651 multiple: Array.isArray(value),
2652 optional: true,
2653 select_placeholder: _('-- Please choose --'),
2654 custom_placeholder: _('-- custom --'),
2655 display_items: 3,
2656 dropdown_items: -1,
2657 create: false,
2658 create_query: '.create-item-input',
2659 create_template: 'script[type="item-template"]'
2660 }, options);
2661 },
2662
2663 /** @override */
2664 render: function() {
2665 var sb = E('div', {
2666 'id': this.options.id,
2667 'class': 'cbi-dropdown',
2668 'multiple': this.options.multiple ? '' : null,
2669 'optional': this.options.optional ? '' : null,
2670 }, E('ul'));
2671
2672 var keys = Object.keys(this.choices);
2673
2674 if (this.options.sort === true)
2675 keys.sort();
2676 else if (Array.isArray(this.options.sort))
2677 keys = this.options.sort;
2678
2679 if (this.options.create)
2680 for (var i = 0; i &lt; this.values.length; i++)
2681 if (!this.choices.hasOwnProperty(this.values[i]))
2682 keys.push(this.values[i]);
2683
2684 for (var i = 0; i &lt; keys.length; i++) {
2685 var label = this.choices[keys[i]];
2686
2687 if (dom.elem(label))
2688 label = label.cloneNode(true);
2689
2690 sb.lastElementChild.appendChild(E('li', {
2691 'data-value': keys[i],
2692 'selected': (this.values.indexOf(keys[i]) > -1) ? '' : null
2693 }, [ label || keys[i] ]));
2694 }
2695
2696 if (this.options.create) {
2697 var createEl = E('input', {
2698 'type': 'text',
2699 'class': 'create-item-input',
2700 'readonly': this.options.readonly ? '' : null,
2701 'maxlength': this.options.maxlength,
2702 'placeholder': this.options.custom_placeholder || this.options.placeholder
2703 });
2704
2705 if (this.options.datatype || this.options.validate)
2706 UI.prototype.addValidator(createEl, this.options.datatype || 'string',
2707 true, this.options.validate, 'blur', 'keyup');
2708
2709 sb.lastElementChild.appendChild(E('li', { 'data-value': '-' }, createEl));
2710 }
2711
2712 if (this.options.create_markup)
2713 sb.appendChild(E('script', { type: 'item-template' },
2714 this.options.create_markup));
2715
2716 return this.bind(sb);
2717 },
2718
2719 /** @private */
2720 bind: function(sb) {
2721 var o = this.options;
2722
2723 o.multiple = sb.hasAttribute('multiple');
2724 o.optional = sb.hasAttribute('optional');
2725 o.placeholder = sb.getAttribute('placeholder') || o.placeholder;
2726 o.display_items = parseInt(sb.getAttribute('display-items') || o.display_items);
2727 o.dropdown_items = parseInt(sb.getAttribute('dropdown-items') || o.dropdown_items);
2728 o.create_query = sb.getAttribute('item-create') || o.create_query;
2729 o.create_template = sb.getAttribute('item-template') || o.create_template;
2730
2731 var ul = sb.querySelector('ul'),
2732 more = sb.appendChild(E('span', { class: 'more', tabindex: -1 }, '···')),
2733 open = sb.appendChild(E('span', { class: 'open', tabindex: -1 }, '▾')),
2734 canary = sb.appendChild(E('div')),
2735 create = sb.querySelector(this.options.create_query),
2736 ndisplay = this.options.display_items,
2737 n = 0;
2738
2739 if (this.options.multiple) {
2740 var items = ul.querySelectorAll('li');
2741
2742 for (var i = 0; i &lt; items.length; i++) {
2743 this.transformItem(sb, items[i]);
2744
2745 if (items[i].hasAttribute('selected') &amp;&amp; ndisplay-- > 0)
2746 items[i].setAttribute('display', n++);
2747 }
2748 }
2749 else {
2750 if (this.options.optional &amp;&amp; !ul.querySelector('li[data-value=""]')) {
2751 var placeholder = E('li', { placeholder: '' },
2752 this.options.select_placeholder || this.options.placeholder);
2753
2754 ul.firstChild
2755 ? ul.insertBefore(placeholder, ul.firstChild)
2756 : ul.appendChild(placeholder);
2757 }
2758
2759 var items = ul.querySelectorAll('li'),
2760 sel = sb.querySelectorAll('[selected]');
2761
2762 sel.forEach(function(s) {
2763 s.removeAttribute('selected');
2764 });
2765
2766 var s = sel[0] || items[0];
2767 if (s) {
2768 s.setAttribute('selected', '');
2769 s.setAttribute('display', n++);
2770 }
2771
2772 ndisplay--;
2773 }
2774
2775 this.saveValues(sb, ul);
2776
2777 ul.setAttribute('tabindex', -1);
2778 sb.setAttribute('tabindex', 0);
2779
2780 if (ndisplay &lt; 0)
2781 sb.setAttribute('more', '')
2782 else
2783 sb.removeAttribute('more');
2784
2785 if (ndisplay == this.options.display_items)
2786 sb.setAttribute('empty', '')
2787 else
2788 sb.removeAttribute('empty');
2789
2790 dom.content(more, (ndisplay == this.options.display_items)
2791 ? (this.options.select_placeholder || this.options.placeholder) : '···');
2792
2793
2794 sb.addEventListener('click', this.handleClick.bind(this));
2795 sb.addEventListener('keydown', this.handleKeydown.bind(this));
2796 sb.addEventListener('cbi-dropdown-close', this.handleDropdownClose.bind(this));
2797 sb.addEventListener('cbi-dropdown-select', this.handleDropdownSelect.bind(this));
2798
2799 if ('ontouchstart' in window) {
2800 sb.addEventListener('touchstart', function(ev) { ev.stopPropagation(); });
2801 window.addEventListener('touchstart', this.closeAllDropdowns);
2802 }
2803 else {
2804 sb.addEventListener('mouseover', this.handleMouseover.bind(this));
2805 sb.addEventListener('focus', this.handleFocus.bind(this));
2806
2807 canary.addEventListener('focus', this.handleCanaryFocus.bind(this));
2808
2809 window.addEventListener('mouseover', this.setFocus);
2810 window.addEventListener('click', this.closeAllDropdowns);
2811 }
2812
2813 if (create) {
2814 create.addEventListener('keydown', this.handleCreateKeydown.bind(this));
2815 create.addEventListener('focus', this.handleCreateFocus.bind(this));
2816 create.addEventListener('blur', this.handleCreateBlur.bind(this));
2817
2818 var li = findParent(create, 'li');
2819
2820 li.setAttribute('unselectable', '');
2821 li.addEventListener('click', this.handleCreateClick.bind(this));
2822 }
2823
2824 this.node = sb;
2825
2826 this.setUpdateEvents(sb, 'cbi-dropdown-open', 'cbi-dropdown-close');
2827 this.setChangeEvents(sb, 'cbi-dropdown-change', 'cbi-dropdown-close');
2828
2829 dom.bindClassInstance(sb, this);
2830
2831 return sb;
2832 },
2833
2834 /** @private */
2835 openDropdown: function(sb) {
2836 var st = window.getComputedStyle(sb, null),
2837 ul = sb.querySelector('ul'),
2838 li = ul.querySelectorAll('li'),
2839 fl = findParent(sb, '.cbi-value-field'),
2840 sel = ul.querySelector('[selected]'),
2841 rect = sb.getBoundingClientRect(),
2842 items = Math.min(this.options.dropdown_items, li.length);
2843
2844 document.querySelectorAll('.cbi-dropdown[open]').forEach(function(s) {
2845 s.dispatchEvent(new CustomEvent('cbi-dropdown-close', {}));
2846 });
2847
2848 sb.setAttribute('open', '');
2849
2850 var pv = ul.cloneNode(true);
2851 pv.classList.add('preview');
2852
2853 if (fl)
2854 fl.classList.add('cbi-dropdown-open');
2855
2856 if ('ontouchstart' in window) {
2857 var vpWidth = Math.max(document.documentElement.clientWidth, window.innerWidth || 0),
2858 vpHeight = Math.max(document.documentElement.clientHeight, window.innerHeight || 0),
2859 start = null;
2860
2861 ul.style.top = sb.offsetHeight + 'px';
2862 ul.style.left = -rect.left + 'px';
2863 ul.style.right = (rect.right - vpWidth) + 'px';
2864 ul.style.maxHeight = (vpHeight * 0.5) + 'px';
2865 ul.style.WebkitOverflowScrolling = 'touch';
2866
2867 function getScrollParent(element) {
2868 var parent = element,
2869 style = getComputedStyle(element),
2870 excludeStaticParent = (style.position === 'absolute');
2871
2872 if (style.position === 'fixed')
2873 return document.body;
2874
2875 while ((parent = parent.parentElement) != null) {
2876 style = getComputedStyle(parent);
2877
2878 if (excludeStaticParent &amp;&amp; style.position === 'static')
2879 continue;
2880
2881 if (/(auto|scroll)/.test(style.overflow + style.overflowY + style.overflowX))
2882 return parent;
2883 }
2884
2885 return document.body;
2886 }
2887
2888 var scrollParent = getScrollParent(sb),
2889 scrollFrom = scrollParent.scrollTop,
2890 scrollTo = scrollFrom + rect.top - vpHeight * 0.5;
2891
2892 var scrollStep = function(timestamp) {
2893 if (!start) {
2894 start = timestamp;
2895 ul.scrollTop = sel ? Math.max(sel.offsetTop - sel.offsetHeight, 0) : 0;
2896 }
2897
2898 var duration = Math.max(timestamp - start, 1);
2899 if (duration &lt; 100) {
2900 scrollParent.scrollTop = scrollFrom + (scrollTo - scrollFrom) * (duration / 100);
2901 window.requestAnimationFrame(scrollStep);
2902 }
2903 else {
2904 scrollParent.scrollTop = scrollTo;
2905 }
2906 };
2907
2908 window.requestAnimationFrame(scrollStep);
2909 }
2910 else {
2911 ul.style.maxHeight = '1px';
2912 ul.style.top = ul.style.bottom = '';
2913
2914 window.requestAnimationFrame(function() {
2915 var itemHeight = li[Math.max(0, li.length - 2)].getBoundingClientRect().height,
2916 fullHeight = 0,
2917 spaceAbove = rect.top,
2918 spaceBelow = window.innerHeight - rect.height - rect.top;
2919
2920 for (var i = 0; i &lt; (items == -1 ? li.length : items); i++)
2921 fullHeight += li[i].getBoundingClientRect().height;
2922
2923 if (fullHeight &lt;= spaceBelow) {
2924 ul.style.top = rect.height + 'px';
2925 ul.style.maxHeight = spaceBelow + 'px';
2926 }
2927 else if (fullHeight &lt;= spaceAbove) {
2928 ul.style.bottom = rect.height + 'px';
2929 ul.style.maxHeight = spaceAbove + 'px';
2930 }
2931 else if (spaceBelow >= spaceAbove) {
2932 ul.style.top = rect.height + 'px';
2933 ul.style.maxHeight = (spaceBelow - (spaceBelow % itemHeight)) + 'px';
2934 }
2935 else {
2936 ul.style.bottom = rect.height + 'px';
2937 ul.style.maxHeight = (spaceAbove - (spaceAbove % itemHeight)) + 'px';
2938 }
2939
2940 ul.scrollTop = sel ? Math.max(sel.offsetTop - sel.offsetHeight, 0) : 0;
2941 });
2942 }
2943
2944 var cboxes = ul.querySelectorAll('[selected] input[type="checkbox"]');
2945 for (var i = 0; i &lt; cboxes.length; i++) {
2946 cboxes[i].checked = true;
2947 cboxes[i].disabled = (cboxes.length == 1 &amp;&amp; !this.options.optional);
2948 };
2949
2950 ul.classList.add('dropdown');
2951
2952 sb.insertBefore(pv, ul.nextElementSibling);
2953
2954 li.forEach(function(l) {
2955 l.setAttribute('tabindex', 0);
2956 });
2957
2958 sb.lastElementChild.setAttribute('tabindex', 0);
2959
2960 this.setFocus(sb, sel || li[0], true);
2961 },
2962
2963 /** @private */
2964 closeDropdown: function(sb, no_focus) {
2965 if (!sb.hasAttribute('open'))
2966 return;
2967
2968 var pv = sb.querySelector('ul.preview'),
2969 ul = sb.querySelector('ul.dropdown'),
2970 li = ul.querySelectorAll('li'),
2971 fl = findParent(sb, '.cbi-value-field');
2972
2973 li.forEach(function(l) { l.removeAttribute('tabindex'); });
2974 sb.lastElementChild.removeAttribute('tabindex');
2975
2976 sb.removeChild(pv);
2977 sb.removeAttribute('open');
2978 sb.style.width = sb.style.height = '';
2979
2980 ul.classList.remove('dropdown');
2981 ul.style.top = ul.style.bottom = ul.style.maxHeight = '';
2982
2983 if (fl)
2984 fl.classList.remove('cbi-dropdown-open');
2985
2986 if (!no_focus)
2987 this.setFocus(sb, sb);
2988
2989 this.saveValues(sb, ul);
2990 },
2991
2992 /** @private */
2993 toggleItem: function(sb, li, force_state) {
2994 if (li.hasAttribute('unselectable'))
2995 return;
2996
2997 if (this.options.multiple) {
2998 var cbox = li.querySelector('input[type="checkbox"]'),
2999 items = li.parentNode.querySelectorAll('li'),
3000 label = sb.querySelector('ul.preview'),
3001 sel = li.parentNode.querySelectorAll('[selected]').length,
3002 more = sb.querySelector('.more'),
3003 ndisplay = this.options.display_items,
3004 n = 0;
3005
3006 if (li.hasAttribute('selected')) {
3007 if (force_state !== true) {
3008 if (sel > 1 || this.options.optional) {
3009 li.removeAttribute('selected');
3010 cbox.checked = cbox.disabled = false;
3011 sel--;
3012 }
3013 else {
3014 cbox.disabled = true;
3015 }
3016 }
3017 }
3018 else {
3019 if (force_state !== false) {
3020 li.setAttribute('selected', '');
3021 cbox.checked = true;
3022 cbox.disabled = false;
3023 sel++;
3024 }
3025 }
3026
3027 while (label &amp;&amp; label.firstElementChild)
3028 label.removeChild(label.firstElementChild);
3029
3030 for (var i = 0; i &lt; items.length; i++) {
3031 items[i].removeAttribute('display');
3032 if (items[i].hasAttribute('selected')) {
3033 if (ndisplay-- > 0) {
3034 items[i].setAttribute('display', n++);
3035 if (label)
3036 label.appendChild(items[i].cloneNode(true));
3037 }
3038 var c = items[i].querySelector('input[type="checkbox"]');
3039 if (c)
3040 c.disabled = (sel == 1 &amp;&amp; !this.options.optional);
3041 }
3042 }
3043
3044 if (ndisplay &lt; 0)
3045 sb.setAttribute('more', '');
3046 else
3047 sb.removeAttribute('more');
3048
3049 if (ndisplay === this.options.display_items)
3050 sb.setAttribute('empty', '');
3051 else
3052 sb.removeAttribute('empty');
3053
3054 dom.content(more, (ndisplay === this.options.display_items)
3055 ? (this.options.select_placeholder || this.options.placeholder) : '···');
3056 }
3057 else {
3058 var sel = li.parentNode.querySelector('[selected]');
3059 if (sel) {
3060 sel.removeAttribute('display');
3061 sel.removeAttribute('selected');
3062 }
3063
3064 li.setAttribute('display', 0);
3065 li.setAttribute('selected', '');
3066
3067 this.closeDropdown(sb, true);
3068 }
3069
3070 this.saveValues(sb, li.parentNode);
3071 },
3072
3073 /** @private */
3074 transformItem: function(sb, li) {
3075 var cbox = E('form', {}, E('input', { type: 'checkbox', tabindex: -1, onclick: 'event.preventDefault()' })),
3076 label = E('label');
3077
3078 while (li.firstChild)
3079 label.appendChild(li.firstChild);
3080
3081 li.appendChild(cbox);
3082 li.appendChild(label);
3083 },
3084
3085 /** @private */
3086 saveValues: function(sb, ul) {
3087 var sel = ul.querySelectorAll('li[selected]'),
3088 div = sb.lastElementChild,
3089 name = this.options.name,
3090 strval = '',
3091 values = [];
3092
3093 while (div.lastElementChild)
3094 div.removeChild(div.lastElementChild);
3095
3096 sel.forEach(function (s) {
3097 if (s.hasAttribute('placeholder'))
3098 return;
3099
3100 var v = {
3101 text: s.innerText,
3102 value: s.hasAttribute('data-value') ? s.getAttribute('data-value') : s.innerText,
3103 element: s
3104 };
3105
3106 div.appendChild(E('input', {
3107 type: 'hidden',
3108 name: name,
3109 value: v.value
3110 }));
3111
3112 values.push(v);
3113
3114 strval += strval.length ? ' ' + v.value : v.value;
3115 });
3116
3117 var detail = {
3118 instance: this,
3119 element: sb
3120 };
3121
3122 if (this.options.multiple)
3123 detail.values = values;
3124 else
3125 detail.value = values.length ? values[0] : null;
3126
3127 sb.value = strval;
3128
3129 sb.dispatchEvent(new CustomEvent('cbi-dropdown-change', {
3130 bubbles: true,
3131 detail: detail
3132 }));
3133 },
3134
3135 /** @private */
3136 setValues: function(sb, values) {
3137 var ul = sb.querySelector('ul');
3138
3139 if (this.options.create) {
3140 for (var value in values) {
3141 this.createItems(sb, value);
3142
3143 if (!this.options.multiple)
3144 break;
3145 }
3146 }
3147
3148 if (this.options.multiple) {
3149 var lis = ul.querySelectorAll('li[data-value]');
3150 for (var i = 0; i &lt; lis.length; i++) {
3151 var value = lis[i].getAttribute('data-value');
3152 if (values === null || !(value in values))
3153 this.toggleItem(sb, lis[i], false);
3154 else
3155 this.toggleItem(sb, lis[i], true);
3156 }
3157 }
3158 else {
3159 var ph = ul.querySelector('li[placeholder]');
3160 if (ph)
3161 this.toggleItem(sb, ph);
3162
3163 var lis = ul.querySelectorAll('li[data-value]');
3164 for (var i = 0; i &lt; lis.length; i++) {
3165 var value = lis[i].getAttribute('data-value');
3166 if (values !== null &amp;&amp; (value in values))
3167 this.toggleItem(sb, lis[i]);
3168 }
3169 }
3170 },
3171
3172 /** @private */
3173 setFocus: function(sb, elem, scroll) {
3174 if (sb &amp;&amp; sb.hasAttribute &amp;&amp; sb.hasAttribute('locked-in'))
3175 return;
3176
3177 if (sb.target &amp;&amp; findParent(sb.target, 'ul.dropdown'))
3178 return;
3179
3180 document.querySelectorAll('.focus').forEach(function(e) {
3181 if (!matchesElem(e, 'input')) {
3182 e.classList.remove('focus');
3183 e.blur();
3184 }
3185 });
3186
3187 if (elem) {
3188 elem.focus();
3189 elem.classList.add('focus');
3190
3191 if (scroll)
3192 elem.parentNode.scrollTop = elem.offsetTop - elem.parentNode.offsetTop;
3193 }
3194 },
3195
3196 /** @private */
3197 createChoiceElement: function(sb, value, label) {
3198 var tpl = sb.querySelector(this.options.create_template),
3199 markup = null;
3200
3201 if (tpl)
3202 markup = (tpl.textContent || tpl.innerHTML || tpl.firstChild.data).replace(/^&lt;!--|-->$/, '').trim();
3203 else
3204 markup = '&lt;li data-value="{{value}}">&lt;span data-label-placeholder="true" />&lt;/li>';
3205
3206 var new_item = E(markup.replace(/{{value}}/g, '%h'.format(value))),
3207 placeholder = new_item.querySelector('[data-label-placeholder]');
3208
3209 if (placeholder) {
3210 var content = E('span', {}, label || this.choices[value] || [ value ]);
3211
3212 while (content.firstChild)
3213 placeholder.parentNode.insertBefore(content.firstChild, placeholder);
3214
3215 placeholder.parentNode.removeChild(placeholder);
3216 }
3217
3218 if (this.options.multiple)
3219 this.transformItem(sb, new_item);
3220
3221 return new_item;
3222 },
3223
3224 /** @private */
3225 createItems: function(sb, value) {
3226 var sbox = this,
3227 val = (value || '').trim(),
3228 ul = sb.querySelector('ul');
3229
3230 if (!sbox.options.multiple)
3231 val = val.length ? [ val ] : [];
3232 else
3233 val = val.length ? val.split(/\s+/) : [];
3234
3235 val.forEach(function(item) {
3236 var new_item = null;
3237
3238 ul.childNodes.forEach(function(li) {
3239 if (li.getAttribute &amp;&amp; li.getAttribute('data-value') === item)
3240 new_item = li;
3241 });
3242
3243 if (!new_item) {
3244 new_item = sbox.createChoiceElement(sb, item);
3245
3246 if (!sbox.options.multiple) {
3247 var old = ul.querySelector('li[created]');
3248 if (old)
3249 ul.removeChild(old);
3250
3251 new_item.setAttribute('created', '');
3252 }
3253
3254 new_item = ul.insertBefore(new_item, ul.lastElementChild);
3255 }
3256
3257 sbox.toggleItem(sb, new_item, true);
3258 sbox.setFocus(sb, new_item, true);
3259 });
3260 },
3261
3262 /**
3263 * Remove all existing choices from the dropdown menu.
3264 *
3265 * This function removes all preexisting dropdown choices from the widget,
3266 * keeping only choices currently being selected unless `reset_values` is
3267 * given, in which case all choices and deselected and removed.
3268 *
3269 * @instance
3270 * @memberof LuCI.ui.Dropdown
3271 * @param {boolean} [reset_value=false]
3272 * If set to `true`, deselect and remove selected choices as well instead
3273 * of keeping them.
3274 */
3275 clearChoices: function(reset_value) {
3276 var ul = this.node.querySelector('ul'),
3277 lis = ul ? ul.querySelectorAll('li[data-value]') : [],
3278 len = lis.length - (this.options.create ? 1 : 0),
3279 val = reset_value ? null : this.getValue();
3280
3281 for (var i = 0; i &lt; len; i++) {
3282 var lival = lis[i].getAttribute('data-value');
3283 if (val == null ||
3284 (!this.options.multiple &amp;&amp; val != lival) ||
3285 (this.options.multiple &amp;&amp; val.indexOf(lival) == -1))
3286 ul.removeChild(lis[i]);
3287 }
3288
3289 if (reset_value)
3290 this.setValues(this.node, {});
3291 },
3292
3293 /**
3294 * Add new choices to the dropdown menu.
3295 *
3296 * This function adds further choices to an existing dropdown menu,
3297 * ignoring choice values which are already present.
3298 *
3299 * @instance
3300 * @memberof LuCI.ui.Dropdown
3301 * @param {string[]} values
3302 * The choice values to add to the dropdown widget.
3303 *
3304 * @param {Object&lt;string, *>} labels
3305 * The choice label values to use when adding dropdown choices. If no
3306 * label is found for a particular choice value, the value itself is used
3307 * as label text. Choice labels may be any valid value accepted by
3308 * {@link LuCI.dom#content}.
3309 */
3310 addChoices: function(values, labels) {
3311 var sb = this.node,
3312 ul = sb.querySelector('ul'),
3313 lis = ul ? ul.querySelectorAll('li[data-value]') : [];
3314
3315 if (!Array.isArray(values))
3316 values = L.toArray(values);
3317
3318 if (!L.isObject(labels))
3319 labels = {};
3320
3321 for (var i = 0; i &lt; values.length; i++) {
3322 var found = false;
3323
3324 for (var j = 0; j &lt; lis.length; j++) {
3325 if (lis[j].getAttribute('data-value') === values[i]) {
3326 found = true;
3327 break;
3328 }
3329 }
3330
3331 if (found)
3332 continue;
3333
3334 ul.insertBefore(
3335 this.createChoiceElement(sb, values[i], labels[values[i]]),
3336 ul.lastElementChild);
3337 }
3338 },
3339
3340 /**
3341 * Close all open dropdown widgets in the current document.
3342 */
3343 closeAllDropdowns: function() {
3344 document.querySelectorAll('.cbi-dropdown[open]').forEach(function(s) {
3345 s.dispatchEvent(new CustomEvent('cbi-dropdown-close', {}));
3346 });
3347 },
3348
3349 /** @private */
3350 handleClick: function(ev) {
3351 var sb = ev.currentTarget;
3352
3353 if (!sb.hasAttribute('open')) {
3354 if (!matchesElem(ev.target, 'input'))
3355 this.openDropdown(sb);
3356 }
3357 else {
3358 var li = findParent(ev.target, 'li');
3359 if (li &amp;&amp; li.parentNode.classList.contains('dropdown'))
3360 this.toggleItem(sb, li);
3361 else if (li &amp;&amp; li.parentNode.classList.contains('preview'))
3362 this.closeDropdown(sb);
3363 else if (matchesElem(ev.target, 'span.open, span.more'))
3364 this.closeDropdown(sb);
3365 }
3366
3367 ev.preventDefault();
3368 ev.stopPropagation();
3369 },
3370
3371 /** @private */
3372 handleKeydown: function(ev) {
3373 var sb = ev.currentTarget;
3374
3375 if (matchesElem(ev.target, 'input'))
3376 return;
3377
3378 if (!sb.hasAttribute('open')) {
3379 switch (ev.keyCode) {
3380 case 37:
3381 case 38:
3382 case 39:
3383 case 40:
3384 this.openDropdown(sb);
3385 ev.preventDefault();
3386 }
3387 }
3388 else {
3389 var active = findParent(document.activeElement, 'li');
3390
3391 switch (ev.keyCode) {
3392 case 27:
3393 this.closeDropdown(sb);
3394 break;
3395
3396 case 13:
3397 if (active) {
3398 if (!active.hasAttribute('selected'))
3399 this.toggleItem(sb, active);
3400 this.closeDropdown(sb);
3401 ev.preventDefault();
3402 }
3403 break;
3404
3405 case 32:
3406 if (active) {
3407 this.toggleItem(sb, active);
3408 ev.preventDefault();
3409 }
3410 break;
3411
3412 case 38:
3413 if (active &amp;&amp; active.previousElementSibling) {
3414 this.setFocus(sb, active.previousElementSibling);
3415 ev.preventDefault();
3416 }
3417 break;
3418
3419 case 40:
3420 if (active &amp;&amp; active.nextElementSibling) {
3421 this.setFocus(sb, active.nextElementSibling);
3422 ev.preventDefault();
3423 }
3424 break;
3425 }
3426 }
3427 },
3428
3429 /** @private */
3430 handleDropdownClose: function(ev) {
3431 var sb = ev.currentTarget;
3432
3433 this.closeDropdown(sb, true);
3434 },
3435
3436 /** @private */
3437 handleDropdownSelect: function(ev) {
3438 var sb = ev.currentTarget,
3439 li = findParent(ev.target, 'li');
3440
3441 if (!li)
3442 return;
3443
3444 this.toggleItem(sb, li);
3445 this.closeDropdown(sb, true);
3446 },
3447
3448 /** @private */
3449 handleMouseover: function(ev) {
3450 var sb = ev.currentTarget;
3451
3452 if (!sb.hasAttribute('open'))
3453 return;
3454
3455 var li = findParent(ev.target, 'li');
3456
3457 if (li &amp;&amp; li.parentNode.classList.contains('dropdown'))
3458 this.setFocus(sb, li);
3459 },
3460
3461 /** @private */
3462 handleFocus: function(ev) {
3463 var sb = ev.currentTarget;
3464
3465 document.querySelectorAll('.cbi-dropdown[open]').forEach(function(s) {
3466 if (s !== sb || sb.hasAttribute('open'))
3467 s.dispatchEvent(new CustomEvent('cbi-dropdown-close', {}));
3468 });
3469 },
3470
3471 /** @private */
3472 handleCanaryFocus: function(ev) {
3473 this.closeDropdown(ev.currentTarget.parentNode);
3474 },
3475
3476 /** @private */
3477 handleCreateKeydown: function(ev) {
3478 var input = ev.currentTarget,
3479 sb = findParent(input, '.cbi-dropdown');
3480
3481 switch (ev.keyCode) {
3482 case 13:
3483 ev.preventDefault();
3484
3485 if (input.classList.contains('cbi-input-invalid'))
3486 return;
3487
3488 this.createItems(sb, input.value);
3489 input.value = '';
3490 input.blur();
3491 break;
3492 }
3493 },
3494
3495 /** @private */
3496 handleCreateFocus: function(ev) {
3497 var input = ev.currentTarget,
3498 cbox = findParent(input, 'li').querySelector('input[type="checkbox"]'),
3499 sb = findParent(input, '.cbi-dropdown');
3500
3501 if (cbox)
3502 cbox.checked = true;
3503
3504 sb.setAttribute('locked-in', '');
3505 },
3506
3507 /** @private */
3508 handleCreateBlur: function(ev) {
3509 var input = ev.currentTarget,
3510 cbox = findParent(input, 'li').querySelector('input[type="checkbox"]'),
3511 sb = findParent(input, '.cbi-dropdown');
3512
3513 if (cbox)
3514 cbox.checked = false;
3515
3516 sb.removeAttribute('locked-in');
3517 },
3518
3519 /** @private */
3520 handleCreateClick: function(ev) {
3521 ev.currentTarget.querySelector(this.options.create_query).focus();
3522 },
3523
3524 /** @override */
3525 setValue: function(values) {
3526 if (this.options.multiple) {
3527 if (!Array.isArray(values))
3528 values = (values != null &amp;&amp; values != '') ? [ values ] : [];
3529
3530 var v = {};
3531
3532 for (var i = 0; i &lt; values.length; i++)
3533 v[values[i]] = true;
3534
3535 this.setValues(this.node, v);
3536 }
3537 else {
3538 var v = {};
3539
3540 if (values != null) {
3541 if (Array.isArray(values))
3542 v[values[0]] = true;
3543 else
3544 v[values] = true;
3545 }
3546
3547 this.setValues(this.node, v);
3548 }
3549 },
3550
3551 /** @override */
3552 getValue: function() {
3553 var div = this.node.lastElementChild,
3554 h = div.querySelectorAll('input[type="hidden"]'),
3555 v = [];
3556
3557 for (var i = 0; i &lt; h.length; i++)
3558 v.push(h[i].value);
3559
3560 return this.options.multiple ? v : v[0];
3561 }
3562 });
3563
3564 /**
3565 * Instantiate a rich dropdown choice widget allowing custom values.
3566 *
3567 * @constructor Combobox
3568 * @memberof LuCI.ui
3569 * @augments LuCI.ui.Dropdown
3570 *
3571 * @classdesc
3572 *
3573 * The `Combobox` class implements a rich, stylable dropdown menu which allows
3574 * to enter custom values. Historically, comboboxes used to be a dedicated
3575 * widget type in LuCI but nowadays they are direct aliases of dropdown widgets
3576 * with a set of enforced default properties for easier instantiation.
3577 *
3578 * UI widget instances are usually not supposed to be created by view code
3579 * directly, instead they're implicitely created by `LuCI.form` when
3580 * instantiating CBI forms.
3581 *
3582 * This class is automatically instantiated as part of `LuCI.ui`. To use it
3583 * in views, use `'require ui'` and refer to `ui.Combobox`. To import it in
3584 * external JavaScript, use `L.require("ui").then(...)` and access the
3585 * `Combobox` property of the class instance value.
3586 *
3587 * @param {string|string[]} [value=null]
3588 * The initial input value(s).
3589 *
3590 * @param {Object&lt;string, *>} choices
3591 * Object containing the selectable choices of the widget. The object keys
3592 * serve as values for the different choices while the values are used as
3593 * choice labels.
3594 *
3595 * @param {LuCI.ui.Combobox.InitOptions} [options]
3596 * Object describing the widget specific options to initialize the dropdown.
3597 */
3598 var UICombobox = UIDropdown.extend(/** @lends LuCI.ui.Combobox.prototype */ {
3599 /**
3600 * Comboboxes support the same properties as
3601 * [Dropdown.InitOptions]{@link LuCI.ui.Dropdown.InitOptions} but enforce
3602 * specific values for the following properties:
3603 *
3604 * @typedef {LuCI.ui.Dropdown.InitOptions} InitOptions
3605 * @memberof LuCI.ui.Combobox
3606 *
3607 * @property {boolean} multiple=false
3608 * Since Comboboxes never allow selecting multiple values, this property
3609 * is forcibly set to `false`.
3610 *
3611 * @property {boolean} create=true
3612 * Since Comboboxes always allow custom choice values, this property is
3613 * forcibly set to `true`.
3614 *
3615 * @property {boolean} optional=true
3616 * Since Comboboxes are always optional, this property is forcibly set to
3617 * `true`.
3618 */
3619 __init__: function(value, choices, options) {
3620 this.super('__init__', [ value, choices, Object.assign({
3621 select_placeholder: _('-- Please choose --'),
3622 custom_placeholder: _('-- custom --'),
3623 dropdown_items: -1,
3624 sort: true
3625 }, options, {
3626 multiple: false,
3627 create: true,
3628 optional: true
3629 }) ]);
3630 }
3631 });
3632
3633 /**
3634 * Instantiate a combo button widget offering multiple action choices.
3635 *
3636 * @constructor ComboButton
3637 * @memberof LuCI.ui
3638 * @augments LuCI.ui.Dropdown
3639 *
3640 * @classdesc
3641 *
3642 * The `ComboButton` class implements a button element which can be expanded
3643 * into a dropdown to chose from a set of different action choices.
3644 *
3645 * UI widget instances are usually not supposed to be created by view code
3646 * directly, instead they're implicitely created by `LuCI.form` when
3647 * instantiating CBI forms.
3648 *
3649 * This class is automatically instantiated as part of `LuCI.ui`. To use it
3650 * in views, use `'require ui'` and refer to `ui.ComboButton`. To import it in
3651 * external JavaScript, use `L.require("ui").then(...)` and access the
3652 * `ComboButton` property of the class instance value.
3653 *
3654 * @param {string|string[]} [value=null]
3655 * The initial input value(s).
3656 *
3657 * @param {Object&lt;string, *>} choices
3658 * Object containing the selectable choices of the widget. The object keys
3659 * serve as values for the different choices while the values are used as
3660 * choice labels.
3661 *
3662 * @param {LuCI.ui.ComboButton.InitOptions} [options]
3663 * Object describing the widget specific options to initialize the button.
3664 */
3665 var UIComboButton = UIDropdown.extend(/** @lends LuCI.ui.ComboButton.prototype */ {
3666 /**
3667 * ComboButtons support the same properties as
3668 * [Dropdown.InitOptions]{@link LuCI.ui.Dropdown.InitOptions} but enforce
3669 * specific values for some properties and add aditional button specific
3670 * properties.
3671 *
3672 * @typedef {LuCI.ui.Dropdown.InitOptions} InitOptions
3673 * @memberof LuCI.ui.ComboButton
3674 *
3675 * @property {boolean} multiple=false
3676 * Since ComboButtons never allow selecting multiple actions, this property
3677 * is forcibly set to `false`.
3678 *
3679 * @property {boolean} create=false
3680 * Since ComboButtons never allow creating custom choices, this property
3681 * is forcibly set to `false`.
3682 *
3683 * @property {boolean} optional=false
3684 * Since ComboButtons must always select one action, this property is
3685 * forcibly set to `false`.
3686 *
3687 * @property {Object&lt;string, string>} [classes]
3688 * Specifies a mapping of choice values to CSS class names. If an action
3689 * choice is selected by the user and if a corresponding entry exists in
3690 * the `classes` object, the class names corresponding to the selected
3691 * value are set on the button element.
3692 *
3693 * This is useful to apply different button styles, such as colors, to the
3694 * combined button depending on the selected action.
3695 *
3696 * @property {function} [click]
3697 * Specifies a handler function to invoke when the user clicks the button.
3698 * This function will be called with the button DOM node as `this` context
3699 * and receive the DOM click event as first as well as the selected action
3700 * choice value as second argument.
3701 */
3702 __init__: function(value, choices, options) {
3703 this.super('__init__', [ value, choices, Object.assign({
3704 sort: true
3705 }, options, {
3706 multiple: false,
3707 create: false,
3708 optional: false
3709 }) ]);
3710 },
3711
3712 /** @override */
3713 render: function(/* ... */) {
3714 var node = UIDropdown.prototype.render.apply(this, arguments),
3715 val = this.getValue();
3716
3717 if (L.isObject(this.options.classes) &amp;&amp; this.options.classes.hasOwnProperty(val))
3718 node.setAttribute('class', 'cbi-dropdown ' + this.options.classes[val]);
3719
3720 return node;
3721 },
3722
3723 /** @private */
3724 handleClick: function(ev) {
3725 var sb = ev.currentTarget,
3726 t = ev.target;
3727
3728 if (sb.hasAttribute('open') || dom.matches(t, '.cbi-dropdown > span.open'))
3729 return UIDropdown.prototype.handleClick.apply(this, arguments);
3730
3731 if (this.options.click)
3732 return this.options.click.call(sb, ev, this.getValue());
3733 },
3734
3735 /** @private */
3736 toggleItem: function(sb /*, ... */) {
3737 var rv = UIDropdown.prototype.toggleItem.apply(this, arguments),
3738 val = this.getValue();
3739
3740 if (L.isObject(this.options.classes) &amp;&amp; this.options.classes.hasOwnProperty(val))
3741 sb.setAttribute('class', 'cbi-dropdown ' + this.options.classes[val]);
3742 else
3743 sb.setAttribute('class', 'cbi-dropdown');
3744
3745 return rv;
3746 }
3747 });
3748
3749 /**
3750 * Instantiate a dynamic list widget.
3751 *
3752 * @constructor DynamicList
3753 * @memberof LuCI.ui
3754 * @augments LuCI.ui.AbstractElement
3755 *
3756 * @classdesc
3757 *
3758 * The `DynamicList` class implements a widget which allows the user to specify
3759 * an arbitrary amount of input values, either from free formed text input or
3760 * from a set of predefined choices.
3761 *
3762 * UI widget instances are usually not supposed to be created by view code
3763 * directly, instead they're implicitely created by `LuCI.form` when
3764 * instantiating CBI forms.
3765 *
3766 * This class is automatically instantiated as part of `LuCI.ui`. To use it
3767 * in views, use `'require ui'` and refer to `ui.DynamicList`. To import it in
3768 * external JavaScript, use `L.require("ui").then(...)` and access the
3769 * `DynamicList` property of the class instance value.
3770 *
3771 * @param {string|string[]} [value=null]
3772 * The initial input value(s).
3773 *
3774 * @param {Object&lt;string, *>} [choices]
3775 * Object containing the selectable choices of the widget. The object keys
3776 * serve as values for the different choices while the values are used as
3777 * choice labels. If omitted, no default choices are presented to the user,
3778 * instead a plain text input field is rendered allowing the user to add
3779 * arbitrary values to the dynamic list.
3780 *
3781 * @param {LuCI.ui.DynamicList.InitOptions} [options]
3782 * Object describing the widget specific options to initialize the dynamic list.
3783 */
3784 var UIDynamicList = UIElement.extend(/** @lends LuCI.ui.DynamicList.prototype */ {
3785 /**
3786 * In case choices are passed to the dynamic list contructor, the widget
3787 * supports the same properties as [Dropdown.InitOptions]{@link LuCI.ui.Dropdown.InitOptions}
3788 * but enforces specific values for some dropdown properties.
3789 *
3790 * @typedef {LuCI.ui.Dropdown.InitOptions} InitOptions
3791 * @memberof LuCI.ui.DynamicList
3792 *
3793 * @property {boolean} multiple=false
3794 * Since dynamic lists never allow selecting multiple choices when adding
3795 * another list item, this property is forcibly set to `false`.
3796 *
3797 * @property {boolean} optional=true
3798 * Since dynamic lists use an embedded dropdown to present a list of
3799 * predefined choice values, the dropdown must be made optional to allow
3800 * it to remain unselected.
3801 */
3802 __init__: function(values, choices, options) {
3803 if (!Array.isArray(values))
3804 values = (values != null &amp;&amp; values != '') ? [ values ] : [];
3805
3806 if (typeof(choices) != 'object')
3807 choices = null;
3808
3809 this.values = values;
3810 this.choices = choices;
3811 this.options = Object.assign({}, options, {
3812 multiple: false,
3813 optional: true
3814 });
3815 },
3816
3817 /** @override */
3818 render: function() {
3819 var dl = E('div', {
3820 'id': this.options.id,
3821 'class': 'cbi-dynlist'
3822 }, E('div', { 'class': 'add-item' }));
3823
3824 if (this.choices) {
3825 if (this.options.placeholder != null)
3826 this.options.select_placeholder = this.options.placeholder;
3827
3828 var cbox = new UICombobox(null, this.choices, this.options);
3829
3830 dl.lastElementChild.appendChild(cbox.render());
3831 }
3832 else {
3833 var inputEl = E('input', {
3834 'id': this.options.id ? 'widget.' + this.options.id : null,
3835 'type': 'text',
3836 'class': 'cbi-input-text',
3837 'placeholder': this.options.placeholder
3838 });
3839
3840 dl.lastElementChild.appendChild(inputEl);
3841 dl.lastElementChild.appendChild(E('div', { 'class': 'btn cbi-button cbi-button-add' }, '+'));
3842
3843 if (this.options.datatype || this.options.validate)
3844 UI.prototype.addValidator(inputEl, this.options.datatype || 'string',
3845 true, this.options.validate, 'blur', 'keyup');
3846 }
3847
3848 for (var i = 0; i &lt; this.values.length; i++) {
3849 var label = this.choices ? this.choices[this.values[i]] : null;
3850
3851 if (dom.elem(label))
3852 label = label.cloneNode(true);
3853
3854 this.addItem(dl, this.values[i], label);
3855 }
3856
3857 return this.bind(dl);
3858 },
3859
3860 /** @private */
3861 bind: function(dl) {
3862 dl.addEventListener('click', L.bind(this.handleClick, this));
3863 dl.addEventListener('keydown', L.bind(this.handleKeydown, this));
3864 dl.addEventListener('cbi-dropdown-change', L.bind(this.handleDropdownChange, this));
3865
3866 this.node = dl;
3867
3868 this.setUpdateEvents(dl, 'cbi-dynlist-change');
3869 this.setChangeEvents(dl, 'cbi-dynlist-change');
3870
3871 dom.bindClassInstance(dl, this);
3872
3873 return dl;
3874 },
3875
3876 /** @private */
3877 addItem: function(dl, value, text, flash) {
3878 var exists = false,
3879 new_item = E('div', { 'class': flash ? 'item flash' : 'item', 'tabindex': 0 }, [
3880 E('span', {}, [ text || value ]),
3881 E('input', {
3882 'type': 'hidden',
3883 'name': this.options.name,
3884 'value': value })]);
3885
3886 dl.querySelectorAll('.item').forEach(function(item) {
3887 if (exists)
3888 return;
3889
3890 var hidden = item.querySelector('input[type="hidden"]');
3891
3892 if (hidden &amp;&amp; hidden.parentNode !== item)
3893 hidden = null;
3894
3895 if (hidden &amp;&amp; hidden.value === value)
3896 exists = true;
3897 });
3898
3899 if (!exists) {
3900 var ai = dl.querySelector('.add-item');
3901 ai.parentNode.insertBefore(new_item, ai);
3902 }
3903
3904 dl.dispatchEvent(new CustomEvent('cbi-dynlist-change', {
3905 bubbles: true,
3906 detail: {
3907 instance: this,
3908 element: dl,
3909 value: value,
3910 add: true
3911 }
3912 }));
3913 },
3914
3915 /** @private */
3916 removeItem: function(dl, item) {
3917 var value = item.querySelector('input[type="hidden"]').value;
3918 var sb = dl.querySelector('.cbi-dropdown');
3919 if (sb)
3920 sb.querySelectorAll('ul > li').forEach(function(li) {
3921 if (li.getAttribute('data-value') === value) {
3922 if (li.hasAttribute('dynlistcustom'))
3923 li.parentNode.removeChild(li);
3924 else
3925 li.removeAttribute('unselectable');
3926 }
3927 });
3928
3929 item.parentNode.removeChild(item);
3930
3931 dl.dispatchEvent(new CustomEvent('cbi-dynlist-change', {
3932 bubbles: true,
3933 detail: {
3934 instance: this,
3935 element: dl,
3936 value: value,
3937 remove: true
3938 }
3939 }));
3940 },
3941
3942 /** @private */
3943 handleClick: function(ev) {
3944 var dl = ev.currentTarget,
3945 item = findParent(ev.target, '.item');
3946
3947 if (item) {
3948 this.removeItem(dl, item);
3949 }
3950 else if (matchesElem(ev.target, '.cbi-button-add')) {
3951 var input = ev.target.previousElementSibling;
3952 if (input.value.length &amp;&amp; !input.classList.contains('cbi-input-invalid')) {
3953 this.addItem(dl, input.value, null, true);
3954 input.value = '';
3955 }
3956 }
3957 },
3958
3959 /** @private */
3960 handleDropdownChange: function(ev) {
3961 var dl = ev.currentTarget,
3962 sbIn = ev.detail.instance,
3963 sbEl = ev.detail.element,
3964 sbVal = ev.detail.value;
3965
3966 if (sbVal === null)
3967 return;
3968
3969 sbIn.setValues(sbEl, null);
3970 sbVal.element.setAttribute('unselectable', '');
3971
3972 if (sbVal.element.hasAttribute('created')) {
3973 sbVal.element.removeAttribute('created');
3974 sbVal.element.setAttribute('dynlistcustom', '');
3975 }
3976
3977 var label = sbVal.text;
3978
3979 if (sbVal.element) {
3980 label = E([]);
3981
3982 for (var i = 0; i &lt; sbVal.element.childNodes.length; i++)
3983 label.appendChild(sbVal.element.childNodes[i].cloneNode(true));
3984 }
3985
3986 this.addItem(dl, sbVal.value, label, true);
3987 },
3988
3989 /** @private */
3990 handleKeydown: function(ev) {
3991 var dl = ev.currentTarget,
3992 item = findParent(ev.target, '.item');
3993
3994 if (item) {
3995 switch (ev.keyCode) {
3996 case 8: /* backspace */
3997 if (item.previousElementSibling)
3998 item.previousElementSibling.focus();
3999
4000 this.removeItem(dl, item);
4001 break;
4002
4003 case 46: /* delete */
4004 if (item.nextElementSibling) {
4005 if (item.nextElementSibling.classList.contains('item'))
4006 item.nextElementSibling.focus();
4007 else
4008 item.nextElementSibling.firstElementChild.focus();
4009 }
4010
4011 this.removeItem(dl, item);
4012 break;
4013 }
4014 }
4015 else if (matchesElem(ev.target, '.cbi-input-text')) {
4016 switch (ev.keyCode) {
4017 case 13: /* enter */
4018 if (ev.target.value.length &amp;&amp; !ev.target.classList.contains('cbi-input-invalid')) {
4019 this.addItem(dl, ev.target.value, null, true);
4020 ev.target.value = '';
4021 ev.target.blur();
4022 ev.target.focus();
4023 }
4024
4025 ev.preventDefault();
4026 break;
4027 }
4028 }
4029 },
4030
4031 /** @override */
4032 getValue: function() {
4033 var items = this.node.querySelectorAll('.item > input[type="hidden"]'),
4034 input = this.node.querySelector('.add-item > input[type="text"]'),
4035 v = [];
4036
4037 for (var i = 0; i &lt; items.length; i++)
4038 v.push(items[i].value);
4039
4040 if (input &amp;&amp; input.value != null &amp;&amp; input.value.match(/\S/) &amp;&amp;
4041 input.classList.contains('cbi-input-invalid') == false &amp;&amp;
4042 v.filter(function(s) { return s == input.value }).length == 0)
4043 v.push(input.value);
4044
4045 return v;
4046 },
4047
4048 /** @override */
4049 setValue: function(values) {
4050 if (!Array.isArray(values))
4051 values = (values != null &amp;&amp; values != '') ? [ values ] : [];
4052
4053 var items = this.node.querySelectorAll('.item');
4054
4055 for (var i = 0; i &lt; items.length; i++)
4056 if (items[i].parentNode === this.node)
4057 this.removeItem(this.node, items[i]);
4058
4059 for (var i = 0; i &lt; values.length; i++)
4060 this.addItem(this.node, values[i],
4061 this.choices ? this.choices[values[i]] : null);
4062 },
4063
4064 /**
4065 * Add new suggested choices to the dynamic list.
4066 *
4067 * This function adds further choices to an existing dynamic list,
4068 * ignoring choice values which are already present.
4069 *
4070 * @instance
4071 * @memberof LuCI.ui.DynamicList
4072 * @param {string[]} values
4073 * The choice values to add to the dynamic lists suggestion dropdown.
4074 *
4075 * @param {Object&lt;string, *>} labels
4076 * The choice label values to use when adding suggested choices. If no
4077 * label is found for a particular choice value, the value itself is used
4078 * as label text. Choice labels may be any valid value accepted by
4079 * {@link LuCI.dom#content}.
4080 */
4081 addChoices: function(values, labels) {
4082 var dl = this.node.lastElementChild.firstElementChild;
4083 dom.callClassMethod(dl, 'addChoices', values, labels);
4084 },
4085
4086 /**
4087 * Remove all existing choices from the dynamic list.
4088 *
4089 * This function removes all preexisting suggested choices from the widget.
4090 *
4091 * @instance
4092 * @memberof LuCI.ui.DynamicList
4093 */
4094 clearChoices: function() {
4095 var dl = this.node.lastElementChild.firstElementChild;
4096 dom.callClassMethod(dl, 'clearChoices');
4097 }
4098 });
4099
4100 /**
4101 * Instantiate a hidden input field widget.
4102 *
4103 * @constructor Hiddenfield
4104 * @memberof LuCI.ui
4105 * @augments LuCI.ui.AbstractElement
4106 *
4107 * @classdesc
4108 *
4109 * The `Hiddenfield` class implements an HTML `&lt;input type="hidden">` field
4110 * which allows to store form data without exposing it to the user.
4111 *
4112 * UI widget instances are usually not supposed to be created by view code
4113 * directly, instead they're implicitely created by `LuCI.form` when
4114 * instantiating CBI forms.
4115 *
4116 * This class is automatically instantiated as part of `LuCI.ui`. To use it
4117 * in views, use `'require ui'` and refer to `ui.Hiddenfield`. To import it in
4118 * external JavaScript, use `L.require("ui").then(...)` and access the
4119 * `Hiddenfield` property of the class instance value.
4120 *
4121 * @param {string|string[]} [value=null]
4122 * The initial input value.
4123 *
4124 * @param {LuCI.ui.AbstractElement.InitOptions} [options]
4125 * Object describing the widget specific options to initialize the hidden input.
4126 */
4127 var UIHiddenfield = UIElement.extend(/** @lends LuCI.ui.Hiddenfield.prototype */ {
4128 __init__: function(value, options) {
4129 this.value = value;
4130 this.options = Object.assign({
4131
4132 }, options);
4133 },
4134
4135 /** @override */
4136 render: function() {
4137 var hiddenEl = E('input', {
4138 'id': this.options.id,
4139 'type': 'hidden',
4140 'value': this.value
4141 });
4142
4143 return this.bind(hiddenEl);
4144 },
4145
4146 /** @private */
4147 bind: function(hiddenEl) {
4148 this.node = hiddenEl;
4149
4150 dom.bindClassInstance(hiddenEl, this);
4151
4152 return hiddenEl;
4153 },
4154
4155 /** @override */
4156 getValue: function() {
4157 return this.node.value;
4158 },
4159
4160 /** @override */
4161 setValue: function(value) {
4162 this.node.value = value;
4163 }
4164 });
4165
4166 /**
4167 * Instantiate a file upload widget.
4168 *
4169 * @constructor FileUpload
4170 * @memberof LuCI.ui
4171 * @augments LuCI.ui.AbstractElement
4172 *
4173 * @classdesc
4174 *
4175 * The `FileUpload` class implements a widget which allows the user to upload,
4176 * browse, select and delete files beneath a predefined remote directory.
4177 *
4178 * UI widget instances are usually not supposed to be created by view code
4179 * directly, instead they're implicitely created by `LuCI.form` when
4180 * instantiating CBI forms.
4181 *
4182 * This class is automatically instantiated as part of `LuCI.ui`. To use it
4183 * in views, use `'require ui'` and refer to `ui.FileUpload`. To import it in
4184 * external JavaScript, use `L.require("ui").then(...)` and access the
4185 * `FileUpload` property of the class instance value.
4186 *
4187 * @param {string|string[]} [value=null]
4188 * The initial input value.
4189 *
4190 * @param {LuCI.ui.DynamicList.InitOptions} [options]
4191 * Object describing the widget specific options to initialize the file
4192 * upload control.
4193 */
4194 var UIFileUpload = UIElement.extend(/** @lends LuCI.ui.FileUpload.prototype */ {
4195 /**
4196 * In addition to the [AbstractElement.InitOptions]{@link LuCI.ui.AbstractElement.InitOptions}
4197 * the following properties are recognized:
4198 *
4199 * @typedef {LuCI.ui.AbstractElement.InitOptions} InitOptions
4200 * @memberof LuCI.ui.FileUpload
4201 *
4202 * @property {boolean} [show_hidden=false]
4203 * Specifies whether hidden files should be displayed when browsing remote
4204 * files. Note that this is not a security feature, hidden files are always
4205 * present in the remote file listings received, this option merely controls
4206 * whether they're displayed or not.
4207 *
4208 * @property {boolean} [enable_upload=true]
4209 * Specifies whether the widget allows the user to upload files. If set to
4210 * `false`, only existing files may be selected. Note that this is not a
4211 * security feature. Whether file upload requests are accepted remotely
4212 * depends on the ACL setup for the current session. This option merely
4213 * controls whether the upload controls are rendered or not.
4214 *
4215 * @property {boolean} [enable_remove=true]
4216 * Specifies whether the widget allows the user to delete remove files.
4217 * If set to `false`, existing files may not be removed. Note that this is
4218 * not a security feature. Whether file delete requests are accepted
4219 * remotely depends on the ACL setup for the current session. This option
4220 * merely controls whether the file remove controls are rendered or not.
4221 *
4222 * @property {string} [root_directory=/etc/luci-uploads]
4223 * Specifies the remote directory the upload and file browsing actions take
4224 * place in. Browsing to directories outside of the root directory is
4225 * prevented by the widget. Note that this is not a security feature.
4226 * Whether remote directories are browseable or not solely depends on the
4227 * ACL setup for the current session.
4228 */
4229 __init__: function(value, options) {
4230 this.value = value;
4231 this.options = Object.assign({
4232 show_hidden: false,
4233 enable_upload: true,
4234 enable_remove: true,
4235 root_directory: '/etc/luci-uploads'
4236 }, options);
4237 },
4238
4239 /** @private */
4240 bind: function(browserEl) {
4241 this.node = browserEl;
4242
4243 this.setUpdateEvents(browserEl, 'cbi-fileupload-select', 'cbi-fileupload-cancel');
4244 this.setChangeEvents(browserEl, 'cbi-fileupload-select', 'cbi-fileupload-cancel');
4245
4246 dom.bindClassInstance(browserEl, this);
4247
4248 return browserEl;
4249 },
4250
4251 /** @override */
4252 render: function() {
4253 return L.resolveDefault(this.value != null ? fs.stat(this.value) : null).then(L.bind(function(stat) {
4254 var label;
4255
4256 if (L.isObject(stat) &amp;&amp; stat.type != 'directory')
4257 this.stat = stat;
4258
4259 if (this.stat != null)
4260 label = [ this.iconForType(this.stat.type), ' %s (%1000mB)'.format(this.truncatePath(this.stat.path), this.stat.size) ];
4261 else if (this.value != null)
4262 label = [ this.iconForType('file'), ' %s (%s)'.format(this.truncatePath(this.value), _('File not accessible')) ];
4263 else
4264 label = [ _('Select file…') ];
4265
4266 return this.bind(E('div', { 'id': this.options.id }, [
4267 E('button', {
4268 'class': 'btn',
4269 'click': UI.prototype.createHandlerFn(this, 'handleFileBrowser')
4270 }, label),
4271 E('div', {
4272 'class': 'cbi-filebrowser'
4273 }),
4274 E('input', {
4275 'type': 'hidden',
4276 'name': this.options.name,
4277 'value': this.value
4278 })
4279 ]));
4280 }, this));
4281 },
4282
4283 /** @private */
4284 truncatePath: function(path) {
4285 if (path.length > 50)
4286 path = path.substring(0, 25) + '…' + path.substring(path.length - 25);
4287
4288 return path;
4289 },
4290
4291 /** @private */
4292 iconForType: function(type) {
4293 switch (type) {
4294 case 'symlink':
4295 return E('img', {
4296 'src': L.resource('cbi/link.gif'),
4297 'title': _('Symbolic link'),
4298 'class': 'middle'
4299 });
4300
4301 case 'directory':
4302 return E('img', {
4303 'src': L.resource('cbi/folder.gif'),
4304 'title': _('Directory'),
4305 'class': 'middle'
4306 });
4307
4308 default:
4309 return E('img', {
4310 'src': L.resource('cbi/file.gif'),
4311 'title': _('File'),
4312 'class': 'middle'
4313 });
4314 }
4315 },
4316
4317 /** @private */
4318 canonicalizePath: function(path) {
4319 return path.replace(/\/{2,}/, '/')
4320 .replace(/\/\.(\/|$)/g, '/')
4321 .replace(/[^\/]+\/\.\.(\/|$)/g, '/')
4322 .replace(/\/$/, '');
4323 },
4324
4325 /** @private */
4326 splitPath: function(path) {
4327 var croot = this.canonicalizePath(this.options.root_directory || '/'),
4328 cpath = this.canonicalizePath(path || '/');
4329
4330 if (cpath.length &lt;= croot.length)
4331 return [ croot ];
4332
4333 if (cpath.charAt(croot.length) != '/')
4334 return [ croot ];
4335
4336 var parts = cpath.substring(croot.length + 1).split(/\//);
4337
4338 parts.unshift(croot);
4339
4340 return parts;
4341 },
4342
4343 /** @private */
4344 handleUpload: function(path, list, ev) {
4345 var form = ev.target.parentNode,
4346 fileinput = form.querySelector('input[type="file"]'),
4347 nameinput = form.querySelector('input[type="text"]'),
4348 filename = (nameinput.value != null ? nameinput.value : '').trim();
4349
4350 ev.preventDefault();
4351
4352 if (filename == '' || filename.match(/\//) || fileinput.files[0] == null)
4353 return;
4354
4355 var existing = list.filter(function(e) { return e.name == filename })[0];
4356
4357 if (existing != null &amp;&amp; existing.type == 'directory')
4358 return alert(_('A directory with the same name already exists.'));
4359 else if (existing != null &amp;&amp; !confirm(_('Overwrite existing file "%s" ?').format(filename)))
4360 return;
4361
4362 var data = new FormData();
4363
4364 data.append('sessionid', L.env.sessionid);
4365 data.append('filename', path + '/' + filename);
4366 data.append('filedata', fileinput.files[0]);
4367
4368 return request.post(L.env.cgi_base + '/cgi-upload', data, {
4369 progress: L.bind(function(btn, ev) {
4370 btn.firstChild.data = '%.2f%%'.format((ev.loaded / ev.total) * 100);
4371 }, this, ev.target)
4372 }).then(L.bind(function(path, ev, res) {
4373 var reply = res.json();
4374
4375 if (L.isObject(reply) &amp;&amp; reply.failure)
4376 alert(_('Upload request failed: %s').format(reply.message));
4377
4378 return this.handleSelect(path, null, ev);
4379 }, this, path, ev));
4380 },
4381
4382 /** @private */
4383 handleDelete: function(path, fileStat, ev) {
4384 var parent = path.replace(/\/[^\/]+$/, '') || '/',
4385 name = path.replace(/^.+\//, ''),
4386 msg;
4387
4388 ev.preventDefault();
4389
4390 if (fileStat.type == 'directory')
4391 msg = _('Do you really want to recursively delete the directory "%s" ?').format(name);
4392 else
4393 msg = _('Do you really want to delete "%s" ?').format(name);
4394
4395 if (confirm(msg)) {
4396 var button = this.node.firstElementChild,
4397 hidden = this.node.lastElementChild;
4398
4399 if (path == hidden.value) {
4400 dom.content(button, _('Select file…'));
4401 hidden.value = '';
4402 }
4403
4404 return fs.remove(path).then(L.bind(function(parent, ev) {
4405 return this.handleSelect(parent, null, ev);
4406 }, this, parent, ev)).catch(function(err) {
4407 alert(_('Delete request failed: %s').format(err.message));
4408 });
4409 }
4410 },
4411
4412 /** @private */
4413 renderUpload: function(path, list) {
4414 if (!this.options.enable_upload)
4415 return E([]);
4416
4417 return E([
4418 E('a', {
4419 'href': '#',
4420 'class': 'btn cbi-button-positive',
4421 'click': function(ev) {
4422 var uploadForm = ev.target.nextElementSibling,
4423 fileInput = uploadForm.querySelector('input[type="file"]');
4424
4425 ev.target.style.display = 'none';
4426 uploadForm.style.display = '';
4427 fileInput.click();
4428 }
4429 }, _('Upload file…')),
4430 E('div', { 'class': 'upload', 'style': 'display:none' }, [
4431 E('input', {
4432 'type': 'file',
4433 'style': 'display:none',
4434 'change': function(ev) {
4435 var nameinput = ev.target.parentNode.querySelector('input[type="text"]'),
4436 uploadbtn = ev.target.parentNode.querySelector('button.cbi-button-save');
4437
4438 nameinput.value = ev.target.value.replace(/^.+[\/\\]/, '');
4439 uploadbtn.disabled = false;
4440 }
4441 }),
4442 E('button', {
4443 'class': 'btn',
4444 'click': function(ev) {
4445 ev.preventDefault();
4446 ev.target.previousElementSibling.click();
4447 }
4448 }, [ _('Browse…') ]),
4449 E('div', {}, E('input', { 'type': 'text', 'placeholder': _('Filename') })),
4450 E('button', {
4451 'class': 'btn cbi-button-save',
4452 'click': UI.prototype.createHandlerFn(this, 'handleUpload', path, list),
4453 'disabled': true
4454 }, [ _('Upload file') ])
4455 ])
4456 ]);
4457 },
4458
4459 /** @private */
4460 renderListing: function(container, path, list) {
4461 var breadcrumb = E('p'),
4462 rows = E('ul');
4463
4464 list.sort(function(a, b) {
4465 var isDirA = (a.type == 'directory'),
4466 isDirB = (b.type == 'directory');
4467
4468 if (isDirA != isDirB)
4469 return isDirA &lt; isDirB;
4470
4471 return a.name > b.name;
4472 });
4473
4474 for (var i = 0; i &lt; list.length; i++) {
4475 if (!this.options.show_hidden &amp;&amp; list[i].name.charAt(0) == '.')
4476 continue;
4477
4478 var entrypath = this.canonicalizePath(path + '/' + list[i].name),
4479 selected = (entrypath == this.node.lastElementChild.value),
4480 mtime = new Date(list[i].mtime * 1000);
4481
4482 rows.appendChild(E('li', [
4483 E('div', { 'class': 'name' }, [
4484 this.iconForType(list[i].type),
4485 ' ',
4486 E('a', {
4487 'href': '#',
4488 'style': selected ? 'font-weight:bold' : null,
4489 'click': UI.prototype.createHandlerFn(this, 'handleSelect',
4490 entrypath, list[i].type != 'directory' ? list[i] : null)
4491 }, '%h'.format(list[i].name))
4492 ]),
4493 E('div', { 'class': 'mtime hide-xs' }, [
4494 ' %04d-%02d-%02d %02d:%02d:%02d '.format(
4495 mtime.getFullYear(),
4496 mtime.getMonth() + 1,
4497 mtime.getDate(),
4498 mtime.getHours(),
4499 mtime.getMinutes(),
4500 mtime.getSeconds())
4501 ]),
4502 E('div', [
4503 selected ? E('button', {
4504 'class': 'btn',
4505 'click': UI.prototype.createHandlerFn(this, 'handleReset')
4506 }, [ _('Deselect') ]) : '',
4507 this.options.enable_remove ? E('button', {
4508 'class': 'btn cbi-button-negative',
4509 'click': UI.prototype.createHandlerFn(this, 'handleDelete', entrypath, list[i])
4510 }, [ _('Delete') ]) : ''
4511 ])
4512 ]));
4513 }
4514
4515 if (!rows.firstElementChild)
4516 rows.appendChild(E('em', _('No entries in this directory')));
4517
4518 var dirs = this.splitPath(path),
4519 cur = '';
4520
4521 for (var i = 0; i &lt; dirs.length; i++) {
4522 cur = cur ? cur + '/' + dirs[i] : dirs[i];
4523 dom.append(breadcrumb, [
4524 i ? ' » ' : '',
4525 E('a', {
4526 'href': '#',
4527 'click': UI.prototype.createHandlerFn(this, 'handleSelect', cur || '/', null)
4528 }, dirs[i] != '' ? '%h'.format(dirs[i]) : E('em', '(root)')),
4529 ]);
4530 }
4531
4532 dom.content(container, [
4533 breadcrumb,
4534 rows,
4535 E('div', { 'class': 'right' }, [
4536 this.renderUpload(path, list),
4537 E('a', {
4538 'href': '#',
4539 'class': 'btn',
4540 'click': UI.prototype.createHandlerFn(this, 'handleCancel')
4541 }, _('Cancel'))
4542 ]),
4543 ]);
4544 },
4545
4546 /** @private */
4547 handleCancel: function(ev) {
4548 var button = this.node.firstElementChild,
4549 browser = button.nextElementSibling;
4550
4551 browser.classList.remove('open');
4552 button.style.display = '';
4553
4554 this.node.dispatchEvent(new CustomEvent('cbi-fileupload-cancel', {}));
4555
4556 ev.preventDefault();
4557 },
4558
4559 /** @private */
4560 handleReset: function(ev) {
4561 var button = this.node.firstElementChild,
4562 hidden = this.node.lastElementChild;
4563
4564 hidden.value = '';
4565 dom.content(button, _('Select file…'));
4566
4567 this.handleCancel(ev);
4568 },
4569
4570 /** @private */
4571 handleSelect: function(path, fileStat, ev) {
4572 var browser = dom.parent(ev.target, '.cbi-filebrowser'),
4573 ul = browser.querySelector('ul');
4574
4575 if (fileStat == null) {
4576 dom.content(ul, E('em', { 'class': 'spinning' }, _('Loading directory contents…')));
4577 L.resolveDefault(fs.list(path), []).then(L.bind(this.renderListing, this, browser, path));
4578 }
4579 else {
4580 var button = this.node.firstElementChild,
4581 hidden = this.node.lastElementChild;
4582
4583 path = this.canonicalizePath(path);
4584
4585 dom.content(button, [
4586 this.iconForType(fileStat.type),
4587 ' %s (%1000mB)'.format(this.truncatePath(path), fileStat.size)
4588 ]);
4589
4590 browser.classList.remove('open');
4591 button.style.display = '';
4592 hidden.value = path;
4593
4594 this.stat = Object.assign({ path: path }, fileStat);
4595 this.node.dispatchEvent(new CustomEvent('cbi-fileupload-select', { detail: this.stat }));
4596 }
4597 },
4598
4599 /** @private */
4600 handleFileBrowser: function(ev) {
4601 var button = ev.target,
4602 browser = button.nextElementSibling,
4603 path = this.stat ? this.stat.path.replace(/\/[^\/]+$/, '') : (this.options.initial_directory || this.options.root_directory);
4604
4605 if (path.indexOf(this.options.root_directory) != 0)
4606 path = this.options.root_directory;
4607
4608 ev.preventDefault();
4609
4610 return L.resolveDefault(fs.list(path), []).then(L.bind(function(button, browser, path, list) {
4611 document.querySelectorAll('.cbi-filebrowser.open').forEach(function(browserEl) {
4612 dom.findClassInstance(browserEl).handleCancel(ev);
4613 });
4614
4615 button.style.display = 'none';
4616 browser.classList.add('open');
4617
4618 return this.renderListing(browser, path, list);
4619 }, this, button, browser, path));
4620 },
4621
4622 /** @override */
4623 getValue: function() {
4624 return this.node.lastElementChild.value;
4625 },
4626
4627 /** @override */
4628 setValue: function(value) {
4629 this.node.lastElementChild.value = value;
4630 }
4631 });
4632
4633 /**
4634 * @class ui
4635 * @memberof LuCI
4636 * @hideconstructor
4637 * @classdesc
4638 *
4639 * Provides high level UI helper functionality.
4640 * To import the class in views, use `'require ui'`, to import it in
4641 * external JavaScript, use `L.require("ui").then(...)`.
4642 */
4643 var UI = baseclass.extend(/** @lends LuCI.ui.prototype */ {
4644 __init__: function() {
4645 modalDiv = document.body.appendChild(
4646 dom.create('div', { id: 'modal_overlay' },
4647 dom.create('div', { class: 'modal', role: 'dialog', 'aria-modal': true })));
4648
4649 tooltipDiv = document.body.appendChild(
4650 dom.create('div', { class: 'cbi-tooltip' }));
4651
4652 /* setup old aliases */
4653 L.showModal = this.showModal;
4654 L.hideModal = this.hideModal;
4655 L.showTooltip = this.showTooltip;
4656 L.hideTooltip = this.hideTooltip;
4657 L.itemlist = this.itemlist;
4658
4659 document.addEventListener('mouseover', this.showTooltip.bind(this), true);
4660 document.addEventListener('mouseout', this.hideTooltip.bind(this), true);
4661 document.addEventListener('focus', this.showTooltip.bind(this), true);
4662 document.addEventListener('blur', this.hideTooltip.bind(this), true);
4663
4664 document.addEventListener('luci-loaded', this.tabs.init.bind(this.tabs));
4665 document.addEventListener('luci-loaded', this.changes.init.bind(this.changes));
4666 document.addEventListener('uci-loaded', this.changes.init.bind(this.changes));
4667 },
4668
4669 /**
4670 * Display a modal overlay dialog with the specified contents.
4671 *
4672 * The modal overlay dialog covers the current view preventing interaction
4673 * with the underlying view contents. Only one modal dialog instance can
4674 * be opened. Invoking showModal() while a modal dialog is already open will
4675 * replace the open dialog with a new one having the specified contents.
4676 *
4677 * Additional CSS class names may be passed to influence the appearence of
4678 * the dialog. Valid values for the classes depend on the underlying theme.
4679 *
4680 * @see LuCI.dom.content
4681 *
4682 * @param {string} [title]
4683 * The title of the dialog. If `null`, no title element will be rendered.
4684 *
4685 * @param {*} contents
4686 * The contents to add to the modal dialog. This should be a DOM node or
4687 * a document fragment in most cases. The value is passed as-is to the
4688 * `dom.content()` function - refer to its documentation for applicable
4689 * values.
4690 *
4691 * @param {...string} [classes]
4692 * A number of extra CSS class names which are set on the modal dialog
4693 * element.
4694 *
4695 * @returns {Node}
4696 * Returns a DOM Node representing the modal dialog element.
4697 */
4698 showModal: function(title, children /* , ... */) {
4699 var dlg = modalDiv.firstElementChild;
4700
4701 dlg.setAttribute('class', 'modal');
4702
4703 for (var i = 2; i &lt; arguments.length; i++)
4704 dlg.classList.add(arguments[i]);
4705
4706 dom.content(dlg, dom.create('h4', {}, title));
4707 dom.append(dlg, children);
4708
4709 document.body.classList.add('modal-overlay-active');
4710
4711 return dlg;
4712 },
4713
4714 /**
4715 * Close the open modal overlay dialog.
4716 *
4717 * This function will close an open modal dialog and restore the normal view
4718 * behaviour. It has no effect if no modal dialog is currently open.
4719 *
4720 * Note that this function is stand-alone, it does not rely on `this` and
4721 * will not invoke other class functions so it suitable to be used as event
4722 * handler as-is without the need to bind it first.
4723 */
4724 hideModal: function() {
4725 document.body.classList.remove('modal-overlay-active');
4726 },
4727
4728 /** @private */
4729 showTooltip: function(ev) {
4730 var target = findParent(ev.target, '[data-tooltip]');
4731
4732 if (!target)
4733 return;
4734
4735 if (tooltipTimeout !== null) {
4736 window.clearTimeout(tooltipTimeout);
4737 tooltipTimeout = null;
4738 }
4739
4740 var rect = target.getBoundingClientRect(),
4741 x = rect.left + window.pageXOffset,
4742 y = rect.top + rect.height + window.pageYOffset;
4743
4744 tooltipDiv.className = 'cbi-tooltip';
4745 tooltipDiv.innerHTML = '▲ ';
4746 tooltipDiv.firstChild.data += target.getAttribute('data-tooltip');
4747
4748 if (target.hasAttribute('data-tooltip-style'))
4749 tooltipDiv.classList.add(target.getAttribute('data-tooltip-style'));
4750
4751 if ((y + tooltipDiv.offsetHeight) > (window.innerHeight + window.pageYOffset)) {
4752 y -= (tooltipDiv.offsetHeight + target.offsetHeight);
4753 tooltipDiv.firstChild.data = '▼ ' + tooltipDiv.firstChild.data.substr(2);
4754 }
4755
4756 tooltipDiv.style.top = y + 'px';
4757 tooltipDiv.style.left = x + 'px';
4758 tooltipDiv.style.opacity = 1;
4759
4760 tooltipDiv.dispatchEvent(new CustomEvent('tooltip-open', {
4761 bubbles: true,
4762 detail: { target: target }
4763 }));
4764 },
4765
4766 /** @private */
4767 hideTooltip: function(ev) {
4768 if (ev.target === tooltipDiv || ev.relatedTarget === tooltipDiv ||
4769 tooltipDiv.contains(ev.target) || tooltipDiv.contains(ev.relatedTarget))
4770 return;
4771
4772 if (tooltipTimeout !== null) {
4773 window.clearTimeout(tooltipTimeout);
4774 tooltipTimeout = null;
4775 }
4776
4777 tooltipDiv.style.opacity = 0;
4778 tooltipTimeout = window.setTimeout(function() { tooltipDiv.removeAttribute('style'); }, 250);
4779
4780 tooltipDiv.dispatchEvent(new CustomEvent('tooltip-close', { bubbles: true }));
4781 },
4782
4783 /**
4784 * Add a notification banner at the top of the current view.
4785 *
4786 * A notification banner is an alert message usually displayed at the
4787 * top of the current view, spanning the entire availibe width.
4788 * Notification banners will stay in place until dismissed by the user.
4789 * Multiple banners may be shown at the same time.
4790 *
4791 * Additional CSS class names may be passed to influence the appearence of
4792 * the banner. Valid values for the classes depend on the underlying theme.
4793 *
4794 * @see LuCI.dom.content
4795 *
4796 * @param {string} [title]
4797 * The title of the notification banner. If `null`, no title element
4798 * will be rendered.
4799 *
4800 * @param {*} contents
4801 * The contents to add to the notification banner. This should be a DOM
4802 * node or a document fragment in most cases. The value is passed as-is
4803 * to the `dom.content()` function - refer to its documentation for
4804 * applicable values.
4805 *
4806 * @param {...string} [classes]
4807 * A number of extra CSS class names which are set on the notification
4808 * banner element.
4809 *
4810 * @returns {Node}
4811 * Returns a DOM Node representing the notification banner element.
4812 */
4813 addNotification: function(title, children /*, ... */) {
4814 var mc = document.querySelector('#maincontent') || document.body;
4815 var msg = E('div', {
4816 'class': 'alert-message fade-in',
4817 'style': 'display:flex',
4818 'transitionend': function(ev) {
4819 var node = ev.currentTarget;
4820 if (node.parentNode &amp;&amp; node.classList.contains('fade-out'))
4821 node.parentNode.removeChild(node);
4822 }
4823 }, [
4824 E('div', { 'style': 'flex:10' }),
4825 E('div', { 'style': 'flex:1 1 auto; display:flex' }, [
4826 E('button', {
4827 'class': 'btn',
4828 'style': 'margin-left:auto; margin-top:auto',
4829 'click': function(ev) {
4830 dom.parent(ev.target, '.alert-message').classList.add('fade-out');
4831 },
4832
4833 }, [ _('Dismiss') ])
4834 ])
4835 ]);
4836
4837 if (title != null)
4838 dom.append(msg.firstElementChild, E('h4', {}, title));
4839
4840 dom.append(msg.firstElementChild, children);
4841
4842 for (var i = 2; i &lt; arguments.length; i++)
4843 msg.classList.add(arguments[i]);
4844
4845 mc.insertBefore(msg, mc.firstElementChild);
4846
4847 return msg;
4848 },
4849
4850 /**
4851 * Display or update an header area indicator.
4852 *
4853 * An indicator is a small label displayed in the header area of the screen
4854 * providing few amounts of status information such as item counts or state
4855 * toggle indicators.
4856 *
4857 * Multiple indicators may be shown at the same time and indicator labels
4858 * may be made clickable to display extended information or to initiate
4859 * further actions.
4860 *
4861 * Indicators can either use a default `active` or a less accented `inactive`
4862 * style which is useful for indicators representing state toggles.
4863 *
4864 * @param {string} id
4865 * The ID of the indicator. If an indicator with the given ID already exists,
4866 * it is updated with the given label and style.
4867 *
4868 * @param {string} label
4869 * The text to display in the indicator label.
4870 *
4871 * @param {function} [handler]
4872 * A handler function to invoke when the indicator label is clicked/touched
4873 * by the user. If omitted, the indicator is not clickable/touchable.
4874 *
4875 * Note that this parameter only applies to new indicators, when updating
4876 * existing labels it is ignored.
4877 *
4878 * @param {string} [style=active]
4879 * The indicator style to use. May be either `active` or `inactive`.
4880 *
4881 * @returns {boolean}
4882 * Returns `true` when the indicator has been updated or `false` when no
4883 * changes were made.
4884 */
4885 showIndicator: function(id, label, handler, style) {
4886 if (indicatorDiv == null) {
4887 indicatorDiv = document.body.querySelector('#indicators');
4888
4889 if (indicatorDiv == null)
4890 return false;
4891 }
4892
4893 var handlerFn = (typeof(handler) == 'function') ? handler : null,
4894 indicatorElem = indicatorDiv.querySelector('span[data-indicator="%s"]'.format(id)) ||
4895 indicatorDiv.appendChild(E('span', {
4896 'data-indicator': id,
4897 'data-clickable': handlerFn ? true : null,
4898 'click': handlerFn
4899 }, ['']));
4900
4901 if (label == indicatorElem.firstChild.data &amp;&amp; style == indicatorElem.getAttribute('data-style'))
4902 return false;
4903
4904 indicatorElem.firstChild.data = label;
4905 indicatorElem.setAttribute('data-style', (style == 'inactive') ? 'inactive' : 'active');
4906 return true;
4907 },
4908
4909 /**
4910 * Remove an header area indicator.
4911 *
4912 * This function removes the given indicator label from the header indicator
4913 * area. When the given indicator is not found, this function does nothing.
4914 *
4915 * @param {string} id
4916 * The ID of the indicator to remove.
4917 *
4918 * @returns {boolean}
4919 * Returns `true` when the indicator has been removed or `false` when the
4920 * requested indicator was not found.
4921 */
4922 hideIndicator: function(id) {
4923 var indicatorElem = indicatorDiv ? indicatorDiv.querySelector('span[data-indicator="%s"]'.format(id)) : null;
4924
4925 if (indicatorElem == null)
4926 return false;
4927
4928 indicatorDiv.removeChild(indicatorElem);
4929 return true;
4930 },
4931
4932 /**
4933 * Formats a series of label/value pairs into list-like markup.
4934 *
4935 * This function transforms a flat array of alternating label and value
4936 * elements into a list-like markup, using the values in `separators` as
4937 * separators and appends the resulting nodes to the given parent DOM node.
4938 *
4939 * Each label is suffixed with `: ` and wrapped into a `&lt;strong>` tag, the
4940 * `&lt;strong>` element and the value corresponding to the label are
4941 * subsequently wrapped into a `&lt;span class="nowrap">` element.
4942 *
4943 * The resulting `&lt;span>` element tuples are joined by the given separators
4944 * to form the final markup which is appened to the given parent DOM node.
4945 *
4946 * @param {Node} node
4947 * The parent DOM node to append the markup to. Any previous child elements
4948 * will be removed.
4949 *
4950 * @param {Array&lt;*>} items
4951 * An alternating array of labels and values. The label values will be
4952 * converted to plain strings, the values are used as-is and may be of
4953 * any type accepted by `LuCI.dom.content()`.
4954 *
4955 * @param {*|Array&lt;*>} [separators=[E('br')]]
4956 * A single value or an array of separator values to separate each
4957 * label/value pair with. The function will cycle through the separators
4958 * when joining the pairs. If omitted, the default separator is a sole HTML
4959 * `&lt;br>` element. Separator values are used as-is and may be of any type
4960 * accepted by `LuCI.dom.content()`.
4961 *
4962 * @returns {Node}
4963 * Returns the parent DOM node the formatted markup has been added to.
4964 */
4965 itemlist: function(node, items, separators) {
4966 var children = [];
4967
4968 if (!Array.isArray(separators))
4969 separators = [ separators || E('br') ];
4970
4971 for (var i = 0; i &lt; items.length; i += 2) {
4972 if (items[i+1] !== null &amp;&amp; items[i+1] !== undefined) {
4973 var sep = separators[(i/2) % separators.length],
4974 cld = [];
4975
4976 children.push(E('span', { class: 'nowrap' }, [
4977 items[i] ? E('strong', items[i] + ': ') : '',
4978 items[i+1]
4979 ]));
4980
4981 if ((i+2) &lt; items.length)
4982 children.push(dom.elem(sep) ? sep.cloneNode(true) : sep);
4983 }
4984 }
4985
4986 dom.content(node, children);
4987
4988 return node;
4989 },
4990
4991 /**
4992 * @class
4993 * @memberof LuCI.ui
4994 * @hideconstructor
4995 * @classdesc
4996 *
4997 * The `tabs` class handles tab menu groups used throughout the view area.
4998 * It takes care of setting up tab groups, tracking their state and handling
4999 * related events.
5000 *
5001 * This class is automatically instantiated as part of `LuCI.ui`. To use it
5002 * in views, use `'require ui'` and refer to `ui.tabs`. To import it in
5003 * external JavaScript, use `L.require("ui").then(...)` and access the
5004 * `tabs` property of the class instance value.
5005 */
5006 tabs: baseclass.singleton(/* @lends LuCI.ui.tabs.prototype */ {
5007 /** @private */
5008 init: function() {
5009 var groups = [], prevGroup = null, currGroup = null;
5010
5011 document.querySelectorAll('[data-tab]').forEach(function(tab) {
5012 var parent = tab.parentNode;
5013
5014 if (dom.matches(tab, 'li') &amp;&amp; dom.matches(parent, 'ul.cbi-tabmenu'))
5015 return;
5016
5017 if (!parent.hasAttribute('data-tab-group'))
5018 parent.setAttribute('data-tab-group', groups.length);
5019
5020 currGroup = +parent.getAttribute('data-tab-group');
5021
5022 if (currGroup !== prevGroup) {
5023 prevGroup = currGroup;
5024
5025 if (!groups[currGroup])
5026 groups[currGroup] = [];
5027 }
5028
5029 groups[currGroup].push(tab);
5030 });
5031
5032 for (var i = 0; i &lt; groups.length; i++)
5033 this.initTabGroup(groups[i]);
5034
5035 document.addEventListener('dependency-update', this.updateTabs.bind(this));
5036
5037 this.updateTabs();
5038 },
5039
5040 /**
5041 * Initializes a new tab group from the given tab pane collection.
5042 *
5043 * This function cycles through the given tab pane DOM nodes, extracts
5044 * their tab IDs, titles and active states, renders a corresponding
5045 * tab menu and prepends it to the tab panes common parent DOM node.
5046 *
5047 * The tab menu labels will be set to the value of the `data-tab-title`
5048 * attribute of each corresponding pane. The last pane with the
5049 * `data-tab-active` attribute set to `true` will be selected by default.
5050 *
5051 * If no pane is marked as active, the first one will be preselected.
5052 *
5053 * @instance
5054 * @memberof LuCI.ui.tabs
5055 * @param {Array&lt;Node>|NodeList} panes
5056 * A collection of tab panes to build a tab group menu for. May be a
5057 * plain array of DOM nodes or a NodeList collection, such as the result
5058 * of a `querySelectorAll()` call or the `.childNodes` property of a
5059 * DOM node.
5060 */
5061 initTabGroup: function(panes) {
5062 if (typeof(panes) != 'object' || !('length' in panes) || panes.length === 0)
5063 return;
5064
5065 var menu = E('ul', { 'class': 'cbi-tabmenu' }),
5066 group = panes[0].parentNode,
5067 groupId = +group.getAttribute('data-tab-group'),
5068 selected = null;
5069
5070 if (group.getAttribute('data-initialized') === 'true')
5071 return;
5072
5073 for (var i = 0, pane; pane = panes[i]; i++) {
5074 var name = pane.getAttribute('data-tab'),
5075 title = pane.getAttribute('data-tab-title'),
5076 active = pane.getAttribute('data-tab-active') === 'true';
5077
5078 menu.appendChild(E('li', {
5079 'style': this.isEmptyPane(pane) ? 'display:none' : null,
5080 'class': active ? 'cbi-tab' : 'cbi-tab-disabled',
5081 'data-tab': name
5082 }, E('a', {
5083 'href': '#',
5084 'click': this.switchTab.bind(this)
5085 }, title)));
5086
5087 if (active)
5088 selected = i;
5089 }
5090
5091 group.parentNode.insertBefore(menu, group);
5092 group.setAttribute('data-initialized', true);
5093
5094 if (selected === null) {
5095 selected = this.getActiveTabId(panes[0]);
5096
5097 if (selected &lt; 0 || selected >= panes.length || this.isEmptyPane(panes[selected])) {
5098 for (var i = 0; i &lt; panes.length; i++) {
5099 if (!this.isEmptyPane(panes[i])) {
5100 selected = i;
5101 break;
5102 }
5103 }
5104 }
5105
5106 menu.childNodes[selected].classList.add('cbi-tab');
5107 menu.childNodes[selected].classList.remove('cbi-tab-disabled');
5108 panes[selected].setAttribute('data-tab-active', 'true');
5109
5110 this.setActiveTabId(panes[selected], selected);
5111 }
5112
5113 panes[selected].dispatchEvent(new CustomEvent('cbi-tab-active', {
5114 detail: { tab: panes[selected].getAttribute('data-tab') }
5115 }));
5116
5117 this.updateTabs(group);
5118 },
5119
5120 /**
5121 * Checks whether the given tab pane node is empty.
5122 *
5123 * @instance
5124 * @memberof LuCI.ui.tabs
5125 * @param {Node} pane
5126 * The tab pane to check.
5127 *
5128 * @returns {boolean}
5129 * Returns `true` if the pane is empty, else `false`.
5130 */
5131 isEmptyPane: function(pane) {
5132 return dom.isEmpty(pane, function(n) { return n.classList.contains('cbi-tab-descr') });
5133 },
5134
5135 /** @private */
5136 getPathForPane: function(pane) {
5137 var path = [], node = null;
5138
5139 for (node = pane ? pane.parentNode : null;
5140 node != null &amp;&amp; node.hasAttribute != null;
5141 node = node.parentNode)
5142 {
5143 if (node.hasAttribute('data-tab'))
5144 path.unshift(node.getAttribute('data-tab'));
5145 else if (node.hasAttribute('data-section-id'))
5146 path.unshift(node.getAttribute('data-section-id'));
5147 }
5148
5149 return path.join('/');
5150 },
5151
5152 /** @private */
5153 getActiveTabState: function() {
5154 var page = document.body.getAttribute('data-page');
5155
5156 try {
5157 var val = JSON.parse(window.sessionStorage.getItem('tab'));
5158 if (val.page === page &amp;&amp; L.isObject(val.paths))
5159 return val;
5160 }
5161 catch(e) {}
5162
5163 window.sessionStorage.removeItem('tab');
5164 return { page: page, paths: {} };
5165 },
5166
5167 /** @private */
5168 getActiveTabId: function(pane) {
5169 var path = this.getPathForPane(pane);
5170 return +this.getActiveTabState().paths[path] || 0;
5171 },
5172
5173 /** @private */
5174 setActiveTabId: function(pane, tabIndex) {
5175 var path = this.getPathForPane(pane);
5176
5177 try {
5178 var state = this.getActiveTabState();
5179 state.paths[path] = tabIndex;
5180
5181 window.sessionStorage.setItem('tab', JSON.stringify(state));
5182 }
5183 catch (e) { return false; }
5184
5185 return true;
5186 },
5187
5188 /** @private */
5189 updateTabs: function(ev, root) {
5190 (root || document).querySelectorAll('[data-tab-title]').forEach(L.bind(function(pane) {
5191 var menu = pane.parentNode.previousElementSibling,
5192 tab = menu ? menu.querySelector('[data-tab="%s"]'.format(pane.getAttribute('data-tab'))) : null,
5193 n_errors = pane.querySelectorAll('.cbi-input-invalid').length;
5194
5195 if (!menu || !tab)
5196 return;
5197
5198 if (this.isEmptyPane(pane)) {
5199 tab.style.display = 'none';
5200 tab.classList.remove('flash');
5201 }
5202 else if (tab.style.display === 'none') {
5203 tab.style.display = '';
5204 requestAnimationFrame(function() { tab.classList.add('flash') });
5205 }
5206
5207 if (n_errors) {
5208 tab.setAttribute('data-errors', n_errors);
5209 tab.setAttribute('data-tooltip', _('%d invalid field(s)').format(n_errors));
5210 tab.setAttribute('data-tooltip-style', 'error');
5211 }
5212 else {
5213 tab.removeAttribute('data-errors');
5214 tab.removeAttribute('data-tooltip');
5215 }
5216 }, this));
5217 },
5218
5219 /** @private */
5220 switchTab: function(ev) {
5221 var tab = ev.target.parentNode,
5222 name = tab.getAttribute('data-tab'),
5223 menu = tab.parentNode,
5224 group = menu.nextElementSibling,
5225 groupId = +group.getAttribute('data-tab-group'),
5226 index = 0;
5227
5228 ev.preventDefault();
5229
5230 if (!tab.classList.contains('cbi-tab-disabled'))
5231 return;
5232
5233 menu.querySelectorAll('[data-tab]').forEach(function(tab) {
5234 tab.classList.remove('cbi-tab');
5235 tab.classList.remove('cbi-tab-disabled');
5236 tab.classList.add(
5237 tab.getAttribute('data-tab') === name ? 'cbi-tab' : 'cbi-tab-disabled');
5238 });
5239
5240 group.childNodes.forEach(function(pane) {
5241 if (dom.matches(pane, '[data-tab]')) {
5242 if (pane.getAttribute('data-tab') === name) {
5243 pane.setAttribute('data-tab-active', 'true');
5244 pane.dispatchEvent(new CustomEvent('cbi-tab-active', { detail: { tab: name } }));
5245 UI.prototype.tabs.setActiveTabId(pane, index);
5246 }
5247 else {
5248 pane.setAttribute('data-tab-active', 'false');
5249 }
5250
5251 index++;
5252 }
5253 });
5254 }
5255 }),
5256
5257 /**
5258 * @typedef {Object} FileUploadReply
5259 * @memberof LuCI.ui
5260
5261 * @property {string} name - Name of the uploaded file without directory components
5262 * @property {number} size - Size of the uploaded file in bytes
5263 * @property {string} checksum - The MD5 checksum of the received file data
5264 * @property {string} sha256sum - The SHA256 checksum of the received file data
5265 */
5266
5267 /**
5268 * Display a modal file upload prompt.
5269 *
5270 * This function opens a modal dialog prompting the user to select and
5271 * upload a file to a predefined remote destination path.
5272 *
5273 * @param {string} path
5274 * The remote file path to upload the local file to.
5275 *
5276 * @param {Node} [progessStatusNode]
5277 * An optional DOM text node whose content text is set to the progress
5278 * percentage value during file upload.
5279 *
5280 * @returns {Promise&lt;LuCI.ui.FileUploadReply>}
5281 * Returns a promise resolving to a file upload status object on success
5282 * or rejecting with an error in case the upload failed or has been
5283 * cancelled by the user.
5284 */
5285 uploadFile: function(path, progressStatusNode) {
5286 return new Promise(function(resolveFn, rejectFn) {
5287 UI.prototype.showModal(_('Uploading file…'), [
5288 E('p', _('Please select the file to upload.')),
5289 E('div', { 'style': 'display:flex' }, [
5290 E('div', { 'class': 'left', 'style': 'flex:1' }, [
5291 E('input', {
5292 type: 'file',
5293 style: 'display:none',
5294 change: function(ev) {
5295 var modal = dom.parent(ev.target, '.modal'),
5296 body = modal.querySelector('p'),
5297 upload = modal.querySelector('.cbi-button-action.important'),
5298 file = ev.currentTarget.files[0];
5299
5300 if (file == null)
5301 return;
5302
5303 dom.content(body, [
5304 E('ul', {}, [
5305 E('li', {}, [ '%s: %s'.format(_('Name'), file.name.replace(/^.*[\\\/]/, '')) ]),
5306 E('li', {}, [ '%s: %1024mB'.format(_('Size'), file.size) ])
5307 ])
5308 ]);
5309
5310 upload.disabled = false;
5311 upload.focus();
5312 }
5313 }),
5314 E('button', {
5315 'class': 'btn',
5316 'click': function(ev) {
5317 ev.target.previousElementSibling.click();
5318 }
5319 }, [ _('Browse…') ])
5320 ]),
5321 E('div', { 'class': 'right', 'style': 'flex:1' }, [
5322 E('button', {
5323 'class': 'btn',
5324 'click': function() {
5325 UI.prototype.hideModal();
5326 rejectFn(new Error('Upload has been cancelled'));
5327 }
5328 }, [ _('Cancel') ]),
5329 ' ',
5330 E('button', {
5331 'class': 'btn cbi-button-action important',
5332 'disabled': true,
5333 'click': function(ev) {
5334 var input = dom.parent(ev.target, '.modal').querySelector('input[type="file"]');
5335
5336 if (!input.files[0])
5337 return;
5338
5339 var progress = E('div', { 'class': 'cbi-progressbar', 'title': '0%' }, E('div', { 'style': 'width:0' }));
5340
5341 UI.prototype.showModal(_('Uploading file…'), [ progress ]);
5342
5343 var data = new FormData();
5344
5345 data.append('sessionid', rpc.getSessionID());
5346 data.append('filename', path);
5347 data.append('filedata', input.files[0]);
5348
5349 var filename = input.files[0].name;
5350
5351 request.post(L.env.cgi_base + '/cgi-upload', data, {
5352 timeout: 0,
5353 progress: function(pev) {
5354 var percent = (pev.loaded / pev.total) * 100;
5355
5356 if (progressStatusNode)
5357 progressStatusNode.data = '%.2f%%'.format(percent);
5358
5359 progress.setAttribute('title', '%.2f%%'.format(percent));
5360 progress.firstElementChild.style.width = '%.2f%%'.format(percent);
5361 }
5362 }).then(function(res) {
5363 var reply = res.json();
5364
5365 UI.prototype.hideModal();
5366
5367 if (L.isObject(reply) &amp;&amp; reply.failure) {
5368 UI.prototype.addNotification(null, E('p', _('Upload request failed: %s').format(reply.message)));
5369 rejectFn(new Error(reply.failure));
5370 }
5371 else {
5372 reply.name = filename;
5373 resolveFn(reply);
5374 }
5375 }, function(err) {
5376 UI.prototype.hideModal();
5377 rejectFn(err);
5378 });
5379 }
5380 }, [ _('Upload') ])
5381 ])
5382 ])
5383 ]);
5384 });
5385 },
5386
5387 /**
5388 * Perform a device connectivity test.
5389 *
5390 * Attempt to fetch a well known ressource from the remote device via HTTP
5391 * in order to test connectivity. This function is mainly useful to wait
5392 * for the router to come back online after a reboot or reconfiguration.
5393 *
5394 * @param {string} [proto=http]
5395 * The protocol to use for fetching the resource. May be either `http`
5396 * (the default) or `https`.
5397 *
5398 * @param {string} [host=window.location.host]
5399 * Override the host address to probe. By default the current host as seen
5400 * in the address bar is probed.
5401 *
5402 * @returns {Promise&lt;Event>}
5403 * Returns a promise resolving to a `load` event in case the device is
5404 * reachable or rejecting with an `error` event in case it is not reachable
5405 * or rejecting with `null` when the connectivity check timed out.
5406 */
5407 pingDevice: function(proto, ipaddr) {
5408 var target = '%s://%s%s?%s'.format(proto || 'http', ipaddr || window.location.host, L.resource('icons/loading.gif'), Math.random());
5409
5410 return new Promise(function(resolveFn, rejectFn) {
5411 var img = new Image();
5412
5413 img.onload = resolveFn;
5414 img.onerror = rejectFn;
5415
5416 window.setTimeout(rejectFn, 1000);
5417
5418 img.src = target;
5419 });
5420 },
5421
5422 /**
5423 * Wait for device to come back online and reconnect to it.
5424 *
5425 * Poll each given hostname or IP address and navigate to it as soon as
5426 * one of the addresses becomes reachable.
5427 *
5428 * @param {...string} [hosts=[window.location.host]]
5429 * The list of IP addresses and host names to check for reachability.
5430 * If omitted, the current value of `window.location.host` is used by
5431 * default.
5432 */
5433 awaitReconnect: function(/* ... */) {
5434 var ipaddrs = arguments.length ? arguments : [ window.location.host ];
5435
5436 window.setTimeout(L.bind(function() {
5437 poll.add(L.bind(function() {
5438 var tasks = [], reachable = false;
5439
5440 for (var i = 0; i &lt; 2; i++)
5441 for (var j = 0; j &lt; ipaddrs.length; j++)
5442 tasks.push(this.pingDevice(i ? 'https' : 'http', ipaddrs[j])
5443 .then(function(ev) { reachable = ev.target.src.replace(/^(https?:\/\/[^\/]+).*$/, '$1/') }, function() {}));
5444
5445 return Promise.all(tasks).then(function() {
5446 if (reachable) {
5447 poll.stop();
5448 window.location = reachable;
5449 }
5450 });
5451 }, this));
5452 }, this), 5000);
5453 },
5454
5455 /**
5456 * @class
5457 * @memberof LuCI.ui
5458 * @hideconstructor
5459 * @classdesc
5460 *
5461 * The `changes` class encapsulates logic for visualizing, applying,
5462 * confirming and reverting staged UCI changesets.
5463 *
5464 * This class is automatically instantiated as part of `LuCI.ui`. To use it
5465 * in views, use `'require ui'` and refer to `ui.changes`. To import it in
5466 * external JavaScript, use `L.require("ui").then(...)` and access the
5467 * `changes` property of the class instance value.
5468 */
5469 changes: baseclass.singleton(/* @lends LuCI.ui.changes.prototype */ {
5470 init: function() {
5471 if (!L.env.sessionid)
5472 return;
5473
5474 return uci.changes().then(L.bind(this.renderChangeIndicator, this));
5475 },
5476
5477 /**
5478 * Set the change count indicator.
5479 *
5480 * This function updates or hides the UCI change count indicator,
5481 * depending on the passed change count. When the count is greater
5482 * than 0, the change indicator is displayed or updated, otherwise it
5483 * is removed.
5484 *
5485 * @instance
5486 * @memberof LuCI.ui.changes
5487 * @param {number} numChanges
5488 * The number of changes to indicate.
5489 */
5490 setIndicator: function(n) {
5491 var i = document.querySelector('.uci_change_indicator');
5492 if (i == null) {
5493 var poll = document.getElementById('xhr_poll_status');
5494 i = poll.parentNode.insertBefore(E('a', {
5495 'href': '#',
5496 'class': 'uci_change_indicator label notice',
5497 'click': L.bind(this.displayChanges, this)
5498 }), poll);
5499 }
5500
5501 if (n > 0) {
5502 dom.content(i, [ _('Unsaved Changes'), ': ', n ]);
5503 i.classList.add('flash');
5504 i.style.display = '';
5505 document.dispatchEvent(new CustomEvent('uci-new-changes'));
5506 }
5507 else {
5508 i.classList.remove('flash');
5509 i.style.display = 'none';
5510 document.dispatchEvent(new CustomEvent('uci-clear-changes'));
5511 }
5512 },
5513
5514 /**
5515 * Update the change count indicator.
5516 *
5517 * This function updates the UCI change count indicator from the given
5518 * UCI changeset structure.
5519 *
5520 * @instance
5521 * @memberof LuCI.ui.changes
5522 * @param {Object&lt;string, Array&lt;LuCI.uci.ChangeRecord>>} changes
5523 * The UCI changeset to count.
5524 */
5525 renderChangeIndicator: function(changes) {
5526 var n_changes = 0;
5527
5528 for (var config in changes)
5529 if (changes.hasOwnProperty(config))
5530 n_changes += changes[config].length;
5531
5532 this.changes = changes;
5533 this.setIndicator(n_changes);
5534 },
5535
5536 /** @private */
5537 changeTemplates: {
5538 'add-3': '&lt;ins>uci add %0 &lt;strong>%3&lt;/strong> # =%2&lt;/ins>',
5539 'set-3': '&lt;ins>uci set %0.&lt;strong>%2&lt;/strong>=%3&lt;/ins>',
5540 'set-4': '&lt;var>&lt;ins>uci set %0.%2.%3=&lt;strong>%4&lt;/strong>&lt;/ins>&lt;/var>',
5541 'remove-2': '&lt;del>uci del %0.&lt;strong>%2&lt;/strong>&lt;/del>',
5542 'remove-3': '&lt;var>&lt;del>uci del %0.%2.&lt;strong>%3&lt;/strong>&lt;/del>&lt;/var>',
5543 'order-3': '&lt;var>uci reorder %0.%2=&lt;strong>%3&lt;/strong>&lt;/var>',
5544 'list-add-4': '&lt;var>&lt;ins>uci add_list %0.%2.%3=&lt;strong>%4&lt;/strong>&lt;/ins>&lt;/var>',
5545 'list-del-4': '&lt;var>&lt;del>uci del_list %0.%2.%3=&lt;strong>%4&lt;/strong>&lt;/del>&lt;/var>',
5546 'rename-3': '&lt;var>uci rename %0.%2=&lt;strong>%3&lt;/strong>&lt;/var>',
5547 'rename-4': '&lt;var>uci rename %0.%2.%3=&lt;strong>%4&lt;/strong>&lt;/var>'
5548 },
5549
5550 /**
5551 * Display the current changelog.
5552 *
5553 * Open a modal dialog visualizing the currently staged UCI changes
5554 * and offer options to revert or apply the shown changes.
5555 *
5556 * @instance
5557 * @memberof LuCI.ui.changes
5558 */
5559 displayChanges: function() {
5560 var list = E('div', { 'class': 'uci-change-list' }),
5561 dlg = UI.prototype.showModal(_('Configuration') + ' / ' + _('Changes'), [
5562 E('div', { 'class': 'cbi-section' }, [
5563 E('strong', _('Legend:')),
5564 E('div', { 'class': 'uci-change-legend' }, [
5565 E('div', { 'class': 'uci-change-legend-label' }, [
5566 E('ins', '&amp;#160;'), ' ', _('Section added') ]),
5567 E('div', { 'class': 'uci-change-legend-label' }, [
5568 E('del', '&amp;#160;'), ' ', _('Section removed') ]),
5569 E('div', { 'class': 'uci-change-legend-label' }, [
5570 E('var', {}, E('ins', '&amp;#160;')), ' ', _('Option changed') ]),
5571 E('div', { 'class': 'uci-change-legend-label' }, [
5572 E('var', {}, E('del', '&amp;#160;')), ' ', _('Option removed') ])]),
5573 E('br'), list,
5574 E('div', { 'class': 'right' }, [
5575 E('button', {
5576 'class': 'btn',
5577 'click': UI.prototype.hideModal
5578 }, [ _('Dismiss') ]), ' ',
5579 E('button', {
5580 'class': 'cbi-button cbi-button-positive important',
5581 'click': L.bind(this.apply, this, true)
5582 }, [ _('Save &amp; Apply') ]), ' ',
5583 E('button', {
5584 'class': 'cbi-button cbi-button-reset',
5585 'click': L.bind(this.revert, this)
5586 }, [ _('Revert') ])])])
5587 ]);
5588
5589 for (var config in this.changes) {
5590 if (!this.changes.hasOwnProperty(config))
5591 continue;
5592
5593 list.appendChild(E('h5', '# /etc/config/%s'.format(config)));
5594
5595 for (var i = 0, added = null; i &lt; this.changes[config].length; i++) {
5596 var chg = this.changes[config][i],
5597 tpl = this.changeTemplates['%s-%d'.format(chg[0], chg.length)];
5598
5599 list.appendChild(E(tpl.replace(/%([01234])/g, function(m0, m1) {
5600 switch (+m1) {
5601 case 0:
5602 return config;
5603
5604 case 2:
5605 if (added != null &amp;&amp; chg[1] == added[0])
5606 return '@' + added[1] + '[-1]';
5607 else
5608 return chg[1];
5609
5610 case 4:
5611 return "'%h'".format(chg[3].replace(/'/g, "'\"'\"'"));
5612
5613 default:
5614 return chg[m1-1];
5615 }
5616 })));
5617
5618 if (chg[0] == 'add')
5619 added = [ chg[1], chg[2] ];
5620 }
5621 }
5622
5623 list.appendChild(E('br'));
5624 dlg.classList.add('uci-dialog');
5625 },
5626
5627 /** @private */
5628 displayStatus: function(type, content) {
5629 if (type) {
5630 var message = UI.prototype.showModal('', '');
5631
5632 message.classList.add('alert-message');
5633 DOMTokenList.prototype.add.apply(message.classList, type.split(/\s+/));
5634
5635 if (content)
5636 dom.content(message, content);
5637
5638 if (!this.was_polling) {
5639 this.was_polling = request.poll.active();
5640 request.poll.stop();
5641 }
5642 }
5643 else {
5644 UI.prototype.hideModal();
5645
5646 if (this.was_polling)
5647 request.poll.start();
5648 }
5649 },
5650
5651 /** @private */
5652 rollback: function(checked) {
5653 if (checked) {
5654 this.displayStatus('warning spinning',
5655 E('p', _('Failed to confirm apply within %ds, waiting for rollback…')
5656 .format(L.env.apply_rollback)));
5657
5658 var call = function(r, data, duration) {
5659 if (r.status === 204) {
5660 UI.prototype.changes.displayStatus('warning', [
5661 E('h4', _('Configuration changes have been rolled back!')),
5662 E('p', _('The device could not be reached within %d seconds after applying the pending changes, which caused the configuration to be rolled back for safety reasons. If you believe that the configuration changes are correct nonetheless, perform an unchecked configuration apply. Alternatively, you can dismiss this warning and edit changes before attempting to apply again, or revert all pending changes to keep the currently working configuration state.').format(L.env.apply_rollback)),
5663 E('div', { 'class': 'right' }, [
5664 E('button', {
5665 'class': 'btn',
5666 'click': L.bind(UI.prototype.changes.displayStatus, UI.prototype.changes, false)
5667 }, [ _('Dismiss') ]), ' ',
5668 E('button', {
5669 'class': 'btn cbi-button-action important',
5670 'click': L.bind(UI.prototype.changes.revert, UI.prototype.changes)
5671 }, [ _('Revert changes') ]), ' ',
5672 E('button', {
5673 'class': 'btn cbi-button-negative important',
5674 'click': L.bind(UI.prototype.changes.apply, UI.prototype.changes, false)
5675 }, [ _('Apply unchecked') ])
5676 ])
5677 ]);
5678
5679 return;
5680 }
5681
5682 var delay = isNaN(duration) ? 0 : Math.max(1000 - duration, 0);
5683 window.setTimeout(function() {
5684 request.request(L.url('admin/uci/confirm'), {
5685 method: 'post',
5686 timeout: L.env.apply_timeout * 1000,
5687 query: { sid: L.env.sessionid, token: L.env.token }
5688 }).then(call);
5689 }, delay);
5690 };
5691
5692 call({ status: 0 });
5693 }
5694 else {
5695 this.displayStatus('warning', [
5696 E('h4', _('Device unreachable!')),
5697 E('p', _('Could not regain access to the device after applying the configuration changes. You might need to reconnect if you modified network related settings such as the IP address or wireless security credentials.'))
5698 ]);
5699 }
5700 },
5701
5702 /** @private */
5703 confirm: function(checked, deadline, override_token) {
5704 var tt;
5705 var ts = Date.now();
5706
5707 this.displayStatus('notice');
5708
5709 if (override_token)
5710 this.confirm_auth = { token: override_token };
5711
5712 var call = function(r, data, duration) {
5713 if (Date.now() >= deadline) {
5714 window.clearTimeout(tt);
5715 UI.prototype.changes.rollback(checked);
5716 return;
5717 }
5718 else if (r &amp;&amp; (r.status === 200 || r.status === 204)) {
5719 document.dispatchEvent(new CustomEvent('uci-applied'));
5720
5721 UI.prototype.changes.setIndicator(0);
5722 UI.prototype.changes.displayStatus('notice',
5723 E('p', _('Configuration changes applied.')));
5724
5725 window.clearTimeout(tt);
5726 window.setTimeout(function() {
5727 //UI.prototype.changes.displayStatus(false);
5728 window.location = window.location.href.split('#')[0];
5729 }, L.env.apply_display * 1000);
5730
5731 return;
5732 }
5733
5734 var delay = isNaN(duration) ? 0 : Math.max(1000 - duration, 0);
5735 window.setTimeout(function() {
5736 request.request(L.url('admin/uci/confirm'), {
5737 method: 'post',
5738 timeout: L.env.apply_timeout * 1000,
5739 query: UI.prototype.changes.confirm_auth
5740 }).then(call, call);
5741 }, delay);
5742 };
5743
5744 var tick = function() {
5745 var now = Date.now();
5746
5747 UI.prototype.changes.displayStatus('notice spinning',
5748 E('p', _('Applying configuration changes… %ds')
5749 .format(Math.max(Math.floor((deadline - Date.now()) / 1000), 0))));
5750
5751 if (now >= deadline)
5752 return;
5753
5754 tt = window.setTimeout(tick, 1000 - (now - ts));
5755 ts = now;
5756 };
5757
5758 tick();
5759
5760 /* wait a few seconds for the settings to become effective */
5761 window.setTimeout(call, Math.max(L.env.apply_holdoff * 1000 - ((ts + L.env.apply_rollback * 1000) - deadline), 1));
5762 },
5763
5764 /**
5765 * Apply the staged configuration changes.
5766 *
5767 * Start applying staged configuration changes and open a modal dialog
5768 * with a progress indication to prevent interaction with the view
5769 * during the apply process. The modal dialog will be automatically
5770 * closed and the current view reloaded once the apply process is
5771 * complete.
5772 *
5773 * @instance
5774 * @memberof LuCI.ui.changes
5775 * @param {boolean} [checked=false]
5776 * Whether to perform a checked (`true`) configuration apply or an
5777 * unchecked (`false`) one.
5778
5779 * In case of a checked apply, the configuration changes must be
5780 * confirmed within a specific time interval, otherwise the device
5781 * will begin to roll back the changes in order to restore the previous
5782 * settings.
5783 */
5784 apply: function(checked) {
5785 this.displayStatus('notice spinning',
5786 E('p', _('Starting configuration apply…')));
5787
5788 request.request(L.url('admin/uci', checked ? 'apply_rollback' : 'apply_unchecked'), {
5789 method: 'post',
5790 query: { sid: L.env.sessionid, token: L.env.token }
5791 }).then(function(r) {
5792 if (r.status === (checked ? 200 : 204)) {
5793 var tok = null; try { tok = r.json(); } catch(e) {}
5794 if (checked &amp;&amp; tok !== null &amp;&amp; typeof(tok) === 'object' &amp;&amp; typeof(tok.token) === 'string')
5795 UI.prototype.changes.confirm_auth = tok;
5796
5797 UI.prototype.changes.confirm(checked, Date.now() + L.env.apply_rollback * 1000);
5798 }
5799 else if (checked &amp;&amp; r.status === 204) {
5800 UI.prototype.changes.displayStatus('notice',
5801 E('p', _('There are no changes to apply')));
5802
5803 window.setTimeout(function() {
5804 UI.prototype.changes.displayStatus(false);
5805 }, L.env.apply_display * 1000);
5806 }
5807 else {
5808 UI.prototype.changes.displayStatus('warning',
5809 E('p', _('Apply request failed with status &lt;code>%h&lt;/code>')
5810 .format(r.responseText || r.statusText || r.status)));
5811
5812 window.setTimeout(function() {
5813 UI.prototype.changes.displayStatus(false);
5814 }, L.env.apply_display * 1000);
5815 }
5816 });
5817 },
5818
5819 /**
5820 * Revert the staged configuration changes.
5821 *
5822 * Start reverting staged configuration changes and open a modal dialog
5823 * with a progress indication to prevent interaction with the view
5824 * during the revert process. The modal dialog will be automatically
5825 * closed and the current view reloaded once the revert process is
5826 * complete.
5827 *
5828 * @instance
5829 * @memberof LuCI.ui.changes
5830 */
5831 revert: function() {
5832 this.displayStatus('notice spinning',
5833 E('p', _('Reverting configuration…')));
5834
5835 request.request(L.url('admin/uci/revert'), {
5836 method: 'post',
5837 query: { sid: L.env.sessionid, token: L.env.token }
5838 }).then(function(r) {
5839 if (r.status === 200) {
5840 document.dispatchEvent(new CustomEvent('uci-reverted'));
5841
5842 UI.prototype.changes.setIndicator(0);
5843 UI.prototype.changes.displayStatus('notice',
5844 E('p', _('Changes have been reverted.')));
5845
5846 window.setTimeout(function() {
5847 //UI.prototype.changes.displayStatus(false);
5848 window.location = window.location.href.split('#')[0];
5849 }, L.env.apply_display * 1000);
5850 }
5851 else {
5852 UI.prototype.changes.displayStatus('warning',
5853 E('p', _('Revert request failed with status &lt;code>%h&lt;/code>')
5854 .format(r.statusText || r.status)));
5855
5856 window.setTimeout(function() {
5857 UI.prototype.changes.displayStatus(false);
5858 }, L.env.apply_display * 1000);
5859 }
5860 });
5861 }
5862 }),
5863
5864 /**
5865 * Add validation constraints to an input element.
5866 *
5867 * Compile the given type expression and optional validator function into
5868 * a validation function and bind it to the specified input element events.
5869 *
5870 * @param {Node} field
5871 * The DOM input element node to bind the validation constraints to.
5872 *
5873 * @param {string} type
5874 * The datatype specification to describe validation constraints.
5875 * Refer to the `LuCI.validation` class documentation for details.
5876 *
5877 * @param {boolean} [optional=false]
5878 * Specifies whether empty values are allowed (`true`) or not (`false`).
5879 * If an input element is not marked optional it must not be empty,
5880 * otherwise it will be marked as invalid.
5881 *
5882 * @param {function} [vfunc]
5883 * Specifies a custom validation function which is invoked after the
5884 * other validation constraints are applied. The validation must return
5885 * `true` to accept the passed value. Any other return type is converted
5886 * to a string and treated as validation error message.
5887 *
5888 * @param {...string} [events=blur, keyup]
5889 * The list of events to bind. Each received event will trigger a field
5890 * validation. If omitted, the `keyup` and `blur` events are bound by
5891 * default.
5892 *
5893 * @returns {function}
5894 * Returns the compiled validator function which can be used to manually
5895 * trigger field validation or to bind it to further events.
5896 *
5897 * @see LuCI.validation
5898 */
5899 addValidator: function(field, type, optional, vfunc /*, ... */) {
5900 if (type == null)
5901 return;
5902
5903 var events = this.varargs(arguments, 3);
5904 if (events.length == 0)
5905 events.push('blur', 'keyup');
5906
5907 try {
5908 var cbiValidator = validation.create(field, type, optional, vfunc),
5909 validatorFn = cbiValidator.validate.bind(cbiValidator);
5910
5911 for (var i = 0; i &lt; events.length; i++)
5912 field.addEventListener(events[i], validatorFn);
5913
5914 validatorFn();
5915
5916 return validatorFn;
5917 }
5918 catch (e) { }
5919 },
5920
5921 /**
5922 * Create a pre-bound event handler function.
5923 *
5924 * Generate and bind a function suitable for use in event handlers. The
5925 * generated function automatically disables the event source element
5926 * and adds an active indication to it by adding appropriate CSS classes.
5927 *
5928 * It will also await any promises returned by the wrapped function and
5929 * re-enable the source element after the promises ran to completion.
5930 *
5931 * @param {*} ctx
5932 * The `this` context to use for the wrapped function.
5933 *
5934 * @param {function|string} fn
5935 * Specifies the function to wrap. In case of a function value, the
5936 * function is used as-is. If a string is specified instead, it is looked
5937 * up in `ctx` to obtain the function to wrap. In both cases the bound
5938 * function will be invoked with `ctx` as `this` context
5939 *
5940 * @param {...*} extra_args
5941 * Any further parameter as passed as-is to the bound event handler
5942 * function in the same order as passed to `createHandlerFn()`.
5943 *
5944 * @returns {function|null}
5945 * Returns the pre-bound handler function which is suitable to be passed
5946 * to `addEventListener()`. Returns `null` if the given `fn` argument is
5947 * a string which could not be found in `ctx` or if `ctx[fn]` is not a
5948 * valid function value.
5949 */
5950 createHandlerFn: function(ctx, fn /*, ... */) {
5951 if (typeof(fn) == 'string')
5952 fn = ctx[fn];
5953
5954 if (typeof(fn) != 'function')
5955 return null;
5956
5957 var arg_offset = arguments.length - 2;
5958
5959 return Function.prototype.bind.apply(function() {
5960 var t = arguments[arg_offset].currentTarget;
5961
5962 t.classList.add('spinning');
5963 t.disabled = true;
5964
5965 if (t.blur)
5966 t.blur();
5967
5968 Promise.resolve(fn.apply(ctx, arguments)).finally(function() {
5969 t.classList.remove('spinning');
5970 t.disabled = false;
5971 });
5972 }, this.varargs(arguments, 2, ctx));
5973 },
5974
5975 /**
5976 * Load specified view class path and set it up.
5977 *
5978 * Transforms the given view path into a class name, requires it
5979 * using [LuCI.require()]{@link LuCI#require} and asserts that the
5980 * resulting class instance is a descendant of
5981 * [LuCI.view]{@link LuCI.view}.
5982 *
5983 * By instantiating the view class, its corresponding contents are
5984 * rendered and included into the view area. Any runtime errors are
5985 * catched and rendered using [LuCI.error()]{@link LuCI#error}.
5986 *
5987 * @param {string} path
5988 * The view path to render.
5989 *
5990 * @returns {Promise&lt;LuCI.view>}
5991 * Returns a promise resolving to the loaded view instance.
5992 */
5993 instantiateView: function(path) {
5994 var className = 'view.%s'.format(path.replace(/\//g, '.'));
5995
5996 return L.require(className).then(function(view) {
5997 if (!(view instanceof View))
5998 throw new TypeError('Loaded class %s is not a descendant of View'.format(className));
5999
6000 return view;
6001 }).catch(function(err) {
6002 dom.content(document.querySelector('#view'), null);
6003 L.error(err);
6004 });
6005 },
6006
6007 AbstractElement: UIElement,
6008
6009 /* Widgets */
6010 Textfield: UITextfield,
6011 Textarea: UITextarea,
6012 Checkbox: UICheckbox,
6013 Select: UISelect,
6014 Dropdown: UIDropdown,
6015 DynamicList: UIDynamicList,
6016 Combobox: UICombobox,
6017 ComboButton: UIComboButton,
6018 Hiddenfield: UIHiddenfield,
6019 FileUpload: UIFileUpload
6020 });
6021
6022 return UI;
6023 </code></pre>
6024 </article>
6025 </section>
6026
6027
6028
6029
6030
6031
6032
6033
6034 <footer>
6035 Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 3.6.3</a> on Fri Apr 03 2020 13:28:08 GMT+0200 (Central European Summer Time)
6036 </footer>
6037 </div>
6038 </div>
6039 <script>prettyPrint();</script>
6040 <script src="scripts/jaguar.js"></script>
6041 </body>
6042 </html>