form.js 133 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722272327242725272627272728272927302731273227332734273527362737273827392740274127422743274427452746274727482749275027512752275327542755275627572758275927602761276227632764276527662767276827692770277127722773277427752776277727782779278027812782278327842785278627872788278927902791279227932794279527962797279827992800280128022803280428052806280728082809281028112812281328142815281628172818281928202821282228232824282528262827282828292830283128322833283428352836283728382839284028412842284328442845284628472848284928502851285228532854285528562857285828592860286128622863286428652866286728682869287028712872287328742875287628772878287928802881288228832884288528862887288828892890289128922893289428952896289728982899290029012902290329042905290629072908290929102911291229132914291529162917291829192920292129222923292429252926292729282929293029312932293329342935293629372938293929402941294229432944294529462947294829492950295129522953295429552956295729582959296029612962296329642965296629672968296929702971297229732974297529762977297829792980298129822983298429852986298729882989299029912992299329942995299629972998299930003001300230033004300530063007300830093010301130123013301430153016301730183019302030213022302330243025302630273028302930303031303230333034303530363037303830393040304130423043304430453046304730483049305030513052305330543055305630573058305930603061306230633064306530663067306830693070307130723073307430753076307730783079308030813082308330843085308630873088308930903091309230933094309530963097309830993100310131023103310431053106310731083109311031113112311331143115311631173118311931203121312231233124312531263127312831293130313131323133313431353136313731383139314031413142314331443145314631473148314931503151315231533154315531563157315831593160316131623163316431653166316731683169317031713172317331743175317631773178317931803181318231833184318531863187318831893190319131923193319431953196319731983199320032013202320332043205320632073208320932103211321232133214321532163217321832193220322132223223322432253226322732283229323032313232323332343235323632373238323932403241324232433244324532463247324832493250325132523253325432553256325732583259326032613262326332643265326632673268326932703271327232733274327532763277327832793280328132823283328432853286328732883289329032913292329332943295329632973298329933003301330233033304330533063307330833093310331133123313331433153316331733183319332033213322332333243325332633273328332933303331333233333334333533363337333833393340334133423343334433453346334733483349335033513352335333543355335633573358335933603361336233633364336533663367336833693370337133723373337433753376337733783379338033813382338333843385338633873388338933903391339233933394339533963397339833993400340134023403340434053406340734083409341034113412341334143415341634173418341934203421342234233424342534263427342834293430343134323433343434353436343734383439344034413442344334443445344634473448344934503451345234533454345534563457345834593460346134623463346434653466346734683469347034713472347334743475347634773478347934803481348234833484348534863487348834893490349134923493349434953496349734983499350035013502350335043505350635073508350935103511351235133514351535163517351835193520352135223523352435253526352735283529353035313532353335343535353635373538353935403541354235433544354535463547354835493550355135523553355435553556355735583559356035613562356335643565356635673568356935703571357235733574357535763577357835793580358135823583358435853586358735883589359035913592359335943595359635973598359936003601360236033604360536063607360836093610361136123613361436153616361736183619362036213622362336243625362636273628362936303631363236333634363536363637363836393640364136423643364436453646364736483649365036513652365336543655365636573658365936603661366236633664366536663667366836693670367136723673367436753676367736783679368036813682368336843685368636873688368936903691369236933694369536963697369836993700370137023703370437053706370737083709371037113712371337143715371637173718371937203721372237233724372537263727372837293730373137323733373437353736373737383739374037413742374337443745374637473748374937503751375237533754375537563757375837593760376137623763376437653766376737683769377037713772377337743775377637773778377937803781378237833784378537863787378837893790379137923793379437953796379737983799380038013802380338043805380638073808380938103811381238133814381538163817381838193820382138223823382438253826382738283829383038313832383338343835383638373838383938403841384238433844384538463847384838493850385138523853385438553856385738583859386038613862386338643865386638673868386938703871387238733874387538763877387838793880388138823883388438853886388738883889389038913892389338943895389638973898389939003901390239033904390539063907390839093910391139123913391439153916391739183919392039213922392339243925392639273928392939303931393239333934393539363937393839393940394139423943394439453946394739483949395039513952395339543955395639573958395939603961396239633964396539663967396839693970397139723973397439753976397739783979398039813982398339843985398639873988398939903991399239933994399539963997399839994000400140024003400440054006400740084009401040114012401340144015401640174018401940204021402240234024402540264027402840294030403140324033403440354036403740384039404040414042404340444045404640474048404940504051405240534054405540564057405840594060406140624063406440654066406740684069407040714072407340744075407640774078407940804081408240834084408540864087408840894090409140924093409440954096409740984099410041014102410341044105410641074108410941104111411241134114411541164117411841194120412141224123412441254126412741284129413041314132413341344135413641374138413941404141414241434144414541464147414841494150415141524153415441554156415741584159416041614162416341644165416641674168416941704171417241734174417541764177417841794180418141824183418441854186418741884189419041914192419341944195419641974198419942004201420242034204420542064207420842094210421142124213421442154216421742184219422042214222422342244225422642274228422942304231423242334234423542364237423842394240424142424243424442454246424742484249425042514252425342544255425642574258425942604261426242634264426542664267426842694270427142724273427442754276427742784279428042814282428342844285428642874288428942904291429242934294429542964297429842994300430143024303430443054306430743084309431043114312431343144315431643174318431943204321432243234324432543264327432843294330433143324333433443354336433743384339434043414342434343444345434643474348434943504351435243534354435543564357435843594360436143624363436443654366436743684369437043714372437343744375437643774378437943804381438243834384438543864387438843894390439143924393439443954396439743984399440044014402440344044405440644074408440944104411441244134414441544164417441844194420442144224423442444254426442744284429443044314432443344344435
  1. 'use strict';
  2. 'require ui';
  3. 'require uci';
  4. 'require rpc';
  5. 'require dom';
  6. 'require baseclass';
  7. var scope = this;
  8. var callSessionAccess = rpc.declare({
  9. object: 'session',
  10. method: 'access',
  11. params: [ 'scope', 'object', 'function' ],
  12. expect: { 'access': false }
  13. });
  14. var CBIJSONConfig = baseclass.extend({
  15. __init__: function(data) {
  16. data = Object.assign({}, data);
  17. this.data = {};
  18. var num_sections = 0,
  19. section_ids = [];
  20. for (var sectiontype in data) {
  21. if (!data.hasOwnProperty(sectiontype))
  22. continue;
  23. if (Array.isArray(data[sectiontype])) {
  24. for (var i = 0, index = 0; i < data[sectiontype].length; i++) {
  25. var item = data[sectiontype][i],
  26. anonymous, name;
  27. if (!L.isObject(item))
  28. continue;
  29. if (typeof(item['.name']) == 'string') {
  30. name = item['.name'];
  31. anonymous = false;
  32. }
  33. else {
  34. name = sectiontype + num_sections;
  35. anonymous = true;
  36. }
  37. if (!this.data.hasOwnProperty(name))
  38. section_ids.push(name);
  39. this.data[name] = Object.assign(item, {
  40. '.index': num_sections++,
  41. '.anonymous': anonymous,
  42. '.name': name,
  43. '.type': sectiontype
  44. });
  45. }
  46. }
  47. else if (L.isObject(data[sectiontype])) {
  48. this.data[sectiontype] = Object.assign(data[sectiontype], {
  49. '.anonymous': false,
  50. '.name': sectiontype,
  51. '.type': sectiontype
  52. });
  53. section_ids.push(sectiontype);
  54. num_sections++;
  55. }
  56. }
  57. section_ids.sort(L.bind(function(a, b) {
  58. var indexA = (this.data[a]['.index'] != null) ? +this.data[a]['.index'] : 9999,
  59. indexB = (this.data[b]['.index'] != null) ? +this.data[b]['.index'] : 9999;
  60. if (indexA != indexB)
  61. return (indexA - indexB);
  62. return (a > b);
  63. }, this));
  64. for (var i = 0; i < section_ids.length; i++)
  65. this.data[section_ids[i]]['.index'] = i;
  66. },
  67. load: function() {
  68. return Promise.resolve(this.data);
  69. },
  70. save: function() {
  71. return Promise.resolve();
  72. },
  73. get: function(config, section, option) {
  74. if (section == null)
  75. return null;
  76. if (option == null)
  77. return this.data[section];
  78. if (!this.data.hasOwnProperty(section))
  79. return null;
  80. var value = this.data[section][option];
  81. if (Array.isArray(value))
  82. return value;
  83. if (value != null)
  84. return String(value);
  85. return null;
  86. },
  87. set: function(config, section, option, value) {
  88. if (section == null || option == null || option.charAt(0) == '.')
  89. return;
  90. if (!this.data.hasOwnProperty(section))
  91. return;
  92. if (value == null)
  93. delete this.data[section][option];
  94. else if (Array.isArray(value))
  95. this.data[section][option] = value;
  96. else
  97. this.data[section][option] = String(value);
  98. },
  99. unset: function(config, section, option) {
  100. return this.set(config, section, option, null);
  101. },
  102. sections: function(config, sectiontype, callback) {
  103. var rv = [];
  104. for (var section_id in this.data)
  105. if (sectiontype == null || this.data[section_id]['.type'] == sectiontype)
  106. rv.push(this.data[section_id]);
  107. rv.sort(function(a, b) { return a['.index'] - b['.index'] });
  108. if (typeof(callback) == 'function')
  109. for (var i = 0; i < rv.length; i++)
  110. callback.call(this, rv[i], rv[i]['.name']);
  111. return rv;
  112. },
  113. add: function(config, sectiontype, sectionname) {
  114. var num_sections_type = 0, next_index = 0;
  115. for (var name in this.data) {
  116. num_sections_type += (this.data[name]['.type'] == sectiontype);
  117. next_index = Math.max(next_index, this.data[name]['.index']);
  118. }
  119. var section_id = sectionname || sectiontype + num_sections_type;
  120. if (!this.data.hasOwnProperty(section_id)) {
  121. this.data[section_id] = {
  122. '.name': section_id,
  123. '.type': sectiontype,
  124. '.anonymous': (sectionname == null),
  125. '.index': next_index + 1
  126. };
  127. }
  128. return section_id;
  129. },
  130. remove: function(config, section) {
  131. if (this.data.hasOwnProperty(section))
  132. delete this.data[section];
  133. },
  134. resolveSID: function(config, section_id) {
  135. return section_id;
  136. },
  137. move: function(config, section_id1, section_id2, after) {
  138. return uci.move.apply(this, [config, section_id1, section_id2, after]);
  139. }
  140. });
  141. /**
  142. * @class AbstractElement
  143. * @memberof LuCI.form
  144. * @hideconstructor
  145. * @classdesc
  146. *
  147. * The `AbstractElement` class serves as abstract base for the different form
  148. * elements implemented by `LuCI.form`. It provides the common logic for
  149. * loading and rendering values, for nesting elements and for defining common
  150. * properties.
  151. *
  152. * This class is private and not directly accessible by user code.
  153. */
  154. var CBIAbstractElement = baseclass.extend(/** @lends LuCI.form.AbstractElement.prototype */ {
  155. __init__: function(title, description) {
  156. this.title = title || '';
  157. this.description = description || '';
  158. this.children = [];
  159. },
  160. /**
  161. * Add another form element as children to this element.
  162. *
  163. * @param {AbstractElement} element
  164. * The form element to add.
  165. */
  166. append: function(obj) {
  167. this.children.push(obj);
  168. },
  169. /**
  170. * Parse this elements form input.
  171. *
  172. * The `parse()` function recursively walks the form element tree and
  173. * triggers input value reading and validation for each encountered element.
  174. *
  175. * Elements which are hidden due to unsatisified dependencies are skipped.
  176. *
  177. * @returns {Promise<void>}
  178. * Returns a promise resolving once this element's value and the values of
  179. * all child elements have been parsed. The returned promise is rejected
  180. * if any parsed values are not meeting the validation constraints of their
  181. * respective elements.
  182. */
  183. parse: function() {
  184. var args = arguments;
  185. this.children.forEach(function(child) {
  186. child.parse.apply(child, args);
  187. });
  188. },
  189. /**
  190. * Render the form element.
  191. *
  192. * The `render()` function recursively walks the form element tree and
  193. * renders the markup for each element, returning the assembled DOM tree.
  194. *
  195. * @abstract
  196. * @returns {Node|Promise<Node>}
  197. * May return a DOM Node or a promise resolving to a DOM node containing
  198. * the form element's markup, including the markup of any child elements.
  199. */
  200. render: function() {
  201. L.error('InternalError', 'Not implemented');
  202. },
  203. /** @private */
  204. loadChildren: function(/* ... */) {
  205. var tasks = [];
  206. if (Array.isArray(this.children))
  207. for (var i = 0; i < this.children.length; i++)
  208. if (!this.children[i].disable)
  209. tasks.push(this.children[i].load.apply(this.children[i], arguments));
  210. return Promise.all(tasks);
  211. },
  212. /** @private */
  213. renderChildren: function(tab_name /*, ... */) {
  214. var tasks = [],
  215. index = 0;
  216. if (Array.isArray(this.children))
  217. for (var i = 0; i < this.children.length; i++)
  218. if (tab_name === null || this.children[i].tab === tab_name)
  219. if (!this.children[i].disable)
  220. tasks.push(this.children[i].render.apply(
  221. this.children[i], this.varargs(arguments, 1, index++)));
  222. return Promise.all(tasks);
  223. },
  224. /**
  225. * Strip any HTML tags from the given input string.
  226. *
  227. * @param {string} input
  228. * The input string to clean.
  229. *
  230. * @returns {string}
  231. * The cleaned input string with HTML removes removed.
  232. */
  233. stripTags: function(s) {
  234. if (typeof(s) == 'string' && !s.match(/[<>]/))
  235. return s;
  236. var x = E('div', {}, s);
  237. return x.textContent || x.innerText || '';
  238. },
  239. /**
  240. * Format the given named property as title string.
  241. *
  242. * This function looks up the given named property and formats its value
  243. * suitable for use as element caption or description string. It also
  244. * strips any HTML tags from the result.
  245. *
  246. * If the property value is a string, it is passed to `String.format()`
  247. * along with any additional parameters passed to `titleFn()`.
  248. *
  249. * If the property value is a function, it is invoked with any additional
  250. * `titleFn()` parameters as arguments and the obtained return value is
  251. * converted to a string.
  252. *
  253. * In all other cases, `null` is returned.
  254. *
  255. * @param {string} property
  256. * The name of the element property to use.
  257. *
  258. * @param {...*} fmt_args
  259. * Extra values to format the title string with.
  260. *
  261. * @returns {string|null}
  262. * The formatted title string or `null` if the property did not exist or
  263. * was neither a string nor a function.
  264. */
  265. titleFn: function(attr /*, ... */) {
  266. var s = null;
  267. if (typeof(this[attr]) == 'function')
  268. s = this[attr].apply(this, this.varargs(arguments, 1));
  269. else if (typeof(this[attr]) == 'string')
  270. s = (arguments.length > 1) ? ''.format.apply(this[attr], this.varargs(arguments, 1)) : this[attr];
  271. if (s != null)
  272. s = this.stripTags(String(s)).trim();
  273. if (s == null || s == '')
  274. return null;
  275. return s;
  276. }
  277. });
  278. /**
  279. * @constructor Map
  280. * @memberof LuCI.form
  281. * @augments LuCI.form.AbstractElement
  282. *
  283. * @classdesc
  284. *
  285. * The `Map` class represents one complete form. A form usually maps one UCI
  286. * configuraton file and is divided into multiple sections containing multiple
  287. * fields each.
  288. *
  289. * It serves as main entry point into the `LuCI.form` for typical view code.
  290. *
  291. * @param {string} config
  292. * The UCI configuration to map. It is automatically loaded along when the
  293. * resulting map instance.
  294. *
  295. * @param {string} [title]
  296. * The title caption of the form. A form title is usually rendered as separate
  297. * headline element before the actual form contents. If omitted, the
  298. * corresponding headline element will not be rendered.
  299. *
  300. * @param {string} [description]
  301. * The description text of the form which is usually rendered as text
  302. * paragraph below the form title and before the actual form conents.
  303. * If omitted, the corresponding paragraph element will not be rendered.
  304. */
  305. var CBIMap = CBIAbstractElement.extend(/** @lends LuCI.form.Map.prototype */ {
  306. __init__: function(config /*, ... */) {
  307. this.super('__init__', this.varargs(arguments, 1));
  308. this.config = config;
  309. this.parsechain = [ config ];
  310. this.data = uci;
  311. },
  312. /**
  313. * Toggle readonly state of the form.
  314. *
  315. * If set to `true`, the Map instance is marked readonly and any form
  316. * option elements added to it will inherit the readonly state.
  317. *
  318. * If left unset, the Map will test the access permission of the primary
  319. * uci configuration upon loading and mark the form readonly if no write
  320. * permissions are granted.
  321. *
  322. * @name LuCI.form.Map.prototype#readonly
  323. * @type boolean
  324. */
  325. /**
  326. * Find all DOM nodes within this Map which match the given search
  327. * parameters. This function is essentially a convenience wrapper around
  328. * `querySelectorAll()`.
  329. *
  330. * This function is sensitive to the amount of arguments passed to it;
  331. * if only one argument is specified, it is used as selector-expression
  332. * as-is. When two arguments are passed, the first argument is treated
  333. * as attribute name, the second one as attribute value to match.
  334. *
  335. * As an example, `map.findElements('input')` would find all `<input>`
  336. * nodes while `map.findElements('type', 'text')` would find any DOM node
  337. * with a `type="text"` attribute.
  338. *
  339. * @param {string} selector_or_attrname
  340. * If invoked with only one parameter, this argument is a
  341. * `querySelectorAll()` compatible selector expression. If invoked with
  342. * two parameters, this argument is the attribute name to filter for.
  343. *
  344. * @param {string} [attrvalue]
  345. * In case the function is invoked with two parameters, this argument
  346. * specifies the attribute value to match.
  347. *
  348. * @throws {InternalError}
  349. * Throws an `InternalError` if more than two function parameters are
  350. * passed.
  351. *
  352. * @returns {NodeList}
  353. * Returns a (possibly empty) DOM `NodeList` containing the found DOM nodes.
  354. */
  355. findElements: function(/* ... */) {
  356. var q = null;
  357. if (arguments.length == 1)
  358. q = arguments[0];
  359. else if (arguments.length == 2)
  360. q = '[%s="%s"]'.format(arguments[0], arguments[1]);
  361. else
  362. L.error('InternalError', 'Expecting one or two arguments to findElements()');
  363. return this.root.querySelectorAll(q);
  364. },
  365. /**
  366. * Find the first DOM node within this Map which matches the given search
  367. * parameters. This function is essentially a convenience wrapper around
  368. * `findElements()` which only returns the first found node.
  369. *
  370. * This function is sensitive to the amount of arguments passed to it;
  371. * if only one argument is specified, it is used as selector-expression
  372. * as-is. When two arguments are passed, the first argument is treated
  373. * as attribute name, the second one as attribute value to match.
  374. *
  375. * As an example, `map.findElement('input')` would find the first `<input>`
  376. * node while `map.findElement('type', 'text')` would find the first DOM
  377. * node with a `type="text"` attribute.
  378. *
  379. * @param {string} selector_or_attrname
  380. * If invoked with only one parameter, this argument is a `querySelector()`
  381. * compatible selector expression. If invoked with two parameters, this
  382. * argument is the attribute name to filter for.
  383. *
  384. * @param {string} [attrvalue]
  385. * In case the function is invoked with two parameters, this argument
  386. * specifies the attribute value to match.
  387. *
  388. * @throws {InternalError}
  389. * Throws an `InternalError` if more than two function parameters are
  390. * passed.
  391. *
  392. * @returns {Node|null}
  393. * Returns the first found DOM node or `null` if no element matched.
  394. */
  395. findElement: function(/* ... */) {
  396. var res = this.findElements.apply(this, arguments);
  397. return res.length ? res[0] : null;
  398. },
  399. /**
  400. * Tie another UCI configuration to the map.
  401. *
  402. * By default, a map instance will only load the UCI configuration file
  403. * specified in the constructor but sometimes access to values from
  404. * further configuration files is required. This function allows for such
  405. * use cases by registering further UCI configuration files which are
  406. * needed by the map.
  407. *
  408. * @param {string} config
  409. * The additional UCI configuration file to tie to the map. If the given
  410. * config already is in the list of required files, it will be ignored.
  411. */
  412. chain: function(config) {
  413. if (this.parsechain.indexOf(config) == -1)
  414. this.parsechain.push(config);
  415. },
  416. /**
  417. * Add a configuration section to the map.
  418. *
  419. * LuCI forms follow the structure of the underlying UCI configurations,
  420. * means that a map, which represents a single UCI configuration, is
  421. * divided into multiple sections which in turn contain an arbitrary
  422. * number of options.
  423. *
  424. * While UCI itself only knows two kinds of sections - named and anonymous
  425. * ones - the form class offers various flavors of form section elements
  426. * to present configuration sections in different ways. Refer to the
  427. * documentation of the different section classes for details.
  428. *
  429. * @param {LuCI.form.AbstractSection} sectionclass
  430. * The section class to use for rendering the configuration section.
  431. * Note that this value must be the class itself, not a class instance
  432. * obtained from calling `new`. It must also be a class dervied from
  433. * `LuCI.form.AbstractSection`.
  434. *
  435. * @param {...string} classargs
  436. * Additional arguments which are passed as-is to the contructor of the
  437. * given section class. Refer to the class specific constructor
  438. * documentation for details.
  439. *
  440. * @returns {LuCI.form.AbstractSection}
  441. * Returns the instantiated section class instance.
  442. */
  443. section: function(cbiClass /*, ... */) {
  444. if (!CBIAbstractSection.isSubclass(cbiClass))
  445. L.error('TypeError', 'Class must be a descendent of CBIAbstractSection');
  446. var obj = cbiClass.instantiate(this.varargs(arguments, 1, this));
  447. this.append(obj);
  448. return obj;
  449. },
  450. /**
  451. * Load the configuration covered by this map.
  452. *
  453. * The `load()` function first loads all referenced UCI configurations,
  454. * then it recursively walks the form element tree and invokes the
  455. * load function of each child element.
  456. *
  457. * @returns {Promise<void>}
  458. * Returns a promise resolving once the entire form completed loading all
  459. * data. The promise may reject with an error if any configuration failed
  460. * to load or if any of the child elements load functions rejected with
  461. * an error.
  462. */
  463. load: function() {
  464. var doCheckACL = (!(this instanceof CBIJSONMap) && this.readonly == null),
  465. loadTasks = [ doCheckACL ? callSessionAccess('uci', this.config, 'write') : true ],
  466. configs = this.parsechain || [ this.config ];
  467. loadTasks.push.apply(loadTasks, configs.map(L.bind(function(config, i) {
  468. return i ? L.resolveDefault(this.data.load(config)) : this.data.load(config);
  469. }, this)));
  470. return Promise.all(loadTasks).then(L.bind(function(res) {
  471. if (res[0] === false)
  472. this.readonly = true;
  473. return this.loadChildren();
  474. }, this));
  475. },
  476. /**
  477. * Parse the form input values.
  478. *
  479. * The `parse()` function recursively walks the form element tree and
  480. * triggers input value reading and validation for each child element.
  481. *
  482. * Elements which are hidden due to unsatisified dependencies are skipped.
  483. *
  484. * @returns {Promise<void>}
  485. * Returns a promise resolving once the entire form completed parsing all
  486. * input values. The returned promise is rejected if any parsed values are
  487. * not meeting the validation constraints of their respective elements.
  488. */
  489. parse: function() {
  490. var tasks = [];
  491. if (Array.isArray(this.children))
  492. for (var i = 0; i < this.children.length; i++)
  493. tasks.push(this.children[i].parse());
  494. return Promise.all(tasks);
  495. },
  496. /**
  497. * Save the form input values.
  498. *
  499. * This function parses the current form, saves the resulting UCI changes,
  500. * reloads the UCI configuration data and redraws the form elements.
  501. *
  502. * @param {function} [cb]
  503. * An optional callback function that is invoked after the form is parsed
  504. * but before the changed UCI data is saved. This is useful to perform
  505. * additional data manipulation steps before saving the changes.
  506. *
  507. * @param {boolean} [silent=false]
  508. * If set to `true`, trigger an alert message to the user in case saving
  509. * the form data failes. Otherwise fail silently.
  510. *
  511. * @returns {Promise<void>}
  512. * Returns a promise resolving once the entire save operation is complete.
  513. * The returned promise is rejected if any step of the save operation
  514. * failed.
  515. */
  516. save: function(cb, silent) {
  517. this.checkDepends();
  518. return this.parse()
  519. .then(cb)
  520. .then(this.data.save.bind(this.data))
  521. .then(this.load.bind(this))
  522. .catch(function(e) {
  523. if (!silent) {
  524. ui.showModal(_('Save error'), [
  525. E('p', {}, [ _('An error occurred while saving the form:') ]),
  526. E('p', {}, [ E('em', { 'style': 'white-space:pre' }, [ e.message ]) ]),
  527. E('div', { 'class': 'right' }, [
  528. E('button', { 'class': 'btn', 'click': ui.hideModal }, [ _('Dismiss') ])
  529. ])
  530. ]);
  531. }
  532. return Promise.reject(e);
  533. }).then(this.renderContents.bind(this));
  534. },
  535. /**
  536. * Reset the form by re-rendering its contents. This will revert all
  537. * unsaved user inputs to their initial form state.
  538. *
  539. * @returns {Promise<Node>}
  540. * Returns a promise resolving to the toplevel form DOM node once the
  541. * re-rendering is complete.
  542. */
  543. reset: function() {
  544. return this.renderContents();
  545. },
  546. /**
  547. * Render the form markup.
  548. *
  549. * @returns {Promise<Node>}
  550. * Returns a promise resolving to the toplevel form DOM node once the
  551. * rendering is complete.
  552. */
  553. render: function() {
  554. return this.load().then(this.renderContents.bind(this));
  555. },
  556. /** @private */
  557. renderContents: function() {
  558. var mapEl = this.root || (this.root = E('div', {
  559. 'id': 'cbi-%s'.format(this.config),
  560. 'class': 'cbi-map',
  561. 'cbi-dependency-check': L.bind(this.checkDepends, this)
  562. }));
  563. dom.bindClassInstance(mapEl, this);
  564. return this.renderChildren(null).then(L.bind(function(nodes) {
  565. var initialRender = !mapEl.firstChild;
  566. dom.content(mapEl, null);
  567. if (this.title != null && this.title != '')
  568. mapEl.appendChild(E('h2', { 'name': 'content' }, this.title));
  569. if (this.description != null && this.description != '')
  570. mapEl.appendChild(E('div', { 'class': 'cbi-map-descr' }, this.description));
  571. if (this.tabbed)
  572. dom.append(mapEl, E('div', { 'class': 'cbi-map-tabbed' }, nodes));
  573. else
  574. dom.append(mapEl, nodes);
  575. if (!initialRender) {
  576. mapEl.classList.remove('flash');
  577. window.setTimeout(function() {
  578. mapEl.classList.add('flash');
  579. }, 1);
  580. }
  581. this.checkDepends();
  582. var tabGroups = mapEl.querySelectorAll('.cbi-map-tabbed, .cbi-section-node-tabbed');
  583. for (var i = 0; i < tabGroups.length; i++)
  584. ui.tabs.initTabGroup(tabGroups[i].childNodes);
  585. return mapEl;
  586. }, this));
  587. },
  588. /**
  589. * Find a form option element instance.
  590. *
  591. * @param {string} name_or_id
  592. * The name or the full ID of the option element to look up.
  593. *
  594. * @param {string} [section_id]
  595. * The ID of the UCI section containing the option to look up. May be
  596. * omitted if a full ID is passed as first argument.
  597. *
  598. * @param {string} [config]
  599. * The name of the UCI configuration the option instance is belonging to.
  600. * Defaults to the main UCI configuration of the map if omitted.
  601. *
  602. * @returns {Array<LuCI.form.AbstractValue,string>|null}
  603. * Returns a two-element array containing the form option instance as
  604. * first item and the corresponding UCI section ID as second item.
  605. * Returns `null` if the option could not be found.
  606. */
  607. lookupOption: function(name, section_id, config_name) {
  608. var id, elem, sid, inst;
  609. if (name.indexOf('.') > -1)
  610. id = 'cbid.%s'.format(name);
  611. else
  612. id = 'cbid.%s.%s.%s'.format(config_name || this.config, section_id, name);
  613. elem = this.findElement('data-field', id);
  614. sid = elem ? id.split(/\./)[2] : null;
  615. inst = elem ? dom.findClassInstance(elem) : null;
  616. return (inst instanceof CBIAbstractValue) ? [ inst, sid ] : null;
  617. },
  618. /** @private */
  619. checkDepends: function(ev, n) {
  620. var changed = false;
  621. for (var i = 0, s = this.children[0]; (s = this.children[i]) != null; i++)
  622. if (s.checkDepends(ev, n))
  623. changed = true;
  624. if (changed && (n || 0) < 10)
  625. this.checkDepends(ev, (n || 10) + 1);
  626. ui.tabs.updateTabs(ev, this.root);
  627. },
  628. /** @private */
  629. isDependencySatisfied: function(depends, config_name, section_id) {
  630. var def = false;
  631. if (!Array.isArray(depends) || !depends.length)
  632. return true;
  633. for (var i = 0; i < depends.length; i++) {
  634. var istat = true,
  635. reverse = depends[i]['!reverse'],
  636. contains = depends[i]['!contains'];
  637. for (var dep in depends[i]) {
  638. if (dep == '!reverse' || dep == '!contains') {
  639. continue;
  640. }
  641. else if (dep == '!default') {
  642. def = true;
  643. istat = false;
  644. }
  645. else {
  646. var res = this.lookupOption(dep, section_id, config_name),
  647. val = (res && res[0].isActive(res[1])) ? res[0].formvalue(res[1]) : null;
  648. var equal = contains
  649. ? isContained(val, depends[i][dep])
  650. : isEqual(val, depends[i][dep]);
  651. istat = (istat && equal);
  652. }
  653. }
  654. if (istat ^ reverse)
  655. return true;
  656. }
  657. return def;
  658. }
  659. });
  660. /**
  661. * @constructor JSONMap
  662. * @memberof LuCI.form
  663. * @augments LuCI.form.Map
  664. *
  665. * @classdesc
  666. *
  667. * A `JSONMap` class functions similar to [LuCI.form.Map]{@link LuCI.form.Map}
  668. * but uses a multidimensional JavaScript object instead of UCI configuration
  669. * as data source.
  670. *
  671. * @param {Object<string, Object<string, *>|Array<Object<string, *>>>} data
  672. * The JavaScript object to use as data source. Internally, the object is
  673. * converted into an UCI-like format. Its toplevel keys are treated like UCI
  674. * section types while the object or array-of-object values are treated as
  675. * section contents.
  676. *
  677. * @param {string} [title]
  678. * The title caption of the form. A form title is usually rendered as separate
  679. * headline element before the actual form contents. If omitted, the
  680. * corresponding headline element will not be rendered.
  681. *
  682. * @param {string} [description]
  683. * The description text of the form which is usually rendered as text
  684. * paragraph below the form title and before the actual form conents.
  685. * If omitted, the corresponding paragraph element will not be rendered.
  686. */
  687. var CBIJSONMap = CBIMap.extend(/** @lends LuCI.form.JSONMap.prototype */ {
  688. __init__: function(data /*, ... */) {
  689. this.super('__init__', this.varargs(arguments, 1, 'json'));
  690. this.config = 'json';
  691. this.parsechain = [ 'json' ];
  692. this.data = new CBIJSONConfig(data);
  693. }
  694. });
  695. /**
  696. * @class AbstractSection
  697. * @memberof LuCI.form
  698. * @augments LuCI.form.AbstractElement
  699. * @hideconstructor
  700. * @classdesc
  701. *
  702. * The `AbstractSection` class serves as abstract base for the different form
  703. * section styles implemented by `LuCI.form`. It provides the common logic for
  704. * enumerating underlying configuration section instances, for registering
  705. * form options and for handling tabs to segment child options.
  706. *
  707. * This class is private and not directly accessible by user code.
  708. */
  709. var CBIAbstractSection = CBIAbstractElement.extend(/** @lends LuCI.form.AbstractSection.prototype */ {
  710. __init__: function(map, sectionType /*, ... */) {
  711. this.super('__init__', this.varargs(arguments, 2));
  712. this.sectiontype = sectionType;
  713. this.map = map;
  714. this.config = map.config;
  715. this.optional = true;
  716. this.addremove = false;
  717. this.dynamic = false;
  718. },
  719. /**
  720. * Access the parent option container instance.
  721. *
  722. * In case this section is nested within an option element container,
  723. * this property will hold a reference to the parent option instance.
  724. *
  725. * If this section is not nested, the property is `null`.
  726. *
  727. * @name LuCI.form.AbstractSection.prototype#parentoption
  728. * @type LuCI.form.AbstractValue
  729. * @readonly
  730. */
  731. /**
  732. * Enumerate the UCI section IDs covered by this form section element.
  733. *
  734. * @abstract
  735. * @throws {InternalError}
  736. * Throws an `InternalError` exception if the function is not implemented.
  737. *
  738. * @returns {string[]}
  739. * Returns an array of UCI section IDs covered by this form element.
  740. * The sections will be rendered in the same order as the returned array.
  741. */
  742. cfgsections: function() {
  743. L.error('InternalError', 'Not implemented');
  744. },
  745. /**
  746. * Filter UCI section IDs to render.
  747. *
  748. * The filter function is invoked for each UCI section ID of a given type
  749. * and controls whether the given UCI section is rendered or ignored by
  750. * the form section element.
  751. *
  752. * The default implementation always returns `true`. User code or
  753. * classes extending `AbstractSection` may overwrite this function with
  754. * custom implementations.
  755. *
  756. * @abstract
  757. * @param {string} section_id
  758. * The UCI section ID to test.
  759. *
  760. * @returns {boolean}
  761. * Returns `true` when the given UCI section ID should be handled and
  762. * `false` when it should be ignored.
  763. */
  764. filter: function(section_id) {
  765. return true;
  766. },
  767. /**
  768. * Load the configuration covered by this section.
  769. *
  770. * The `load()` function recursively walks the section element tree and
  771. * invokes the load function of each child option element.
  772. *
  773. * @returns {Promise<void>}
  774. * Returns a promise resolving once the values of all child elements have
  775. * been loaded. The promise may reject with an error if any of the child
  776. * elements load functions rejected with an error.
  777. */
  778. load: function() {
  779. var section_ids = this.cfgsections(),
  780. tasks = [];
  781. if (Array.isArray(this.children))
  782. for (var i = 0; i < section_ids.length; i++)
  783. tasks.push(this.loadChildren(section_ids[i])
  784. .then(Function.prototype.bind.call(function(section_id, set_values) {
  785. for (var i = 0; i < set_values.length; i++)
  786. this.children[i].cfgvalue(section_id, set_values[i]);
  787. }, this, section_ids[i])));
  788. return Promise.all(tasks);
  789. },
  790. /**
  791. * Parse this sections form input.
  792. *
  793. * The `parse()` function recursively walks the section element tree and
  794. * triggers input value reading and validation for each encountered child
  795. * option element.
  796. *
  797. * Options which are hidden due to unsatisified dependencies are skipped.
  798. *
  799. * @returns {Promise<void>}
  800. * Returns a promise resolving once the values of all child elements have
  801. * been parsed. The returned promise is rejected if any parsed values are
  802. * not meeting the validation constraints of their respective elements.
  803. */
  804. parse: function() {
  805. var section_ids = this.cfgsections(),
  806. tasks = [];
  807. if (Array.isArray(this.children))
  808. for (var i = 0; i < section_ids.length; i++)
  809. for (var j = 0; j < this.children.length; j++)
  810. tasks.push(this.children[j].parse(section_ids[i]));
  811. return Promise.all(tasks);
  812. },
  813. /**
  814. * Add an option tab to the section.
  815. *
  816. * The child option elements of a section may be divided into multiple
  817. * tabs to provide a better overview to the user.
  818. *
  819. * Before options can be moved into a tab pane, the corresponding tab
  820. * has to be defined first, which is done by calling this function.
  821. *
  822. * Note that once tabs are defined, user code must use the `taboption()`
  823. * method to add options to specific tabs. Option elements added by
  824. * `option()` will not be assigned to any tab and not be rendered in this
  825. * case.
  826. *
  827. * @param {string} name
  828. * The name of the tab to register. It may be freely chosen and just serves
  829. * as an identifier to differentiate tabs.
  830. *
  831. * @param {string} title
  832. * The human readable caption of the tab.
  833. *
  834. * @param {string} [description]
  835. * An additional description text for the corresponding tab pane. It is
  836. * displayed as text paragraph below the tab but before the tab pane
  837. * contents. If omitted, no description will be rendered.
  838. *
  839. * @throws {Error}
  840. * Throws an exeption if a tab with the same `name` already exists.
  841. */
  842. tab: function(name, title, description) {
  843. if (this.tabs && this.tabs[name])
  844. throw 'Tab already declared';
  845. var entry = {
  846. name: name,
  847. title: title,
  848. description: description,
  849. children: []
  850. };
  851. this.tabs = this.tabs || [];
  852. this.tabs.push(entry);
  853. this.tabs[name] = entry;
  854. this.tab_names = this.tab_names || [];
  855. this.tab_names.push(name);
  856. },
  857. /**
  858. * Add a configuration option widget to the section.
  859. *
  860. * Note that [taboption()]{@link LuCI.form.AbstractSection#taboption}
  861. * should be used instead if this form section element uses tabs.
  862. *
  863. * @param {LuCI.form.AbstractValue} optionclass
  864. * The option class to use for rendering the configuration option. Note
  865. * that this value must be the class itself, not a class instance obtained
  866. * from calling `new`. It must also be a class dervied from
  867. * [LuCI.form.AbstractSection]{@link LuCI.form.AbstractSection}.
  868. *
  869. * @param {...*} classargs
  870. * Additional arguments which are passed as-is to the contructor of the
  871. * given option class. Refer to the class specific constructor
  872. * documentation for details.
  873. *
  874. * @throws {TypeError}
  875. * Throws a `TypeError` exception in case the passed class value is not a
  876. * descendent of `AbstractValue`.
  877. *
  878. * @returns {LuCI.form.AbstractValue}
  879. * Returns the instantiated option class instance.
  880. */
  881. option: function(cbiClass /*, ... */) {
  882. if (!CBIAbstractValue.isSubclass(cbiClass))
  883. throw L.error('TypeError', 'Class must be a descendent of CBIAbstractValue');
  884. var obj = cbiClass.instantiate(this.varargs(arguments, 1, this.map, this));
  885. this.append(obj);
  886. return obj;
  887. },
  888. /**
  889. * Add a configuration option widget to a tab of the section.
  890. *
  891. * @param {string} tabname
  892. * The name of the section tab to add the option element to.
  893. *
  894. * @param {LuCI.form.AbstractValue} optionclass
  895. * The option class to use for rendering the configuration option. Note
  896. * that this value must be the class itself, not a class instance obtained
  897. * from calling `new`. It must also be a class dervied from
  898. * [LuCI.form.AbstractSection]{@link LuCI.form.AbstractSection}.
  899. *
  900. * @param {...*} classargs
  901. * Additional arguments which are passed as-is to the contructor of the
  902. * given option class. Refer to the class specific constructor
  903. * documentation for details.
  904. *
  905. * @throws {ReferenceError}
  906. * Throws a `ReferenceError` exception when the given tab name does not
  907. * exist.
  908. *
  909. * @throws {TypeError}
  910. * Throws a `TypeError` exception in case the passed class value is not a
  911. * descendent of `AbstractValue`.
  912. *
  913. * @returns {LuCI.form.AbstractValue}
  914. * Returns the instantiated option class instance.
  915. */
  916. taboption: function(tabName /*, ... */) {
  917. if (!this.tabs || !this.tabs[tabName])
  918. throw L.error('ReferenceError', 'Associated tab not declared');
  919. var obj = this.option.apply(this, this.varargs(arguments, 1));
  920. obj.tab = tabName;
  921. this.tabs[tabName].children.push(obj);
  922. return obj;
  923. },
  924. /**
  925. * Query underlying option configuration values.
  926. *
  927. * This function is sensitive to the amount of arguments passed to it;
  928. * if only one argument is specified, the configuration values of all
  929. * options within this section are returned as dictionary.
  930. *
  931. * If both the section ID and an option name are supplied, this function
  932. * returns the configuration value of the specified option only.
  933. *
  934. * @param {string} section_id
  935. * The configuration section ID
  936. *
  937. * @param {string} [option]
  938. * The name of the option to query
  939. *
  940. * @returns {null|string|string[]|Object<string, null|string|string[]>}
  941. * Returns either a dictionary of option names and their corresponding
  942. * configuration values or just a single configuration value, depending
  943. * on the amount of passed arguments.
  944. */
  945. cfgvalue: function(section_id, option) {
  946. var rv = (arguments.length == 1) ? {} : null;
  947. for (var i = 0, o; (o = this.children[i]) != null; i++)
  948. if (rv)
  949. rv[o.option] = o.cfgvalue(section_id);
  950. else if (o.option == option)
  951. return o.cfgvalue(section_id);
  952. return rv;
  953. },
  954. /**
  955. * Query underlying option widget input values.
  956. *
  957. * This function is sensitive to the amount of arguments passed to it;
  958. * if only one argument is specified, the widget input values of all
  959. * options within this section are returned as dictionary.
  960. *
  961. * If both the section ID and an option name are supplied, this function
  962. * returns the widget input value of the specified option only.
  963. *
  964. * @param {string} section_id
  965. * The configuration section ID
  966. *
  967. * @param {string} [option]
  968. * The name of the option to query
  969. *
  970. * @returns {null|string|string[]|Object<string, null|string|string[]>}
  971. * Returns either a dictionary of option names and their corresponding
  972. * widget input values or just a single widget input value, depending
  973. * on the amount of passed arguments.
  974. */
  975. formvalue: function(section_id, option) {
  976. var rv = (arguments.length == 1) ? {} : null;
  977. for (var i = 0, o; (o = this.children[i]) != null; i++) {
  978. var func = this.map.root ? this.children[i].formvalue : this.children[i].cfgvalue;
  979. if (rv)
  980. rv[o.option] = func.call(o, section_id);
  981. else if (o.option == option)
  982. return func.call(o, section_id);
  983. }
  984. return rv;
  985. },
  986. /**
  987. * Obtain underlying option LuCI.ui widget instances.
  988. *
  989. * This function is sensitive to the amount of arguments passed to it;
  990. * if only one argument is specified, the LuCI.ui widget instances of all
  991. * options within this section are returned as dictionary.
  992. *
  993. * If both the section ID and an option name are supplied, this function
  994. * returns the LuCI.ui widget instance value of the specified option only.
  995. *
  996. * @param {string} section_id
  997. * The configuration section ID
  998. *
  999. * @param {string} [option]
  1000. * The name of the option to query
  1001. *
  1002. * @returns {null|LuCI.ui.AbstractElement|Object<string, null|LuCI.ui.AbstractElement>}
  1003. * Returns either a dictionary of option names and their corresponding
  1004. * widget input values or just a single widget input value, depending
  1005. * on the amount of passed arguments.
  1006. */
  1007. getUIElement: function(section_id, option) {
  1008. var rv = (arguments.length == 1) ? {} : null;
  1009. for (var i = 0, o; (o = this.children[i]) != null; i++)
  1010. if (rv)
  1011. rv[o.option] = o.getUIElement(section_id);
  1012. else if (o.option == option)
  1013. return o.getUIElement(section_id);
  1014. return rv;
  1015. },
  1016. /**
  1017. * Obtain underlying option objects.
  1018. *
  1019. * This function is sensitive to the amount of arguments passed to it;
  1020. * if no option name is specified, all options within this section are
  1021. * returned as dictionary.
  1022. *
  1023. * If an option name is supplied, this function returns the matching
  1024. * LuCI.form.AbstractValue instance only.
  1025. *
  1026. * @param {string} [option]
  1027. * The name of the option object to obtain
  1028. *
  1029. * @returns {null|LuCI.form.AbstractValue|Object<string, LuCI.form.AbstractValue>}
  1030. * Returns either a dictionary of option names and their corresponding
  1031. * option instance objects or just a single object instance value,
  1032. * depending on the amount of passed arguments.
  1033. */
  1034. getOption: function(option) {
  1035. var rv = (arguments.length == 0) ? {} : null;
  1036. for (var i = 0, o; (o = this.children[i]) != null; i++)
  1037. if (rv)
  1038. rv[o.option] = o;
  1039. else if (o.option == option)
  1040. return o;
  1041. return rv;
  1042. },
  1043. /** @private */
  1044. renderUCISection: function(section_id) {
  1045. var renderTasks = [];
  1046. if (!this.tabs)
  1047. return this.renderOptions(null, section_id);
  1048. for (var i = 0; i < this.tab_names.length; i++)
  1049. renderTasks.push(this.renderOptions(this.tab_names[i], section_id));
  1050. return Promise.all(renderTasks)
  1051. .then(this.renderTabContainers.bind(this, section_id));
  1052. },
  1053. /** @private */
  1054. renderTabContainers: function(section_id, nodes) {
  1055. var config_name = this.uciconfig || this.map.config,
  1056. containerEls = E([]);
  1057. for (var i = 0; i < nodes.length; i++) {
  1058. var tab_name = this.tab_names[i],
  1059. tab_data = this.tabs[tab_name],
  1060. containerEl = E('div', {
  1061. 'id': 'container.%s.%s.%s'.format(config_name, section_id, tab_name),
  1062. 'data-tab': tab_name,
  1063. 'data-tab-title': tab_data.title,
  1064. 'data-tab-active': tab_name === this.selected_tab
  1065. });
  1066. if (tab_data.description != null && tab_data.description != '')
  1067. containerEl.appendChild(
  1068. E('div', { 'class': 'cbi-tab-descr' }, tab_data.description));
  1069. containerEl.appendChild(nodes[i]);
  1070. containerEls.appendChild(containerEl);
  1071. }
  1072. return containerEls;
  1073. },
  1074. /** @private */
  1075. renderOptions: function(tab_name, section_id) {
  1076. var in_table = (this instanceof CBITableSection);
  1077. return this.renderChildren(tab_name, section_id, in_table).then(function(nodes) {
  1078. var optionEls = E([]);
  1079. for (var i = 0; i < nodes.length; i++)
  1080. optionEls.appendChild(nodes[i]);
  1081. return optionEls;
  1082. });
  1083. },
  1084. /** @private */
  1085. checkDepends: function(ev, n) {
  1086. var changed = false,
  1087. sids = this.cfgsections();
  1088. for (var i = 0, sid = sids[0]; (sid = sids[i]) != null; i++) {
  1089. for (var j = 0, o = this.children[0]; (o = this.children[j]) != null; j++) {
  1090. var isActive = o.isActive(sid),
  1091. isSatisified = o.checkDepends(sid);
  1092. if (isActive != isSatisified) {
  1093. o.setActive(sid, !isActive);
  1094. isActive = !isActive;
  1095. changed = true;
  1096. }
  1097. if (!n && isActive)
  1098. o.triggerValidation(sid);
  1099. }
  1100. }
  1101. return changed;
  1102. }
  1103. });
  1104. var isEqual = function(x, y) {
  1105. if (typeof(y) == 'object' && y instanceof RegExp)
  1106. return (x == null) ? false : y.test(x);
  1107. if (x != null && y != null && typeof(x) != typeof(y))
  1108. return false;
  1109. if ((x == null && y != null) || (x != null && y == null))
  1110. return false;
  1111. if (Array.isArray(x)) {
  1112. if (x.length != y.length)
  1113. return false;
  1114. for (var i = 0; i < x.length; i++)
  1115. if (!isEqual(x[i], y[i]))
  1116. return false;
  1117. }
  1118. else if (typeof(x) == 'object') {
  1119. for (var k in x) {
  1120. if (x.hasOwnProperty(k) && !y.hasOwnProperty(k))
  1121. return false;
  1122. if (!isEqual(x[k], y[k]))
  1123. return false;
  1124. }
  1125. for (var k in y)
  1126. if (y.hasOwnProperty(k) && !x.hasOwnProperty(k))
  1127. return false;
  1128. }
  1129. else if (x != y) {
  1130. return false;
  1131. }
  1132. return true;
  1133. };
  1134. var isContained = function(x, y) {
  1135. if (Array.isArray(x)) {
  1136. for (var i = 0; i < x.length; i++)
  1137. if (x[i] == y)
  1138. return true;
  1139. }
  1140. else if (L.isObject(x)) {
  1141. if (x.hasOwnProperty(y) && x[y] != null)
  1142. return true;
  1143. }
  1144. else if (typeof(x) == 'string') {
  1145. return (x.indexOf(y) > -1);
  1146. }
  1147. return false;
  1148. };
  1149. /**
  1150. * @class AbstractValue
  1151. * @memberof LuCI.form
  1152. * @augments LuCI.form.AbstractElement
  1153. * @hideconstructor
  1154. * @classdesc
  1155. *
  1156. * The `AbstractValue` class serves as abstract base for the different form
  1157. * option styles implemented by `LuCI.form`. It provides the common logic for
  1158. * handling option input values, for dependencies among options and for
  1159. * validation constraints that should be applied to entered values.
  1160. *
  1161. * This class is private and not directly accessible by user code.
  1162. */
  1163. var CBIAbstractValue = CBIAbstractElement.extend(/** @lends LuCI.form.AbstractValue.prototype */ {
  1164. __init__: function(map, section, option /*, ... */) {
  1165. this.super('__init__', this.varargs(arguments, 3));
  1166. this.section = section;
  1167. this.option = option;
  1168. this.map = map;
  1169. this.config = map.config;
  1170. this.deps = [];
  1171. this.initial = {};
  1172. this.rmempty = true;
  1173. this.default = null;
  1174. this.size = null;
  1175. this.optional = false;
  1176. },
  1177. /**
  1178. * If set to `false`, the underlying option value is retained upon saving
  1179. * the form when the option element is disabled due to unsatisfied
  1180. * dependency constraints.
  1181. *
  1182. * @name LuCI.form.AbstractValue.prototype#rmempty
  1183. * @type boolean
  1184. * @default true
  1185. */
  1186. /**
  1187. * If set to `true`, the underlying ui input widget is allowed to be empty,
  1188. * otherwise the option element is marked invalid when no value is entered
  1189. * or selected by the user.
  1190. *
  1191. * @name LuCI.form.AbstractValue.prototype#optional
  1192. * @type boolean
  1193. * @default false
  1194. */
  1195. /**
  1196. * Sets a default value to use when the underlying UCI option is not set.
  1197. *
  1198. * @name LuCI.form.AbstractValue.prototype#default
  1199. * @type *
  1200. * @default null
  1201. */
  1202. /**
  1203. * Specifies a datatype constraint expression to validate input values
  1204. * against. Refer to {@link LuCI.validation} for details on the format.
  1205. *
  1206. * If the user entered input does not match the datatype validation, the
  1207. * option element is marked as invalid.
  1208. *
  1209. * @name LuCI.form.AbstractValue.prototype#datatype
  1210. * @type string
  1211. * @default null
  1212. */
  1213. /**
  1214. * Specifies a custom validation function to test the user input for
  1215. * validity. The validation function must return `true` to accept the
  1216. * value. Any other return value type is converted to a string and
  1217. * displayed to the user as validation error message.
  1218. *
  1219. * If the user entered input does not pass the validation function, the
  1220. * option element is marked as invalid.
  1221. *
  1222. * @name LuCI.form.AbstractValue.prototype#validate
  1223. * @type function
  1224. * @default null
  1225. */
  1226. /**
  1227. * Override the UCI configuration name to read the option value from.
  1228. *
  1229. * By default, the configuration name is inherited from the parent Map.
  1230. * By setting this property, a deviating configuration may be specified.
  1231. *
  1232. * The default is null, means inheriting from the parent form.
  1233. *
  1234. * @name LuCI.form.AbstractValue.prototype#uciconfig
  1235. * @type string
  1236. * @default null
  1237. */
  1238. /**
  1239. * Override the UCI section name to read the option value from.
  1240. *
  1241. * By default, the section ID is inherited from the parent section element.
  1242. * By setting this property, a deviating section may be specified.
  1243. *
  1244. * The default is null, means inheriting from the parent section.
  1245. *
  1246. * @name LuCI.form.AbstractValue.prototype#ucisection
  1247. * @type string
  1248. * @default null
  1249. */
  1250. /**
  1251. * Override the UCI option name to read the value from.
  1252. *
  1253. * By default, the elements name, which is passed as third argument to
  1254. * the constructor, is used as UCI option name. By setting this property,
  1255. * a deviating UCI option may be specified.
  1256. *
  1257. * The default is null, means using the option element name.
  1258. *
  1259. * @name LuCI.form.AbstractValue.prototype#ucioption
  1260. * @type string
  1261. * @default null
  1262. */
  1263. /**
  1264. * Mark grid section option element as editable.
  1265. *
  1266. * Options which are displayed in the table portion of a `GridSection`
  1267. * instance are rendered as readonly text by default. By setting the
  1268. * `editable` property of a child option element to `true`, that element
  1269. * is rendered as full input widget within its cell instead of a text only
  1270. * preview.
  1271. *
  1272. * This property has no effect on options that are not children of grid
  1273. * section elements.
  1274. *
  1275. * @name LuCI.form.AbstractValue.prototype#editable
  1276. * @type boolean
  1277. * @default false
  1278. */
  1279. /**
  1280. * Move grid section option element into the table, the modal popup or both.
  1281. *
  1282. * If this property is `null` (the default), the option element is
  1283. * displayed in both the table preview area and the per-section instance
  1284. * modal popup of a grid section. When it is set to `false` the option
  1285. * is only shown in the table but not the modal popup. When set to `true`,
  1286. * the option is only visible in the modal popup but not the table.
  1287. *
  1288. * This property has no effect on options that are not children of grid
  1289. * section elements.
  1290. *
  1291. * @name LuCI.form.AbstractValue.prototype#modalonly
  1292. * @type boolean
  1293. * @default null
  1294. */
  1295. /**
  1296. * Make option element readonly.
  1297. *
  1298. * This property defaults to the readonly state of the parent form element.
  1299. * When set to `true`, the underlying widget is rendered in disabled state,
  1300. * means its contents cannot be changed and the widget cannot be interacted
  1301. * with.
  1302. *
  1303. * @name LuCI.form.AbstractValue.prototype#readonly
  1304. * @type boolean
  1305. * @default false
  1306. */
  1307. /**
  1308. * Override the cell width of a table or grid section child option.
  1309. *
  1310. * If the property is set to a numeric value, it is treated as pixel width
  1311. * which is set on the containing cell element of the option, essentially
  1312. * forcing a certain column width. When the property is set to a string
  1313. * value, it is applied as-is to the CSS `width` property.
  1314. *
  1315. * This property has no effect on options that are not children of grid or
  1316. * table section elements.
  1317. *
  1318. * @name LuCI.form.AbstractValue.prototype#width
  1319. * @type number|string
  1320. * @default null
  1321. */
  1322. /**
  1323. * Register a custom value change handler.
  1324. *
  1325. * If this property is set to a function value, the function is invoked
  1326. * whenever the value of the underlying UI input element is changing.
  1327. *
  1328. * The invoked handler function will receive the DOM click element as
  1329. * first and the underlying configuration section ID as well as the input
  1330. * value as second and third argument respectively.
  1331. *
  1332. * @name LuCI.form.AbstractValue.prototype#onchange
  1333. * @type function
  1334. * @default null
  1335. */
  1336. /**
  1337. * Add a dependency contraint to the option.
  1338. *
  1339. * Dependency constraints allow making the presence of option elements
  1340. * dependant on the current values of certain other options within the
  1341. * same form. An option element with unsatisfied dependencies will be
  1342. * hidden from the view and its current value is omitted when saving.
  1343. *
  1344. * Multiple constraints (that is, multiple calls to `depends()`) are
  1345. * treated as alternatives, forming a logical "or" expression.
  1346. *
  1347. * By passing an object of name => value pairs as first argument, it is
  1348. * possible to depend on multiple options simultaneously, allowing to form
  1349. * a logical "and" expression.
  1350. *
  1351. * Option names may be given in "dot notation" which allows to reference
  1352. * option elements outside of the current form section. If a name without
  1353. * dot is specified, it refers to an option within the same configuration
  1354. * section. If specified as <code>configname.sectionid.optionname</code>,
  1355. * options anywhere within the same form may be specified.
  1356. *
  1357. * The object notation also allows for a number of special keys which are
  1358. * not treated as option names but as modifiers to influence the dependency
  1359. * constraint evaluation. The associated value of these special "tag" keys
  1360. * is ignored. The recognized tags are:
  1361. *
  1362. * <ul>
  1363. * <li>
  1364. * <code>!reverse</code><br>
  1365. * Invert the dependency, instead of requiring another option to be
  1366. * equal to the dependency value, that option should <em>not</em> be
  1367. * equal.
  1368. * </li>
  1369. * <li>
  1370. * <code>!contains</code><br>
  1371. * Instead of requiring an exact match, the dependency is considered
  1372. * satisfied when the dependency value is contained within the option
  1373. * value.
  1374. * </li>
  1375. * <li>
  1376. * <code>!default</code><br>
  1377. * The dependency is always satisfied
  1378. * </li>
  1379. * </ul>
  1380. *
  1381. * Examples:
  1382. *
  1383. * <ul>
  1384. * <li>
  1385. * <code>opt.depends("foo", "test")</code><br>
  1386. * Require the value of `foo` to be `test`.
  1387. * </li>
  1388. * <li>
  1389. * <code>opt.depends({ foo: "test" })</code><br>
  1390. * Equivalent to the previous example.
  1391. * </li>
  1392. * <li>
  1393. * <code>opt.depends({ foo: /test/ })</code><br>
  1394. * Require the value of `foo` to match the regular expression `/test/`.
  1395. * </li>
  1396. * <li>
  1397. * <code>opt.depends({ foo: "test", bar: "qrx" })</code><br>
  1398. * Require the value of `foo` to be `test` and the value of `bar` to be
  1399. * `qrx`.
  1400. * </li>
  1401. * <li>
  1402. * <code>opt.depends({ foo: "test" })<br>
  1403. * opt.depends({ bar: "qrx" })</code><br>
  1404. * Require either <code>foo</code> to be set to <code>test</code>,
  1405. * <em>or</em> the <code>bar</code> option to be <code>qrx</code>.
  1406. * </li>
  1407. * <li>
  1408. * <code>opt.depends("test.section1.foo", "bar")</code><br>
  1409. * Require the "foo" form option within the "section1" section to be
  1410. * set to "bar".
  1411. * </li>
  1412. * <li>
  1413. * <code>opt.depends({ foo: "test", "!contains": true })</code><br>
  1414. * Require the "foo" option value to contain the substring "test".
  1415. * </li>
  1416. * </ul>
  1417. *
  1418. * @param {string|Object<string, string|RegExp>} optionname_or_depends
  1419. * The name of the option to depend on or an object describing multiple
  1420. * dependencies which must be satified (a logical "and" expression).
  1421. *
  1422. * @param {string} optionvalue|RegExp
  1423. * When invoked with a plain option name as first argument, this parameter
  1424. * specifies the expected value. In case an object is passed as first
  1425. * argument, this parameter is ignored.
  1426. */
  1427. depends: function(field, value) {
  1428. var deps;
  1429. if (typeof(field) === 'string')
  1430. deps = {}, deps[field] = value;
  1431. else
  1432. deps = field;
  1433. this.deps.push(deps);
  1434. },
  1435. /** @private */
  1436. transformDepList: function(section_id, deplist) {
  1437. var list = deplist || this.deps,
  1438. deps = [];
  1439. if (Array.isArray(list)) {
  1440. for (var i = 0; i < list.length; i++) {
  1441. var dep = {};
  1442. for (var k in list[i]) {
  1443. if (list[i].hasOwnProperty(k)) {
  1444. if (k.charAt(0) === '!')
  1445. dep[k] = list[i][k];
  1446. else if (k.indexOf('.') !== -1)
  1447. dep['cbid.%s'.format(k)] = list[i][k];
  1448. else
  1449. dep['cbid.%s.%s.%s'.format(
  1450. this.uciconfig || this.section.uciconfig || this.map.config,
  1451. this.ucisection || section_id,
  1452. k
  1453. )] = list[i][k];
  1454. }
  1455. }
  1456. for (var k in dep) {
  1457. if (dep.hasOwnProperty(k)) {
  1458. deps.push(dep);
  1459. break;
  1460. }
  1461. }
  1462. }
  1463. }
  1464. return deps;
  1465. },
  1466. /** @private */
  1467. transformChoices: function() {
  1468. if (!Array.isArray(this.keylist) || this.keylist.length == 0)
  1469. return null;
  1470. var choices = {};
  1471. for (var i = 0; i < this.keylist.length; i++)
  1472. choices[this.keylist[i]] = this.vallist[i];
  1473. return choices;
  1474. },
  1475. /** @private */
  1476. checkDepends: function(section_id) {
  1477. var config_name = this.uciconfig || this.section.uciconfig || this.map.config,
  1478. active = this.map.isDependencySatisfied(this.deps, config_name, section_id);
  1479. if (active)
  1480. this.updateDefaultValue(section_id);
  1481. return active;
  1482. },
  1483. /** @private */
  1484. updateDefaultValue: function(section_id) {
  1485. if (!L.isObject(this.defaults))
  1486. return;
  1487. var config_name = this.uciconfig || this.section.uciconfig || this.map.config,
  1488. cfgvalue = L.toArray(this.cfgvalue(section_id))[0],
  1489. default_defval = null, satisified_defval = null;
  1490. for (var value in this.defaults) {
  1491. if (!this.defaults[value] || this.defaults[value].length == 0) {
  1492. default_defval = value;
  1493. continue;
  1494. }
  1495. else if (this.map.isDependencySatisfied(this.defaults[value], config_name, section_id)) {
  1496. satisified_defval = value;
  1497. break;
  1498. }
  1499. }
  1500. if (satisified_defval == null)
  1501. satisified_defval = default_defval;
  1502. var node = this.map.findElement('id', this.cbid(section_id));
  1503. if (node && node.getAttribute('data-changed') != 'true' && satisified_defval != null && cfgvalue == null)
  1504. dom.callClassMethod(node, 'setValue', satisified_defval);
  1505. this.default = satisified_defval;
  1506. },
  1507. /**
  1508. * Obtain the internal ID ("cbid") of the element instance.
  1509. *
  1510. * Since each form section element may map multiple underlying
  1511. * configuration sections, the configuration section ID is required to
  1512. * form a fully qualified ID pointing to the specific element instance
  1513. * within the given specific section.
  1514. *
  1515. * @param {string} section_id
  1516. * The configuration section ID
  1517. *
  1518. * @throws {TypeError}
  1519. * Throws a `TypeError` exception when no `section_id` was specified.
  1520. *
  1521. * @returns {string}
  1522. * Returns the element ID.
  1523. */
  1524. cbid: function(section_id) {
  1525. if (section_id == null)
  1526. L.error('TypeError', 'Section ID required');
  1527. return 'cbid.%s.%s.%s'.format(
  1528. this.uciconfig || this.section.uciconfig || this.map.config,
  1529. section_id, this.option);
  1530. },
  1531. /**
  1532. * Load the underlying configuration value.
  1533. *
  1534. * The default implementation of this method reads and returns the
  1535. * underlying UCI option value (or the related JavaScript property for
  1536. * `JSONMap` instances). It may be overwritten by user code to load data
  1537. * from nonstandard sources.
  1538. *
  1539. * @param {string} section_id
  1540. * The configuration section ID
  1541. *
  1542. * @throws {TypeError}
  1543. * Throws a `TypeError` exception when no `section_id` was specified.
  1544. *
  1545. * @returns {*|Promise<*>}
  1546. * Returns the configuration value to initialize the option element with.
  1547. * The return value of this function is filtered through `Promise.resolve()`
  1548. * so it may return promises if overridden by user code.
  1549. */
  1550. load: function(section_id) {
  1551. if (section_id == null)
  1552. L.error('TypeError', 'Section ID required');
  1553. return this.map.data.get(
  1554. this.uciconfig || this.section.uciconfig || this.map.config,
  1555. this.ucisection || section_id,
  1556. this.ucioption || this.option);
  1557. },
  1558. /**
  1559. * Obtain the underlying `LuCI.ui` element instance.
  1560. *
  1561. * @param {string} section_id
  1562. * The configuration section ID
  1563. *
  1564. * @throws {TypeError}
  1565. * Throws a `TypeError` exception when no `section_id` was specified.
  1566. *
  1567. * @return {LuCI.ui.AbstractElement|null}
  1568. * Returns the `LuCI.ui` element instance or `null` in case the form
  1569. * option implementation does not use `LuCI.ui` widgets.
  1570. */
  1571. getUIElement: function(section_id) {
  1572. var node = this.map.findElement('id', this.cbid(section_id)),
  1573. inst = node ? dom.findClassInstance(node) : null;
  1574. return (inst instanceof ui.AbstractElement) ? inst : null;
  1575. },
  1576. /**
  1577. * Query the underlying configuration value.
  1578. *
  1579. * The default implementation of this method returns the cached return
  1580. * value of [load()]{@link LuCI.form.AbstractValue#load}. It may be
  1581. * overwritten by user code to obtain the configuration value in a
  1582. * different way.
  1583. *
  1584. * @param {string} section_id
  1585. * The configuration section ID
  1586. *
  1587. * @throws {TypeError}
  1588. * Throws a `TypeError` exception when no `section_id` was specified.
  1589. *
  1590. * @returns {*}
  1591. * Returns the configuration value.
  1592. */
  1593. cfgvalue: function(section_id, set_value) {
  1594. if (section_id == null)
  1595. L.error('TypeError', 'Section ID required');
  1596. if (arguments.length == 2) {
  1597. this.data = this.data || {};
  1598. this.data[section_id] = set_value;
  1599. }
  1600. return this.data ? this.data[section_id] : null;
  1601. },
  1602. /**
  1603. * Query the current form input value.
  1604. *
  1605. * The default implementation of this method returns the current input
  1606. * value of the underlying [LuCI.ui]{@link LuCI.ui.AbstractElement} widget.
  1607. * It may be overwritten by user code to handle input values differently.
  1608. *
  1609. * @param {string} section_id
  1610. * The configuration section ID
  1611. *
  1612. * @throws {TypeError}
  1613. * Throws a `TypeError` exception when no `section_id` was specified.
  1614. *
  1615. * @returns {*}
  1616. * Returns the current input value.
  1617. */
  1618. formvalue: function(section_id) {
  1619. var elem = this.getUIElement(section_id);
  1620. return elem ? elem.getValue() : null;
  1621. },
  1622. /**
  1623. * Obtain a textual input representation.
  1624. *
  1625. * The default implementation of this method returns the HTML escaped
  1626. * current input value of the underlying
  1627. * [LuCI.ui]{@link LuCI.ui.AbstractElement} widget. User code or specific
  1628. * option element implementations may overwrite this function to apply a
  1629. * different logic, e.g. to return `Yes` or `No` depending on the checked
  1630. * state of checkbox elements.
  1631. *
  1632. * @param {string} section_id
  1633. * The configuration section ID
  1634. *
  1635. * @throws {TypeError}
  1636. * Throws a `TypeError` exception when no `section_id` was specified.
  1637. *
  1638. * @returns {string}
  1639. * Returns the text representation of the current input value.
  1640. */
  1641. textvalue: function(section_id) {
  1642. var cval = this.cfgvalue(section_id);
  1643. if (cval == null)
  1644. cval = this.default;
  1645. if (Array.isArray(cval))
  1646. cval = cval.join(' ');
  1647. return (cval != null) ? '%h'.format(cval) : null;
  1648. },
  1649. /**
  1650. * Apply custom validation logic.
  1651. *
  1652. * This method is invoked whenever incremental validation is performed on
  1653. * the user input, e.g. on keyup or blur events.
  1654. *
  1655. * The default implementation of this method does nothing and always
  1656. * returns `true`. User code may overwrite this method to provide
  1657. * additional validation logic which is not covered by data type
  1658. * constraints.
  1659. *
  1660. * @abstract
  1661. * @param {string} section_id
  1662. * The configuration section ID
  1663. *
  1664. * @param {*} value
  1665. * The value to validate
  1666. *
  1667. * @returns {*}
  1668. * The method shall return `true` to accept the given value. Any other
  1669. * return value is treated as failure, converted to a string and displayed
  1670. * as error message to the user.
  1671. */
  1672. validate: function(section_id, value) {
  1673. return true;
  1674. },
  1675. /**
  1676. * Test whether the input value is currently valid.
  1677. *
  1678. * @param {string} section_id
  1679. * The configuration section ID
  1680. *
  1681. * @returns {boolean}
  1682. * Returns `true` if the input value currently is valid, otherwise it
  1683. * returns `false`.
  1684. */
  1685. isValid: function(section_id) {
  1686. var elem = this.getUIElement(section_id);
  1687. return elem ? elem.isValid() : true;
  1688. },
  1689. /**
  1690. * Test whether the option element is currently active.
  1691. *
  1692. * An element is active when it is not hidden due to unsatisfied dependency
  1693. * constraints.
  1694. *
  1695. * @param {string} section_id
  1696. * The configuration section ID
  1697. *
  1698. * @returns {boolean}
  1699. * Returns `true` if the option element currently is active, otherwise it
  1700. * returns `false`.
  1701. */
  1702. isActive: function(section_id) {
  1703. var field = this.map.findElement('data-field', this.cbid(section_id));
  1704. return (field != null && !field.classList.contains('hidden'));
  1705. },
  1706. /** @private */
  1707. setActive: function(section_id, active) {
  1708. var field = this.map.findElement('data-field', this.cbid(section_id));
  1709. if (field && field.classList.contains('hidden') == active) {
  1710. field.classList[active ? 'remove' : 'add']('hidden');
  1711. if (dom.matches(field.parentNode, '.td.cbi-value-field'))
  1712. field.parentNode.classList[active ? 'remove' : 'add']('inactive');
  1713. return true;
  1714. }
  1715. return false;
  1716. },
  1717. /** @private */
  1718. triggerValidation: function(section_id) {
  1719. var elem = this.getUIElement(section_id);
  1720. return elem ? elem.triggerValidation() : true;
  1721. },
  1722. /**
  1723. * Parse the option element input.
  1724. *
  1725. * The function is invoked when the `parse()` method has been invoked on
  1726. * the parent form and triggers input value reading and validation.
  1727. *
  1728. * @param {string} section_id
  1729. * The configuration section ID
  1730. *
  1731. * @returns {Promise<void>}
  1732. * Returns a promise resolving once the input value has been read and
  1733. * validated or rejecting in case the input value does not meet the
  1734. * validation constraints.
  1735. */
  1736. parse: function(section_id) {
  1737. var active = this.isActive(section_id),
  1738. cval = this.cfgvalue(section_id),
  1739. fval = active ? this.formvalue(section_id) : null;
  1740. if (active && !this.isValid(section_id)) {
  1741. var title = this.stripTags(this.title).trim();
  1742. return Promise.reject(new TypeError(_('Option "%s" contains an invalid input value.').format(title || this.option)));
  1743. }
  1744. if (fval != '' && fval != null) {
  1745. if (this.forcewrite || !isEqual(cval, fval))
  1746. return Promise.resolve(this.write(section_id, fval));
  1747. }
  1748. else {
  1749. if (!active || this.rmempty || this.optional) {
  1750. return Promise.resolve(this.remove(section_id));
  1751. }
  1752. else if (!isEqual(cval, fval)) {
  1753. var title = this.stripTags(this.title).trim();
  1754. return Promise.reject(new TypeError(_('Option "%s" must not be empty.').format(title || this.option)));
  1755. }
  1756. }
  1757. return Promise.resolve();
  1758. },
  1759. /**
  1760. * Write the current input value into the configuration.
  1761. *
  1762. * This function is invoked upon saving the parent form when the option
  1763. * element is valid and when its input value has been changed compared to
  1764. * the initial value returned by
  1765. * [cfgvalue()]{@link LuCI.form.AbstractValue#cfgvalue}.
  1766. *
  1767. * The default implementation simply sets the given input value in the
  1768. * UCI configuration (or the associated JavaScript object property in
  1769. * case of `JSONMap` forms). It may be overwritten by user code to
  1770. * implement alternative save logic, e.g. to transform the input value
  1771. * before it is written.
  1772. *
  1773. * @param {string} section_id
  1774. * The configuration section ID
  1775. *
  1776. * @param {string|string[]} formvalue
  1777. * The input value to write.
  1778. */
  1779. write: function(section_id, formvalue) {
  1780. return this.map.data.set(
  1781. this.uciconfig || this.section.uciconfig || this.map.config,
  1782. this.ucisection || section_id,
  1783. this.ucioption || this.option,
  1784. formvalue);
  1785. },
  1786. /**
  1787. * Remove the corresponding value from the configuration.
  1788. *
  1789. * This function is invoked upon saving the parent form when the option
  1790. * element has been hidden due to unsatisfied dependencies or when the
  1791. * user cleared the input value and the option is marked optional.
  1792. *
  1793. * The default implementation simply removes the associated option from the
  1794. * UCI configuration (or the associated JavaScript object property in
  1795. * case of `JSONMap` forms). It may be overwritten by user code to
  1796. * implement alternative removal logic, e.g. to retain the original value.
  1797. *
  1798. * @param {string} section_id
  1799. * The configuration section ID
  1800. */
  1801. remove: function(section_id) {
  1802. return this.map.data.unset(
  1803. this.uciconfig || this.section.uciconfig || this.map.config,
  1804. this.ucisection || section_id,
  1805. this.ucioption || this.option);
  1806. }
  1807. });
  1808. /**
  1809. * @class TypedSection
  1810. * @memberof LuCI.form
  1811. * @augments LuCI.form.AbstractSection
  1812. * @hideconstructor
  1813. * @classdesc
  1814. *
  1815. * The `TypedSection` class maps all or - if `filter()` is overwritten - a
  1816. * subset of the underlying UCI configuration sections of a given type.
  1817. *
  1818. * Layout wise, the configuration section instances mapped by the section
  1819. * element (sometimes referred to as "section nodes") are stacked beneath
  1820. * each other in a single column, with an optional section remove button next
  1821. * to each section node and a section add button at the end, depending on the
  1822. * value of the `addremove` property.
  1823. *
  1824. * @param {LuCI.form.Map|LuCI.form.JSONMap} form
  1825. * The configuration form this section is added to. It is automatically passed
  1826. * by [section()]{@link LuCI.form.Map#section}.
  1827. *
  1828. * @param {string} section_type
  1829. * The type of the UCI section to map.
  1830. *
  1831. * @param {string} [title]
  1832. * The title caption of the form section element.
  1833. *
  1834. * @param {string} [description]
  1835. * The description text of the form section element.
  1836. */
  1837. var CBITypedSection = CBIAbstractSection.extend(/** @lends LuCI.form.TypedSection.prototype */ {
  1838. __name__: 'CBI.TypedSection',
  1839. /**
  1840. * If set to `true`, the user may add or remove instances from the form
  1841. * section widget, otherwise only preexisting sections may be edited.
  1842. * The default is `false`.
  1843. *
  1844. * @name LuCI.form.TypedSection.prototype#addremove
  1845. * @type boolean
  1846. * @default false
  1847. */
  1848. /**
  1849. * If set to `true`, mapped section instances are treated as anonymous
  1850. * UCI sections, which means that section instance elements will be
  1851. * rendered without title element and that no name is required when adding
  1852. * new sections. The default is `false`.
  1853. *
  1854. * @name LuCI.form.TypedSection.prototype#anonymous
  1855. * @type boolean
  1856. * @default false
  1857. */
  1858. /**
  1859. * When set to `true`, instead of rendering section instances one below
  1860. * another, treat each instance as separate tab pane and render a tab menu
  1861. * at the top of the form section element, allowing the user to switch
  1862. * among instances. The default is `false`.
  1863. *
  1864. * @name LuCI.form.TypedSection.prototype#tabbed
  1865. * @type boolean
  1866. * @default false
  1867. */
  1868. /**
  1869. * Override the caption used for the section add button at the bottom of
  1870. * the section form element. If set to a string, it will be used as-is,
  1871. * if set to a function, the function will be invoked and its return value
  1872. * is used as caption, after converting it to a string. If this property
  1873. * is not set, the default is `Add`.
  1874. *
  1875. * @name LuCI.form.TypedSection.prototype#addbtntitle
  1876. * @type string|function
  1877. * @default null
  1878. */
  1879. /**
  1880. * Override the UCI configuration name to read the section IDs from. By
  1881. * default, the configuration name is inherited from the parent `Map`.
  1882. * By setting this property, a deviating configuration may be specified.
  1883. * The default is `null`, means inheriting from the parent form.
  1884. *
  1885. * @name LuCI.form.TypedSection.prototype#uciconfig
  1886. * @type string
  1887. * @default null
  1888. */
  1889. /** @override */
  1890. cfgsections: function() {
  1891. return this.map.data.sections(this.uciconfig || this.map.config, this.sectiontype)
  1892. .map(function(s) { return s['.name'] })
  1893. .filter(L.bind(this.filter, this));
  1894. },
  1895. /** @private */
  1896. handleAdd: function(ev, name) {
  1897. var config_name = this.uciconfig || this.map.config;
  1898. this.map.data.add(config_name, this.sectiontype, name);
  1899. return this.map.save(null, true);
  1900. },
  1901. /** @private */
  1902. handleRemove: function(section_id, ev) {
  1903. var config_name = this.uciconfig || this.map.config;
  1904. this.map.data.remove(config_name, section_id);
  1905. return this.map.save(null, true);
  1906. },
  1907. /** @private */
  1908. renderSectionAdd: function(extra_class) {
  1909. if (!this.addremove)
  1910. return E([]);
  1911. var createEl = E('div', { 'class': 'cbi-section-create' }),
  1912. config_name = this.uciconfig || this.map.config,
  1913. btn_title = this.titleFn('addbtntitle');
  1914. if (extra_class != null)
  1915. createEl.classList.add(extra_class);
  1916. if (this.anonymous) {
  1917. createEl.appendChild(E('button', {
  1918. 'class': 'cbi-button cbi-button-add',
  1919. 'title': btn_title || _('Add'),
  1920. 'click': ui.createHandlerFn(this, 'handleAdd'),
  1921. 'disabled': this.map.readonly || null
  1922. }, [ btn_title || _('Add') ]));
  1923. }
  1924. else {
  1925. var nameEl = E('input', {
  1926. 'type': 'text',
  1927. 'class': 'cbi-section-create-name',
  1928. 'disabled': this.map.readonly || null
  1929. });
  1930. dom.append(createEl, [
  1931. E('div', {}, nameEl),
  1932. E('input', {
  1933. 'class': 'cbi-button cbi-button-add',
  1934. 'type': 'submit',
  1935. 'value': btn_title || _('Add'),
  1936. 'title': btn_title || _('Add'),
  1937. 'click': ui.createHandlerFn(this, function(ev) {
  1938. if (nameEl.classList.contains('cbi-input-invalid'))
  1939. return;
  1940. return this.handleAdd(ev, nameEl.value);
  1941. }),
  1942. 'disabled': this.map.readonly || null
  1943. })
  1944. ]);
  1945. ui.addValidator(nameEl, 'uciname', true, 'blur', 'keyup');
  1946. }
  1947. return createEl;
  1948. },
  1949. /** @private */
  1950. renderSectionPlaceholder: function() {
  1951. return E([
  1952. E('em', _('This section contains no values yet')),
  1953. E('br'), E('br')
  1954. ]);
  1955. },
  1956. /** @private */
  1957. renderContents: function(cfgsections, nodes) {
  1958. var section_id = null,
  1959. config_name = this.uciconfig || this.map.config,
  1960. sectionEl = E('div', {
  1961. 'id': 'cbi-%s-%s'.format(config_name, this.sectiontype),
  1962. 'class': 'cbi-section',
  1963. 'data-tab': (this.map.tabbed && !this.parentoption) ? this.sectiontype : null,
  1964. 'data-tab-title': (this.map.tabbed && !this.parentoption) ? this.title || this.sectiontype : null
  1965. });
  1966. if (this.title != null && this.title != '')
  1967. sectionEl.appendChild(E('h3', {}, this.title));
  1968. if (this.description != null && this.description != '')
  1969. sectionEl.appendChild(E('div', { 'class': 'cbi-section-descr' }, this.description));
  1970. for (var i = 0; i < nodes.length; i++) {
  1971. if (this.addremove) {
  1972. sectionEl.appendChild(
  1973. E('div', { 'class': 'cbi-section-remove right' },
  1974. E('button', {
  1975. 'class': 'cbi-button',
  1976. 'name': 'cbi.rts.%s.%s'.format(config_name, cfgsections[i]),
  1977. 'data-section-id': cfgsections[i],
  1978. 'click': ui.createHandlerFn(this, 'handleRemove', cfgsections[i]),
  1979. 'disabled': this.map.readonly || null
  1980. }, [ _('Delete') ])));
  1981. }
  1982. if (!this.anonymous)
  1983. sectionEl.appendChild(E('h3', cfgsections[i].toUpperCase()));
  1984. sectionEl.appendChild(E('div', {
  1985. 'id': 'cbi-%s-%s'.format(config_name, cfgsections[i]),
  1986. 'class': this.tabs
  1987. ? 'cbi-section-node cbi-section-node-tabbed' : 'cbi-section-node',
  1988. 'data-section-id': cfgsections[i]
  1989. }, nodes[i]));
  1990. }
  1991. if (nodes.length == 0)
  1992. sectionEl.appendChild(this.renderSectionPlaceholder());
  1993. sectionEl.appendChild(this.renderSectionAdd());
  1994. dom.bindClassInstance(sectionEl, this);
  1995. return sectionEl;
  1996. },
  1997. /** @override */
  1998. render: function() {
  1999. var cfgsections = this.cfgsections(),
  2000. renderTasks = [];
  2001. for (var i = 0; i < cfgsections.length; i++)
  2002. renderTasks.push(this.renderUCISection(cfgsections[i]));
  2003. return Promise.all(renderTasks).then(this.renderContents.bind(this, cfgsections));
  2004. }
  2005. });
  2006. /**
  2007. * @class TableSection
  2008. * @memberof LuCI.form
  2009. * @augments LuCI.form.TypedSection
  2010. * @hideconstructor
  2011. * @classdesc
  2012. *
  2013. * The `TableSection` class maps all or - if `filter()` is overwritten - a
  2014. * subset of the underlying UCI configuration sections of a given type.
  2015. *
  2016. * Layout wise, the configuration section instances mapped by the section
  2017. * element (sometimes referred to as "section nodes") are rendered as rows
  2018. * within an HTML table element, with an optional section remove button in the
  2019. * last column and a section add button below the table, depending on the
  2020. * value of the `addremove` property.
  2021. *
  2022. * @param {LuCI.form.Map|LuCI.form.JSONMap} form
  2023. * The configuration form this section is added to. It is automatically passed
  2024. * by [section()]{@link LuCI.form.Map#section}.
  2025. *
  2026. * @param {string} section_type
  2027. * The type of the UCI section to map.
  2028. *
  2029. * @param {string} [title]
  2030. * The title caption of the form section element.
  2031. *
  2032. * @param {string} [description]
  2033. * The description text of the form section element.
  2034. */
  2035. var CBITableSection = CBITypedSection.extend(/** @lends LuCI.form.TableSection.prototype */ {
  2036. __name__: 'CBI.TableSection',
  2037. /**
  2038. * If set to `true`, the user may add or remove instances from the form
  2039. * section widget, otherwise only preexisting sections may be edited.
  2040. * The default is `false`.
  2041. *
  2042. * @name LuCI.form.TableSection.prototype#addremove
  2043. * @type boolean
  2044. * @default false
  2045. */
  2046. /**
  2047. * If set to `true`, mapped section instances are treated as anonymous
  2048. * UCI sections, which means that section instance elements will be
  2049. * rendered without title element and that no name is required when adding
  2050. * new sections. The default is `false`.
  2051. *
  2052. * @name LuCI.form.TableSection.prototype#anonymous
  2053. * @type boolean
  2054. * @default false
  2055. */
  2056. /**
  2057. * Override the caption used for the section add button at the bottom of
  2058. * the section form element. If set to a string, it will be used as-is,
  2059. * if set to a function, the function will be invoked and its return value
  2060. * is used as caption, after converting it to a string. If this property
  2061. * is not set, the default is `Add`.
  2062. *
  2063. * @name LuCI.form.TableSection.prototype#addbtntitle
  2064. * @type string|function
  2065. * @default null
  2066. */
  2067. /**
  2068. * Override the per-section instance title caption shown in the first
  2069. * column of the table unless `anonymous` is set to true. If set to a
  2070. * string, it will be used as `String.format()` pattern with the name of
  2071. * the underlying UCI section as first argument, if set to a function, the
  2072. * function will be invoked with the section name as first argument and
  2073. * its return value is used as caption, after converting it to a string.
  2074. * If this property is not set, the default is the name of the underlying
  2075. * UCI configuration section.
  2076. *
  2077. * @name LuCI.form.TableSection.prototype#sectiontitle
  2078. * @type string|function
  2079. * @default null
  2080. */
  2081. /**
  2082. * Override the per-section instance modal popup title caption shown when
  2083. * clicking the `More…` button in a section specifying `max_cols`. If set
  2084. * to a string, it will be used as `String.format()` pattern with the name
  2085. * of the underlying UCI section as first argument, if set to a function,
  2086. * the function will be invoked with the section name as first argument and
  2087. * its return value is used as caption, after converting it to a string.
  2088. * If this property is not set, the default is the name of the underlying
  2089. * UCI configuration section.
  2090. *
  2091. * @name LuCI.form.TableSection.prototype#modaltitle
  2092. * @type string|function
  2093. * @default null
  2094. */
  2095. /**
  2096. * Override the UCI configuration name to read the section IDs from. By
  2097. * default, the configuration name is inherited from the parent `Map`.
  2098. * By setting this property, a deviating configuration may be specified.
  2099. * The default is `null`, means inheriting from the parent form.
  2100. *
  2101. * @name LuCI.form.TableSection.prototype#uciconfig
  2102. * @type string
  2103. * @default null
  2104. */
  2105. /**
  2106. * Specify a maximum amount of columns to display. By default, one table
  2107. * column is rendered for each child option of the form section element.
  2108. * When this option is set to a positive number, then no more columns than
  2109. * the given amount are rendered. When the number of child options exceeds
  2110. * the specified amount, a `More…` button is rendered in the last column,
  2111. * opening a modal dialog presenting all options elements in `NamedSection`
  2112. * style when clicked.
  2113. *
  2114. * @name LuCI.form.TableSection.prototype#max_cols
  2115. * @type number
  2116. * @default null
  2117. */
  2118. /**
  2119. * If set to `true`, alternating `cbi-rowstyle-1` and `cbi-rowstyle-2` CSS
  2120. * classes are added to the table row elements. Not all LuCI themes
  2121. * implement these row style classes. The default is `false`.
  2122. *
  2123. * @name LuCI.form.TableSection.prototype#rowcolors
  2124. * @type boolean
  2125. * @default false
  2126. */
  2127. /**
  2128. * Enables a per-section instance row `Edit` button which triggers a certain
  2129. * action when clicked. If set to a string, the string value is used
  2130. * as `String.format()` pattern with the name of the underlying UCI section
  2131. * as first format argument. The result is then interpreted as URL which
  2132. * LuCI will navigate to when the user clicks the edit button.
  2133. *
  2134. * If set to a function, this function will be registered as click event
  2135. * handler on the rendered edit button, receiving the section instance
  2136. * name as first and the DOM click event as second argument.
  2137. *
  2138. * @name LuCI.form.TableSection.prototype#extedit
  2139. * @type string|function
  2140. * @default null
  2141. */
  2142. /**
  2143. * If set to `true`, a sort button is added to the last column, allowing
  2144. * the user to reorder the section instances mapped by the section form
  2145. * element.
  2146. *
  2147. * @name LuCI.form.TableSection.prototype#sortable
  2148. * @type boolean
  2149. * @default false
  2150. */
  2151. /**
  2152. * If set to `true`, the header row with the options descriptions will
  2153. * not be displayed. By default, descriptions row is automatically displayed
  2154. * when at least one option has a description.
  2155. *
  2156. * @name LuCI.form.TableSection.prototype#nodescriptions
  2157. * @type boolean
  2158. * @default false
  2159. */
  2160. /**
  2161. * The `TableSection` implementation does not support option tabbing, so
  2162. * its implementation of `tab()` will always throw an exception when
  2163. * invoked.
  2164. *
  2165. * @override
  2166. * @throws Throws an exception when invoked.
  2167. */
  2168. tab: function() {
  2169. throw 'Tabs are not supported by TableSection';
  2170. },
  2171. /** @private */
  2172. renderContents: function(cfgsections, nodes) {
  2173. var section_id = null,
  2174. config_name = this.uciconfig || this.map.config,
  2175. max_cols = isNaN(this.max_cols) ? this.children.length : this.max_cols,
  2176. has_more = max_cols < this.children.length,
  2177. sectionEl = E('div', {
  2178. 'id': 'cbi-%s-%s'.format(config_name, this.sectiontype),
  2179. 'class': 'cbi-section cbi-tblsection',
  2180. 'data-tab': (this.map.tabbed && !this.parentoption) ? this.sectiontype : null,
  2181. 'data-tab-title': (this.map.tabbed && !this.parentoption) ? this.title || this.sectiontype : null
  2182. }),
  2183. tableEl = E('table', {
  2184. 'class': 'table cbi-section-table'
  2185. });
  2186. if (this.title != null && this.title != '')
  2187. sectionEl.appendChild(E('h3', {}, this.title));
  2188. if (this.description != null && this.description != '')
  2189. sectionEl.appendChild(E('div', { 'class': 'cbi-section-descr' }, this.description));
  2190. tableEl.appendChild(this.renderHeaderRows(max_cols));
  2191. for (var i = 0; i < nodes.length; i++) {
  2192. var sectionname = this.titleFn('sectiontitle', cfgsections[i]);
  2193. if (sectionname == null)
  2194. sectionname = cfgsections[i];
  2195. var trEl = E('tr', {
  2196. 'id': 'cbi-%s-%s'.format(config_name, cfgsections[i]),
  2197. 'class': 'tr cbi-section-table-row',
  2198. 'data-sid': cfgsections[i],
  2199. 'draggable': this.sortable ? true : null,
  2200. 'mousedown': this.sortable ? L.bind(this.handleDragInit, this) : null,
  2201. 'dragstart': this.sortable ? L.bind(this.handleDragStart, this) : null,
  2202. 'dragover': this.sortable ? L.bind(this.handleDragOver, this) : null,
  2203. 'dragenter': this.sortable ? L.bind(this.handleDragEnter, this) : null,
  2204. 'dragleave': this.sortable ? L.bind(this.handleDragLeave, this) : null,
  2205. 'dragend': this.sortable ? L.bind(this.handleDragEnd, this) : null,
  2206. 'drop': this.sortable ? L.bind(this.handleDrop, this) : null,
  2207. 'data-title': (sectionname && (!this.anonymous || this.sectiontitle)) ? sectionname : null,
  2208. 'data-section-id': cfgsections[i]
  2209. });
  2210. if (this.extedit || this.rowcolors)
  2211. trEl.classList.add(!(tableEl.childNodes.length % 2)
  2212. ? 'cbi-rowstyle-1' : 'cbi-rowstyle-2');
  2213. for (var j = 0; j < max_cols && nodes[i].firstChild; j++)
  2214. trEl.appendChild(nodes[i].firstChild);
  2215. trEl.appendChild(this.renderRowActions(cfgsections[i], has_more ? _('More…') : null));
  2216. tableEl.appendChild(trEl);
  2217. }
  2218. if (nodes.length == 0)
  2219. tableEl.appendChild(E('tr', { 'class': 'tr cbi-section-table-row placeholder' },
  2220. E('td', { 'class': 'td' },
  2221. E('em', {}, _('This section contains no values yet')))));
  2222. sectionEl.appendChild(tableEl);
  2223. sectionEl.appendChild(this.renderSectionAdd('cbi-tblsection-create'));
  2224. dom.bindClassInstance(sectionEl, this);
  2225. return sectionEl;
  2226. },
  2227. /** @private */
  2228. renderHeaderRows: function(max_cols, has_action) {
  2229. var has_titles = false,
  2230. has_descriptions = false,
  2231. max_cols = isNaN(this.max_cols) ? this.children.length : this.max_cols,
  2232. has_more = max_cols < this.children.length,
  2233. anon_class = (!this.anonymous || this.sectiontitle) ? 'named' : 'anonymous',
  2234. trEls = E([]);
  2235. for (var i = 0, opt; i < max_cols && (opt = this.children[i]) != null; i++) {
  2236. if (opt.modalonly)
  2237. continue;
  2238. has_titles = has_titles || !!opt.title;
  2239. has_descriptions = has_descriptions || !!opt.description;
  2240. }
  2241. if (has_titles) {
  2242. var trEl = E('tr', {
  2243. 'class': 'tr cbi-section-table-titles ' + anon_class,
  2244. 'data-title': (!this.anonymous || this.sectiontitle) ? _('Name') : null
  2245. });
  2246. for (var i = 0, opt; i < max_cols && (opt = this.children[i]) != null; i++) {
  2247. if (opt.modalonly)
  2248. continue;
  2249. trEl.appendChild(E('th', {
  2250. 'class': 'th cbi-section-table-cell',
  2251. 'data-widget': opt.__name__
  2252. }));
  2253. if (opt.width != null)
  2254. trEl.lastElementChild.style.width =
  2255. (typeof(opt.width) == 'number') ? opt.width+'px' : opt.width;
  2256. if (opt.titleref)
  2257. trEl.lastElementChild.appendChild(E('a', {
  2258. 'href': opt.titleref,
  2259. 'class': 'cbi-title-ref',
  2260. 'title': this.titledesc || _('Go to relevant configuration page')
  2261. }, opt.title));
  2262. else
  2263. dom.content(trEl.lastElementChild, opt.title);
  2264. }
  2265. if (this.sortable || this.extedit || this.addremove || has_more || has_action)
  2266. trEl.appendChild(E('th', {
  2267. 'class': 'th cbi-section-table-cell cbi-section-actions'
  2268. }));
  2269. trEls.appendChild(trEl);
  2270. }
  2271. if (has_descriptions && !this.nodescriptions) {
  2272. var trEl = E('tr', {
  2273. 'class': 'tr cbi-section-table-descr ' + anon_class
  2274. });
  2275. for (var i = 0, opt; i < max_cols && (opt = this.children[i]) != null; i++) {
  2276. if (opt.modalonly)
  2277. continue;
  2278. trEl.appendChild(E('th', {
  2279. 'class': 'th cbi-section-table-cell',
  2280. 'data-widget': opt.__name__
  2281. }, opt.description));
  2282. if (opt.width != null)
  2283. trEl.lastElementChild.style.width =
  2284. (typeof(opt.width) == 'number') ? opt.width+'px' : opt.width;
  2285. }
  2286. if (this.sortable || this.extedit || this.addremove || has_more || has_action)
  2287. trEl.appendChild(E('th', {
  2288. 'class': 'th cbi-section-table-cell cbi-section-actions'
  2289. }));
  2290. trEls.appendChild(trEl);
  2291. }
  2292. return trEls;
  2293. },
  2294. /** @private */
  2295. renderRowActions: function(section_id, more_label) {
  2296. var config_name = this.uciconfig || this.map.config;
  2297. if (!this.sortable && !this.extedit && !this.addremove && !more_label)
  2298. return E([]);
  2299. var tdEl = E('td', {
  2300. 'class': 'td cbi-section-table-cell nowrap cbi-section-actions'
  2301. }, E('div'));
  2302. if (this.sortable) {
  2303. dom.append(tdEl.lastElementChild, [
  2304. E('div', {
  2305. 'title': _('Drag to reorder'),
  2306. 'class': 'btn cbi-button drag-handle center',
  2307. 'style': 'cursor:move',
  2308. 'disabled': this.map.readonly || null
  2309. }, '☰')
  2310. ]);
  2311. }
  2312. if (this.extedit) {
  2313. var evFn = null;
  2314. if (typeof(this.extedit) == 'function')
  2315. evFn = L.bind(this.extedit, this);
  2316. else if (typeof(this.extedit) == 'string')
  2317. evFn = L.bind(function(sid, ev) {
  2318. location.href = this.extedit.format(sid);
  2319. }, this, section_id);
  2320. dom.append(tdEl.lastElementChild,
  2321. E('button', {
  2322. 'title': _('Edit'),
  2323. 'class': 'cbi-button cbi-button-edit',
  2324. 'click': evFn
  2325. }, [ _('Edit') ])
  2326. );
  2327. }
  2328. if (more_label) {
  2329. dom.append(tdEl.lastElementChild,
  2330. E('button', {
  2331. 'title': more_label,
  2332. 'class': 'cbi-button cbi-button-edit',
  2333. 'click': ui.createHandlerFn(this, 'renderMoreOptionsModal', section_id)
  2334. }, [ more_label ])
  2335. );
  2336. }
  2337. if (this.addremove) {
  2338. var btn_title = this.titleFn('removebtntitle', section_id);
  2339. dom.append(tdEl.lastElementChild,
  2340. E('button', {
  2341. 'title': btn_title || _('Delete'),
  2342. 'class': 'cbi-button cbi-button-remove',
  2343. 'click': ui.createHandlerFn(this, 'handleRemove', section_id),
  2344. 'disabled': this.map.readonly || null
  2345. }, [ btn_title || _('Delete') ])
  2346. );
  2347. }
  2348. return tdEl;
  2349. },
  2350. /** @private */
  2351. handleDragInit: function(ev) {
  2352. scope.dragState = { node: ev.target };
  2353. },
  2354. /** @private */
  2355. handleDragStart: function(ev) {
  2356. if (!scope.dragState || !scope.dragState.node.classList.contains('drag-handle')) {
  2357. scope.dragState = null;
  2358. ev.preventDefault();
  2359. return false;
  2360. }
  2361. scope.dragState.node = dom.parent(scope.dragState.node, '.tr');
  2362. ev.dataTransfer.setData('text', 'drag');
  2363. ev.target.style.opacity = 0.4;
  2364. },
  2365. /** @private */
  2366. handleDragOver: function(ev) {
  2367. var n = scope.dragState.targetNode,
  2368. r = scope.dragState.rect,
  2369. t = r.top + r.height / 2;
  2370. if (ev.clientY <= t) {
  2371. n.classList.remove('drag-over-below');
  2372. n.classList.add('drag-over-above');
  2373. }
  2374. else {
  2375. n.classList.remove('drag-over-above');
  2376. n.classList.add('drag-over-below');
  2377. }
  2378. ev.dataTransfer.dropEffect = 'move';
  2379. ev.preventDefault();
  2380. return false;
  2381. },
  2382. /** @private */
  2383. handleDragEnter: function(ev) {
  2384. scope.dragState.rect = ev.currentTarget.getBoundingClientRect();
  2385. scope.dragState.targetNode = ev.currentTarget;
  2386. },
  2387. /** @private */
  2388. handleDragLeave: function(ev) {
  2389. ev.currentTarget.classList.remove('drag-over-above');
  2390. ev.currentTarget.classList.remove('drag-over-below');
  2391. },
  2392. /** @private */
  2393. handleDragEnd: function(ev) {
  2394. var n = ev.target;
  2395. n.style.opacity = '';
  2396. n.classList.add('flash');
  2397. n.parentNode.querySelectorAll('.drag-over-above, .drag-over-below')
  2398. .forEach(function(tr) {
  2399. tr.classList.remove('drag-over-above');
  2400. tr.classList.remove('drag-over-below');
  2401. });
  2402. },
  2403. /** @private */
  2404. handleDrop: function(ev) {
  2405. var s = scope.dragState;
  2406. if (s.node && s.targetNode) {
  2407. var config_name = this.uciconfig || this.map.config,
  2408. ref_node = s.targetNode,
  2409. after = false;
  2410. if (ref_node.classList.contains('drag-over-below')) {
  2411. ref_node = ref_node.nextElementSibling;
  2412. after = true;
  2413. }
  2414. var sid1 = s.node.getAttribute('data-sid'),
  2415. sid2 = s.targetNode.getAttribute('data-sid');
  2416. s.node.parentNode.insertBefore(s.node, ref_node);
  2417. this.map.data.move(config_name, sid1, sid2, after);
  2418. }
  2419. scope.dragState = null;
  2420. ev.target.style.opacity = '';
  2421. ev.stopPropagation();
  2422. ev.preventDefault();
  2423. return false;
  2424. },
  2425. /** @private */
  2426. handleModalCancel: function(modalMap, ev) {
  2427. return Promise.resolve(ui.hideModal());
  2428. },
  2429. /** @private */
  2430. handleModalSave: function(modalMap, ev) {
  2431. return modalMap.save(null, true)
  2432. .then(L.bind(this.map.load, this.map))
  2433. .then(L.bind(this.map.reset, this.map))
  2434. .then(ui.hideModal)
  2435. .catch(function() {});
  2436. },
  2437. /**
  2438. * Add further options to the per-section instanced modal popup.
  2439. *
  2440. * This function may be overwritten by user code to perform additional
  2441. * setup steps before displaying the more options modal which is useful to
  2442. * e.g. query additional data or to inject further option elements.
  2443. *
  2444. * The default implementation of this function does nothing.
  2445. *
  2446. * @abstract
  2447. * @param {LuCI.form.NamedSection} modalSection
  2448. * The `NamedSection` instance about to be rendered in the modal popup.
  2449. *
  2450. * @param {string} section_id
  2451. * The ID of the underlying UCI section the modal popup belongs to.
  2452. *
  2453. * @param {Event} ev
  2454. * The DOM event emitted by clicking the `More…` button.
  2455. *
  2456. * @returns {*|Promise<*>}
  2457. * Return values of this function are ignored but if a promise is returned,
  2458. * it is run to completion before the rendering is continued, allowing
  2459. * custom logic to perform asynchroneous work before the modal dialog
  2460. * is shown.
  2461. */
  2462. addModalOptions: function(modalSection, section_id, ev) {
  2463. },
  2464. /** @private */
  2465. renderMoreOptionsModal: function(section_id, ev) {
  2466. var parent = this.map,
  2467. title = parent.title,
  2468. name = null,
  2469. m = new CBIMap(this.map.config, null, null),
  2470. s = m.section(CBINamedSection, section_id, this.sectiontype);
  2471. m.parent = parent;
  2472. m.readonly = parent.readonly;
  2473. s.tabs = this.tabs;
  2474. s.tab_names = this.tab_names;
  2475. if ((name = this.titleFn('modaltitle', section_id)) != null)
  2476. title = name;
  2477. else if ((name = this.titleFn('sectiontitle', section_id)) != null)
  2478. title = '%s - %s'.format(parent.title, name);
  2479. else if (!this.anonymous)
  2480. title = '%s - %s'.format(parent.title, section_id);
  2481. for (var i = 0; i < this.children.length; i++) {
  2482. var o1 = this.children[i];
  2483. if (o1.modalonly === false)
  2484. continue;
  2485. var o2 = s.option(o1.constructor, o1.option, o1.title, o1.description);
  2486. for (var k in o1) {
  2487. if (!o1.hasOwnProperty(k))
  2488. continue;
  2489. switch (k) {
  2490. case 'map':
  2491. case 'section':
  2492. case 'option':
  2493. case 'title':
  2494. case 'description':
  2495. continue;
  2496. default:
  2497. o2[k] = o1[k];
  2498. }
  2499. }
  2500. }
  2501. return Promise.resolve(this.addModalOptions(s, section_id, ev)).then(L.bind(m.render, m)).then(L.bind(function(nodes) {
  2502. ui.showModal(title, [
  2503. nodes,
  2504. E('div', { 'class': 'right' }, [
  2505. E('button', {
  2506. 'class': 'btn',
  2507. 'click': ui.createHandlerFn(this, 'handleModalCancel', m)
  2508. }, [ _('Dismiss') ]), ' ',
  2509. E('button', {
  2510. 'class': 'cbi-button cbi-button-positive important',
  2511. 'click': ui.createHandlerFn(this, 'handleModalSave', m),
  2512. 'disabled': m.readonly || null
  2513. }, [ _('Save') ])
  2514. ])
  2515. ], 'cbi-modal');
  2516. }, this)).catch(L.error);
  2517. }
  2518. });
  2519. /**
  2520. * @class GridSection
  2521. * @memberof LuCI.form
  2522. * @augments LuCI.form.TableSection
  2523. * @hideconstructor
  2524. * @classdesc
  2525. *
  2526. * The `GridSection` class maps all or - if `filter()` is overwritten - a
  2527. * subset of the underlying UCI configuration sections of a given type.
  2528. *
  2529. * A grid section functions similar to a {@link LuCI.form.TableSection} but
  2530. * supports tabbing in the modal overlay. Option elements added with
  2531. * [option()]{@link LuCI.form.GridSection#option} are shown in the table while
  2532. * elements added with [taboption()]{@link LuCI.form.GridSection#taboption}
  2533. * are displayed in the modal popup.
  2534. *
  2535. * Another important difference is that the table cells show a readonly text
  2536. * preview of the corresponding option elements by default, unless the child
  2537. * option element is explicitely made writable by setting the `editable`
  2538. * property to `true`.
  2539. *
  2540. * Additionally, the grid section honours a `modalonly` property of child
  2541. * option elements. Refer to the [AbstractValue]{@link LuCI.form.AbstractValue}
  2542. * documentation for details.
  2543. *
  2544. * Layout wise, a grid section looks mostly identical to table sections.
  2545. *
  2546. * @param {LuCI.form.Map|LuCI.form.JSONMap} form
  2547. * The configuration form this section is added to. It is automatically passed
  2548. * by [section()]{@link LuCI.form.Map#section}.
  2549. *
  2550. * @param {string} section_type
  2551. * The type of the UCI section to map.
  2552. *
  2553. * @param {string} [title]
  2554. * The title caption of the form section element.
  2555. *
  2556. * @param {string} [description]
  2557. * The description text of the form section element.
  2558. */
  2559. var CBIGridSection = CBITableSection.extend(/** @lends LuCI.form.GridSection.prototype */ {
  2560. /**
  2561. * Add an option tab to the section.
  2562. *
  2563. * The modal option elements of a grid section may be divided into multiple
  2564. * tabs to provide a better overview to the user.
  2565. *
  2566. * Before options can be moved into a tab pane, the corresponding tab
  2567. * has to be defined first, which is done by calling this function.
  2568. *
  2569. * Note that tabs are only effective in modal popups, options added with
  2570. * `option()` will not be assigned to a specific tab and are rendered in
  2571. * the table view only.
  2572. *
  2573. * @param {string} name
  2574. * The name of the tab to register. It may be freely chosen and just serves
  2575. * as an identifier to differentiate tabs.
  2576. *
  2577. * @param {string} title
  2578. * The human readable caption of the tab.
  2579. *
  2580. * @param {string} [description]
  2581. * An additional description text for the corresponding tab pane. It is
  2582. * displayed as text paragraph below the tab but before the tab pane
  2583. * contents. If omitted, no description will be rendered.
  2584. *
  2585. * @throws {Error}
  2586. * Throws an exeption if a tab with the same `name` already exists.
  2587. */
  2588. tab: function(name, title, description) {
  2589. CBIAbstractSection.prototype.tab.call(this, name, title, description);
  2590. },
  2591. /** @private */
  2592. handleAdd: function(ev, name) {
  2593. var config_name = this.uciconfig || this.map.config,
  2594. section_id = this.map.data.add(config_name, this.sectiontype, name);
  2595. this.addedSection = section_id;
  2596. return this.renderMoreOptionsModal(section_id);
  2597. },
  2598. /** @private */
  2599. handleModalSave: function(/* ... */) {
  2600. return this.super('handleModalSave', arguments)
  2601. .then(L.bind(function() { this.addedSection = null }, this));
  2602. },
  2603. /** @private */
  2604. handleModalCancel: function(/* ... */) {
  2605. var config_name = this.uciconfig || this.map.config;
  2606. if (this.addedSection != null) {
  2607. this.map.data.remove(config_name, this.addedSection);
  2608. this.addedSection = null;
  2609. }
  2610. return this.super('handleModalCancel', arguments);
  2611. },
  2612. /** @private */
  2613. renderUCISection: function(section_id) {
  2614. return this.renderOptions(null, section_id);
  2615. },
  2616. /** @private */
  2617. renderChildren: function(tab_name, section_id, in_table) {
  2618. var tasks = [], index = 0;
  2619. for (var i = 0, opt; (opt = this.children[i]) != null; i++) {
  2620. if (opt.disable || opt.modalonly)
  2621. continue;
  2622. if (opt.editable)
  2623. tasks.push(opt.render(index++, section_id, in_table));
  2624. else
  2625. tasks.push(this.renderTextValue(section_id, opt));
  2626. }
  2627. return Promise.all(tasks);
  2628. },
  2629. /** @private */
  2630. renderTextValue: function(section_id, opt) {
  2631. var title = this.stripTags(opt.title).trim(),
  2632. descr = this.stripTags(opt.description).trim(),
  2633. value = opt.textvalue(section_id);
  2634. return E('td', {
  2635. 'class': 'td cbi-value-field',
  2636. 'data-title': (title != '') ? title : null,
  2637. 'data-description': (descr != '') ? descr : null,
  2638. 'data-name': opt.option,
  2639. 'data-widget': opt.typename || opt.__name__
  2640. }, (value != null) ? value : E('em', _('none')));
  2641. },
  2642. /** @private */
  2643. renderHeaderRows: function(section_id) {
  2644. return this.super('renderHeaderRows', [ NaN, true ]);
  2645. },
  2646. /** @private */
  2647. renderRowActions: function(section_id) {
  2648. return this.super('renderRowActions', [ section_id, _('Edit') ]);
  2649. },
  2650. /** @override */
  2651. parse: function() {
  2652. var section_ids = this.cfgsections(),
  2653. tasks = [];
  2654. if (Array.isArray(this.children)) {
  2655. for (var i = 0; i < section_ids.length; i++) {
  2656. for (var j = 0; j < this.children.length; j++) {
  2657. if (!this.children[j].editable || this.children[j].modalonly)
  2658. continue;
  2659. tasks.push(this.children[j].parse(section_ids[i]));
  2660. }
  2661. }
  2662. }
  2663. return Promise.all(tasks);
  2664. }
  2665. });
  2666. /**
  2667. * @class NamedSection
  2668. * @memberof LuCI.form
  2669. * @augments LuCI.form.AbstractSection
  2670. * @hideconstructor
  2671. * @classdesc
  2672. *
  2673. * The `NamedSection` class maps exactly one UCI section instance which is
  2674. * specified when constructing the class instance.
  2675. *
  2676. * Layout and functionality wise, a named section is essentially a
  2677. * `TypedSection` which allows exactly one section node.
  2678. *
  2679. * @param {LuCI.form.Map|LuCI.form.JSONMap} form
  2680. * The configuration form this section is added to. It is automatically passed
  2681. * by [section()]{@link LuCI.form.Map#section}.
  2682. *
  2683. * @param {string} section_id
  2684. * The name (ID) of the UCI section to map.
  2685. *
  2686. * @param {string} section_type
  2687. * The type of the UCI section to map.
  2688. *
  2689. * @param {string} [title]
  2690. * The title caption of the form section element.
  2691. *
  2692. * @param {string} [description]
  2693. * The description text of the form section element.
  2694. */
  2695. var CBINamedSection = CBIAbstractSection.extend(/** @lends LuCI.form.NamedSection.prototype */ {
  2696. __name__: 'CBI.NamedSection',
  2697. __init__: function(map, section_id /*, ... */) {
  2698. this.super('__init__', this.varargs(arguments, 2, map));
  2699. this.section = section_id;
  2700. },
  2701. /**
  2702. * If set to `true`, the user may remove or recreate the sole mapped
  2703. * configuration instance from the form section widget, otherwise only a
  2704. * preexisting section may be edited. The default is `false`.
  2705. *
  2706. * @name LuCI.form.NamedSection.prototype#addremove
  2707. * @type boolean
  2708. * @default false
  2709. */
  2710. /**
  2711. * Override the UCI configuration name to read the section IDs from. By
  2712. * default, the configuration name is inherited from the parent `Map`.
  2713. * By setting this property, a deviating configuration may be specified.
  2714. * The default is `null`, means inheriting from the parent form.
  2715. *
  2716. * @name LuCI.form.NamedSection.prototype#uciconfig
  2717. * @type string
  2718. * @default null
  2719. */
  2720. /**
  2721. * The `NamedSection` class overwrites the generic `cfgsections()`
  2722. * implementation to return a one-element array containing the mapped
  2723. * section ID as sole element. User code should not normally change this.
  2724. *
  2725. * @returns {string[]}
  2726. * Returns a one-element array containing the mapped section ID.
  2727. */
  2728. cfgsections: function() {
  2729. return [ this.section ];
  2730. },
  2731. /** @private */
  2732. handleAdd: function(ev) {
  2733. var section_id = this.section,
  2734. config_name = this.uciconfig || this.map.config;
  2735. this.map.data.add(config_name, this.sectiontype, section_id);
  2736. return this.map.save(null, true);
  2737. },
  2738. /** @private */
  2739. handleRemove: function(ev) {
  2740. var section_id = this.section,
  2741. config_name = this.uciconfig || this.map.config;
  2742. this.map.data.remove(config_name, section_id);
  2743. return this.map.save(null, true);
  2744. },
  2745. /** @private */
  2746. renderContents: function(data) {
  2747. var ucidata = data[0], nodes = data[1],
  2748. section_id = this.section,
  2749. config_name = this.uciconfig || this.map.config,
  2750. sectionEl = E('div', {
  2751. 'id': ucidata ? null : 'cbi-%s-%s'.format(config_name, section_id),
  2752. 'class': 'cbi-section',
  2753. 'data-tab': (this.map.tabbed && !this.parentoption) ? this.sectiontype : null,
  2754. 'data-tab-title': (this.map.tabbed && !this.parentoption) ? this.title || this.sectiontype : null
  2755. });
  2756. if (typeof(this.title) === 'string' && this.title !== '')
  2757. sectionEl.appendChild(E('h3', {}, this.title));
  2758. if (typeof(this.description) === 'string' && this.description !== '')
  2759. sectionEl.appendChild(E('div', { 'class': 'cbi-section-descr' }, this.description));
  2760. if (ucidata) {
  2761. if (this.addremove) {
  2762. sectionEl.appendChild(
  2763. E('div', { 'class': 'cbi-section-remove right' },
  2764. E('button', {
  2765. 'class': 'cbi-button',
  2766. 'click': ui.createHandlerFn(this, 'handleRemove'),
  2767. 'disabled': this.map.readonly || null
  2768. }, [ _('Delete') ])));
  2769. }
  2770. sectionEl.appendChild(E('div', {
  2771. 'id': 'cbi-%s-%s'.format(config_name, section_id),
  2772. 'class': this.tabs
  2773. ? 'cbi-section-node cbi-section-node-tabbed' : 'cbi-section-node',
  2774. 'data-section-id': section_id
  2775. }, nodes));
  2776. }
  2777. else if (this.addremove) {
  2778. sectionEl.appendChild(
  2779. E('button', {
  2780. 'class': 'cbi-button cbi-button-add',
  2781. 'click': ui.createHandlerFn(this, 'handleAdd'),
  2782. 'disabled': this.map.readonly || null
  2783. }, [ _('Add') ]));
  2784. }
  2785. dom.bindClassInstance(sectionEl, this);
  2786. return sectionEl;
  2787. },
  2788. /** @override */
  2789. render: function() {
  2790. var config_name = this.uciconfig || this.map.config,
  2791. section_id = this.section;
  2792. return Promise.all([
  2793. this.map.data.get(config_name, section_id),
  2794. this.renderUCISection(section_id)
  2795. ]).then(this.renderContents.bind(this));
  2796. }
  2797. });
  2798. /**
  2799. * @class Value
  2800. * @memberof LuCI.form
  2801. * @augments LuCI.form.AbstractValue
  2802. * @hideconstructor
  2803. * @classdesc
  2804. *
  2805. * The `Value` class represents a simple one-line form input using the
  2806. * {@link LuCI.ui.Textfield} or - in case choices are added - the
  2807. * {@link LuCI.ui.Combobox} class as underlying widget.
  2808. *
  2809. * @param {LuCI.form.Map|LuCI.form.JSONMap} form
  2810. * The configuration form this section is added to. It is automatically passed
  2811. * by [option()]{@link LuCI.form.AbstractSection#option} or
  2812. * [taboption()]{@link LuCI.form.AbstractSection#taboption} when adding the
  2813. * option to the section.
  2814. *
  2815. * @param {LuCI.form.AbstractSection} section
  2816. * The configuration section this option is added to. It is automatically passed
  2817. * by [option()]{@link LuCI.form.AbstractSection#option} or
  2818. * [taboption()]{@link LuCI.form.AbstractSection#taboption} when adding the
  2819. * option to the section.
  2820. *
  2821. * @param {string} option
  2822. * The name of the UCI option to map.
  2823. *
  2824. * @param {string} [title]
  2825. * The title caption of the option element.
  2826. *
  2827. * @param {string} [description]
  2828. * The description text of the option element.
  2829. */
  2830. var CBIValue = CBIAbstractValue.extend(/** @lends LuCI.form.Value.prototype */ {
  2831. __name__: 'CBI.Value',
  2832. /**
  2833. * If set to `true`, the field is rendered as password input, otherwise
  2834. * as plain text input.
  2835. *
  2836. * @name LuCI.form.Value.prototype#password
  2837. * @type boolean
  2838. * @default false
  2839. */
  2840. /**
  2841. * Set a placeholder string to use when the input field is empty.
  2842. *
  2843. * @name LuCI.form.Value.prototype#placeholder
  2844. * @type string
  2845. * @default null
  2846. */
  2847. /**
  2848. * Add a predefined choice to the form option. By adding one or more
  2849. * choices, the plain text input field is turned into a combobox widget
  2850. * which prompts the user to select a predefined choice, or to enter a
  2851. * custom value.
  2852. *
  2853. * @param {string} key
  2854. * The choice value to add.
  2855. *
  2856. * @param {Node|string} value
  2857. * The caption for the choice value. May be a DOM node, a document fragment
  2858. * or a plain text string. If omitted, the `key` value is used as caption.
  2859. */
  2860. value: function(key, val) {
  2861. this.keylist = this.keylist || [];
  2862. this.keylist.push(String(key));
  2863. this.vallist = this.vallist || [];
  2864. this.vallist.push(dom.elem(val) ? val : String(val != null ? val : key));
  2865. },
  2866. /** @override */
  2867. render: function(option_index, section_id, in_table) {
  2868. return Promise.resolve(this.cfgvalue(section_id))
  2869. .then(this.renderWidget.bind(this, section_id, option_index))
  2870. .then(this.renderFrame.bind(this, section_id, in_table, option_index));
  2871. },
  2872. /** @private */
  2873. handleValueChange: function(section_id, state, ev) {
  2874. if (typeof(this.onchange) != 'function')
  2875. return;
  2876. var value = this.formvalue(section_id);
  2877. if (isEqual(value, state.previousValue))
  2878. return;
  2879. state.previousValue = value;
  2880. this.onchange.call(this, ev, section_id, value);
  2881. },
  2882. /** @private */
  2883. renderFrame: function(section_id, in_table, option_index, nodes) {
  2884. var config_name = this.uciconfig || this.section.uciconfig || this.map.config,
  2885. depend_list = this.transformDepList(section_id),
  2886. optionEl;
  2887. if (in_table) {
  2888. var title = this.stripTags(this.title).trim();
  2889. optionEl = E('td', {
  2890. 'class': 'td cbi-value-field',
  2891. 'data-title': (title != '') ? title : null,
  2892. 'data-description': this.stripTags(this.description).trim(),
  2893. 'data-name': this.option,
  2894. 'data-widget': this.typename || (this.template ? this.template.replace(/^.+\//, '') : null) || this.__name__
  2895. }, E('div', {
  2896. 'id': 'cbi-%s-%s-%s'.format(config_name, section_id, this.option),
  2897. 'data-index': option_index,
  2898. 'data-depends': depend_list,
  2899. 'data-field': this.cbid(section_id)
  2900. }));
  2901. }
  2902. else {
  2903. optionEl = E('div', {
  2904. 'class': 'cbi-value',
  2905. 'id': 'cbi-%s-%s-%s'.format(config_name, section_id, this.option),
  2906. 'data-index': option_index,
  2907. 'data-depends': depend_list,
  2908. 'data-field': this.cbid(section_id),
  2909. 'data-name': this.option,
  2910. 'data-widget': this.typename || (this.template ? this.template.replace(/^.+\//, '') : null) || this.__name__
  2911. });
  2912. if (this.last_child)
  2913. optionEl.classList.add('cbi-value-last');
  2914. if (typeof(this.title) === 'string' && this.title !== '') {
  2915. optionEl.appendChild(E('label', {
  2916. 'class': 'cbi-value-title',
  2917. 'for': 'widget.cbid.%s.%s.%s'.format(config_name, section_id, this.option),
  2918. 'click': function(ev) {
  2919. var node = ev.currentTarget,
  2920. elem = node.nextElementSibling.querySelector('#' + node.getAttribute('for')) || node.nextElementSibling.querySelector('[data-widget-id="' + node.getAttribute('for') + '"]');
  2921. if (elem) {
  2922. elem.click();
  2923. elem.focus();
  2924. }
  2925. }
  2926. },
  2927. this.titleref ? E('a', {
  2928. 'class': 'cbi-title-ref',
  2929. 'href': this.titleref,
  2930. 'title': this.titledesc || _('Go to relevant configuration page')
  2931. }, this.title) : this.title));
  2932. optionEl.appendChild(E('div', { 'class': 'cbi-value-field' }));
  2933. }
  2934. }
  2935. if (nodes)
  2936. (optionEl.lastChild || optionEl).appendChild(nodes);
  2937. if (!in_table && typeof(this.description) === 'string' && this.description !== '')
  2938. dom.append(optionEl.lastChild || optionEl,
  2939. E('div', { 'class': 'cbi-value-description' }, this.description));
  2940. if (depend_list && depend_list.length)
  2941. optionEl.classList.add('hidden');
  2942. optionEl.addEventListener('widget-change',
  2943. L.bind(this.map.checkDepends, this.map));
  2944. optionEl.addEventListener('widget-change',
  2945. L.bind(this.handleValueChange, this, section_id, {}));
  2946. dom.bindClassInstance(optionEl, this);
  2947. return optionEl;
  2948. },
  2949. /** @private */
  2950. renderWidget: function(section_id, option_index, cfgvalue) {
  2951. var value = (cfgvalue != null) ? cfgvalue : this.default,
  2952. choices = this.transformChoices(),
  2953. widget;
  2954. if (choices) {
  2955. var placeholder = (this.optional || this.rmempty)
  2956. ? E('em', _('unspecified')) : _('-- Please choose --');
  2957. widget = new ui.Combobox(Array.isArray(value) ? value.join(' ') : value, choices, {
  2958. id: this.cbid(section_id),
  2959. sort: this.keylist,
  2960. optional: this.optional || this.rmempty,
  2961. datatype: this.datatype,
  2962. select_placeholder: this.placeholder || placeholder,
  2963. validate: L.bind(this.validate, this, section_id),
  2964. disabled: (this.readonly != null) ? this.readonly : this.map.readonly
  2965. });
  2966. }
  2967. else {
  2968. widget = new ui.Textfield(Array.isArray(value) ? value.join(' ') : value, {
  2969. id: this.cbid(section_id),
  2970. password: this.password,
  2971. optional: this.optional || this.rmempty,
  2972. datatype: this.datatype,
  2973. placeholder: this.placeholder,
  2974. validate: L.bind(this.validate, this, section_id),
  2975. disabled: (this.readonly != null) ? this.readonly : this.map.readonly
  2976. });
  2977. }
  2978. return widget.render();
  2979. }
  2980. });
  2981. /**
  2982. * @class DynamicList
  2983. * @memberof LuCI.form
  2984. * @augments LuCI.form.Value
  2985. * @hideconstructor
  2986. * @classdesc
  2987. *
  2988. * The `DynamicList` class represents a multi value widget allowing the user
  2989. * to enter multiple unique values, optionally selected from a set of
  2990. * predefined choices. It builds upon the {@link LuCI.ui.DynamicList} widget.
  2991. *
  2992. * @param {LuCI.form.Map|LuCI.form.JSONMap} form
  2993. * The configuration form this section is added to. It is automatically passed
  2994. * by [option()]{@link LuCI.form.AbstractSection#option} or
  2995. * [taboption()]{@link LuCI.form.AbstractSection#taboption} when adding the
  2996. * option to the section.
  2997. *
  2998. * @param {LuCI.form.AbstractSection} section
  2999. * The configuration section this option is added to. It is automatically passed
  3000. * by [option()]{@link LuCI.form.AbstractSection#option} or
  3001. * [taboption()]{@link LuCI.form.AbstractSection#taboption} when adding the
  3002. * option to the section.
  3003. *
  3004. * @param {string} option
  3005. * The name of the UCI option to map.
  3006. *
  3007. * @param {string} [title]
  3008. * The title caption of the option element.
  3009. *
  3010. * @param {string} [description]
  3011. * The description text of the option element.
  3012. */
  3013. var CBIDynamicList = CBIValue.extend(/** @lends LuCI.form.DynamicList.prototype */ {
  3014. __name__: 'CBI.DynamicList',
  3015. /** @private */
  3016. renderWidget: function(section_id, option_index, cfgvalue) {
  3017. var value = (cfgvalue != null) ? cfgvalue : this.default,
  3018. choices = this.transformChoices(),
  3019. items = L.toArray(value);
  3020. var widget = new ui.DynamicList(items, choices, {
  3021. id: this.cbid(section_id),
  3022. sort: this.keylist,
  3023. optional: this.optional || this.rmempty,
  3024. datatype: this.datatype,
  3025. placeholder: this.placeholder,
  3026. validate: L.bind(this.validate, this, section_id),
  3027. disabled: (this.readonly != null) ? this.readonly : this.map.readonly
  3028. });
  3029. return widget.render();
  3030. },
  3031. });
  3032. /**
  3033. * @class ListValue
  3034. * @memberof LuCI.form
  3035. * @augments LuCI.form.Value
  3036. * @hideconstructor
  3037. * @classdesc
  3038. *
  3039. * The `ListValue` class implements a simple static HTML select element
  3040. * allowing the user to chose a single value from a set of predefined choices.
  3041. * It builds upon the {@link LuCI.ui.Select} widget.
  3042. *
  3043. * @param {LuCI.form.Map|LuCI.form.JSONMap} form
  3044. * The configuration form this section is added to. It is automatically passed
  3045. * by [option()]{@link LuCI.form.AbstractSection#option} or
  3046. * [taboption()]{@link LuCI.form.AbstractSection#taboption} when adding the
  3047. * option to the section.
  3048. *
  3049. * @param {LuCI.form.AbstractSection} section
  3050. * The configuration section this option is added to. It is automatically passed
  3051. * by [option()]{@link LuCI.form.AbstractSection#option} or
  3052. * [taboption()]{@link LuCI.form.AbstractSection#taboption} when adding the
  3053. * option to the section.
  3054. *
  3055. * @param {string} option
  3056. * The name of the UCI option to map.
  3057. *
  3058. * @param {string} [title]
  3059. * The title caption of the option element.
  3060. *
  3061. * @param {string} [description]
  3062. * The description text of the option element.
  3063. */
  3064. var CBIListValue = CBIValue.extend(/** @lends LuCI.form.ListValue.prototype */ {
  3065. __name__: 'CBI.ListValue',
  3066. __init__: function() {
  3067. this.super('__init__', arguments);
  3068. this.widget = 'select';
  3069. this.orientation = 'horizontal';
  3070. this.deplist = [];
  3071. },
  3072. /**
  3073. * Set the size attribute of the underlying HTML select element.
  3074. *
  3075. * @name LuCI.form.ListValue.prototype#size
  3076. * @type number
  3077. * @default null
  3078. */
  3079. /**
  3080. * Set the type of the underlying form controls.
  3081. *
  3082. * May be one of `select` or `radio`. If set to `select`, an HTML
  3083. * select element is rendered, otherwise a collection of `radio`
  3084. * elements is used.
  3085. *
  3086. * @name LuCI.form.ListValue.prototype#widget
  3087. * @type string
  3088. * @default select
  3089. */
  3090. /**
  3091. * Set the orientation of the underlying radio or checkbox elements.
  3092. *
  3093. * May be one of `horizontal` or `vertical`. Only applies to non-select
  3094. * widget types.
  3095. *
  3096. * @name LuCI.form.ListValue.prototype#orientation
  3097. * @type string
  3098. * @default horizontal
  3099. */
  3100. /** @private */
  3101. renderWidget: function(section_id, option_index, cfgvalue) {
  3102. var choices = this.transformChoices();
  3103. var widget = new ui.Select((cfgvalue != null) ? cfgvalue : this.default, choices, {
  3104. id: this.cbid(section_id),
  3105. size: this.size,
  3106. sort: this.keylist,
  3107. widget: this.widget,
  3108. optional: this.optional,
  3109. orientation: this.orientation,
  3110. placeholder: this.placeholder,
  3111. validate: L.bind(this.validate, this, section_id),
  3112. disabled: (this.readonly != null) ? this.readonly : this.map.readonly
  3113. });
  3114. return widget.render();
  3115. },
  3116. });
  3117. /**
  3118. * @class FlagValue
  3119. * @memberof LuCI.form
  3120. * @augments LuCI.form.Value
  3121. * @hideconstructor
  3122. * @classdesc
  3123. *
  3124. * The `FlagValue` element builds upon the {@link LuCI.ui.Checkbox} widget to
  3125. * implement a simple checkbox element.
  3126. *
  3127. * @param {LuCI.form.Map|LuCI.form.JSONMap} form
  3128. * The configuration form this section is added to. It is automatically passed
  3129. * by [option()]{@link LuCI.form.AbstractSection#option} or
  3130. * [taboption()]{@link LuCI.form.AbstractSection#taboption} when adding the
  3131. * option to the section.
  3132. *
  3133. * @param {LuCI.form.AbstractSection} section
  3134. * The configuration section this option is added to. It is automatically passed
  3135. * by [option()]{@link LuCI.form.AbstractSection#option} or
  3136. * [taboption()]{@link LuCI.form.AbstractSection#taboption} when adding the
  3137. * option to the section.
  3138. *
  3139. * @param {string} option
  3140. * The name of the UCI option to map.
  3141. *
  3142. * @param {string} [title]
  3143. * The title caption of the option element.
  3144. *
  3145. * @param {string} [description]
  3146. * The description text of the option element.
  3147. */
  3148. var CBIFlagValue = CBIValue.extend(/** @lends LuCI.form.FlagValue.prototype */ {
  3149. __name__: 'CBI.FlagValue',
  3150. __init__: function() {
  3151. this.super('__init__', arguments);
  3152. this.enabled = '1';
  3153. this.disabled = '0';
  3154. this.default = this.disabled;
  3155. },
  3156. /**
  3157. * Sets the input value to use for the checkbox checked state.
  3158. *
  3159. * @name LuCI.form.FlagValue.prototype#enabled
  3160. * @type number
  3161. * @default 1
  3162. */
  3163. /**
  3164. * Sets the input value to use for the checkbox unchecked state.
  3165. *
  3166. * @name LuCI.form.FlagValue.prototype#disabled
  3167. * @type number
  3168. * @default 0
  3169. */
  3170. /**
  3171. * Set a tooltip for the flag option.
  3172. *
  3173. * If set to a string, it will be used as-is as a tooltip.
  3174. *
  3175. * If set to a function, the function will be invoked and the return
  3176. * value will be shown as a tooltip. If the return value of the function
  3177. * is `null` no tooltip will be set.
  3178. *
  3179. * @name LuCI.form.TypedSection.prototype#tooltip
  3180. * @type string|function
  3181. * @default null
  3182. */
  3183. /**
  3184. * Set a tooltip icon.
  3185. *
  3186. * If set, this icon will be shown for the default one.
  3187. * This could also be a png icon from the resources directory.
  3188. *
  3189. * @name LuCI.form.TypedSection.prototype#tooltipicon
  3190. * @type string
  3191. * @default 'ℹ️';
  3192. */
  3193. /** @private */
  3194. renderWidget: function(section_id, option_index, cfgvalue) {
  3195. var tooltip = null;
  3196. if (typeof(this.tooltip) == 'function')
  3197. tooltip = this.tooltip.apply(this, [section_id]);
  3198. else if (typeof(this.tooltip) == 'string')
  3199. tooltip = (arguments.length > 1) ? ''.format.apply(this.tooltip, this.varargs(arguments, 1)) : this.tooltip;
  3200. var widget = new ui.Checkbox((cfgvalue != null) ? cfgvalue : this.default, {
  3201. id: this.cbid(section_id),
  3202. value_enabled: this.enabled,
  3203. value_disabled: this.disabled,
  3204. validate: L.bind(this.validate, this, section_id),
  3205. tooltip: tooltip,
  3206. tooltipicon: this.tooltipicon,
  3207. disabled: (this.readonly != null) ? this.readonly : this.map.readonly
  3208. });
  3209. return widget.render();
  3210. },
  3211. /**
  3212. * Query the checked state of the underlying checkbox widget and return
  3213. * either the `enabled` or the `disabled` property value, depending on
  3214. * the checked state.
  3215. *
  3216. * @override
  3217. */
  3218. formvalue: function(section_id) {
  3219. var elem = this.getUIElement(section_id),
  3220. checked = elem ? elem.isChecked() : false;
  3221. return checked ? this.enabled : this.disabled;
  3222. },
  3223. /**
  3224. * Query the checked state of the underlying checkbox widget and return
  3225. * either a localized `Yes` or `No` string, depending on the checked state.
  3226. *
  3227. * @override
  3228. */
  3229. textvalue: function(section_id) {
  3230. var cval = this.cfgvalue(section_id);
  3231. if (cval == null)
  3232. cval = this.default;
  3233. return (cval == this.enabled) ? _('Yes') : _('No');
  3234. },
  3235. /** @override */
  3236. parse: function(section_id) {
  3237. if (this.isActive(section_id)) {
  3238. var fval = this.formvalue(section_id);
  3239. if (!this.isValid(section_id)) {
  3240. var title = this.stripTags(this.title).trim();
  3241. return Promise.reject(new TypeError(_('Option "%s" contains an invalid input value.').format(title || this.option)));
  3242. }
  3243. if (fval == this.default && (this.optional || this.rmempty))
  3244. return Promise.resolve(this.remove(section_id));
  3245. else
  3246. return Promise.resolve(this.write(section_id, fval));
  3247. }
  3248. else {
  3249. return Promise.resolve(this.remove(section_id));
  3250. }
  3251. },
  3252. });
  3253. /**
  3254. * @class MultiValue
  3255. * @memberof LuCI.form
  3256. * @augments LuCI.form.DynamicList
  3257. * @hideconstructor
  3258. * @classdesc
  3259. *
  3260. * The `MultiValue` class is a modified variant of the `DynamicList` element
  3261. * which leverages the {@link LuCI.ui.Dropdown} widget to implement a multi
  3262. * select dropdown element.
  3263. *
  3264. * @param {LuCI.form.Map|LuCI.form.JSONMap} form
  3265. * The configuration form this section is added to. It is automatically passed
  3266. * by [option()]{@link LuCI.form.AbstractSection#option} or
  3267. * [taboption()]{@link LuCI.form.AbstractSection#taboption} when adding the
  3268. * option to the section.
  3269. *
  3270. * @param {LuCI.form.AbstractSection} section
  3271. * The configuration section this option is added to. It is automatically passed
  3272. * by [option()]{@link LuCI.form.AbstractSection#option} or
  3273. * [taboption()]{@link LuCI.form.AbstractSection#taboption} when adding the
  3274. * option to the section.
  3275. *
  3276. * @param {string} option
  3277. * The name of the UCI option to map.
  3278. *
  3279. * @param {string} [title]
  3280. * The title caption of the option element.
  3281. *
  3282. * @param {string} [description]
  3283. * The description text of the option element.
  3284. */
  3285. var CBIMultiValue = CBIDynamicList.extend(/** @lends LuCI.form.MultiValue.prototype */ {
  3286. __name__: 'CBI.MultiValue',
  3287. __init__: function() {
  3288. this.super('__init__', arguments);
  3289. this.placeholder = _('-- Please choose --');
  3290. },
  3291. /**
  3292. * Allows to specify the [display_items]{@link LuCI.ui.Dropdown.InitOptions}
  3293. * property of the underlying dropdown widget. If omitted, the value of
  3294. * the `size` property is used or `3` when `size` is unspecified as well.
  3295. *
  3296. * @name LuCI.form.MultiValue.prototype#display_size
  3297. * @type number
  3298. * @default null
  3299. */
  3300. /**
  3301. * Allows to specify the [dropdown_items]{@link LuCI.ui.Dropdown.InitOptions}
  3302. * property of the underlying dropdown widget. If omitted, the value of
  3303. * the `size` property is used or `-1` when `size` is unspecified as well.
  3304. *
  3305. * @name LuCI.form.MultiValue.prototype#dropdown_size
  3306. * @type number
  3307. * @default null
  3308. */
  3309. /** @private */
  3310. renderWidget: function(section_id, option_index, cfgvalue) {
  3311. var value = (cfgvalue != null) ? cfgvalue : this.default,
  3312. choices = this.transformChoices();
  3313. var widget = new ui.Dropdown(L.toArray(value), choices, {
  3314. id: this.cbid(section_id),
  3315. sort: this.keylist,
  3316. multiple: true,
  3317. optional: this.optional || this.rmempty,
  3318. select_placeholder: this.placeholder,
  3319. display_items: this.display_size || this.size || 3,
  3320. dropdown_items: this.dropdown_size || this.size || -1,
  3321. validate: L.bind(this.validate, this, section_id),
  3322. disabled: (this.readonly != null) ? this.readonly : this.map.readonly
  3323. });
  3324. return widget.render();
  3325. },
  3326. });
  3327. /**
  3328. * @class TextValue
  3329. * @memberof LuCI.form
  3330. * @augments LuCI.form.Value
  3331. * @hideconstructor
  3332. * @classdesc
  3333. *
  3334. * The `TextValue` class implements a multi-line textarea input using
  3335. * {@link LuCI.ui.Textarea}.
  3336. *
  3337. * @param {LuCI.form.Map|LuCI.form.JSONMap} form
  3338. * The configuration form this section is added to. It is automatically passed
  3339. * by [option()]{@link LuCI.form.AbstractSection#option} or
  3340. * [taboption()]{@link LuCI.form.AbstractSection#taboption} when adding the
  3341. * option to the section.
  3342. *
  3343. * @param {LuCI.form.AbstractSection} section
  3344. * The configuration section this option is added to. It is automatically passed
  3345. * by [option()]{@link LuCI.form.AbstractSection#option} or
  3346. * [taboption()]{@link LuCI.form.AbstractSection#taboption} when adding the
  3347. * option to the section.
  3348. *
  3349. * @param {string} option
  3350. * The name of the UCI option to map.
  3351. *
  3352. * @param {string} [title]
  3353. * The title caption of the option element.
  3354. *
  3355. * @param {string} [description]
  3356. * The description text of the option element.
  3357. */
  3358. var CBITextValue = CBIValue.extend(/** @lends LuCI.form.TextValue.prototype */ {
  3359. __name__: 'CBI.TextValue',
  3360. /** @ignore */
  3361. value: null,
  3362. /**
  3363. * Enforces the use of a monospace font for the textarea contents when set
  3364. * to `true`.
  3365. *
  3366. * @name LuCI.form.TextValue.prototype#monospace
  3367. * @type boolean
  3368. * @default false
  3369. */
  3370. /**
  3371. * Allows to specify the [cols]{@link LuCI.ui.Textarea.InitOptions}
  3372. * property of the underlying textarea widget.
  3373. *
  3374. * @name LuCI.form.TextValue.prototype#cols
  3375. * @type number
  3376. * @default null
  3377. */
  3378. /**
  3379. * Allows to specify the [rows]{@link LuCI.ui.Textarea.InitOptions}
  3380. * property of the underlying textarea widget.
  3381. *
  3382. * @name LuCI.form.TextValue.prototype#rows
  3383. * @type number
  3384. * @default null
  3385. */
  3386. /**
  3387. * Allows to specify the [wrap]{@link LuCI.ui.Textarea.InitOptions}
  3388. * property of the underlying textarea widget.
  3389. *
  3390. * @name LuCI.form.TextValue.prototype#wrap
  3391. * @type number
  3392. * @default null
  3393. */
  3394. /** @private */
  3395. renderWidget: function(section_id, option_index, cfgvalue) {
  3396. var value = (cfgvalue != null) ? cfgvalue : this.default;
  3397. var widget = new ui.Textarea(value, {
  3398. id: this.cbid(section_id),
  3399. optional: this.optional || this.rmempty,
  3400. placeholder: this.placeholder,
  3401. monospace: this.monospace,
  3402. cols: this.cols,
  3403. rows: this.rows,
  3404. wrap: this.wrap,
  3405. validate: L.bind(this.validate, this, section_id),
  3406. disabled: (this.readonly != null) ? this.readonly : this.map.readonly
  3407. });
  3408. return widget.render();
  3409. }
  3410. });
  3411. /**
  3412. * @class DummyValue
  3413. * @memberof LuCI.form
  3414. * @augments LuCI.form.Value
  3415. * @hideconstructor
  3416. * @classdesc
  3417. *
  3418. * The `DummyValue` element wraps an {@link LuCI.ui.Hiddenfield} widget and
  3419. * renders the underlying UCI option or default value as readonly text.
  3420. *
  3421. * @param {LuCI.form.Map|LuCI.form.JSONMap} form
  3422. * The configuration form this section is added to. It is automatically passed
  3423. * by [option()]{@link LuCI.form.AbstractSection#option} or
  3424. * [taboption()]{@link LuCI.form.AbstractSection#taboption} when adding the
  3425. * option to the section.
  3426. *
  3427. * @param {LuCI.form.AbstractSection} section
  3428. * The configuration section this option is added to. It is automatically passed
  3429. * by [option()]{@link LuCI.form.AbstractSection#option} or
  3430. * [taboption()]{@link LuCI.form.AbstractSection#taboption} when adding the
  3431. * option to the section.
  3432. *
  3433. * @param {string} option
  3434. * The name of the UCI option to map.
  3435. *
  3436. * @param {string} [title]
  3437. * The title caption of the option element.
  3438. *
  3439. * @param {string} [description]
  3440. * The description text of the option element.
  3441. */
  3442. var CBIDummyValue = CBIValue.extend(/** @lends LuCI.form.DummyValue.prototype */ {
  3443. __name__: 'CBI.DummyValue',
  3444. /**
  3445. * Set an URL which is opened when clicking on the dummy value text.
  3446. *
  3447. * By setting this property, the dummy value text is wrapped in an `<a>`
  3448. * element with the property value used as `href` attribute.
  3449. *
  3450. * @name LuCI.form.DummyValue.prototype#href
  3451. * @type string
  3452. * @default null
  3453. */
  3454. /**
  3455. * Treat the UCI option value (or the `default` property value) as HTML.
  3456. *
  3457. * By default, the value text is HTML escaped before being rendered as
  3458. * text. In some cases it may be needed to actually interpret and render
  3459. * HTML contents as-is. When set to `true`, HTML escaping is disabled.
  3460. *
  3461. * @name LuCI.form.DummyValue.prototype#rawhtml
  3462. * @type boolean
  3463. * @default null
  3464. */
  3465. /** @private */
  3466. renderWidget: function(section_id, option_index, cfgvalue) {
  3467. var value = (cfgvalue != null) ? cfgvalue : this.default,
  3468. hiddenEl = new ui.Hiddenfield(value, { id: this.cbid(section_id) }),
  3469. outputEl = E('div');
  3470. if (this.href && !((this.readonly != null) ? this.readonly : this.map.readonly))
  3471. outputEl.appendChild(E('a', { 'href': this.href }));
  3472. dom.append(outputEl.lastChild || outputEl,
  3473. this.rawhtml ? value : [ value ]);
  3474. return E([
  3475. outputEl,
  3476. hiddenEl.render()
  3477. ]);
  3478. },
  3479. /** @override */
  3480. remove: function() {},
  3481. /** @override */
  3482. write: function() {}
  3483. });
  3484. /**
  3485. * @class ButtonValue
  3486. * @memberof LuCI.form
  3487. * @augments LuCI.form.Value
  3488. * @hideconstructor
  3489. * @classdesc
  3490. *
  3491. * The `DummyValue` element wraps an {@link LuCI.ui.Hiddenfield} widget and
  3492. * renders the underlying UCI option or default value as readonly text.
  3493. *
  3494. * @param {LuCI.form.Map|LuCI.form.JSONMap} form
  3495. * The configuration form this section is added to. It is automatically passed
  3496. * by [option()]{@link LuCI.form.AbstractSection#option} or
  3497. * [taboption()]{@link LuCI.form.AbstractSection#taboption} when adding the
  3498. * option to the section.
  3499. *
  3500. * @param {LuCI.form.AbstractSection} section
  3501. * The configuration section this option is added to. It is automatically passed
  3502. * by [option()]{@link LuCI.form.AbstractSection#option} or
  3503. * [taboption()]{@link LuCI.form.AbstractSection#taboption} when adding the
  3504. * option to the section.
  3505. *
  3506. * @param {string} option
  3507. * The name of the UCI option to map.
  3508. *
  3509. * @param {string} [title]
  3510. * The title caption of the option element.
  3511. *
  3512. * @param {string} [description]
  3513. * The description text of the option element.
  3514. */
  3515. var CBIButtonValue = CBIValue.extend(/** @lends LuCI.form.ButtonValue.prototype */ {
  3516. __name__: 'CBI.ButtonValue',
  3517. /**
  3518. * Override the rendered button caption.
  3519. *
  3520. * By default, the option title - which is passed as fourth argument to the
  3521. * constructor - is used as caption for the button element. When setting
  3522. * this property to a string, it is used as `String.format()` pattern with
  3523. * the underlying UCI section name passed as first format argument. When
  3524. * set to a function, it is invoked passing the section ID as sole argument
  3525. * and the resulting return value is converted to a string before being
  3526. * used as button caption.
  3527. *
  3528. * The default is `null`, means the option title is used as caption.
  3529. *
  3530. * @name LuCI.form.ButtonValue.prototype#inputtitle
  3531. * @type string|function
  3532. * @default null
  3533. */
  3534. /**
  3535. * Override the button style class.
  3536. *
  3537. * By setting this property, a specific `cbi-button-*` CSS class can be
  3538. * selected to influence the style of the resulting button.
  3539. *
  3540. * Suitable values which are implemented by most themes are `positive`,
  3541. * `negative` and `primary`.
  3542. *
  3543. * The default is `null`, means a neutral button styling is used.
  3544. *
  3545. * @name LuCI.form.ButtonValue.prototype#inputstyle
  3546. * @type string
  3547. * @default null
  3548. */
  3549. /**
  3550. * Override the button click action.
  3551. *
  3552. * By default, the underlying UCI option (or default property) value is
  3553. * copied into a hidden field tied to the button element and the save
  3554. * action is triggered on the parent form element.
  3555. *
  3556. * When this property is set to a function, it is invoked instead of
  3557. * performing the default actions. The handler function will receive the
  3558. * DOM click element as first and the underlying configuration section ID
  3559. * as second argument.
  3560. *
  3561. * @name LuCI.form.ButtonValue.prototype#onclick
  3562. * @type function
  3563. * @default null
  3564. */
  3565. /** @private */
  3566. renderWidget: function(section_id, option_index, cfgvalue) {
  3567. var value = (cfgvalue != null) ? cfgvalue : this.default,
  3568. hiddenEl = new ui.Hiddenfield(value, { id: this.cbid(section_id) }),
  3569. outputEl = E('div'),
  3570. btn_title = this.titleFn('inputtitle', section_id) || this.titleFn('title', section_id);
  3571. if (value !== false)
  3572. dom.content(outputEl, [
  3573. E('button', {
  3574. 'class': 'cbi-button cbi-button-%s'.format(this.inputstyle || 'button'),
  3575. 'click': ui.createHandlerFn(this, function(section_id, ev) {
  3576. if (this.onclick)
  3577. return this.onclick(ev, section_id);
  3578. ev.currentTarget.parentNode.nextElementSibling.value = value;
  3579. return this.map.save();
  3580. }, section_id),
  3581. 'disabled': ((this.readonly != null) ? this.readonly : this.map.readonly) || null
  3582. }, [ btn_title ])
  3583. ]);
  3584. else
  3585. dom.content(outputEl, ' - ');
  3586. return E([
  3587. outputEl,
  3588. hiddenEl.render()
  3589. ]);
  3590. }
  3591. });
  3592. /**
  3593. * @class HiddenValue
  3594. * @memberof LuCI.form
  3595. * @augments LuCI.form.Value
  3596. * @hideconstructor
  3597. * @classdesc
  3598. *
  3599. * The `HiddenValue` element wraps an {@link LuCI.ui.Hiddenfield} widget.
  3600. *
  3601. * Hidden value widgets used to be necessary in legacy code which actually
  3602. * submitted the underlying HTML form the server. With client side handling of
  3603. * forms, there are more efficient ways to store hidden state data.
  3604. *
  3605. * Since this widget has no visible content, the title and description values
  3606. * of this form element should be set to `null` as well to avoid a broken or
  3607. * distorted form layout when rendering the option element.
  3608. *
  3609. * @param {LuCI.form.Map|LuCI.form.JSONMap} form
  3610. * The configuration form this section is added to. It is automatically passed
  3611. * by [option()]{@link LuCI.form.AbstractSection#option} or
  3612. * [taboption()]{@link LuCI.form.AbstractSection#taboption} when adding the
  3613. * option to the section.
  3614. *
  3615. * @param {LuCI.form.AbstractSection} section
  3616. * The configuration section this option is added to. It is automatically passed
  3617. * by [option()]{@link LuCI.form.AbstractSection#option} or
  3618. * [taboption()]{@link LuCI.form.AbstractSection#taboption} when adding the
  3619. * option to the section.
  3620. *
  3621. * @param {string} option
  3622. * The name of the UCI option to map.
  3623. *
  3624. * @param {string} [title]
  3625. * The title caption of the option element.
  3626. *
  3627. * @param {string} [description]
  3628. * The description text of the option element.
  3629. */
  3630. var CBIHiddenValue = CBIValue.extend(/** @lends LuCI.form.HiddenValue.prototype */ {
  3631. __name__: 'CBI.HiddenValue',
  3632. /** @private */
  3633. renderWidget: function(section_id, option_index, cfgvalue) {
  3634. var widget = new ui.Hiddenfield((cfgvalue != null) ? cfgvalue : this.default, {
  3635. id: this.cbid(section_id)
  3636. });
  3637. return widget.render();
  3638. }
  3639. });
  3640. /**
  3641. * @class FileUpload
  3642. * @memberof LuCI.form
  3643. * @augments LuCI.form.Value
  3644. * @hideconstructor
  3645. * @classdesc
  3646. *
  3647. * The `FileUpload` element wraps an {@link LuCI.ui.FileUpload} widget and
  3648. * offers the ability to browse, upload and select remote files.
  3649. *
  3650. * @param {LuCI.form.Map|LuCI.form.JSONMap} form
  3651. * The configuration form this section is added to. It is automatically passed
  3652. * by [option()]{@link LuCI.form.AbstractSection#option} or
  3653. * [taboption()]{@link LuCI.form.AbstractSection#taboption} when adding the
  3654. * option to the section.
  3655. *
  3656. * @param {LuCI.form.AbstractSection} section
  3657. * The configuration section this option is added to. It is automatically passed
  3658. * by [option()]{@link LuCI.form.AbstractSection#option} or
  3659. * [taboption()]{@link LuCI.form.AbstractSection#taboption} when adding the
  3660. * option to the section.
  3661. *
  3662. * @param {string} option
  3663. * The name of the UCI option to map.
  3664. *
  3665. * @param {string} [title]
  3666. * The title caption of the option element.
  3667. *
  3668. * @param {string} [description]
  3669. * The description text of the option element.
  3670. */
  3671. var CBIFileUpload = CBIValue.extend(/** @lends LuCI.form.FileUpload.prototype */ {
  3672. __name__: 'CBI.FileSelect',
  3673. __init__: function(/* ... */) {
  3674. this.super('__init__', arguments);
  3675. this.show_hidden = false;
  3676. this.enable_upload = true;
  3677. this.enable_remove = true;
  3678. this.root_directory = '/etc/luci-uploads';
  3679. },
  3680. /**
  3681. * Toggle display of hidden files.
  3682. *
  3683. * Display hidden files when rendering the remote directory listing.
  3684. * Note that this is merely a cosmetic feature, hidden files are always
  3685. * included in received remote file listings.
  3686. *
  3687. * The default is `false`, means hidden files are not displayed.
  3688. *
  3689. * @name LuCI.form.FileUpload.prototype#show_hidden
  3690. * @type boolean
  3691. * @default false
  3692. */
  3693. /**
  3694. * Toggle file upload functionality.
  3695. *
  3696. * When set to `true`, the underlying widget provides a button which lets
  3697. * the user select and upload local files to the remote system.
  3698. * Note that this is merely a cosmetic feature, remote upload access is
  3699. * controlled by the session ACL rules.
  3700. *
  3701. * The default is `true`, means file upload functionality is displayed.
  3702. *
  3703. * @name LuCI.form.FileUpload.prototype#enable_upload
  3704. * @type boolean
  3705. * @default true
  3706. */
  3707. /**
  3708. * Toggle remote file delete functionality.
  3709. *
  3710. * When set to `true`, the underlying widget provides a buttons which let
  3711. * the user delete files from remote directories. Note that this is merely
  3712. * a cosmetic feature, remote delete permissions are controlled by the
  3713. * session ACL rules.
  3714. *
  3715. * The default is `true`, means file removal buttons are displayed.
  3716. *
  3717. * @name LuCI.form.FileUpload.prototype#enable_remove
  3718. * @type boolean
  3719. * @default true
  3720. */
  3721. /**
  3722. * Specify the root directory for file browsing.
  3723. *
  3724. * This property defines the topmost directory the file browser widget may
  3725. * navigate to, the UI will not allow browsing directories outside this
  3726. * prefix. Note that this is merely a cosmetic feature, remote file access
  3727. * and directory listing permissions are controlled by the session ACL
  3728. * rules.
  3729. *
  3730. * The default is `/etc/luci-uploads`.
  3731. *
  3732. * @name LuCI.form.FileUpload.prototype#root_directory
  3733. * @type string
  3734. * @default /etc/luci-uploads
  3735. */
  3736. /** @private */
  3737. renderWidget: function(section_id, option_index, cfgvalue) {
  3738. var browserEl = new ui.FileUpload((cfgvalue != null) ? cfgvalue : this.default, {
  3739. id: this.cbid(section_id),
  3740. name: this.cbid(section_id),
  3741. show_hidden: this.show_hidden,
  3742. enable_upload: this.enable_upload,
  3743. enable_remove: this.enable_remove,
  3744. root_directory: this.root_directory,
  3745. disabled: (this.readonly != null) ? this.readonly : this.map.readonly
  3746. });
  3747. return browserEl.render();
  3748. }
  3749. });
  3750. /**
  3751. * @class SectionValue
  3752. * @memberof LuCI.form
  3753. * @augments LuCI.form.Value
  3754. * @hideconstructor
  3755. * @classdesc
  3756. *
  3757. * The `SectionValue` widget embeds a form section element within an option
  3758. * element container, allowing to nest form sections into other sections.
  3759. *
  3760. * @param {LuCI.form.Map|LuCI.form.JSONMap} form
  3761. * The configuration form this section is added to. It is automatically passed
  3762. * by [option()]{@link LuCI.form.AbstractSection#option} or
  3763. * [taboption()]{@link LuCI.form.AbstractSection#taboption} when adding the
  3764. * option to the section.
  3765. *
  3766. * @param {LuCI.form.AbstractSection} section
  3767. * The configuration section this option is added to. It is automatically passed
  3768. * by [option()]{@link LuCI.form.AbstractSection#option} or
  3769. * [taboption()]{@link LuCI.form.AbstractSection#taboption} when adding the
  3770. * option to the section.
  3771. *
  3772. * @param {string} option
  3773. * The internal name of the option element holding the section. Since a section
  3774. * container element does not read or write any configuration itself, the name
  3775. * is only used internally and does not need to relate to any underlying UCI
  3776. * option name.
  3777. *
  3778. * @param {LuCI.form.AbstractSection} subsection_class
  3779. * The class to use for instantiating the nested section element. Note that
  3780. * the class value itself is expected here, not a class instance obtained by
  3781. * calling `new`. The given class argument must be a subclass of the
  3782. * `AbstractSection` class.
  3783. *
  3784. * @param {...*} [class_args]
  3785. * All further arguments are passed as-is to the subclass constructor. Refer
  3786. * to the corresponding class constructor documentations for details.
  3787. */
  3788. var CBISectionValue = CBIValue.extend(/** @lends LuCI.form.SectionValue.prototype */ {
  3789. __name__: 'CBI.ContainerValue',
  3790. __init__: function(map, section, option, cbiClass /*, ... */) {
  3791. this.super('__init__', [map, section, option]);
  3792. if (!CBIAbstractSection.isSubclass(cbiClass))
  3793. throw 'Sub section must be a descendent of CBIAbstractSection';
  3794. this.subsection = cbiClass.instantiate(this.varargs(arguments, 4, this.map));
  3795. this.subsection.parentoption = this;
  3796. },
  3797. /**
  3798. * Access the embedded section instance.
  3799. *
  3800. * This property holds a reference to the instantiated nested section.
  3801. *
  3802. * @name LuCI.form.SectionValue.prototype#subsection
  3803. * @type LuCI.form.AbstractSection
  3804. * @readonly
  3805. */
  3806. /** @override */
  3807. load: function(section_id) {
  3808. return this.subsection.load(section_id);
  3809. },
  3810. /** @override */
  3811. parse: function(section_id) {
  3812. return this.subsection.parse(section_id);
  3813. },
  3814. /** @private */
  3815. renderWidget: function(section_id, option_index, cfgvalue) {
  3816. return this.subsection.render(section_id);
  3817. },
  3818. /** @private */
  3819. checkDepends: function(section_id) {
  3820. this.subsection.checkDepends(section_id);
  3821. return CBIValue.prototype.checkDepends.apply(this, [ section_id ]);
  3822. },
  3823. /**
  3824. * Since the section container is not rendering an own widget,
  3825. * its `value()` implementation is a no-op.
  3826. *
  3827. * @override
  3828. */
  3829. value: function() {},
  3830. /**
  3831. * Since the section container is not tied to any UCI configuration,
  3832. * its `write()` implementation is a no-op.
  3833. *
  3834. * @override
  3835. */
  3836. write: function() {},
  3837. /**
  3838. * Since the section container is not tied to any UCI configuration,
  3839. * its `remove()` implementation is a no-op.
  3840. *
  3841. * @override
  3842. */
  3843. remove: function() {},
  3844. /**
  3845. * Since the section container is not tied to any UCI configuration,
  3846. * its `cfgvalue()` implementation will always return `null`.
  3847. *
  3848. * @override
  3849. * @returns {null}
  3850. */
  3851. cfgvalue: function() { return null },
  3852. /**
  3853. * Since the section container is not tied to any UCI configuration,
  3854. * its `formvalue()` implementation will always return `null`.
  3855. *
  3856. * @override
  3857. * @returns {null}
  3858. */
  3859. formvalue: function() { return null }
  3860. });
  3861. /**
  3862. * @class form
  3863. * @memberof LuCI
  3864. * @hideconstructor
  3865. * @classdesc
  3866. *
  3867. * The LuCI form class provides high level abstractions for creating creating
  3868. * UCI- or JSON backed configurations forms.
  3869. *
  3870. * To import the class in views, use `'require form'`, to import it in
  3871. * external JavaScript, use `L.require("form").then(...)`.
  3872. *
  3873. * A typical form is created by first constructing a
  3874. * {@link LuCI.form.Map} or {@link LuCI.form.JSONMap} instance using `new` and
  3875. * by subsequently adding sections and options to it. Finally
  3876. * [render()]{@link LuCI.form.Map#render} is invoked on the instance to
  3877. * assemble the HTML markup and insert it into the DOM.
  3878. *
  3879. * Example:
  3880. *
  3881. * <pre>
  3882. * 'use strict';
  3883. * 'require form';
  3884. *
  3885. * var m, s, o;
  3886. *
  3887. * m = new form.Map('example', 'Example form',
  3888. * 'This is an example form mapping the contents of /etc/config/example');
  3889. *
  3890. * s = m.section(form.NamedSection, 'first_section', 'example', 'The first section',
  3891. * 'This sections maps "config example first_section" of /etc/config/example');
  3892. *
  3893. * o = s.option(form.Flag, 'some_bool', 'A checkbox option');
  3894. *
  3895. * o = s.option(form.ListValue, 'some_choice', 'A select element');
  3896. * o.value('choice1', 'The first choice');
  3897. * o.value('choice2', 'The second choice');
  3898. *
  3899. * m.render().then(function(node) {
  3900. * document.body.appendChild(node);
  3901. * });
  3902. * </pre>
  3903. */
  3904. return baseclass.extend(/** @lends LuCI.form.prototype */ {
  3905. Map: CBIMap,
  3906. JSONMap: CBIJSONMap,
  3907. AbstractSection: CBIAbstractSection,
  3908. AbstractValue: CBIAbstractValue,
  3909. TypedSection: CBITypedSection,
  3910. TableSection: CBITableSection,
  3911. GridSection: CBIGridSection,
  3912. NamedSection: CBINamedSection,
  3913. Value: CBIValue,
  3914. DynamicList: CBIDynamicList,
  3915. ListValue: CBIListValue,
  3916. Flag: CBIFlagValue,
  3917. MultiValue: CBIMultiValue,
  3918. TextValue: CBITextValue,
  3919. DummyValue: CBIDummyValue,
  3920. Button: CBIButtonValue,
  3921. HiddenValue: CBIHiddenValue,
  3922. FileUpload: CBIFileUpload,
  3923. SectionValue: CBISectionValue
  3924. });