Built files from Bizgaze WebServer
Vous ne pouvez pas sélectionner plus de 25 sujets Les noms de sujets doivent commencer par une lettre ou un nombre, peuvent contenir des tirets ('-') et peuvent comporter jusqu'à 35 caractères.

query-builder.js 187KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738273927402741274227432744274527462747274827492750275127522753275427552756275727582759276027612762276327642765276627672768276927702771277227732774277527762777277827792780278127822783278427852786278727882789279027912792279327942795279627972798279928002801280228032804280528062807280828092810281128122813281428152816281728182819282028212822282328242825282628272828282928302831283228332834283528362837283828392840284128422843284428452846284728482849285028512852285328542855285628572858285928602861286228632864286528662867286828692870287128722873287428752876287728782879288028812882288328842885288628872888288928902891289228932894289528962897289828992900290129022903290429052906290729082909291029112912291329142915291629172918291929202921292229232924292529262927292829292930293129322933293429352936293729382939294029412942294329442945294629472948294929502951295229532954295529562957295829592960296129622963296429652966296729682969297029712972297329742975297629772978297929802981298229832984298529862987298829892990299129922993299429952996299729982999300030013002300330043005300630073008300930103011301230133014301530163017301830193020302130223023302430253026302730283029303030313032303330343035303630373038303930403041304230433044304530463047304830493050305130523053305430553056305730583059306030613062306330643065306630673068306930703071307230733074307530763077307830793080308130823083308430853086308730883089309030913092309330943095309630973098309931003101310231033104310531063107310831093110311131123113311431153116311731183119312031213122312331243125312631273128312931303131313231333134313531363137313831393140314131423143314431453146314731483149315031513152315331543155315631573158315931603161316231633164316531663167316831693170317131723173317431753176317731783179318031813182318331843185318631873188318931903191319231933194319531963197319831993200320132023203320432053206320732083209321032113212321332143215321632173218321932203221322232233224322532263227322832293230323132323233323432353236323732383239324032413242324332443245324632473248324932503251325232533254325532563257325832593260326132623263326432653266326732683269327032713272327332743275327632773278327932803281328232833284328532863287328832893290329132923293329432953296329732983299330033013302330333043305330633073308330933103311331233133314331533163317331833193320332133223323332433253326332733283329333033313332333333343335333633373338333933403341334233433344334533463347334833493350335133523353335433553356335733583359336033613362336333643365336633673368336933703371337233733374337533763377337833793380338133823383338433853386338733883389339033913392339333943395339633973398339934003401340234033404340534063407340834093410341134123413341434153416341734183419342034213422342334243425342634273428342934303431343234333434343534363437343834393440344134423443344434453446344734483449345034513452345334543455345634573458345934603461346234633464346534663467346834693470347134723473347434753476347734783479348034813482348334843485348634873488348934903491349234933494349534963497349834993500350135023503350435053506350735083509351035113512351335143515351635173518351935203521352235233524352535263527352835293530353135323533353435353536353735383539354035413542354335443545354635473548354935503551355235533554355535563557355835593560356135623563356435653566356735683569357035713572357335743575357635773578357935803581358235833584358535863587358835893590359135923593359435953596359735983599360036013602360336043605360636073608360936103611361236133614361536163617361836193620362136223623362436253626362736283629363036313632363336343635363636373638363936403641364236433644364536463647364836493650365136523653365436553656365736583659366036613662366336643665366636673668366936703671367236733674367536763677367836793680368136823683368436853686368736883689369036913692369336943695369636973698369937003701370237033704370537063707370837093710371137123713371437153716371737183719372037213722372337243725372637273728372937303731373237333734373537363737373837393740374137423743374437453746374737483749375037513752375337543755375637573758375937603761376237633764376537663767376837693770377137723773377437753776377737783779378037813782378337843785378637873788378937903791379237933794379537963797379837993800380138023803380438053806380738083809381038113812381338143815381638173818381938203821382238233824382538263827382838293830383138323833383438353836383738383839384038413842384338443845384638473848384938503851385238533854385538563857385838593860386138623863386438653866386738683869387038713872387338743875387638773878387938803881388238833884388538863887388838893890389138923893389438953896389738983899390039013902390339043905390639073908390939103911391239133914391539163917391839193920392139223923392439253926392739283929393039313932393339343935393639373938393939403941394239433944394539463947394839493950395139523953395439553956395739583959396039613962396339643965396639673968396939703971397239733974397539763977397839793980398139823983398439853986398739883989399039913992399339943995399639973998399940004001400240034004400540064007400840094010401140124013401440154016401740184019402040214022402340244025402640274028402940304031403240334034403540364037403840394040404140424043404440454046404740484049405040514052405340544055405640574058405940604061406240634064406540664067406840694070407140724073407440754076407740784079408040814082408340844085408640874088408940904091409240934094409540964097409840994100410141024103410441054106410741084109411041114112411341144115411641174118411941204121412241234124412541264127412841294130413141324133413441354136413741384139414041414142414341444145414641474148414941504151415241534154415541564157415841594160416141624163416441654166416741684169417041714172417341744175417641774178417941804181418241834184418541864187418841894190419141924193419441954196419741984199420042014202420342044205420642074208420942104211421242134214421542164217421842194220422142224223422442254226422742284229423042314232423342344235423642374238423942404241424242434244424542464247424842494250425142524253425442554256425742584259426042614262426342644265426642674268426942704271427242734274427542764277427842794280428142824283428442854286428742884289429042914292429342944295429642974298429943004301430243034304430543064307430843094310431143124313431443154316431743184319432043214322432343244325432643274328432943304331433243334334433543364337433843394340434143424343434443454346434743484349435043514352435343544355435643574358435943604361436243634364436543664367436843694370437143724373437443754376437743784379438043814382438343844385438643874388438943904391439243934394439543964397439843994400440144024403440444054406440744084409441044114412441344144415441644174418441944204421442244234424442544264427442844294430443144324433443444354436443744384439444044414442444344444445444644474448444944504451445244534454445544564457445844594460446144624463446444654466446744684469447044714472447344744475447644774478447944804481448244834484448544864487448844894490449144924493449444954496449744984499450045014502450345044505450645074508450945104511451245134514451545164517451845194520452145224523452445254526452745284529453045314532453345344535453645374538453945404541454245434544454545464547454845494550455145524553455445554556455745584559456045614562456345644565456645674568456945704571457245734574457545764577457845794580458145824583458445854586458745884589459045914592459345944595459645974598459946004601460246034604460546064607460846094610461146124613461446154616461746184619462046214622462346244625462646274628462946304631463246334634463546364637463846394640464146424643464446454646464746484649465046514652465346544655465646574658465946604661466246634664466546664667466846694670467146724673467446754676467746784679468046814682468346844685468646874688468946904691469246934694469546964697469846994700470147024703470447054706470747084709471047114712471347144715471647174718471947204721472247234724472547264727472847294730473147324733473447354736473747384739474047414742474347444745474647474748474947504751475247534754475547564757475847594760476147624763476447654766476747684769477047714772477347744775477647774778477947804781478247834784478547864787478847894790479147924793479447954796479747984799480048014802480348044805480648074808480948104811481248134814481548164817481848194820482148224823482448254826482748284829483048314832483348344835483648374838483948404841484248434844484548464847484848494850485148524853485448554856485748584859486048614862486348644865486648674868486948704871487248734874487548764877487848794880488148824883488448854886488748884889489048914892489348944895489648974898489949004901490249034904490549064907490849094910491149124913491449154916491749184919492049214922492349244925492649274928492949304931493249334934493549364937493849394940494149424943494449454946494749484949495049514952495349544955495649574958495949604961496249634964496549664967496849694970497149724973497449754976497749784979498049814982498349844985498649874988498949904991499249934994499549964997499849995000500150025003500450055006500750085009501050115012501350145015501650175018501950205021502250235024502550265027502850295030503150325033503450355036503750385039504050415042504350445045504650475048504950505051505250535054505550565057505850595060506150625063506450655066506750685069507050715072507350745075507650775078507950805081508250835084508550865087508850895090509150925093509450955096509750985099510051015102510351045105510651075108510951105111511251135114511551165117511851195120512151225123512451255126512751285129513051315132513351345135513651375138513951405141514251435144514551465147514851495150515151525153515451555156515751585159516051615162516351645165516651675168516951705171517251735174517551765177517851795180518151825183518451855186518751885189519051915192519351945195519651975198519952005201520252035204520552065207520852095210521152125213521452155216521752185219522052215222522352245225522652275228522952305231523252335234523552365237523852395240524152425243524452455246524752485249525052515252525352545255525652575258525952605261526252635264526552665267526852695270527152725273527452755276527752785279528052815282528352845285528652875288528952905291529252935294529552965297529852995300530153025303530453055306530753085309531053115312531353145315531653175318531953205321532253235324532553265327532853295330533153325333533453355336533753385339534053415342534353445345534653475348534953505351535253535354535553565357535853595360536153625363536453655366536753685369537053715372537353745375537653775378537953805381538253835384538553865387538853895390539153925393539453955396539753985399540054015402540354045405540654075408540954105411541254135414541554165417541854195420542154225423542454255426542754285429543054315432543354345435543654375438543954405441544254435444544554465447544854495450545154525453545454555456545754585459546054615462546354645465546654675468546954705471547254735474547554765477547854795480548154825483548454855486548754885489549054915492549354945495549654975498549955005501550255035504550555065507550855095510551155125513551455155516551755185519552055215522552355245525552655275528552955305531553255335534553555365537553855395540554155425543554455455546554755485549555055515552555355545555555655575558555955605561556255635564556555665567556855695570557155725573557455755576557755785579558055815582558355845585558655875588558955905591559255935594559555965597559855995600560156025603560456055606560756085609561056115612561356145615561656175618561956205621562256235624562556265627562856295630563156325633563456355636563756385639564056415642564356445645564656475648564956505651565256535654565556565657565856595660566156625663566456655666566756685669567056715672567356745675567656775678567956805681568256835684568556865687568856895690569156925693569456955696569756985699570057015702570357045705570657075708570957105711571257135714571557165717571857195720572157225723572457255726572757285729573057315732573357345735573657375738573957405741574257435744574557465747574857495750575157525753575457555756575757585759576057615762576357645765576657675768576957705771577257735774577557765777577857795780578157825783578457855786578757885789579057915792579357945795579657975798579958005801580258035804580558065807580858095810581158125813581458155816581758185819582058215822582358245825582658275828582958305831583258335834583558365837583858395840584158425843584458455846584758485849585058515852585358545855585658575858585958605861586258635864586558665867586858695870587158725873587458755876587758785879588058815882588358845885588658875888588958905891589258935894589558965897589858995900590159025903590459055906590759085909591059115912591359145915591659175918591959205921592259235924592559265927592859295930593159325933593459355936593759385939594059415942594359445945594659475948594959505951595259535954595559565957595859595960596159625963596459655966596759685969597059715972597359745975597659775978597959805981598259835984598559865987598859895990599159925993599459955996599759985999600060016002600360046005600660076008600960106011601260136014601560166017601860196020602160226023602460256026602760286029603060316032603360346035603660376038603960406041604260436044604560466047604860496050605160526053605460556056605760586059606060616062606360646065606660676068606960706071607260736074607560766077607860796080608160826083608460856086608760886089609060916092609360946095609660976098609961006101610261036104610561066107610861096110611161126113611461156116611761186119612061216122612361246125612661276128612961306131613261336134613561366137613861396140614161426143614461456146614761486149615061516152615361546155615661576158615961606161616261636164616561666167616861696170617161726173617461756176617761786179618061816182618361846185618661876188618961906191619261936194619561966197619861996200
  1. /*!
  2. * jQuery QueryBuilder 2.5.2
  3. * Copyright 2014-2018 Damien "Mistic" Sorel (http://www.strangeplanet.fr)
  4. * Licensed under MIT (https://opensource.org/licenses/MIT)
  5. */
  6. (function(root, factory) {
  7. if (typeof define == 'function' && define.amd) {
  8. define(['jquery', 'dot/doT', 'jquery-extendext'], factory);
  9. }
  10. else if (typeof module === 'object' && module.exports) {
  11. module.exports = factory(require('jquery'), require('dot/doT'), require('jquery-extendext'));
  12. }
  13. else {
  14. factory(root.jQuery, root.doT);
  15. }
  16. }(this, function($, doT) {
  17. "use strict";
  18. /**
  19. * @typedef {object} Filter
  20. * @memberof QueryBuilder
  21. * @description See {@link http://querybuilder.js.org/index.html#filters}
  22. */
  23. /**
  24. * @typedef {object} Operator
  25. * @memberof QueryBuilder
  26. * @description See {@link http://querybuilder.js.org/index.html#operators}
  27. */
  28. /**
  29. * @param {jQuery} $el
  30. * @param {object} options - see {@link http://querybuilder.js.org/#options}
  31. * @constructor
  32. */
  33. var QueryBuilder = function($el, options) {
  34. $el[0].queryBuilder = this;
  35. /**
  36. * Element container
  37. * @member {jQuery}
  38. * @readonly
  39. */
  40. this.$el = $el;
  41. /**
  42. * Configuration object
  43. * @member {object}
  44. * @readonly
  45. */
  46. this.settings = $.extendext(true, 'replace', {}, QueryBuilder.DEFAULTS, options);
  47. /**
  48. * Internal model
  49. * @member {Model}
  50. * @readonly
  51. */
  52. this.model = new Model();
  53. /**
  54. * Internal status
  55. * @member {object}
  56. * @property {string} id - id of the container
  57. * @property {boolean} generated_id - if the container id has been generated
  58. * @property {int} group_id - current group id
  59. * @property {int} rule_id - current rule id
  60. * @property {boolean} has_optgroup - if filters have optgroups
  61. * @property {boolean} has_operator_optgroup - if operators have optgroups
  62. * @readonly
  63. * @private
  64. */
  65. this.status = {
  66. id: null,
  67. generated_id: false,
  68. group_id: 0,
  69. rule_id: 0,
  70. has_optgroup: false,
  71. has_operator_optgroup: false
  72. };
  73. /**
  74. * List of filters
  75. * @member {QueryBuilder.Filter[]}
  76. * @readonly
  77. */
  78. this.filters = this.settings.filters;
  79. /**
  80. * List of icons
  81. * @member {object.<string, string>}
  82. * @readonly
  83. */
  84. this.icons = this.settings.icons;
  85. /**
  86. * List of operators
  87. * @member {QueryBuilder.Operator[]}
  88. * @readonly
  89. */
  90. this.operators = this.settings.operators;
  91. /**
  92. * List of templates
  93. * @member {object.<string, function>}
  94. * @readonly
  95. */
  96. this.templates = this.settings.templates;
  97. /**
  98. * Plugins configuration
  99. * @member {object.<string, object>}
  100. * @readonly
  101. */
  102. this.plugins = this.settings.plugins;
  103. /**
  104. * Translations object
  105. * @member {object}
  106. * @readonly
  107. */
  108. this.lang = null;
  109. // translations : english << 'lang_code' << custom
  110. if (QueryBuilder.regional['en'] === undefined) {
  111. Utils.error('Config', '"i18n/en.js" not loaded.');
  112. }
  113. this.lang = $.extendext(true, 'replace', {}, QueryBuilder.regional['en'], QueryBuilder.regional[this.settings.lang_code], this.settings.lang);
  114. // "allow_groups" can be boolean or int
  115. if (this.settings.allow_groups === false) {
  116. this.settings.allow_groups = 0;
  117. }
  118. else if (this.settings.allow_groups === true) {
  119. this.settings.allow_groups = -1;
  120. }
  121. // init templates
  122. Object.keys(this.templates).forEach(function(tpl) {
  123. if (!this.templates[tpl]) {
  124. this.templates[tpl] = QueryBuilder.templates[tpl];
  125. }
  126. if (typeof this.templates[tpl] == 'string') {
  127. this.templates[tpl] = doT.template(this.templates[tpl]);
  128. }
  129. }, this);
  130. // ensure we have a container id
  131. if (!this.$el.attr('id')) {
  132. this.$el.attr('id', 'qb_' + Math.floor(Math.random() * 99999));
  133. this.status.generated_id = true;
  134. }
  135. this.status.id = this.$el.attr('id');
  136. // INIT
  137. this.$el.addClass('query-builder form-inline');
  138. this.filters = this.checkFilters(this.filters);
  139. this.operators = this.checkOperators(this.operators);
  140. this.bindEvents();
  141. this.initPlugins();
  142. };
  143. $.extend(QueryBuilder.prototype, /** @lends QueryBuilder.prototype */ {
  144. /**
  145. * Triggers an event on the builder container
  146. * @param {string} type
  147. * @returns {$.Event}
  148. */
  149. trigger: function(type) {
  150. var event = new $.Event(this._tojQueryEvent(type), {
  151. builder: this
  152. });
  153. this.$el.triggerHandler(event, Array.prototype.slice.call(arguments, 1));
  154. return event;
  155. },
  156. /**
  157. * Triggers an event on the builder container and returns the modified value
  158. * @param {string} type
  159. * @param {*} value
  160. * @returns {*}
  161. */
  162. change: function(type, value) {
  163. var event = new $.Event(this._tojQueryEvent(type, true), {
  164. builder: this,
  165. value: value
  166. });
  167. this.$el.triggerHandler(event, Array.prototype.slice.call(arguments, 2));
  168. return event.value;
  169. },
  170. /**
  171. * Attaches an event listener on the builder container
  172. * @param {string} type
  173. * @param {function} cb
  174. * @returns {QueryBuilder}
  175. */
  176. on: function(type, cb) {
  177. this.$el.on(this._tojQueryEvent(type), cb);
  178. return this;
  179. },
  180. /**
  181. * Removes an event listener from the builder container
  182. * @param {string} type
  183. * @param {function} [cb]
  184. * @returns {QueryBuilder}
  185. */
  186. off: function(type, cb) {
  187. this.$el.off(this._tojQueryEvent(type), cb);
  188. return this;
  189. },
  190. /**
  191. * Attaches an event listener called once on the builder container
  192. * @param {string} type
  193. * @param {function} cb
  194. * @returns {QueryBuilder}
  195. */
  196. once: function(type, cb) {
  197. this.$el.one(this._tojQueryEvent(type), cb);
  198. return this;
  199. },
  200. /**
  201. * Appends `.queryBuilder` and optionally `.filter` to the events names
  202. * @param {string} name
  203. * @param {boolean} [filter=false]
  204. * @returns {string}
  205. * @private
  206. */
  207. _tojQueryEvent: function(name, filter) {
  208. return name.split(' ').map(function(type) {
  209. return type + '.queryBuilder' + (filter ? '.filter' : '');
  210. }).join(' ');
  211. }
  212. });
  213. /**
  214. * Allowed types and their internal representation
  215. * @type {object.<string, string>}
  216. * @readonly
  217. * @private
  218. */
  219. QueryBuilder.types = {
  220. 'string': 'string',
  221. 'integer': 'number',
  222. 'double': 'number',
  223. 'date': 'datetime',
  224. 'time': 'datetime',
  225. 'datetime': 'datetime',
  226. 'boolean': 'boolean'
  227. };
  228. /**
  229. * Allowed inputs
  230. * @type {string[]}
  231. * @readonly
  232. * @private
  233. */
  234. QueryBuilder.inputs = [
  235. 'text',
  236. 'number',
  237. 'textarea',
  238. 'radio',
  239. 'checkbox',
  240. 'select'
  241. ];
  242. /**
  243. * Runtime modifiable options with `setOptions` method
  244. * @type {string[]}
  245. * @readonly
  246. * @private
  247. */
  248. QueryBuilder.modifiable_options = [
  249. 'display_errors',
  250. 'allow_groups',
  251. 'allow_empty',
  252. 'default_condition',
  253. 'default_filter'
  254. ];
  255. /**
  256. * CSS selectors for common components
  257. * @type {object.<string, string>}
  258. * @readonly
  259. */
  260. QueryBuilder.selectors = {
  261. group_container: '.rules-group-container',
  262. rule_container: '.rule-container',
  263. filter_container: '.rule-filter-container',
  264. operator_container: '.rule-operator-container',
  265. value_container: '.rule-value-container',
  266. error_container: '.error-container',
  267. condition_container: '.rules-group-header .group-conditions',
  268. rule_header: '.rule-header',
  269. group_header: '.rules-group-header',
  270. group_actions: '.group-actions',
  271. rule_actions: '.rule-actions',
  272. rules_list: '.rules-group-body>.rules-list',
  273. group_condition: '.rules-group-header [name$=_cond]',
  274. rule_filter: '.rule-filter-container [name$=_filter]',
  275. rule_operator: '.rule-operator-container [name$=_operator]',
  276. rule_value: '.rule-value-container [name*=_value_]',
  277. add_rule: '[data-add=rule]',
  278. delete_rule: '[data-delete=rule]',
  279. add_group: '[data-add=group]',
  280. delete_group: '[data-delete=group]'
  281. };
  282. /**
  283. * Template strings (see template.js)
  284. * @type {object.<string, string>}
  285. * @readonly
  286. */
  287. QueryBuilder.templates = {};
  288. /**
  289. * Localized strings (see i18n/)
  290. * @type {object.<string, object>}
  291. * @readonly
  292. */
  293. QueryBuilder.regional = {};
  294. /**
  295. * Default operators
  296. * @type {object.<string, object>}
  297. * @readonly
  298. */
  299. QueryBuilder.OPERATORS = {
  300. equal: { type: 'equal', nb_inputs: 1, multiple: false, apply_to: ['string', 'number', 'datetime', 'boolean'] },
  301. not_equal: { type: 'not_equal', nb_inputs: 1, multiple: false, apply_to: ['string', 'number', 'datetime', 'boolean'] },
  302. in: { type: 'in', nb_inputs: 1, multiple: true, apply_to: ['string', 'number', 'datetime'] },
  303. not_in: { type: 'not_in', nb_inputs: 1, multiple: true, apply_to: ['string', 'number', 'datetime'] },
  304. less: { type: 'less', nb_inputs: 1, multiple: false, apply_to: ['number', 'datetime'] },
  305. less_or_equal: { type: 'less_or_equal', nb_inputs: 1, multiple: false, apply_to: ['number', 'datetime'] },
  306. greater: { type: 'greater', nb_inputs: 1, multiple: false, apply_to: ['number', 'datetime'] },
  307. greater_or_equal: { type: 'greater_or_equal', nb_inputs: 1, multiple: false, apply_to: ['number', 'datetime'] },
  308. between: { type: 'between', nb_inputs: 2, multiple: false, apply_to: ['number', 'datetime'] },
  309. not_between: { type: 'not_between', nb_inputs: 2, multiple: false, apply_to: ['number', 'datetime'] },
  310. begins_with: { type: 'begins_with', nb_inputs: 1, multiple: false, apply_to: ['string'] },
  311. not_begins_with: { type: 'not_begins_with', nb_inputs: 1, multiple: false, apply_to: ['string'] },
  312. contains: { type: 'contains', nb_inputs: 1, multiple: false, apply_to: ['string'] },
  313. not_contains: { type: 'not_contains', nb_inputs: 1, multiple: false, apply_to: ['string'] },
  314. ends_with: { type: 'ends_with', nb_inputs: 1, multiple: false, apply_to: ['string'] },
  315. not_ends_with: { type: 'not_ends_with', nb_inputs: 1, multiple: false, apply_to: ['string'] },
  316. is_empty: { type: 'is_empty', nb_inputs: 0, multiple: false, apply_to: ['string'] },
  317. is_not_empty: { type: 'is_not_empty', nb_inputs: 0, multiple: false, apply_to: ['string'] },
  318. is_null: { type: 'is_null', nb_inputs: 0, multiple: false, apply_to: ['string', 'number', 'datetime', 'boolean'] },
  319. is_not_null: { type: 'is_not_null', nb_inputs: 0, multiple: false, apply_to: ['string', 'number', 'datetime', 'boolean'] }
  320. };
  321. /**
  322. * Default configuration
  323. * @type {object}
  324. * @readonly
  325. */
  326. QueryBuilder.DEFAULTS = {
  327. filters: [],
  328. plugins: [],
  329. sort_filters: false,
  330. display_errors: true,
  331. allow_groups: -1,
  332. allow_empty: false,
  333. conditions: ['AND', 'OR'],
  334. default_condition: 'AND',
  335. inputs_separator: ' , ',
  336. select_placeholder: '------',
  337. display_empty_filter: true,
  338. default_filter: null,
  339. optgroups: {},
  340. default_rule_flags: {
  341. filter_readonly: false,
  342. operator_readonly: false,
  343. value_readonly: false,
  344. no_delete: false
  345. },
  346. default_group_flags: {
  347. condition_readonly: false,
  348. no_add_rule: false,
  349. no_add_group: false,
  350. no_delete: false
  351. },
  352. templates: {
  353. group: null,
  354. rule: null,
  355. filterSelect: null,
  356. operatorSelect: null,
  357. ruleValueSelect: null
  358. },
  359. lang_code: 'en',
  360. lang: {},
  361. operators: [
  362. 'equal',
  363. 'not_equal',
  364. 'in',
  365. 'not_in',
  366. 'less',
  367. 'less_or_equal',
  368. 'greater',
  369. 'greater_or_equal',
  370. 'between',
  371. 'not_between',
  372. 'begins_with',
  373. 'not_begins_with',
  374. 'contains',
  375. 'not_contains',
  376. 'ends_with',
  377. 'not_ends_with',
  378. 'is_empty',
  379. 'is_not_empty',
  380. 'is_null',
  381. 'is_not_null'
  382. ],
  383. icons: {
  384. add_group: 'glyphicon glyphicon-plus-sign',
  385. add_rule: 'glyphicon glyphicon-plus',
  386. remove_group: 'glyphicon glyphicon-remove',
  387. remove_rule: 'glyphicon glyphicon-remove',
  388. error: 'glyphicon glyphicon-warning-sign'
  389. }
  390. };
  391. /**
  392. * @module plugins
  393. */
  394. /**
  395. * Definition of available plugins
  396. * @type {object.<String, object>}
  397. */
  398. QueryBuilder.plugins = {};
  399. /**
  400. * Gets or extends the default configuration
  401. * @param {object} [options] - new configuration
  402. * @returns {undefined|object} nothing or configuration object (copy)
  403. */
  404. QueryBuilder.defaults = function(options) {
  405. if (typeof options == 'object') {
  406. $.extendext(true, 'replace', QueryBuilder.DEFAULTS, options);
  407. }
  408. else if (typeof options == 'string') {
  409. if (typeof QueryBuilder.DEFAULTS[options] == 'object') {
  410. return $.extend(true, {}, QueryBuilder.DEFAULTS[options]);
  411. }
  412. else {
  413. return QueryBuilder.DEFAULTS[options];
  414. }
  415. }
  416. else {
  417. return $.extend(true, {}, QueryBuilder.DEFAULTS);
  418. }
  419. };
  420. /**
  421. * Registers a new plugin
  422. * @param {string} name
  423. * @param {function} fct - init function
  424. * @param {object} [def] - default options
  425. */
  426. QueryBuilder.define = function(name, fct, def) {
  427. QueryBuilder.plugins[name] = {
  428. fct: fct,
  429. def: def || {}
  430. };
  431. };
  432. /**
  433. * Adds new methods to QueryBuilder prototype
  434. * @param {object.<string, function>} methods
  435. */
  436. QueryBuilder.extend = function(methods) {
  437. $.extend(QueryBuilder.prototype, methods);
  438. };
  439. /**
  440. * Initializes plugins for an instance
  441. * @throws ConfigError
  442. * @private
  443. */
  444. QueryBuilder.prototype.initPlugins = function() {
  445. if (!this.plugins) {
  446. return;
  447. }
  448. if ($.isArray(this.plugins)) {
  449. var tmp = {};
  450. this.plugins.forEach(function(plugin) {
  451. tmp[plugin] = null;
  452. });
  453. this.plugins = tmp;
  454. }
  455. Object.keys(this.plugins).forEach(function(plugin) {
  456. if (plugin in QueryBuilder.plugins) {
  457. this.plugins[plugin] = $.extend(true, {},
  458. QueryBuilder.plugins[plugin].def,
  459. this.plugins[plugin] || {}
  460. );
  461. QueryBuilder.plugins[plugin].fct.call(this, this.plugins[plugin]);
  462. }
  463. else {
  464. Utils.error('Config', 'Unable to find plugin "{0}"', plugin);
  465. }
  466. }, this);
  467. };
  468. /**
  469. * Returns the config of a plugin, if the plugin is not loaded, returns the default config.
  470. * @param {string} name
  471. * @param {string} [property]
  472. * @throws ConfigError
  473. * @returns {*}
  474. */
  475. QueryBuilder.prototype.getPluginOptions = function(name, property) {
  476. var plugin;
  477. if (this.plugins && this.plugins[name]) {
  478. plugin = this.plugins[name];
  479. }
  480. else if (QueryBuilder.plugins[name]) {
  481. plugin = QueryBuilder.plugins[name].def;
  482. }
  483. if (plugin) {
  484. if (property) {
  485. return plugin[property];
  486. }
  487. else {
  488. return plugin;
  489. }
  490. }
  491. else {
  492. Utils.error('Config', 'Unable to find plugin "{0}"', name);
  493. }
  494. };
  495. /**
  496. * Final initialisation of the builder
  497. * @param {object} [rules]
  498. * @fires QueryBuilder.afterInit
  499. * @private
  500. */
  501. QueryBuilder.prototype.init = function(rules) {
  502. /**
  503. * When the initilization is done, just before creating the root group
  504. * @event afterInit
  505. * @memberof QueryBuilder
  506. */
  507. this.trigger('afterInit');
  508. if (rules) {
  509. this.setRules(rules);
  510. delete this.settings.rules;
  511. }
  512. else {
  513. this.setRoot(true);
  514. }
  515. };
  516. /**
  517. * Checks the configuration of each filter
  518. * @param {QueryBuilder.Filter[]} filters
  519. * @returns {QueryBuilder.Filter[]}
  520. * @throws ConfigError
  521. */
  522. QueryBuilder.prototype.checkFilters = function(filters) {
  523. var definedFilters = [];
  524. if (!filters || filters.length === 0) {
  525. Utils.error('Config', 'Missing filters list');
  526. }
  527. filters.forEach(function(filter, i) {
  528. if (!filter.id) {
  529. Utils.error('Config', 'Missing filter {0} id', i);
  530. }
  531. if (definedFilters.indexOf(filter.id) != -1) {
  532. Utils.error('Config', 'Filter "{0}" already defined', filter.id);
  533. }
  534. definedFilters.push(filter.id);
  535. if (!filter.type) {
  536. filter.type = 'string';
  537. }
  538. else if (!QueryBuilder.types[filter.type]) {
  539. Utils.error('Config', 'Invalid type "{0}"', filter.type);
  540. }
  541. if (!filter.input) {
  542. filter.input = QueryBuilder.types[filter.type] === 'number' ? 'number' : 'text';
  543. }
  544. else if (typeof filter.input != 'function' && QueryBuilder.inputs.indexOf(filter.input) == -1) {
  545. Utils.error('Config', 'Invalid input "{0}"', filter.input);
  546. }
  547. if (filter.operators) {
  548. filter.operators.forEach(function(operator) {
  549. if (typeof operator != 'string') {
  550. Utils.error('Config', 'Filter operators must be global operators types (string)');
  551. }
  552. });
  553. }
  554. if (!filter.field) {
  555. filter.field = filter.id;
  556. }
  557. if (!filter.label) {
  558. filter.label = filter.field;
  559. }
  560. if (!filter.optgroup) {
  561. filter.optgroup = null;
  562. }
  563. else {
  564. this.status.has_optgroup = true;
  565. // register optgroup if needed
  566. if (!this.settings.optgroups[filter.optgroup]) {
  567. this.settings.optgroups[filter.optgroup] = filter.optgroup;
  568. }
  569. }
  570. switch (filter.input) {
  571. case 'radio':
  572. case 'checkbox':
  573. if (!filter.values || filter.values.length < 1) {
  574. Utils.error('Config', 'Missing filter "{0}" values', filter.id);
  575. }
  576. break;
  577. case 'select':
  578. var cleanValues = [];
  579. filter.has_optgroup = false;
  580. Utils.iterateOptions(filter.values, function(value, label, optgroup) {
  581. cleanValues.push({
  582. value: value,
  583. label: label,
  584. optgroup: optgroup || null
  585. });
  586. if (optgroup) {
  587. filter.has_optgroup = true;
  588. // register optgroup if needed
  589. if (!this.settings.optgroups[optgroup]) {
  590. this.settings.optgroups[optgroup] = optgroup;
  591. }
  592. }
  593. }.bind(this));
  594. if (filter.has_optgroup) {
  595. filter.values = Utils.groupSort(cleanValues, 'optgroup');
  596. }
  597. else {
  598. filter.values = cleanValues;
  599. }
  600. if (filter.placeholder) {
  601. if (filter.placeholder_value === undefined) {
  602. filter.placeholder_value = -1;
  603. }
  604. filter.values.forEach(function(entry) {
  605. if (entry.value == filter.placeholder_value) {
  606. Utils.error('Config', 'Placeholder of filter "{0}" overlaps with one of its values', filter.id);
  607. }
  608. });
  609. }
  610. break;
  611. }
  612. }, this);
  613. if (this.settings.sort_filters) {
  614. if (typeof this.settings.sort_filters == 'function') {
  615. filters.sort(this.settings.sort_filters);
  616. }
  617. else {
  618. var self = this;
  619. filters.sort(function(a, b) {
  620. return self.translate(a.label).localeCompare(self.translate(b.label));
  621. });
  622. }
  623. }
  624. if (this.status.has_optgroup) {
  625. filters = Utils.groupSort(filters, 'optgroup');
  626. }
  627. return filters;
  628. };
  629. /**
  630. * Checks the configuration of each operator
  631. * @param {QueryBuilder.Operator[]} operators
  632. * @returns {QueryBuilder.Operator[]}
  633. * @throws ConfigError
  634. */
  635. QueryBuilder.prototype.checkOperators = function(operators) {
  636. var definedOperators = [];
  637. operators.forEach(function(operator, i) {
  638. if (typeof operator == 'string') {
  639. if (!QueryBuilder.OPERATORS[operator]) {
  640. Utils.error('Config', 'Unknown operator "{0}"', operator);
  641. }
  642. operators[i] = operator = $.extendext(true, 'replace', {}, QueryBuilder.OPERATORS[operator]);
  643. }
  644. else {
  645. if (!operator.type) {
  646. Utils.error('Config', 'Missing "type" for operator {0}', i);
  647. }
  648. if (QueryBuilder.OPERATORS[operator.type]) {
  649. operators[i] = operator = $.extendext(true, 'replace', {}, QueryBuilder.OPERATORS[operator.type], operator);
  650. }
  651. if (operator.nb_inputs === undefined || operator.apply_to === undefined) {
  652. Utils.error('Config', 'Missing "nb_inputs" and/or "apply_to" for operator "{0}"', operator.type);
  653. }
  654. }
  655. if (definedOperators.indexOf(operator.type) != -1) {
  656. Utils.error('Config', 'Operator "{0}" already defined', operator.type);
  657. }
  658. definedOperators.push(operator.type);
  659. if (!operator.optgroup) {
  660. operator.optgroup = null;
  661. }
  662. else {
  663. this.status.has_operator_optgroup = true;
  664. // register optgroup if needed
  665. if (!this.settings.optgroups[operator.optgroup]) {
  666. this.settings.optgroups[operator.optgroup] = operator.optgroup;
  667. }
  668. }
  669. }, this);
  670. if (this.status.has_operator_optgroup) {
  671. operators = Utils.groupSort(operators, 'optgroup');
  672. }
  673. return operators;
  674. };
  675. /**
  676. * Adds all events listeners to the builder
  677. * @private
  678. */
  679. QueryBuilder.prototype.bindEvents = function() {
  680. var self = this;
  681. var Selectors = QueryBuilder.selectors;
  682. // group condition change
  683. this.$el.on('change.queryBuilder', Selectors.group_condition, function() {
  684. if ($(this).is(':checked')) {
  685. var $group = $(this).closest(Selectors.group_container);
  686. self.getModel($group).condition = $(this).val();
  687. }
  688. });
  689. // rule filter change
  690. this.$el.on('change.queryBuilder', Selectors.rule_filter, function() {
  691. var $rule = $(this).closest(Selectors.rule_container);
  692. self.getModel($rule).filter = self.getFilterById($(this).val());
  693. });
  694. // rule operator change
  695. this.$el.on('change.queryBuilder', Selectors.rule_operator, function() {
  696. var $rule = $(this).closest(Selectors.rule_container);
  697. self.getModel($rule).operator = self.getOperatorByType($(this).val());
  698. });
  699. // add rule button
  700. this.$el.on('click.queryBuilder', Selectors.add_rule, function() {
  701. var $group = $(this).closest(Selectors.group_container);
  702. self.addRule(self.getModel($group));
  703. });
  704. // delete rule button
  705. this.$el.on('click.queryBuilder', Selectors.delete_rule, function() {
  706. var $rule = $(this).closest(Selectors.rule_container);
  707. self.deleteRule(self.getModel($rule));
  708. });
  709. if (this.settings.allow_groups !== 0) {
  710. // add group button
  711. this.$el.on('click.queryBuilder', Selectors.add_group, function() {
  712. var $group = $(this).closest(Selectors.group_container);
  713. self.addGroup(self.getModel($group));
  714. });
  715. // delete group button
  716. this.$el.on('click.queryBuilder', Selectors.delete_group, function() {
  717. var $group = $(this).closest(Selectors.group_container);
  718. self.deleteGroup(self.getModel($group));
  719. });
  720. }
  721. // model events
  722. this.model.on({
  723. 'drop': function(e, node) {
  724. node.$el.remove();
  725. self.refreshGroupsConditions();
  726. },
  727. 'add': function(e, parent, node, index) {
  728. if (index === 0) {
  729. node.$el.prependTo(parent.$el.find('>' + QueryBuilder.selectors.rules_list));
  730. }
  731. else {
  732. node.$el.insertAfter(parent.rules[index - 1].$el);
  733. }
  734. self.refreshGroupsConditions();
  735. },
  736. 'move': function(e, node, group, index) {
  737. node.$el.detach();
  738. if (index === 0) {
  739. node.$el.prependTo(group.$el.find('>' + QueryBuilder.selectors.rules_list));
  740. }
  741. else {
  742. node.$el.insertAfter(group.rules[index - 1].$el);
  743. }
  744. self.refreshGroupsConditions();
  745. },
  746. 'update': function(e, node, field, value, oldValue) {
  747. if (node instanceof Rule) {
  748. switch (field) {
  749. case 'error':
  750. self.updateError(node);
  751. break;
  752. case 'flags':
  753. self.applyRuleFlags(node);
  754. break;
  755. case 'filter':
  756. self.updateRuleFilter(node, oldValue);
  757. break;
  758. case 'operator':
  759. self.updateRuleOperator(node, oldValue);
  760. break;
  761. case 'value':
  762. self.updateRuleValue(node, oldValue);
  763. break;
  764. }
  765. }
  766. else {
  767. switch (field) {
  768. case 'error':
  769. self.updateError(node);
  770. break;
  771. case 'flags':
  772. self.applyGroupFlags(node);
  773. break;
  774. case 'condition':
  775. self.updateGroupCondition(node, oldValue);
  776. break;
  777. }
  778. }
  779. }
  780. });
  781. };
  782. /**
  783. * Creates the root group
  784. * @param {boolean} [addRule=true] - adds a default empty rule
  785. * @param {object} [data] - group custom data
  786. * @param {object} [flags] - flags to apply to the group
  787. * @returns {Group} root group
  788. * @fires QueryBuilder.afterAddGroup
  789. */
  790. QueryBuilder.prototype.setRoot = function(addRule, data, flags) {
  791. addRule = (addRule === undefined || addRule === true);
  792. var group_id = this.nextGroupId();
  793. var $group = $(this.getGroupTemplate(group_id, 1));
  794. this.$el.append($group);
  795. this.model.root = new Group(null, $group);
  796. this.model.root.model = this.model;
  797. this.model.root.data = data;
  798. this.model.root.flags = $.extend({}, this.settings.default_group_flags, flags);
  799. this.model.root.condition = this.settings.default_condition;
  800. this.trigger('afterAddGroup', this.model.root);
  801. if (addRule) {
  802. this.addRule(this.model.root);
  803. }
  804. return this.model.root;
  805. };
  806. /**
  807. * Adds a new group
  808. * @param {Group} parent
  809. * @param {boolean} [addRule=true] - adds a default empty rule
  810. * @param {object} [data] - group custom data
  811. * @param {object} [flags] - flags to apply to the group
  812. * @returns {Group}
  813. * @fires QueryBuilder.beforeAddGroup
  814. * @fires QueryBuilder.afterAddGroup
  815. */
  816. QueryBuilder.prototype.addGroup = function(parent, addRule, data, flags) {
  817. addRule = (addRule === undefined || addRule === true);
  818. var level = parent.level + 1;
  819. /**
  820. * Just before adding a group, can be prevented.
  821. * @event beforeAddGroup
  822. * @memberof QueryBuilder
  823. * @param {Group} parent
  824. * @param {boolean} addRule - if an empty rule will be added in the group
  825. * @param {int} level - nesting level of the group, 1 is the root group
  826. */
  827. var e = this.trigger('beforeAddGroup', parent, addRule, level);
  828. if (e.isDefaultPrevented()) {
  829. return null;
  830. }
  831. var group_id = this.nextGroupId();
  832. var $group = $(this.getGroupTemplate(group_id, level));
  833. var model = parent.addGroup($group);
  834. model.data = data;
  835. model.flags = $.extend({}, this.settings.default_group_flags, flags);
  836. model.condition = this.settings.default_condition;
  837. /**
  838. * Just after adding a group
  839. * @event afterAddGroup
  840. * @memberof QueryBuilder
  841. * @param {Group} group
  842. */
  843. this.trigger('afterAddGroup', model);
  844. /**
  845. * After any change in the rules
  846. * @event rulesChanged
  847. * @memberof QueryBuilder
  848. */
  849. this.trigger('rulesChanged');
  850. if (addRule) {
  851. this.addRule(model);
  852. }
  853. return model;
  854. };
  855. /**
  856. * Tries to delete a group. The group is not deleted if at least one rule is flagged `no_delete`.
  857. * @param {Group} group
  858. * @returns {boolean} if the group has been deleted
  859. * @fires QueryBuilder.beforeDeleteGroup
  860. * @fires QueryBuilder.afterDeleteGroup
  861. */
  862. QueryBuilder.prototype.deleteGroup = function(group) {
  863. if (group.isRoot()) {
  864. return false;
  865. }
  866. /**
  867. * Just before deleting a group, can be prevented
  868. * @event beforeDeleteGroup
  869. * @memberof QueryBuilder
  870. * @param {Group} parent
  871. */
  872. var e = this.trigger('beforeDeleteGroup', group);
  873. if (e.isDefaultPrevented()) {
  874. return false;
  875. }
  876. var del = true;
  877. group.each('reverse', function(rule) {
  878. del &= this.deleteRule(rule);
  879. }, function(group) {
  880. del &= this.deleteGroup(group);
  881. }, this);
  882. if (del) {
  883. group.drop();
  884. /**
  885. * Just after deleting a group
  886. * @event afterDeleteGroup
  887. * @memberof QueryBuilder
  888. */
  889. this.trigger('afterDeleteGroup');
  890. this.trigger('rulesChanged');
  891. }
  892. return del;
  893. };
  894. /**
  895. * Performs actions when a group's condition changes
  896. * @param {Group} group
  897. * @param {object} previousCondition
  898. * @fires QueryBuilder.afterUpdateGroupCondition
  899. * @private
  900. */
  901. QueryBuilder.prototype.updateGroupCondition = function(group, previousCondition) {
  902. group.$el.find('>' + QueryBuilder.selectors.group_condition).each(function() {
  903. var $this = $(this);
  904. $this.prop('checked', $this.val() === group.condition);
  905. $this.parent().toggleClass('active', $this.val() === group.condition);
  906. });
  907. /**
  908. * After the group condition has been modified
  909. * @event afterUpdateGroupCondition
  910. * @memberof QueryBuilder
  911. * @param {Group} group
  912. * @param {object} previousCondition
  913. */
  914. this.trigger('afterUpdateGroupCondition', group, previousCondition);
  915. this.trigger('rulesChanged');
  916. };
  917. /**
  918. * Updates the visibility of conditions based on number of rules inside each group
  919. * @private
  920. */
  921. QueryBuilder.prototype.refreshGroupsConditions = function() {
  922. (function walk(group) {
  923. if (!group.flags || (group.flags && !group.flags.condition_readonly)) {
  924. group.$el.find('>' + QueryBuilder.selectors.group_condition).prop('disabled', group.rules.length <= 1)
  925. .parent().toggleClass('disabled', group.rules.length <= 1);
  926. }
  927. group.each(null, function(group) {
  928. walk(group);
  929. }, this);
  930. }(this.model.root));
  931. };
  932. /**
  933. * Adds a new rule
  934. * @param {Group} parent
  935. * @param {object} [data] - rule custom data
  936. * @param {object} [flags] - flags to apply to the rule
  937. * @returns {Rule}
  938. * @fires QueryBuilder.beforeAddRule
  939. * @fires QueryBuilder.afterAddRule
  940. * @fires QueryBuilder.changer:getDefaultFilter
  941. */
  942. QueryBuilder.prototype.addRule = function(parent, data, flags) {
  943. /**
  944. * Just before adding a rule, can be prevented
  945. * @event beforeAddRule
  946. * @memberof QueryBuilder
  947. * @param {Group} parent
  948. */
  949. var e = this.trigger('beforeAddRule', parent);
  950. if (e.isDefaultPrevented()) {
  951. return null;
  952. }
  953. var rule_id = this.nextRuleId();
  954. var $rule = $(this.getRuleTemplate(rule_id));
  955. var model = parent.addRule($rule);
  956. model.data = data;
  957. model.flags = $.extend({}, this.settings.default_rule_flags, flags);
  958. /**
  959. * Just after adding a rule
  960. * @event afterAddRule
  961. * @memberof QueryBuilder
  962. * @param {Rule} rule
  963. */
  964. this.trigger('afterAddRule', model);
  965. this.trigger('rulesChanged');
  966. this.createRuleFilters(model);
  967. if (this.settings.default_filter || !this.settings.display_empty_filter) {
  968. /**
  969. * Modifies the default filter for a rule
  970. * @event changer:getDefaultFilter
  971. * @memberof QueryBuilder
  972. * @param {QueryBuilder.Filter} filter
  973. * @param {Rule} rule
  974. * @returns {QueryBuilder.Filter}
  975. */
  976. model.filter = this.change('getDefaultFilter',
  977. this.getFilterById(this.settings.default_filter || this.filters[0].id),
  978. model
  979. );
  980. }
  981. return model;
  982. };
  983. /**
  984. * Tries to delete a rule
  985. * @param {Rule} rule
  986. * @returns {boolean} if the rule has been deleted
  987. * @fires QueryBuilder.beforeDeleteRule
  988. * @fires QueryBuilder.afterDeleteRule
  989. */
  990. QueryBuilder.prototype.deleteRule = function(rule) {
  991. if (rule.flags.no_delete) {
  992. return false;
  993. }
  994. /**
  995. * Just before deleting a rule, can be prevented
  996. * @event beforeDeleteRule
  997. * @memberof QueryBuilder
  998. * @param {Rule} rule
  999. */
  1000. var e = this.trigger('beforeDeleteRule', rule);
  1001. if (e.isDefaultPrevented()) {
  1002. return false;
  1003. }
  1004. rule.drop();
  1005. /**
  1006. * Just after deleting a rule
  1007. * @event afterDeleteRule
  1008. * @memberof QueryBuilder
  1009. */
  1010. this.trigger('afterDeleteRule');
  1011. this.trigger('rulesChanged');
  1012. return true;
  1013. };
  1014. /**
  1015. * Creates the filters for a rule
  1016. * @param {Rule} rule
  1017. * @fires QueryBuilder.changer:getRuleFilters
  1018. * @fires QueryBuilder.afterCreateRuleFilters
  1019. * @private
  1020. */
  1021. QueryBuilder.prototype.createRuleFilters = function(rule) {
  1022. /**
  1023. * Modifies the list a filters available for a rule
  1024. * @event changer:getRuleFilters
  1025. * @memberof QueryBuilder
  1026. * @param {QueryBuilder.Filter[]} filters
  1027. * @param {Rule} rule
  1028. * @returns {QueryBuilder.Filter[]}
  1029. */
  1030. var filters = this.change('getRuleFilters', this.filters, rule);
  1031. var $filterSelect = $(this.getRuleFilterSelect(rule, filters));
  1032. rule.$el.find(QueryBuilder.selectors.filter_container).html($filterSelect);
  1033. /**
  1034. * After creating the dropdown for filters
  1035. * @event afterCreateRuleFilters
  1036. * @memberof QueryBuilder
  1037. * @param {Rule} rule
  1038. */
  1039. this.trigger('afterCreateRuleFilters', rule);
  1040. this.applyRuleFlags(rule);
  1041. };
  1042. /**
  1043. * Creates the operators for a rule and init the rule operator
  1044. * @param {Rule} rule
  1045. * @fires QueryBuilder.afterCreateRuleOperators
  1046. * @private
  1047. */
  1048. QueryBuilder.prototype.createRuleOperators = function(rule) {
  1049. var $operatorContainer = rule.$el.find(QueryBuilder.selectors.operator_container).empty();
  1050. if (!rule.filter) {
  1051. return;
  1052. }
  1053. var operators = this.getOperators(rule.filter);
  1054. var $operatorSelect = $(this.getRuleOperatorSelect(rule, operators));
  1055. $operatorContainer.html($operatorSelect);
  1056. // set the operator without triggering update event
  1057. if (rule.filter.default_operator) {
  1058. rule.__.operator = this.getOperatorByType(rule.filter.default_operator);
  1059. }
  1060. else {
  1061. rule.__.operator = operators[0];
  1062. }
  1063. rule.$el.find(QueryBuilder.selectors.rule_operator).val(rule.operator.type);
  1064. /**
  1065. * After creating the dropdown for operators
  1066. * @event afterCreateRuleOperators
  1067. * @memberof QueryBuilder
  1068. * @param {Rule} rule
  1069. * @param {QueryBuilder.Operator[]} operators - allowed operators for this rule
  1070. */
  1071. this.trigger('afterCreateRuleOperators', rule, operators);
  1072. this.applyRuleFlags(rule);
  1073. };
  1074. /**
  1075. * Creates the main input for a rule
  1076. * @param {Rule} rule
  1077. * @fires QueryBuilder.afterCreateRuleInput
  1078. * @private
  1079. */
  1080. QueryBuilder.prototype.createRuleInput = function(rule) {
  1081. var $valueContainer = rule.$el.find(QueryBuilder.selectors.value_container).empty();
  1082. rule.__.value = undefined;
  1083. if (!rule.filter || !rule.operator || rule.operator.nb_inputs === 0) {
  1084. return;
  1085. }
  1086. var self = this;
  1087. var $inputs = $();
  1088. var filter = rule.filter;
  1089. for (var i = 0; i < rule.operator.nb_inputs; i++) {
  1090. var $ruleInput = $(this.getRuleInput(rule, i));
  1091. if (i > 0) $valueContainer.append(this.settings.inputs_separator);
  1092. $valueContainer.append($ruleInput);
  1093. $inputs = $inputs.add($ruleInput);
  1094. }
  1095. $valueContainer.css('display', '');
  1096. $inputs.on('change ' + (filter.input_event || ''), function() {
  1097. if (!rule._updating_input) {
  1098. rule._updating_value = true;
  1099. rule.value = self.getRuleInputValue(rule);
  1100. rule._updating_value = false;
  1101. }
  1102. });
  1103. if (filter.plugin) {
  1104. $inputs[filter.plugin](filter.plugin_config || {});
  1105. }
  1106. /**
  1107. * After creating the input for a rule and initializing optional plugin
  1108. * @event afterCreateRuleInput
  1109. * @memberof QueryBuilder
  1110. * @param {Rule} rule
  1111. */
  1112. this.trigger('afterCreateRuleInput', rule);
  1113. if (filter.default_value !== undefined) {
  1114. rule.value = filter.default_value;
  1115. }
  1116. else {
  1117. rule._updating_value = true;
  1118. rule.value = self.getRuleInputValue(rule);
  1119. rule._updating_value = false;
  1120. }
  1121. this.applyRuleFlags(rule);
  1122. };
  1123. /**
  1124. * Performs action when a rule's filter changes
  1125. * @param {Rule} rule
  1126. * @param {object} previousFilter
  1127. * @fires QueryBuilder.afterUpdateRuleFilter
  1128. * @private
  1129. */
  1130. QueryBuilder.prototype.updateRuleFilter = function(rule, previousFilter) {
  1131. this.createRuleOperators(rule);
  1132. this.createRuleInput(rule);
  1133. rule.$el.find(QueryBuilder.selectors.rule_filter).val(rule.filter ? rule.filter.id : '-1');
  1134. // clear rule data if the filter changed
  1135. if (previousFilter && rule.filter && previousFilter.id !== rule.filter.id) {
  1136. rule.data = undefined;
  1137. }
  1138. /**
  1139. * After the filter has been updated and the operators and input re-created
  1140. * @event afterUpdateRuleFilter
  1141. * @memberof QueryBuilder
  1142. * @param {Rule} rule
  1143. * @param {object} previousFilter
  1144. */
  1145. this.trigger('afterUpdateRuleFilter', rule, previousFilter);
  1146. this.trigger('rulesChanged');
  1147. };
  1148. /**
  1149. * Performs actions when a rule's operator changes
  1150. * @param {Rule} rule
  1151. * @param {object} previousOperator
  1152. * @fires QueryBuilder.afterUpdateRuleOperator
  1153. * @private
  1154. */
  1155. QueryBuilder.prototype.updateRuleOperator = function(rule, previousOperator) {
  1156. var $valueContainer = rule.$el.find(QueryBuilder.selectors.value_container);
  1157. if (!rule.operator || rule.operator.nb_inputs === 0) {
  1158. $valueContainer.hide();
  1159. rule.__.value = undefined;
  1160. }
  1161. else {
  1162. $valueContainer.css('display', '');
  1163. if ($valueContainer.is(':empty') || !previousOperator ||
  1164. rule.operator.nb_inputs !== previousOperator.nb_inputs ||
  1165. rule.operator.optgroup !== previousOperator.optgroup
  1166. ) {
  1167. this.createRuleInput(rule);
  1168. }
  1169. }
  1170. if (rule.operator) {
  1171. rule.$el.find(QueryBuilder.selectors.rule_operator).val(rule.operator.type);
  1172. // refresh value if the format changed for this operator
  1173. rule.__.value = this.getRuleInputValue(rule);
  1174. }
  1175. /**
  1176. * After the operator has been updated and the input optionally re-created
  1177. * @event afterUpdateRuleOperator
  1178. * @memberof QueryBuilder
  1179. * @param {Rule} rule
  1180. * @param {object} previousOperator
  1181. */
  1182. this.trigger('afterUpdateRuleOperator', rule, previousOperator);
  1183. this.trigger('rulesChanged');
  1184. };
  1185. /**
  1186. * Performs actions when rule's value changes
  1187. * @param {Rule} rule
  1188. * @param {object} previousValue
  1189. * @fires QueryBuilder.afterUpdateRuleValue
  1190. * @private
  1191. */
  1192. QueryBuilder.prototype.updateRuleValue = function(rule, previousValue) {
  1193. if (!rule._updating_value) {
  1194. this.setRuleInputValue(rule, rule.value);
  1195. }
  1196. /**
  1197. * After the rule value has been modified
  1198. * @event afterUpdateRuleValue
  1199. * @memberof QueryBuilder
  1200. * @param {Rule} rule
  1201. * @param {*} previousValue
  1202. */
  1203. this.trigger('afterUpdateRuleValue', rule, previousValue);
  1204. this.trigger('rulesChanged');
  1205. };
  1206. /**
  1207. * Changes a rule's properties depending on its flags
  1208. * @param {Rule} rule
  1209. * @fires QueryBuilder.afterApplyRuleFlags
  1210. * @private
  1211. */
  1212. QueryBuilder.prototype.applyRuleFlags = function(rule) {
  1213. var flags = rule.flags;
  1214. var Selectors = QueryBuilder.selectors;
  1215. rule.$el.find(Selectors.rule_filter).prop('disabled', flags.filter_readonly);
  1216. rule.$el.find(Selectors.rule_operator).prop('disabled', flags.operator_readonly);
  1217. rule.$el.find(Selectors.rule_value).prop('disabled', flags.value_readonly);
  1218. if (flags.no_delete) {
  1219. rule.$el.find(Selectors.delete_rule).remove();
  1220. }
  1221. /**
  1222. * After rule's flags has been applied
  1223. * @event afterApplyRuleFlags
  1224. * @memberof QueryBuilder
  1225. * @param {Rule} rule
  1226. */
  1227. this.trigger('afterApplyRuleFlags', rule);
  1228. };
  1229. /**
  1230. * Changes group's properties depending on its flags
  1231. * @param {Group} group
  1232. * @fires QueryBuilder.afterApplyGroupFlags
  1233. * @private
  1234. */
  1235. QueryBuilder.prototype.applyGroupFlags = function(group) {
  1236. var flags = group.flags;
  1237. var Selectors = QueryBuilder.selectors;
  1238. group.$el.find('>' + Selectors.group_condition).prop('disabled', flags.condition_readonly)
  1239. .parent().toggleClass('readonly', flags.condition_readonly);
  1240. if (flags.no_add_rule) {
  1241. group.$el.find(Selectors.add_rule).remove();
  1242. }
  1243. if (flags.no_add_group) {
  1244. group.$el.find(Selectors.add_group).remove();
  1245. }
  1246. if (flags.no_delete) {
  1247. group.$el.find(Selectors.delete_group).remove();
  1248. }
  1249. /**
  1250. * After group's flags has been applied
  1251. * @event afterApplyGroupFlags
  1252. * @memberof QueryBuilder
  1253. * @param {Group} group
  1254. */
  1255. this.trigger('afterApplyGroupFlags', group);
  1256. };
  1257. /**
  1258. * Clears all errors markers
  1259. * @param {Node} [node] default is root Group
  1260. */
  1261. QueryBuilder.prototype.clearErrors = function(node) {
  1262. node = node || this.model.root;
  1263. if (!node) {
  1264. return;
  1265. }
  1266. node.error = null;
  1267. if (node instanceof Group) {
  1268. node.each(function(rule) {
  1269. rule.error = null;
  1270. }, function(group) {
  1271. this.clearErrors(group);
  1272. }, this);
  1273. }
  1274. };
  1275. /**
  1276. * Adds/Removes error on a Rule or Group
  1277. * @param {Node} node
  1278. * @fires QueryBuilder.changer:displayError
  1279. * @private
  1280. */
  1281. QueryBuilder.prototype.updateError = function(node) {
  1282. if (this.settings.display_errors) {
  1283. if (node.error === null) {
  1284. node.$el.removeClass('has-error');
  1285. }
  1286. else {
  1287. var errorMessage = this.translate('errors', node.error[0]);
  1288. errorMessage = Utils.fmt(errorMessage, node.error.slice(1));
  1289. /**
  1290. * Modifies an error message before display
  1291. * @event changer:displayError
  1292. * @memberof QueryBuilder
  1293. * @param {string} errorMessage - the error message (translated and formatted)
  1294. * @param {array} error - the raw error array (error code and optional arguments)
  1295. * @param {Node} node
  1296. * @returns {string}
  1297. */
  1298. errorMessage = this.change('displayError', errorMessage, node.error, node);
  1299. node.$el.addClass('has-error')
  1300. .find(QueryBuilder.selectors.error_container).eq(0)
  1301. .attr('title', errorMessage);
  1302. }
  1303. }
  1304. };
  1305. /**
  1306. * Triggers a validation error event
  1307. * @param {Node} node
  1308. * @param {string|array} error
  1309. * @param {*} value
  1310. * @fires QueryBuilder.validationError
  1311. * @private
  1312. */
  1313. QueryBuilder.prototype.triggerValidationError = function(node, error, value) {
  1314. if (!$.isArray(error)) {
  1315. error = [error];
  1316. }
  1317. /**
  1318. * Fired when a validation error occurred, can be prevented
  1319. * @event validationError
  1320. * @memberof QueryBuilder
  1321. * @param {Node} node
  1322. * @param {string} error
  1323. * @param {*} value
  1324. */
  1325. var e = this.trigger('validationError', node, error, value);
  1326. if (!e.isDefaultPrevented()) {
  1327. node.error = error;
  1328. }
  1329. };
  1330. /**
  1331. * Destroys the builder
  1332. * @fires QueryBuilder.beforeDestroy
  1333. */
  1334. QueryBuilder.prototype.destroy = function() {
  1335. /**
  1336. * Before the {@link QueryBuilder#destroy} method
  1337. * @event beforeDestroy
  1338. * @memberof QueryBuilder
  1339. */
  1340. this.trigger('beforeDestroy');
  1341. if (this.status.generated_id) {
  1342. this.$el.removeAttr('id');
  1343. }
  1344. this.clear();
  1345. this.model = null;
  1346. this.$el
  1347. .off('.queryBuilder')
  1348. .removeClass('query-builder')
  1349. .removeData('queryBuilder');
  1350. delete this.$el[0].queryBuilder;
  1351. };
  1352. /**
  1353. * Clear all rules and resets the root group
  1354. * @fires QueryBuilder.beforeReset
  1355. * @fires QueryBuilder.afterReset
  1356. */
  1357. QueryBuilder.prototype.reset = function() {
  1358. /**
  1359. * Before the {@link QueryBuilder#reset} method, can be prevented
  1360. * @event beforeReset
  1361. * @memberof QueryBuilder
  1362. */
  1363. var e = this.trigger('beforeReset');
  1364. if (e.isDefaultPrevented()) {
  1365. return;
  1366. }
  1367. this.status.group_id = 1;
  1368. this.status.rule_id = 0;
  1369. this.model.root.empty();
  1370. this.model.root.data = undefined;
  1371. this.model.root.flags = $.extend({}, this.settings.default_group_flags);
  1372. this.model.root.condition = this.settings.default_condition;
  1373. this.addRule(this.model.root);
  1374. /**
  1375. * After the {@link QueryBuilder#reset} method
  1376. * @event afterReset
  1377. * @memberof QueryBuilder
  1378. */
  1379. this.trigger('afterReset');
  1380. this.trigger('rulesChanged');
  1381. };
  1382. /**
  1383. * Clears all rules and removes the root group
  1384. * @fires QueryBuilder.beforeClear
  1385. * @fires QueryBuilder.afterClear
  1386. */
  1387. QueryBuilder.prototype.clear = function() {
  1388. /**
  1389. * Before the {@link QueryBuilder#clear} method, can be prevented
  1390. * @event beforeClear
  1391. * @memberof QueryBuilder
  1392. */
  1393. var e = this.trigger('beforeClear');
  1394. if (e.isDefaultPrevented()) {
  1395. return;
  1396. }
  1397. this.status.group_id = 0;
  1398. this.status.rule_id = 0;
  1399. if (this.model.root) {
  1400. this.model.root.drop();
  1401. this.model.root = null;
  1402. }
  1403. /**
  1404. * After the {@link QueryBuilder#clear} method
  1405. * @event afterClear
  1406. * @memberof QueryBuilder
  1407. */
  1408. this.trigger('afterClear');
  1409. this.trigger('rulesChanged');
  1410. };
  1411. /**
  1412. * Modifies the builder configuration.<br>
  1413. * Only options defined in QueryBuilder.modifiable_options are modifiable
  1414. * @param {object} options
  1415. */
  1416. QueryBuilder.prototype.setOptions = function(options) {
  1417. $.each(options, function(opt, value) {
  1418. if (QueryBuilder.modifiable_options.indexOf(opt) !== -1) {
  1419. this.settings[opt] = value;
  1420. }
  1421. }.bind(this));
  1422. };
  1423. /**
  1424. * Returns the model associated to a DOM object, or the root model
  1425. * @param {jQuery} [target]
  1426. * @returns {Node}
  1427. */
  1428. QueryBuilder.prototype.getModel = function(target) {
  1429. if (!target) {
  1430. return this.model.root;
  1431. }
  1432. else if (target instanceof Node) {
  1433. return target;
  1434. }
  1435. else {
  1436. return $(target).data('queryBuilderModel');
  1437. }
  1438. };
  1439. /**
  1440. * Validates the whole builder
  1441. * @param {object} [options]
  1442. * @param {boolean} [options.skip_empty=false] - skips validating rules that have no filter selected
  1443. * @returns {boolean}
  1444. * @fires QueryBuilder.changer:validate
  1445. */
  1446. QueryBuilder.prototype.validate = function(options) {
  1447. options = $.extend({
  1448. skip_empty: false
  1449. }, options);
  1450. this.clearErrors();
  1451. var self = this;
  1452. var valid = (function parse(group) {
  1453. var done = 0;
  1454. var errors = 0;
  1455. group.each(function(rule) {
  1456. if (!rule.filter && options.skip_empty) {
  1457. return;
  1458. }
  1459. if (!rule.filter) {
  1460. self.triggerValidationError(rule, 'no_filter', null);
  1461. errors++;
  1462. return;
  1463. }
  1464. if (!rule.operator) {
  1465. self.triggerValidationError(rule, 'no_operator', null);
  1466. errors++;
  1467. return;
  1468. }
  1469. if (rule.operator.nb_inputs !== 0) {
  1470. var valid = self.validateValue(rule, rule.value);
  1471. if (valid !== true) {
  1472. self.triggerValidationError(rule, valid, rule.value);
  1473. errors++;
  1474. return;
  1475. }
  1476. }
  1477. done++;
  1478. }, function(group) {
  1479. var res = parse(group);
  1480. if (res === true) {
  1481. done++;
  1482. }
  1483. else if (res === false) {
  1484. errors++;
  1485. }
  1486. });
  1487. if (errors > 0) {
  1488. return false;
  1489. }
  1490. else if (done === 0 && !group.isRoot() && options.skip_empty) {
  1491. return null;
  1492. }
  1493. else if (done === 0 && (!self.settings.allow_empty || !group.isRoot())) {
  1494. self.triggerValidationError(group, 'empty_group', null);
  1495. return false;
  1496. }
  1497. return true;
  1498. }(this.model.root));
  1499. /**
  1500. * Modifies the result of the {@link QueryBuilder#validate} method
  1501. * @event changer:validate
  1502. * @memberof QueryBuilder
  1503. * @param {boolean} valid
  1504. * @returns {boolean}
  1505. */
  1506. return this.change('validate', valid);
  1507. };
  1508. /**
  1509. * Gets an object representing current rules
  1510. * @param {object} [options]
  1511. * @param {boolean|string} [options.get_flags=false] - export flags, true: only changes from default flags or 'all'
  1512. * @param {boolean} [options.allow_invalid=false] - returns rules even if they are invalid
  1513. * @param {boolean} [options.skip_empty=false] - remove rules that have no filter selected
  1514. * @returns {object}
  1515. * @fires QueryBuilder.changer:ruleToJson
  1516. * @fires QueryBuilder.changer:groupToJson
  1517. * @fires QueryBuilder.changer:getRules
  1518. */
  1519. QueryBuilder.prototype.getRules = function(options) {
  1520. options = $.extend({
  1521. get_flags: false,
  1522. allow_invalid: false,
  1523. skip_empty: false
  1524. }, options);
  1525. var valid = this.validate(options);
  1526. if (!valid && !options.allow_invalid) {
  1527. return null;
  1528. }
  1529. var self = this;
  1530. var out = (function parse(group) {
  1531. var groupData = {
  1532. condition: group.condition,
  1533. rules: []
  1534. };
  1535. if (group.data) {
  1536. groupData.data = $.extendext(true, 'replace', {}, group.data);
  1537. }
  1538. if (options.get_flags) {
  1539. var flags = self.getGroupFlags(group.flags, options.get_flags === 'all');
  1540. if (!$.isEmptyObject(flags)) {
  1541. groupData.flags = flags;
  1542. }
  1543. }
  1544. group.each(function(rule) {
  1545. if (!rule.filter && options.skip_empty) {
  1546. return;
  1547. }
  1548. var value = null;
  1549. if (!rule.operator || rule.operator.nb_inputs !== 0) {
  1550. value = rule.value;
  1551. }
  1552. var ruleData = {
  1553. id: rule.filter ? rule.filter.id : null,
  1554. field: rule.filter ? rule.filter.field : null,
  1555. type: rule.filter ? rule.filter.type : null,
  1556. input: rule.filter ? rule.filter.input : null,
  1557. operator: rule.operator ? rule.operator.type : null,
  1558. value: value
  1559. };
  1560. if (rule.filter && rule.filter.data || rule.data) {
  1561. ruleData.data = $.extendext(true, 'replace', {}, rule.filter.data, rule.data);
  1562. }
  1563. if (options.get_flags) {
  1564. var flags = self.getRuleFlags(rule.flags, options.get_flags === 'all');
  1565. if (!$.isEmptyObject(flags)) {
  1566. ruleData.flags = flags;
  1567. }
  1568. }
  1569. /**
  1570. * Modifies the JSON generated from a Rule object
  1571. * @event changer:ruleToJson
  1572. * @memberof QueryBuilder
  1573. * @param {object} json
  1574. * @param {Rule} rule
  1575. * @returns {object}
  1576. */
  1577. groupData.rules.push(self.change('ruleToJson', ruleData, rule));
  1578. }, function(model) {
  1579. var data = parse(model);
  1580. if (data.rules.length !== 0 || !options.skip_empty) {
  1581. groupData.rules.push(data);
  1582. }
  1583. }, this);
  1584. /**
  1585. * Modifies the JSON generated from a Group object
  1586. * @event changer:groupToJson
  1587. * @memberof QueryBuilder
  1588. * @param {object} json
  1589. * @param {Group} group
  1590. * @returns {object}
  1591. */
  1592. return self.change('groupToJson', groupData, group);
  1593. }(this.model.root));
  1594. out.valid = valid;
  1595. /**
  1596. * Modifies the result of the {@link QueryBuilder#getRules} method
  1597. * @event changer:getRules
  1598. * @memberof QueryBuilder
  1599. * @param {object} json
  1600. * @returns {object}
  1601. */
  1602. return this.change('getRules', out);
  1603. };
  1604. /**
  1605. * Sets rules from object
  1606. * @param {object} data
  1607. * @param {object} [options]
  1608. * @param {boolean} [options.allow_invalid=false] - silent-fail if the data are invalid
  1609. * @throws RulesError, UndefinedConditionError
  1610. * @fires QueryBuilder.changer:setRules
  1611. * @fires QueryBuilder.changer:jsonToRule
  1612. * @fires QueryBuilder.changer:jsonToGroup
  1613. * @fires QueryBuilder.afterSetRules
  1614. */
  1615. QueryBuilder.prototype.setRules = function(data, options) {
  1616. options = $.extend({
  1617. allow_invalid: false
  1618. }, options);
  1619. if ($.isArray(data)) {
  1620. data = {
  1621. condition: this.settings.default_condition,
  1622. rules: data
  1623. };
  1624. }
  1625. if (!data || !data.rules || (data.rules.length === 0 && !this.settings.allow_empty)) {
  1626. Utils.error('RulesParse', 'Incorrect data object passed');
  1627. }
  1628. this.clear();
  1629. this.setRoot(false, data.data, this.parseGroupFlags(data));
  1630. /**
  1631. * Modifies data before the {@link QueryBuilder#setRules} method
  1632. * @event changer:setRules
  1633. * @memberof QueryBuilder
  1634. * @param {object} json
  1635. * @param {object} options
  1636. * @returns {object}
  1637. */
  1638. data = this.change('setRules', data, options);
  1639. var self = this;
  1640. (function add(data, group) {
  1641. if (group === null) {
  1642. return;
  1643. }
  1644. if (data.condition === undefined) {
  1645. data.condition = self.settings.default_condition;
  1646. }
  1647. else if (self.settings.conditions.indexOf(data.condition) == -1) {
  1648. Utils.error(!options.allow_invalid, 'UndefinedCondition', 'Invalid condition "{0}"', data.condition);
  1649. data.condition = self.settings.default_condition;
  1650. }
  1651. group.condition = data.condition;
  1652. data.rules.forEach(function(item) {
  1653. var model;
  1654. if (item.rules !== undefined) {
  1655. if (self.settings.allow_groups !== -1 && self.settings.allow_groups < group.level) {
  1656. Utils.error(!options.allow_invalid, 'RulesParse', 'No more than {0} groups are allowed', self.settings.allow_groups);
  1657. self.reset();
  1658. }
  1659. else {
  1660. model = self.addGroup(group, false, item.data, self.parseGroupFlags(item));
  1661. if (model === null) {
  1662. return;
  1663. }
  1664. add(item, model);
  1665. }
  1666. }
  1667. else {
  1668. if (!item.empty) {
  1669. if (item.id === undefined) {
  1670. Utils.error(!options.allow_invalid, 'RulesParse', 'Missing rule field id');
  1671. item.empty = true;
  1672. }
  1673. if (item.operator === undefined) {
  1674. item.operator = 'equal';
  1675. }
  1676. }
  1677. model = self.addRule(group, item.data, self.parseRuleFlags(item));
  1678. if (model === null) {
  1679. return;
  1680. }
  1681. if (!item.empty) {
  1682. model.filter = self.getFilterById(item.id, !options.allow_invalid);
  1683. }
  1684. if (model.filter) {
  1685. model.operator = self.getOperatorByType(item.operator, !options.allow_invalid);
  1686. if (!model.operator) {
  1687. model.operator = self.getOperators(model.filter)[0];
  1688. }
  1689. }
  1690. if (model.operator && model.operator.nb_inputs !== 0) {
  1691. if (item.value !== undefined) {
  1692. model.value = item.value;
  1693. }
  1694. else if (model.filter.default_value !== undefined) {
  1695. model.value = model.filter.default_value;
  1696. }
  1697. }
  1698. /**
  1699. * Modifies the Rule object generated from the JSON
  1700. * @event changer:jsonToRule
  1701. * @memberof QueryBuilder
  1702. * @param {Rule} rule
  1703. * @param {object} json
  1704. * @returns {Rule} the same rule
  1705. */
  1706. if (self.change('jsonToRule', model, item) != model) {
  1707. Utils.error('RulesParse', 'Plugin tried to change rule reference');
  1708. }
  1709. }
  1710. });
  1711. /**
  1712. * Modifies the Group object generated from the JSON
  1713. * @event changer:jsonToGroup
  1714. * @memberof QueryBuilder
  1715. * @param {Group} group
  1716. * @param {object} json
  1717. * @returns {Group} the same group
  1718. */
  1719. if (self.change('jsonToGroup', group, data) != group) {
  1720. Utils.error('RulesParse', 'Plugin tried to change group reference');
  1721. }
  1722. }(data, this.model.root));
  1723. /**
  1724. * After the {@link QueryBuilder#setRules} method
  1725. * @event afterSetRules
  1726. * @memberof QueryBuilder
  1727. */
  1728. this.trigger('afterSetRules');
  1729. };
  1730. /**
  1731. * Performs value validation
  1732. * @param {Rule} rule
  1733. * @param {string|string[]} value
  1734. * @returns {array|boolean} true or error array
  1735. * @fires QueryBuilder.changer:validateValue
  1736. */
  1737. QueryBuilder.prototype.validateValue = function(rule, value) {
  1738. var validation = rule.filter.validation || {};
  1739. var result = true;
  1740. if (validation.callback) {
  1741. result = validation.callback.call(this, value, rule);
  1742. }
  1743. else {
  1744. result = this._validateValue(rule, value);
  1745. }
  1746. /**
  1747. * Modifies the result of the rule validation method
  1748. * @event changer:validateValue
  1749. * @memberof QueryBuilder
  1750. * @param {array|boolean} result - true or an error array
  1751. * @param {*} value
  1752. * @param {Rule} rule
  1753. * @returns {array|boolean}
  1754. */
  1755. return this.change('validateValue', result, value, rule);
  1756. };
  1757. /**
  1758. * Default validation function
  1759. * @param {Rule} rule
  1760. * @param {string|string[]} value
  1761. * @returns {array|boolean} true or error array
  1762. * @throws ConfigError
  1763. * @private
  1764. */
  1765. QueryBuilder.prototype._validateValue = function(rule, value) {
  1766. var filter = rule.filter;
  1767. var operator = rule.operator;
  1768. var validation = filter.validation || {};
  1769. var result = true;
  1770. var tmp, tempValue;
  1771. if (rule.operator.nb_inputs === 1) {
  1772. value = [value];
  1773. }
  1774. for (var i = 0; i < operator.nb_inputs; i++) {
  1775. if (!operator.multiple && $.isArray(value[i]) && value[i].length > 1) {
  1776. result = ['operator_not_multiple', operator.type, this.translate('operators', operator.type)];
  1777. break;
  1778. }
  1779. switch (filter.input) {
  1780. case 'radio':
  1781. if (value[i] === undefined || value[i].length === 0) {
  1782. if (!validation.allow_empty_value) {
  1783. result = ['radio_empty'];
  1784. }
  1785. break;
  1786. }
  1787. break;
  1788. case 'checkbox':
  1789. if (value[i] === undefined || value[i].length === 0) {
  1790. if (!validation.allow_empty_value) {
  1791. result = ['checkbox_empty'];
  1792. }
  1793. break;
  1794. }
  1795. break;
  1796. case 'select':
  1797. if (value[i] === undefined || value[i].length === 0 || (filter.placeholder && value[i] == filter.placeholder_value)) {
  1798. if (!validation.allow_empty_value) {
  1799. result = ['select_empty'];
  1800. }
  1801. break;
  1802. }
  1803. break;
  1804. default:
  1805. tempValue = $.isArray(value[i]) ? value[i] : [value[i]];
  1806. for (var j = 0; j < tempValue.length; j++) {
  1807. switch (QueryBuilder.types[filter.type]) {
  1808. case 'string':
  1809. if (tempValue[j] === undefined || tempValue[j].length === 0) {
  1810. if (!validation.allow_empty_value) {
  1811. result = ['string_empty'];
  1812. }
  1813. break;
  1814. }
  1815. if (validation.min !== undefined) {
  1816. if (tempValue[j].length < parseInt(validation.min)) {
  1817. result = [this.getValidationMessage(validation, 'min', 'string_exceed_min_length'), validation.min];
  1818. break;
  1819. }
  1820. }
  1821. if (validation.max !== undefined) {
  1822. if (tempValue[j].length > parseInt(validation.max)) {
  1823. result = [this.getValidationMessage(validation, 'max', 'string_exceed_max_length'), validation.max];
  1824. break;
  1825. }
  1826. }
  1827. if (validation.format) {
  1828. if (typeof validation.format == 'string') {
  1829. validation.format = new RegExp(validation.format);
  1830. }
  1831. if (!validation.format.test(tempValue[j])) {
  1832. result = [this.getValidationMessage(validation, 'format', 'string_invalid_format'), validation.format];
  1833. break;
  1834. }
  1835. }
  1836. break;
  1837. case 'number':
  1838. if (tempValue[j] === undefined || tempValue[j].length === 0) {
  1839. if (!validation.allow_empty_value) {
  1840. result = ['number_nan'];
  1841. }
  1842. break;
  1843. }
  1844. if (isNaN(tempValue[j])) {
  1845. result = ['number_nan'];
  1846. break;
  1847. }
  1848. if (filter.type == 'integer') {
  1849. if (parseInt(tempValue[j]) != tempValue[j]) {
  1850. result = ['number_not_integer'];
  1851. break;
  1852. }
  1853. }
  1854. else {
  1855. if (parseFloat(tempValue[j]) != tempValue[j]) {
  1856. result = ['number_not_double'];
  1857. break;
  1858. }
  1859. }
  1860. if (validation.min !== undefined) {
  1861. if (tempValue[j] < parseFloat(validation.min)) {
  1862. result = [this.getValidationMessage(validation, 'min', 'number_exceed_min'), validation.min];
  1863. break;
  1864. }
  1865. }
  1866. if (validation.max !== undefined) {
  1867. if (tempValue[j] > parseFloat(validation.max)) {
  1868. result = [this.getValidationMessage(validation, 'max', 'number_exceed_max'), validation.max];
  1869. break;
  1870. }
  1871. }
  1872. if (validation.step !== undefined && validation.step !== 'any') {
  1873. var v = (tempValue[j] / validation.step).toPrecision(14);
  1874. if (parseInt(v) != v) {
  1875. result = [this.getValidationMessage(validation, 'step', 'number_wrong_step'), validation.step];
  1876. break;
  1877. }
  1878. }
  1879. break;
  1880. case 'datetime':
  1881. if (tempValue[j] === undefined || tempValue[j].length === 0) {
  1882. if (!validation.allow_empty_value) {
  1883. result = ['datetime_empty'];
  1884. }
  1885. break;
  1886. }
  1887. // we need MomentJS
  1888. if (validation.format) {
  1889. if (!('moment' in window)) {
  1890. Utils.error('MissingLibrary', 'MomentJS is required for Date/Time validation. Get it here http://momentjs.com');
  1891. }
  1892. var datetime = moment(tempValue[j], validation.format);
  1893. if (!datetime.isValid()) {
  1894. result = [this.getValidationMessage(validation, 'format', 'datetime_invalid'), validation.format];
  1895. break;
  1896. }
  1897. else {
  1898. if (validation.min) {
  1899. if (datetime < moment(validation.min, validation.format)) {
  1900. result = [this.getValidationMessage(validation, 'min', 'datetime_exceed_min'), validation.min];
  1901. break;
  1902. }
  1903. }
  1904. if (validation.max) {
  1905. if (datetime > moment(validation.max, validation.format)) {
  1906. result = [this.getValidationMessage(validation, 'max', 'datetime_exceed_max'), validation.max];
  1907. break;
  1908. }
  1909. }
  1910. }
  1911. }
  1912. break;
  1913. case 'boolean':
  1914. if (tempValue[j] === undefined || tempValue[j].length === 0) {
  1915. if (!validation.allow_empty_value) {
  1916. result = ['boolean_not_valid'];
  1917. }
  1918. break;
  1919. }
  1920. tmp = ('' + tempValue[j]).trim().toLowerCase();
  1921. if (tmp !== 'true' && tmp !== 'false' && tmp !== '1' && tmp !== '0' && tempValue[j] !== 1 && tempValue[j] !== 0) {
  1922. result = ['boolean_not_valid'];
  1923. break;
  1924. }
  1925. }
  1926. if (result !== true) {
  1927. break;
  1928. }
  1929. }
  1930. }
  1931. if (result !== true) {
  1932. break;
  1933. }
  1934. }
  1935. if ((rule.operator.type === 'between' || rule.operator.type === 'not_between') && value.length === 2) {
  1936. switch (QueryBuilder.types[filter.type]) {
  1937. case 'number':
  1938. if (value[0] > value[1]) {
  1939. result = ['number_between_invalid', value[0], value[1]];
  1940. }
  1941. break;
  1942. case 'datetime':
  1943. // we need MomentJS
  1944. if (validation.format) {
  1945. if (!('moment' in window)) {
  1946. Utils.error('MissingLibrary', 'MomentJS is required for Date/Time validation. Get it here http://momentjs.com');
  1947. }
  1948. if (moment(value[0], validation.format).isAfter(moment(value[1], validation.format))) {
  1949. result = ['datetime_between_invalid', value[0], value[1]];
  1950. }
  1951. }
  1952. break;
  1953. }
  1954. }
  1955. return result;
  1956. };
  1957. /**
  1958. * Returns an incremented group ID
  1959. * @returns {string}
  1960. * @private
  1961. */
  1962. QueryBuilder.prototype.nextGroupId = function() {
  1963. return this.status.id + '_group_' + (this.status.group_id++);
  1964. };
  1965. /**
  1966. * Returns an incremented rule ID
  1967. * @returns {string}
  1968. * @private
  1969. */
  1970. QueryBuilder.prototype.nextRuleId = function() {
  1971. return this.status.id + '_rule_' + (this.status.rule_id++);
  1972. };
  1973. /**
  1974. * Returns the operators for a filter
  1975. * @param {string|object} filter - filter id or filter object
  1976. * @returns {object[]}
  1977. * @fires QueryBuilder.changer:getOperators
  1978. * @private
  1979. */
  1980. QueryBuilder.prototype.getOperators = function(filter) {
  1981. if (typeof filter == 'string') {
  1982. filter = this.getFilterById(filter);
  1983. }
  1984. var result = [];
  1985. for (var i = 0, l = this.operators.length; i < l; i++) {
  1986. // filter operators check
  1987. if (filter.operators) {
  1988. if (filter.operators.indexOf(this.operators[i].type) == -1) {
  1989. continue;
  1990. }
  1991. }
  1992. // type check
  1993. else if (this.operators[i].apply_to.indexOf(QueryBuilder.types[filter.type]) == -1) {
  1994. continue;
  1995. }
  1996. result.push(this.operators[i]);
  1997. }
  1998. // keep sort order defined for the filter
  1999. if (filter.operators) {
  2000. result.sort(function(a, b) {
  2001. return filter.operators.indexOf(a.type) - filter.operators.indexOf(b.type);
  2002. });
  2003. }
  2004. /**
  2005. * Modifies the operators available for a filter
  2006. * @event changer:getOperators
  2007. * @memberof QueryBuilder
  2008. * @param {QueryBuilder.Operator[]} operators
  2009. * @param {QueryBuilder.Filter} filter
  2010. * @returns {QueryBuilder.Operator[]}
  2011. */
  2012. return this.change('getOperators', result, filter);
  2013. };
  2014. /**
  2015. * Returns a particular filter by its id
  2016. * @param {string} id
  2017. * @param {boolean} [doThrow=true]
  2018. * @returns {object|null}
  2019. * @throws UndefinedFilterError
  2020. * @private
  2021. */
  2022. QueryBuilder.prototype.getFilterById = function(id, doThrow) {
  2023. if (id == '-1') {
  2024. return null;
  2025. }
  2026. for (var i = 0, l = this.filters.length; i < l; i++) {
  2027. if (this.filters[i].id == id) {
  2028. return this.filters[i];
  2029. }
  2030. }
  2031. Utils.error(doThrow !== false, 'UndefinedFilter', 'Undefined filter "{0}"', id);
  2032. return null;
  2033. };
  2034. /**
  2035. * Returns a particular operator by its type
  2036. * @param {string} type
  2037. * @param {boolean} [doThrow=true]
  2038. * @returns {object|null}
  2039. * @throws UndefinedOperatorError
  2040. * @private
  2041. */
  2042. QueryBuilder.prototype.getOperatorByType = function(type, doThrow) {
  2043. if (type == '-1') {
  2044. return null;
  2045. }
  2046. for (var i = 0, l = this.operators.length; i < l; i++) {
  2047. if (this.operators[i].type == type) {
  2048. return this.operators[i];
  2049. }
  2050. }
  2051. Utils.error(doThrow !== false, 'UndefinedOperator', 'Undefined operator "{0}"', type);
  2052. return null;
  2053. };
  2054. /**
  2055. * Returns rule's current input value
  2056. * @param {Rule} rule
  2057. * @returns {*}
  2058. * @fires QueryBuilder.changer:getRuleValue
  2059. * @private
  2060. */
  2061. QueryBuilder.prototype.getRuleInputValue = function(rule) {
  2062. var filter = rule.filter;
  2063. var operator = rule.operator;
  2064. var value = [];
  2065. if (filter.valueGetter) {
  2066. value = filter.valueGetter.call(this, rule);
  2067. }
  2068. else {
  2069. var $value = rule.$el.find(QueryBuilder.selectors.value_container);
  2070. for (var i = 0; i < operator.nb_inputs; i++) {
  2071. var name = Utils.escapeElementId(rule.id + '_value_' + i);
  2072. var tmp;
  2073. switch (filter.input) {
  2074. case 'radio':
  2075. value.push($value.find('[name=' + name + ']:checked').val());
  2076. break;
  2077. case 'checkbox':
  2078. tmp = [];
  2079. // jshint loopfunc:true
  2080. $value.find('[name=' + name + ']:checked').each(function() {
  2081. tmp.push($(this).val());
  2082. });
  2083. // jshint loopfunc:false
  2084. value.push(tmp);
  2085. break;
  2086. case 'select':
  2087. if (filter.multiple) {
  2088. tmp = [];
  2089. // jshint loopfunc:true
  2090. $value.find('[name=' + name + '] option:selected').each(function() {
  2091. tmp.push($(this).val());
  2092. });
  2093. // jshint loopfunc:false
  2094. value.push(tmp);
  2095. }
  2096. else {
  2097. value.push($value.find('[name=' + name + '] option:selected').val());
  2098. }
  2099. break;
  2100. default:
  2101. value.push($value.find('[name=' + name + ']').val());
  2102. }
  2103. }
  2104. value = value.map(function(val) {
  2105. if (operator.multiple && filter.value_separator && typeof val == 'string') {
  2106. val = val.split(filter.value_separator);
  2107. }
  2108. if ($.isArray(val)) {
  2109. return val.map(function(subval) {
  2110. return Utils.changeType(subval, filter.type);
  2111. });
  2112. }
  2113. else {
  2114. return Utils.changeType(val, filter.type);
  2115. }
  2116. });
  2117. if (operator.nb_inputs === 1) {
  2118. value = value[0];
  2119. }
  2120. // @deprecated
  2121. if (filter.valueParser) {
  2122. value = filter.valueParser.call(this, rule, value);
  2123. }
  2124. }
  2125. /**
  2126. * Modifies the rule's value grabbed from the DOM
  2127. * @event changer:getRuleValue
  2128. * @memberof QueryBuilder
  2129. * @param {*} value
  2130. * @param {Rule} rule
  2131. * @returns {*}
  2132. */
  2133. return this.change('getRuleValue', value, rule);
  2134. };
  2135. /**
  2136. * Sets the value of a rule's input
  2137. * @param {Rule} rule
  2138. * @param {*} value
  2139. * @private
  2140. */
  2141. QueryBuilder.prototype.setRuleInputValue = function(rule, value) {
  2142. var filter = rule.filter;
  2143. var operator = rule.operator;
  2144. if (!filter || !operator) {
  2145. return;
  2146. }
  2147. rule._updating_input = true;
  2148. if (filter.valueSetter) {
  2149. filter.valueSetter.call(this, rule, value);
  2150. }
  2151. else {
  2152. var $value = rule.$el.find(QueryBuilder.selectors.value_container);
  2153. if (operator.nb_inputs == 1) {
  2154. value = [value];
  2155. }
  2156. for (var i = 0; i < operator.nb_inputs; i++) {
  2157. var name = Utils.escapeElementId(rule.id + '_value_' + i);
  2158. switch (filter.input) {
  2159. case 'radio':
  2160. $value.find('[name=' + name + '][value="' + value[i] + '"]').prop('checked', true).trigger('change');
  2161. break;
  2162. case 'checkbox':
  2163. if (!$.isArray(value[i])) {
  2164. value[i] = [value[i]];
  2165. }
  2166. // jshint loopfunc:true
  2167. value[i].forEach(function(value) {
  2168. $value.find('[name=' + name + '][value="' + value + '"]').prop('checked', true).trigger('change');
  2169. });
  2170. // jshint loopfunc:false
  2171. break;
  2172. default:
  2173. if (operator.multiple && filter.value_separator && $.isArray(value[i])) {
  2174. value[i] = value[i].join(filter.value_separator);
  2175. }
  2176. $value.find('[name=' + name + ']').val(value[i]).trigger('change');
  2177. break;
  2178. }
  2179. }
  2180. }
  2181. rule._updating_input = false;
  2182. };
  2183. /**
  2184. * Parses rule flags
  2185. * @param {object} rule
  2186. * @returns {object}
  2187. * @fires QueryBuilder.changer:parseRuleFlags
  2188. * @private
  2189. */
  2190. QueryBuilder.prototype.parseRuleFlags = function(rule) {
  2191. var flags = $.extend({}, this.settings.default_rule_flags);
  2192. if (rule.readonly) {
  2193. $.extend(flags, {
  2194. filter_readonly: true,
  2195. operator_readonly: true,
  2196. value_readonly: true,
  2197. no_delete: true
  2198. });
  2199. }
  2200. if (rule.flags) {
  2201. $.extend(flags, rule.flags);
  2202. }
  2203. /**
  2204. * Modifies the consolidated rule's flags
  2205. * @event changer:parseRuleFlags
  2206. * @memberof QueryBuilder
  2207. * @param {object} flags
  2208. * @param {object} rule - <b>not</b> a Rule object
  2209. * @returns {object}
  2210. */
  2211. return this.change('parseRuleFlags', flags, rule);
  2212. };
  2213. /**
  2214. * Gets a copy of flags of a rule
  2215. * @param {object} flags
  2216. * @param {boolean} [all=false] - return all flags or only changes from default flags
  2217. * @returns {object}
  2218. * @private
  2219. */
  2220. QueryBuilder.prototype.getRuleFlags = function(flags, all) {
  2221. if (all) {
  2222. return $.extend({}, flags);
  2223. }
  2224. else {
  2225. var ret = {};
  2226. $.each(this.settings.default_rule_flags, function(key, value) {
  2227. if (flags[key] !== value) {
  2228. ret[key] = flags[key];
  2229. }
  2230. });
  2231. return ret;
  2232. }
  2233. };
  2234. /**
  2235. * Parses group flags
  2236. * @param {object} group
  2237. * @returns {object}
  2238. * @fires QueryBuilder.changer:parseGroupFlags
  2239. * @private
  2240. */
  2241. QueryBuilder.prototype.parseGroupFlags = function(group) {
  2242. var flags = $.extend({}, this.settings.default_group_flags);
  2243. if (group.readonly) {
  2244. $.extend(flags, {
  2245. condition_readonly: true,
  2246. no_add_rule: true,
  2247. no_add_group: true,
  2248. no_delete: true
  2249. });
  2250. }
  2251. if (group.flags) {
  2252. $.extend(flags, group.flags);
  2253. }
  2254. /**
  2255. * Modifies the consolidated group's flags
  2256. * @event changer:parseGroupFlags
  2257. * @memberof QueryBuilder
  2258. * @param {object} flags
  2259. * @param {object} group - <b>not</b> a Group object
  2260. * @returns {object}
  2261. */
  2262. return this.change('parseGroupFlags', flags, group);
  2263. };
  2264. /**
  2265. * Gets a copy of flags of a group
  2266. * @param {object} flags
  2267. * @param {boolean} [all=false] - return all flags or only changes from default flags
  2268. * @returns {object}
  2269. * @private
  2270. */
  2271. QueryBuilder.prototype.getGroupFlags = function(flags, all) {
  2272. if (all) {
  2273. return $.extend({}, flags);
  2274. }
  2275. else {
  2276. var ret = {};
  2277. $.each(this.settings.default_group_flags, function(key, value) {
  2278. if (flags[key] !== value) {
  2279. ret[key] = flags[key];
  2280. }
  2281. });
  2282. return ret;
  2283. }
  2284. };
  2285. /**
  2286. * Translate a label either by looking in the `lang` object or in itself if it's an object where keys are language codes
  2287. * @param {string} [category]
  2288. * @param {string|object} key
  2289. * @returns {string}
  2290. * @fires QueryBuilder.changer:translate
  2291. */
  2292. QueryBuilder.prototype.translate = function(category, key) {
  2293. if (!key) {
  2294. key = category;
  2295. category = undefined;
  2296. }
  2297. var translation;
  2298. if (typeof key === 'object') {
  2299. translation = key[this.settings.lang_code] || key['en'];
  2300. }
  2301. else {
  2302. translation = (category ? this.lang[category] : this.lang)[key] || key;
  2303. }
  2304. /**
  2305. * Modifies the translated label
  2306. * @event changer:translate
  2307. * @memberof QueryBuilder
  2308. * @param {string} translation
  2309. * @param {string|object} key
  2310. * @param {string} [category]
  2311. * @returns {string}
  2312. */
  2313. return this.change('translate', translation, key, category);
  2314. };
  2315. /**
  2316. * Returns a validation message
  2317. * @param {object} validation
  2318. * @param {string} type
  2319. * @param {string} def
  2320. * @returns {string}
  2321. * @private
  2322. */
  2323. QueryBuilder.prototype.getValidationMessage = function(validation, type, def) {
  2324. return validation.messages && validation.messages[type] || def;
  2325. };
  2326. QueryBuilder.templates.group = '\
  2327. <div id="{{= it.group_id }}" class="rules-group-container"> \
  2328. <div class="rules-group-header"> \
  2329. <div class="btn-group pull-right group-actions"> \
  2330. <button type="button" class="btn btn-xs btn-success" data-add="rule"> \
  2331. <i class="{{= it.icons.add_rule }}"></i> {{= it.translate("add_rule") }} \
  2332. </button> \
  2333. {{? it.settings.allow_groups===-1 || it.settings.allow_groups>=it.level }} \
  2334. <button type="button" class="btn btn-xs btn-success" data-add="group"> \
  2335. <i class="{{= it.icons.add_group }}"></i> {{= it.translate("add_group") }} \
  2336. </button> \
  2337. {{?}} \
  2338. {{? it.level>1 }} \
  2339. <button type="button" class="btn btn-xs btn-danger" data-delete="group"> \
  2340. <i class="{{= it.icons.remove_group }}"></i> {{= it.translate("delete_group") }} \
  2341. </button> \
  2342. {{?}} \
  2343. </div> \
  2344. <div class="btn-group group-conditions"> \
  2345. {{~ it.conditions: condition }} \
  2346. <label class="btn btn-xs btn-primary"> \
  2347. <input type="radio" name="{{= it.group_id }}_cond" value="{{= condition }}"> {{= it.translate("conditions", condition) }} \
  2348. </label> \
  2349. {{~}} \
  2350. </div> \
  2351. {{? it.settings.display_errors }} \
  2352. <div class="error-container"><i class="{{= it.icons.error }}"></i></div> \
  2353. {{?}} \
  2354. </div> \
  2355. <div class=rules-group-body> \
  2356. <div class=rules-list></div> \
  2357. </div> \
  2358. </div>';
  2359. QueryBuilder.templates.rule = '\
  2360. <div id="{{= it.rule_id }}" class="rule-container"> \
  2361. <div class="rule-header"> \
  2362. <div class="btn-group pull-right rule-actions"> \
  2363. <button type="button" class="btn btn-xs btn-danger" data-delete="rule"> \
  2364. <i class="{{= it.icons.remove_rule }}"></i> {{= it.translate("delete_rule") }} \
  2365. </button> \
  2366. </div> \
  2367. </div> \
  2368. {{? it.settings.display_errors }} \
  2369. <div class="error-container"><i class="{{= it.icons.error }}"></i></div> \
  2370. {{?}} \
  2371. <div class="rule-filter-container"></div> \
  2372. <div class="rule-operator-container"></div> \
  2373. <div class="rule-value-container"></div> \
  2374. </div>';
  2375. QueryBuilder.templates.filterSelect = '\
  2376. {{ var optgroup = null; }} \
  2377. <select class="form-control" name="{{= it.rule.id }}_filter"> \
  2378. {{? it.settings.display_empty_filter }} \
  2379. <option value="-1">{{= it.settings.select_placeholder }}</option> \
  2380. {{?}} \
  2381. {{~ it.filters: filter }} \
  2382. {{? optgroup !== filter.optgroup }} \
  2383. {{? optgroup !== null }}</optgroup>{{?}} \
  2384. {{? (optgroup = filter.optgroup) !== null }} \
  2385. <optgroup label="{{= it.translate(it.settings.optgroups[optgroup]) }}"> \
  2386. {{?}} \
  2387. {{?}} \
  2388. <option value="{{= filter.id }}" {{? filter.icon}}data-icon="{{= filter.icon}}"{{?}}>{{= it.translate(filter.label) }}</option> \
  2389. {{~}} \
  2390. {{? optgroup !== null }}</optgroup>{{?}} \
  2391. </select>';
  2392. QueryBuilder.templates.operatorSelect = '\
  2393. {{? it.operators.length === 1 }} \
  2394. <span> \
  2395. {{= it.translate("operators", it.operators[0].type) }} \
  2396. </span> \
  2397. {{?}} \
  2398. {{ var optgroup = null; }} \
  2399. <select class="form-control {{? it.operators.length === 1 }}hide{{?}}" name="{{= it.rule.id }}_operator"> \
  2400. {{~ it.operators: operator }} \
  2401. {{? optgroup !== operator.optgroup }} \
  2402. {{? optgroup !== null }}</optgroup>{{?}} \
  2403. {{? (optgroup = operator.optgroup) !== null }} \
  2404. <optgroup label="{{= it.translate(it.settings.optgroups[optgroup]) }}"> \
  2405. {{?}} \
  2406. {{?}} \
  2407. <option value="{{= operator.type }}" {{? operator.icon}}data-icon="{{= operator.icon}}"{{?}}>{{= it.translate("operators", operator.type) }}</option> \
  2408. {{~}} \
  2409. {{? optgroup !== null }}</optgroup>{{?}} \
  2410. </select>';
  2411. QueryBuilder.templates.ruleValueSelect = '\
  2412. {{ var optgroup = null; }} \
  2413. <select class="form-control" name="{{= it.name }}" {{? it.rule.filter.multiple }}multiple{{?}}> \
  2414. {{? it.rule.filter.placeholder }} \
  2415. <option value="{{= it.rule.filter.placeholder_value }}" disabled selected>{{= it.rule.filter.placeholder }}</option> \
  2416. {{?}} \
  2417. {{~ it.rule.filter.values: entry }} \
  2418. {{? optgroup !== entry.optgroup }} \
  2419. {{? optgroup !== null }}</optgroup>{{?}} \
  2420. {{? (optgroup = entry.optgroup) !== null }} \
  2421. <optgroup label="{{= it.translate(it.settings.optgroups[optgroup]) }}"> \
  2422. {{?}} \
  2423. {{?}} \
  2424. <option value="{{= entry.value }}">{{= entry.label }}</option> \
  2425. {{~}} \
  2426. {{? optgroup !== null }}</optgroup>{{?}} \
  2427. </select>';
  2428. /**
  2429. * Returns group's HTML
  2430. * @param {string} group_id
  2431. * @param {int} level
  2432. * @returns {string}
  2433. * @fires QueryBuilder.changer:getGroupTemplate
  2434. * @private
  2435. */
  2436. QueryBuilder.prototype.getGroupTemplate = function(group_id, level) {
  2437. var h = this.templates.group({
  2438. builder: this,
  2439. group_id: group_id,
  2440. level: level,
  2441. conditions: this.settings.conditions,
  2442. icons: this.icons,
  2443. settings: this.settings,
  2444. translate: this.translate.bind(this)
  2445. });
  2446. /**
  2447. * Modifies the raw HTML of a group
  2448. * @event changer:getGroupTemplate
  2449. * @memberof QueryBuilder
  2450. * @param {string} html
  2451. * @param {int} level
  2452. * @returns {string}
  2453. */
  2454. return this.change('getGroupTemplate', h, level);
  2455. };
  2456. /**
  2457. * Returns rule's HTML
  2458. * @param {string} rule_id
  2459. * @returns {string}
  2460. * @fires QueryBuilder.changer:getRuleTemplate
  2461. * @private
  2462. */
  2463. QueryBuilder.prototype.getRuleTemplate = function(rule_id) {
  2464. var h = this.templates.rule({
  2465. builder: this,
  2466. rule_id: rule_id,
  2467. icons: this.icons,
  2468. settings: this.settings,
  2469. translate: this.translate.bind(this)
  2470. });
  2471. /**
  2472. * Modifies the raw HTML of a rule
  2473. * @event changer:getRuleTemplate
  2474. * @memberof QueryBuilder
  2475. * @param {string} html
  2476. * @returns {string}
  2477. */
  2478. return this.change('getRuleTemplate', h);
  2479. };
  2480. /**
  2481. * Returns rule's filter HTML
  2482. * @param {Rule} rule
  2483. * @param {object[]} filters
  2484. * @returns {string}
  2485. * @fires QueryBuilder.changer:getRuleFilterTemplate
  2486. * @private
  2487. */
  2488. QueryBuilder.prototype.getRuleFilterSelect = function(rule, filters) {
  2489. var h = this.templates.filterSelect({
  2490. builder: this,
  2491. rule: rule,
  2492. filters: filters,
  2493. icons: this.icons,
  2494. settings: this.settings,
  2495. translate: this.translate.bind(this)
  2496. });
  2497. /**
  2498. * Modifies the raw HTML of the rule's filter dropdown
  2499. * @event changer:getRuleFilterSelect
  2500. * @memberof QueryBuilder
  2501. * @param {string} html
  2502. * @param {Rule} rule
  2503. * @param {QueryBuilder.Filter[]} filters
  2504. * @returns {string}
  2505. */
  2506. return this.change('getRuleFilterSelect', h, rule, filters);
  2507. };
  2508. /**
  2509. * Returns rule's operator HTML
  2510. * @param {Rule} rule
  2511. * @param {object[]} operators
  2512. * @returns {string}
  2513. * @fires QueryBuilder.changer:getRuleOperatorTemplate
  2514. * @private
  2515. */
  2516. QueryBuilder.prototype.getRuleOperatorSelect = function(rule, operators) {
  2517. var h = this.templates.operatorSelect({
  2518. builder: this,
  2519. rule: rule,
  2520. operators: operators,
  2521. icons: this.icons,
  2522. settings: this.settings,
  2523. translate: this.translate.bind(this)
  2524. });
  2525. /**
  2526. * Modifies the raw HTML of the rule's operator dropdown
  2527. * @event changer:getRuleOperatorSelect
  2528. * @memberof QueryBuilder
  2529. * @param {string} html
  2530. * @param {Rule} rule
  2531. * @param {QueryBuilder.Operator[]} operators
  2532. * @returns {string}
  2533. */
  2534. return this.change('getRuleOperatorSelect', h, rule, operators);
  2535. };
  2536. /**
  2537. * Returns the rule's value select HTML
  2538. * @param {string} name
  2539. * @param {Rule} rule
  2540. * @returns {string}
  2541. * @fires QueryBuilder.changer:getRuleValueSelect
  2542. * @private
  2543. */
  2544. QueryBuilder.prototype.getRuleValueSelect = function(name, rule) {
  2545. var h = this.templates.ruleValueSelect({
  2546. builder: this,
  2547. name: name,
  2548. rule: rule,
  2549. icons: this.icons,
  2550. settings: this.settings,
  2551. translate: this.translate.bind(this)
  2552. });
  2553. /**
  2554. * Modifies the raw HTML of the rule's value dropdown (in case of a "select filter)
  2555. * @event changer:getRuleValueSelect
  2556. * @memberof QueryBuilder
  2557. * @param {string} html
  2558. * @param [string} name
  2559. * @param {Rule} rule
  2560. * @returns {string}
  2561. */
  2562. return this.change('getRuleValueSelect', h, name, rule);
  2563. };
  2564. /**
  2565. * Returns the rule's value HTML
  2566. * @param {Rule} rule
  2567. * @param {int} value_id
  2568. * @returns {string}
  2569. * @fires QueryBuilder.changer:getRuleInput
  2570. * @private
  2571. */
  2572. QueryBuilder.prototype.getRuleInput = function(rule, value_id) {
  2573. var filter = rule.filter;
  2574. var validation = rule.filter.validation || {};
  2575. var name = rule.id + '_value_' + value_id;
  2576. var c = filter.vertical ? ' class=block' : '';
  2577. var h = '';
  2578. if (typeof filter.input == 'function') {
  2579. h = filter.input.call(this, rule, name);
  2580. }
  2581. else {
  2582. switch (filter.input) {
  2583. case 'radio':
  2584. case 'checkbox':
  2585. Utils.iterateOptions(filter.values, function(key, val) {
  2586. h += '<label' + c + '><input type="' + filter.input + '" name="' + name + '" value="' + key + '"> ' + val + '</label> ';
  2587. });
  2588. break;
  2589. case 'select':
  2590. h = this.getRuleValueSelect(name, rule);
  2591. break;
  2592. case 'textarea':
  2593. h += '<textarea class="form-control" name="' + name + '"';
  2594. if (filter.size) h += ' cols="' + filter.size + '"';
  2595. if (filter.rows) h += ' rows="' + filter.rows + '"';
  2596. if (validation.min !== undefined) h += ' minlength="' + validation.min + '"';
  2597. if (validation.max !== undefined) h += ' maxlength="' + validation.max + '"';
  2598. if (filter.placeholder) h += ' placeholder="' + filter.placeholder + '"';
  2599. h += '></textarea>';
  2600. break;
  2601. case 'number':
  2602. h += '<input class="form-control" type="number" name="' + name + '"';
  2603. if (validation.step !== undefined) h += ' step="' + validation.step + '"';
  2604. if (validation.min !== undefined) h += ' min="' + validation.min + '"';
  2605. if (validation.max !== undefined) h += ' max="' + validation.max + '"';
  2606. if (filter.placeholder) h += ' placeholder="' + filter.placeholder + '"';
  2607. if (filter.size) h += ' size="' + filter.size + '"';
  2608. h += '>';
  2609. break;
  2610. default:
  2611. h += '<input class="form-control" type="text" name="' + name + '"';
  2612. if (filter.placeholder) h += ' placeholder="' + filter.placeholder + '"';
  2613. if (filter.type === 'string' && validation.min !== undefined) h += ' minlength="' + validation.min + '"';
  2614. if (filter.type === 'string' && validation.max !== undefined) h += ' maxlength="' + validation.max + '"';
  2615. if (filter.size) h += ' size="' + filter.size + '"';
  2616. h += '>';
  2617. }
  2618. }
  2619. /**
  2620. * Modifies the raw HTML of the rule's input
  2621. * @event changer:getRuleInput
  2622. * @memberof QueryBuilder
  2623. * @param {string} html
  2624. * @param {Rule} rule
  2625. * @param {string} name - the name that the input must have
  2626. * @returns {string}
  2627. */
  2628. return this.change('getRuleInput', h, rule, name);
  2629. };
  2630. /**
  2631. * @namespace
  2632. */
  2633. var Utils = {};
  2634. /**
  2635. * @member {object}
  2636. * @memberof QueryBuilder
  2637. * @see Utils
  2638. */
  2639. QueryBuilder.utils = Utils;
  2640. /**
  2641. * @callback Utils#OptionsIteratee
  2642. * @param {string} key
  2643. * @param {string} value
  2644. * @param {string} [optgroup]
  2645. */
  2646. /**
  2647. * Iterates over radio/checkbox/selection options, it accept four formats
  2648. *
  2649. * @example
  2650. * // array of values
  2651. * options = ['one', 'two', 'three']
  2652. * @example
  2653. * // simple key-value map
  2654. * options = {1: 'one', 2: 'two', 3: 'three'}
  2655. * @example
  2656. * // array of 1-element maps
  2657. * options = [{1: 'one'}, {2: 'two'}, {3: 'three'}]
  2658. * @example
  2659. * // array of elements
  2660. * options = [{value: 1, label: 'one', optgroup: 'group'}, {value: 2, label: 'two'}]
  2661. *
  2662. * @param {object|array} options
  2663. * @param {Utils#OptionsIteratee} tpl
  2664. */
  2665. Utils.iterateOptions = function(options, tpl) {
  2666. if (options) {
  2667. if ($.isArray(options)) {
  2668. options.forEach(function(entry) {
  2669. if ($.isPlainObject(entry)) {
  2670. // array of elements
  2671. if ('value' in entry) {
  2672. tpl(entry.value, entry.label || entry.value, entry.optgroup);
  2673. }
  2674. // array of one-element maps
  2675. else {
  2676. $.each(entry, function(key, val) {
  2677. tpl(key, val);
  2678. return false; // break after first entry
  2679. });
  2680. }
  2681. }
  2682. // array of values
  2683. else {
  2684. tpl(entry, entry);
  2685. }
  2686. });
  2687. }
  2688. // unordered map
  2689. else {
  2690. $.each(options, function(key, val) {
  2691. tpl(key, val);
  2692. });
  2693. }
  2694. }
  2695. };
  2696. /**
  2697. * Replaces {0}, {1}, ... in a string
  2698. * @param {string} str
  2699. * @param {...*} args
  2700. * @returns {string}
  2701. */
  2702. Utils.fmt = function(str, args) {
  2703. if (!Array.isArray(args)) {
  2704. args = Array.prototype.slice.call(arguments, 1);
  2705. }
  2706. return str.replace(/{([0-9]+)}/g, function(m, i) {
  2707. return args[parseInt(i)];
  2708. });
  2709. };
  2710. /**
  2711. * Throws an Error object with custom name or logs an error
  2712. * @param {boolean} [doThrow=true]
  2713. * @param {string} type
  2714. * @param {string} message
  2715. * @param {...*} args
  2716. */
  2717. Utils.error = function() {
  2718. var i = 0;
  2719. var doThrow = typeof arguments[i] === 'boolean' ? arguments[i++] : true;
  2720. var type = arguments[i++];
  2721. var message = arguments[i++];
  2722. var args = Array.isArray(arguments[i]) ? arguments[i] : Array.prototype.slice.call(arguments, i);
  2723. if (doThrow) {
  2724. var err = new Error(Utils.fmt(message, args));
  2725. err.name = type + 'Error';
  2726. err.args = args;
  2727. throw err;
  2728. }
  2729. else {
  2730. console.error(type + 'Error: ' + Utils.fmt(message, args));
  2731. }
  2732. };
  2733. /**
  2734. * Changes the type of a value to int, float or bool
  2735. * @param {*} value
  2736. * @param {string} type - 'integer', 'double', 'boolean' or anything else (passthrough)
  2737. * @returns {*}
  2738. */
  2739. Utils.changeType = function(value, type) {
  2740. if (value === '' || value === undefined) {
  2741. return undefined;
  2742. }
  2743. switch (type) {
  2744. // @formatter:off
  2745. case 'integer':
  2746. if (typeof value === 'string' && !/^-?\d+$/.test(value)) {
  2747. return value;
  2748. }
  2749. return parseInt(value);
  2750. case 'double':
  2751. if (typeof value === 'string' && !/^-?\d+\.?\d*$/.test(value)) {
  2752. return value;
  2753. }
  2754. return parseFloat(value);
  2755. case 'boolean':
  2756. if (typeof value === 'string' && !/^(0|1|true|false){1}$/i.test(value)) {
  2757. return value;
  2758. }
  2759. return value === true || value === 1 || value.toLowerCase() === 'true' || value === '1';
  2760. default: return value;
  2761. // @formatter:on
  2762. }
  2763. };
  2764. /**
  2765. * Escapes a string like PHP's mysql_real_escape_string does
  2766. * @param {string} value
  2767. * @returns {string}
  2768. */
  2769. Utils.escapeString = function(value) {
  2770. if (typeof value != 'string') {
  2771. return value;
  2772. }
  2773. return value
  2774. .replace(/[\0\n\r\b\\\'\"]/g, function(s) {
  2775. switch (s) {
  2776. // @formatter:off
  2777. case '\0': return '\\0';
  2778. case '\n': return '\\n';
  2779. case '\r': return '\\r';
  2780. case '\b': return '\\b';
  2781. default: return '\\' + s;
  2782. // @formatter:off
  2783. }
  2784. })
  2785. // uglify compliant
  2786. .replace(/\t/g, '\\t')
  2787. .replace(/\x1a/g, '\\Z');
  2788. };
  2789. /**
  2790. * Escapes a string for use in regex
  2791. * @param {string} str
  2792. * @returns {string}
  2793. */
  2794. Utils.escapeRegExp = function(str) {
  2795. return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&');
  2796. };
  2797. /**
  2798. * Escapes a string for use in HTML element id
  2799. * @param {string} str
  2800. * @returns {string}
  2801. */
  2802. Utils.escapeElementId = function(str) {
  2803. // Regex based on that suggested by:
  2804. // https://learn.jquery.com/using-jquery-core/faq/how-do-i-select-an-element-by-an-id-that-has-characters-used-in-css-notation/
  2805. // - escapes : . [ ] ,
  2806. // - avoids escaping already escaped values
  2807. return (str) ? str.replace(/(\\)?([:.\[\],])/g,
  2808. function( $0, $1, $2 ) { return $1 ? $0 : '\\' + $2; }) : str;
  2809. };
  2810. /**
  2811. * Sorts objects by grouping them by `key`, preserving initial order when possible
  2812. * @param {object[]} items
  2813. * @param {string} key
  2814. * @returns {object[]}
  2815. */
  2816. Utils.groupSort = function(items, key) {
  2817. var optgroups = [];
  2818. var newItems = [];
  2819. items.forEach(function(item) {
  2820. var idx;
  2821. if (item[key]) {
  2822. idx = optgroups.lastIndexOf(item[key]);
  2823. if (idx == -1) {
  2824. idx = optgroups.length;
  2825. }
  2826. else {
  2827. idx++;
  2828. }
  2829. }
  2830. else {
  2831. idx = optgroups.length;
  2832. }
  2833. optgroups.splice(idx, 0, item[key]);
  2834. newItems.splice(idx, 0, item);
  2835. });
  2836. return newItems;
  2837. };
  2838. /**
  2839. * Defines properties on an Node prototype with getter and setter.<br>
  2840. * Update events are emitted in the setter through root Model (if any).<br>
  2841. * The object must have a `__` object, non enumerable property to store values.
  2842. * @param {function} obj
  2843. * @param {string[]} fields
  2844. */
  2845. Utils.defineModelProperties = function(obj, fields) {
  2846. fields.forEach(function(field) {
  2847. Object.defineProperty(obj.prototype, field, {
  2848. enumerable: true,
  2849. get: function() {
  2850. return this.__[field];
  2851. },
  2852. set: function(value) {
  2853. var previousValue = (this.__[field] !== null && typeof this.__[field] == 'object') ?
  2854. $.extend({}, this.__[field]) :
  2855. this.__[field];
  2856. this.__[field] = value;
  2857. if (this.model !== null) {
  2858. /**
  2859. * After a value of the model changed
  2860. * @event model:update
  2861. * @memberof Model
  2862. * @param {Node} node
  2863. * @param {string} field
  2864. * @param {*} value
  2865. * @param {*} previousValue
  2866. */
  2867. this.model.trigger('update', this, field, value, previousValue);
  2868. }
  2869. }
  2870. });
  2871. });
  2872. };
  2873. /**
  2874. * Main object storing data model and emitting model events
  2875. * @constructor
  2876. */
  2877. function Model() {
  2878. /**
  2879. * @member {Group}
  2880. * @readonly
  2881. */
  2882. this.root = null;
  2883. /**
  2884. * Base for event emitting
  2885. * @member {jQuery}
  2886. * @readonly
  2887. * @private
  2888. */
  2889. this.$ = $(this);
  2890. }
  2891. $.extend(Model.prototype, /** @lends Model.prototype */ {
  2892. /**
  2893. * Triggers an event on the model
  2894. * @param {string} type
  2895. * @returns {$.Event}
  2896. */
  2897. trigger: function(type) {
  2898. var event = new $.Event(type);
  2899. this.$.triggerHandler(event, Array.prototype.slice.call(arguments, 1));
  2900. return event;
  2901. },
  2902. /**
  2903. * Attaches an event listener on the model
  2904. * @param {string} type
  2905. * @param {function} cb
  2906. * @returns {Model}
  2907. */
  2908. on: function() {
  2909. this.$.on.apply(this.$, Array.prototype.slice.call(arguments));
  2910. return this;
  2911. },
  2912. /**
  2913. * Removes an event listener from the model
  2914. * @param {string} type
  2915. * @param {function} [cb]
  2916. * @returns {Model}
  2917. */
  2918. off: function() {
  2919. this.$.off.apply(this.$, Array.prototype.slice.call(arguments));
  2920. return this;
  2921. },
  2922. /**
  2923. * Attaches an event listener called once on the model
  2924. * @param {string} type
  2925. * @param {function} cb
  2926. * @returns {Model}
  2927. */
  2928. once: function() {
  2929. this.$.one.apply(this.$, Array.prototype.slice.call(arguments));
  2930. return this;
  2931. }
  2932. });
  2933. /**
  2934. * Root abstract object
  2935. * @constructor
  2936. * @param {Node} [parent]
  2937. * @param {jQuery} $el
  2938. */
  2939. var Node = function(parent, $el) {
  2940. if (!(this instanceof Node)) {
  2941. return new Node(parent, $el);
  2942. }
  2943. Object.defineProperty(this, '__', { value: {} });
  2944. $el.data('queryBuilderModel', this);
  2945. /**
  2946. * @name level
  2947. * @member {int}
  2948. * @memberof Node
  2949. * @instance
  2950. * @readonly
  2951. */
  2952. this.__.level = 1;
  2953. /**
  2954. * @name error
  2955. * @member {string}
  2956. * @memberof Node
  2957. * @instance
  2958. */
  2959. this.__.error = null;
  2960. /**
  2961. * @name flags
  2962. * @member {object}
  2963. * @memberof Node
  2964. * @instance
  2965. * @readonly
  2966. */
  2967. this.__.flags = {};
  2968. /**
  2969. * @name data
  2970. * @member {object}
  2971. * @memberof Node
  2972. * @instance
  2973. */
  2974. this.__.data = undefined;
  2975. /**
  2976. * @member {jQuery}
  2977. * @readonly
  2978. */
  2979. this.$el = $el;
  2980. /**
  2981. * @member {string}
  2982. * @readonly
  2983. */
  2984. this.id = $el[0].id;
  2985. /**
  2986. * @member {Model}
  2987. * @readonly
  2988. */
  2989. this.model = null;
  2990. /**
  2991. * @member {Group}
  2992. * @readonly
  2993. */
  2994. this.parent = parent;
  2995. };
  2996. Utils.defineModelProperties(Node, ['level', 'error', 'data', 'flags']);
  2997. Object.defineProperty(Node.prototype, 'parent', {
  2998. enumerable: true,
  2999. get: function() {
  3000. return this.__.parent;
  3001. },
  3002. set: function(value) {
  3003. this.__.parent = value;
  3004. this.level = value === null ? 1 : value.level + 1;
  3005. this.model = value === null ? null : value.model;
  3006. }
  3007. });
  3008. /**
  3009. * Checks if this Node is the root
  3010. * @returns {boolean}
  3011. */
  3012. Node.prototype.isRoot = function() {
  3013. return (this.level === 1);
  3014. };
  3015. /**
  3016. * Returns the node position inside its parent
  3017. * @returns {int}
  3018. */
  3019. Node.prototype.getPos = function() {
  3020. if (this.isRoot()) {
  3021. return -1;
  3022. }
  3023. else {
  3024. return this.parent.getNodePos(this);
  3025. }
  3026. };
  3027. /**
  3028. * Deletes self
  3029. * @fires Model.model:drop
  3030. */
  3031. Node.prototype.drop = function() {
  3032. var model = this.model;
  3033. if (!!this.parent) {
  3034. this.parent.removeNode(this);
  3035. }
  3036. this.$el.removeData('queryBuilderModel');
  3037. if (model !== null) {
  3038. /**
  3039. * After a node of the model has been removed
  3040. * @event model:drop
  3041. * @memberof Model
  3042. * @param {Node} node
  3043. */
  3044. model.trigger('drop', this);
  3045. }
  3046. };
  3047. /**
  3048. * Moves itself after another Node
  3049. * @param {Node} target
  3050. * @fires Model.model:move
  3051. */
  3052. Node.prototype.moveAfter = function(target) {
  3053. if (!this.isRoot()) {
  3054. this.move(target.parent, target.getPos() + 1);
  3055. }
  3056. };
  3057. /**
  3058. * Moves itself at the beginning of parent or another Group
  3059. * @param {Group} [target]
  3060. * @fires Model.model:move
  3061. */
  3062. Node.prototype.moveAtBegin = function(target) {
  3063. if (!this.isRoot()) {
  3064. if (target === undefined) {
  3065. target = this.parent;
  3066. }
  3067. this.move(target, 0);
  3068. }
  3069. };
  3070. /**
  3071. * Moves itself at the end of parent or another Group
  3072. * @param {Group} [target]
  3073. * @fires Model.model:move
  3074. */
  3075. Node.prototype.moveAtEnd = function(target) {
  3076. if (!this.isRoot()) {
  3077. if (target === undefined) {
  3078. target = this.parent;
  3079. }
  3080. this.move(target, target.length() === 0 ? 0 : target.length() - 1);
  3081. }
  3082. };
  3083. /**
  3084. * Moves itself at specific position of Group
  3085. * @param {Group} target
  3086. * @param {int} index
  3087. * @fires Model.model:move
  3088. */
  3089. Node.prototype.move = function(target, index) {
  3090. if (!this.isRoot()) {
  3091. if (typeof target === 'number') {
  3092. index = target;
  3093. target = this.parent;
  3094. }
  3095. this.parent.removeNode(this);
  3096. target.insertNode(this, index, false);
  3097. if (this.model !== null) {
  3098. /**
  3099. * After a node of the model has been moved
  3100. * @event model:move
  3101. * @memberof Model
  3102. * @param {Node} node
  3103. * @param {Node} target
  3104. * @param {int} index
  3105. */
  3106. this.model.trigger('move', this, target, index);
  3107. }
  3108. }
  3109. };
  3110. /**
  3111. * Group object
  3112. * @constructor
  3113. * @extends Node
  3114. * @param {Group} [parent]
  3115. * @param {jQuery} $el
  3116. */
  3117. var Group = function(parent, $el) {
  3118. if (!(this instanceof Group)) {
  3119. return new Group(parent, $el);
  3120. }
  3121. Node.call(this, parent, $el);
  3122. /**
  3123. * @member {object[]}
  3124. * @readonly
  3125. */
  3126. this.rules = [];
  3127. /**
  3128. * @name condition
  3129. * @member {string}
  3130. * @memberof Group
  3131. * @instance
  3132. */
  3133. this.__.condition = null;
  3134. };
  3135. Group.prototype = Object.create(Node.prototype);
  3136. Group.prototype.constructor = Group;
  3137. Utils.defineModelProperties(Group, ['condition']);
  3138. /**
  3139. * Removes group's content
  3140. */
  3141. Group.prototype.empty = function() {
  3142. this.each('reverse', function(rule) {
  3143. rule.drop();
  3144. }, function(group) {
  3145. group.drop();
  3146. });
  3147. };
  3148. /**
  3149. * Deletes self
  3150. */
  3151. Group.prototype.drop = function() {
  3152. this.empty();
  3153. Node.prototype.drop.call(this);
  3154. };
  3155. /**
  3156. * Returns the number of children
  3157. * @returns {int}
  3158. */
  3159. Group.prototype.length = function() {
  3160. return this.rules.length;
  3161. };
  3162. /**
  3163. * Adds a Node at specified index
  3164. * @param {Node} node
  3165. * @param {int} [index=end]
  3166. * @param {boolean} [trigger=false] - fire 'add' event
  3167. * @returns {Node} the inserted node
  3168. * @fires Model.model:add
  3169. */
  3170. Group.prototype.insertNode = function(node, index, trigger) {
  3171. if (index === undefined) {
  3172. index = this.length();
  3173. }
  3174. this.rules.splice(index, 0, node);
  3175. node.parent = this;
  3176. if (trigger && this.model !== null) {
  3177. /**
  3178. * After a node of the model has been added
  3179. * @event model:add
  3180. * @memberof Model
  3181. * @param {Node} parent
  3182. * @param {Node} node
  3183. * @param {int} index
  3184. */
  3185. this.model.trigger('add', this, node, index);
  3186. }
  3187. return node;
  3188. };
  3189. /**
  3190. * Adds a new Group at specified index
  3191. * @param {jQuery} $el
  3192. * @param {int} [index=end]
  3193. * @returns {Group}
  3194. * @fires Model.model:add
  3195. */
  3196. Group.prototype.addGroup = function($el, index) {
  3197. return this.insertNode(new Group(this, $el), index, true);
  3198. };
  3199. /**
  3200. * Adds a new Rule at specified index
  3201. * @param {jQuery} $el
  3202. * @param {int} [index=end]
  3203. * @returns {Rule}
  3204. * @fires Model.model:add
  3205. */
  3206. Group.prototype.addRule = function($el, index) {
  3207. return this.insertNode(new Rule(this, $el), index, true);
  3208. };
  3209. /**
  3210. * Deletes a specific Node
  3211. * @param {Node} node
  3212. */
  3213. Group.prototype.removeNode = function(node) {
  3214. var index = this.getNodePos(node);
  3215. if (index !== -1) {
  3216. node.parent = null;
  3217. this.rules.splice(index, 1);
  3218. }
  3219. };
  3220. /**
  3221. * Returns the position of a child Node
  3222. * @param {Node} node
  3223. * @returns {int}
  3224. */
  3225. Group.prototype.getNodePos = function(node) {
  3226. return this.rules.indexOf(node);
  3227. };
  3228. /**
  3229. * @callback Model#GroupIteratee
  3230. * @param {Node} node
  3231. * @returns {boolean} stop the iteration
  3232. */
  3233. /**
  3234. * Iterate over all Nodes
  3235. * @param {boolean} [reverse=false] - iterate in reverse order, required if you delete nodes
  3236. * @param {Model#GroupIteratee} cbRule - callback for Rules (can be `null` but not omitted)
  3237. * @param {Model#GroupIteratee} [cbGroup] - callback for Groups
  3238. * @param {object} [context] - context for callbacks
  3239. * @returns {boolean} if the iteration has been stopped by a callback
  3240. */
  3241. Group.prototype.each = function(reverse, cbRule, cbGroup, context) {
  3242. if (typeof reverse !== 'boolean' && typeof reverse !== 'string') {
  3243. context = cbGroup;
  3244. cbGroup = cbRule;
  3245. cbRule = reverse;
  3246. reverse = false;
  3247. }
  3248. context = context === undefined ? null : context;
  3249. var i = reverse ? this.rules.length - 1 : 0;
  3250. var l = reverse ? 0 : this.rules.length - 1;
  3251. var c = reverse ? -1 : 1;
  3252. var next = function() {
  3253. return reverse ? i >= l : i <= l;
  3254. };
  3255. var stop = false;
  3256. for (; next(); i += c) {
  3257. if (this.rules[i] instanceof Group) {
  3258. if (!!cbGroup) {
  3259. stop = cbGroup.call(context, this.rules[i]) === false;
  3260. }
  3261. }
  3262. else if (!!cbRule) {
  3263. stop = cbRule.call(context, this.rules[i]) === false;
  3264. }
  3265. if (stop) {
  3266. break;
  3267. }
  3268. }
  3269. return !stop;
  3270. };
  3271. /**
  3272. * Checks if the group contains a particular Node
  3273. * @param {Node} node
  3274. * @param {boolean} [recursive=false]
  3275. * @returns {boolean}
  3276. */
  3277. Group.prototype.contains = function(node, recursive) {
  3278. if (this.getNodePos(node) !== -1) {
  3279. return true;
  3280. }
  3281. else if (!recursive) {
  3282. return false;
  3283. }
  3284. else {
  3285. // the loop will return with false as soon as the Node is found
  3286. return !this.each(function() {
  3287. return true;
  3288. }, function(group) {
  3289. return !group.contains(node, true);
  3290. });
  3291. }
  3292. };
  3293. /**
  3294. * Rule object
  3295. * @constructor
  3296. * @extends Node
  3297. * @param {Group} parent
  3298. * @param {jQuery} $el
  3299. */
  3300. var Rule = function(parent, $el) {
  3301. if (!(this instanceof Rule)) {
  3302. return new Rule(parent, $el);
  3303. }
  3304. Node.call(this, parent, $el);
  3305. this._updating_value = false;
  3306. this._updating_input = false;
  3307. /**
  3308. * @name filter
  3309. * @member {QueryBuilder.Filter}
  3310. * @memberof Rule
  3311. * @instance
  3312. */
  3313. this.__.filter = null;
  3314. /**
  3315. * @name operator
  3316. * @member {QueryBuilder.Operator}
  3317. * @memberof Rule
  3318. * @instance
  3319. */
  3320. this.__.operator = null;
  3321. /**
  3322. * @name value
  3323. * @member {*}
  3324. * @memberof Rule
  3325. * @instance
  3326. */
  3327. this.__.value = undefined;
  3328. };
  3329. Rule.prototype = Object.create(Node.prototype);
  3330. Rule.prototype.constructor = Rule;
  3331. Utils.defineModelProperties(Rule, ['filter', 'operator', 'value']);
  3332. /**
  3333. * Checks if this Node is the root
  3334. * @returns {boolean} always false
  3335. */
  3336. Rule.prototype.isRoot = function() {
  3337. return false;
  3338. };
  3339. /**
  3340. * @member {function}
  3341. * @memberof QueryBuilder
  3342. * @see Group
  3343. */
  3344. QueryBuilder.Group = Group;
  3345. /**
  3346. * @member {function}
  3347. * @memberof QueryBuilder
  3348. * @see Rule
  3349. */
  3350. QueryBuilder.Rule = Rule;
  3351. /**
  3352. * The {@link http://learn.jquery.com/plugins/|jQuery Plugins} namespace
  3353. * @external "jQuery.fn"
  3354. */
  3355. /**
  3356. * Instanciates or accesses the {@link QueryBuilder} on an element
  3357. * @function
  3358. * @memberof external:"jQuery.fn"
  3359. * @param {*} option - initial configuration or method name
  3360. * @param {...*} args - method arguments
  3361. *
  3362. * @example
  3363. * $('#builder').queryBuilder({ /** configuration object *\/ });
  3364. * @example
  3365. * $('#builder').queryBuilder('methodName', methodParam1, methodParam2);
  3366. */
  3367. $.fn.queryBuilder = function(option) {
  3368. if (this.length === 0) {
  3369. Utils.error('Config', 'No target defined');
  3370. }
  3371. if (this.length > 1) {
  3372. Utils.error('Config', 'Unable to initialize on multiple target');
  3373. }
  3374. var data = this.data('queryBuilder');
  3375. var options = (typeof option == 'object' && option) || {};
  3376. if (!data && option == 'destroy') {
  3377. return this;
  3378. }
  3379. if (!data) {
  3380. var builder = new QueryBuilder(this, options);
  3381. this.data('queryBuilder', builder);
  3382. builder.init(options.rules);
  3383. }
  3384. if (typeof option == 'string') {
  3385. return data[option].apply(data, Array.prototype.slice.call(arguments, 1));
  3386. }
  3387. return this;
  3388. };
  3389. /**
  3390. * @function
  3391. * @memberof external:"jQuery.fn"
  3392. * @see QueryBuilder
  3393. */
  3394. $.fn.queryBuilder.constructor = QueryBuilder;
  3395. /**
  3396. * @function
  3397. * @memberof external:"jQuery.fn"
  3398. * @see QueryBuilder.defaults
  3399. */
  3400. $.fn.queryBuilder.defaults = QueryBuilder.defaults;
  3401. /**
  3402. * @function
  3403. * @memberof external:"jQuery.fn"
  3404. * @see QueryBuilder.defaults
  3405. */
  3406. $.fn.queryBuilder.extend = QueryBuilder.extend;
  3407. /**
  3408. * @function
  3409. * @memberof external:"jQuery.fn"
  3410. * @see QueryBuilder.define
  3411. */
  3412. $.fn.queryBuilder.define = QueryBuilder.define;
  3413. /**
  3414. * @function
  3415. * @memberof external:"jQuery.fn"
  3416. * @see QueryBuilder.regional
  3417. */
  3418. $.fn.queryBuilder.regional = QueryBuilder.regional;
  3419. /**
  3420. * @class BtCheckbox
  3421. * @memberof module:plugins
  3422. * @description Applies Awesome Bootstrap Checkbox for checkbox and radio inputs.
  3423. * @param {object} [options]
  3424. * @param {string} [options.font='glyphicons']
  3425. * @param {string} [options.color='default']
  3426. */
  3427. QueryBuilder.define('bt-checkbox', function(options) {
  3428. if (options.font == 'glyphicons') {
  3429. this.$el.addClass('bt-checkbox-glyphicons');
  3430. }
  3431. this.on('getRuleInput.filter', function(h, rule, name) {
  3432. var filter = rule.filter;
  3433. if ((filter.input === 'radio' || filter.input === 'checkbox') && !filter.plugin) {
  3434. h.value = '';
  3435. if (!filter.colors) {
  3436. filter.colors = {};
  3437. }
  3438. if (filter.color) {
  3439. filter.colors._def_ = filter.color;
  3440. }
  3441. var style = filter.vertical ? ' style="display:block"' : '';
  3442. var i = 0;
  3443. Utils.iterateOptions(filter.values, function(key, val) {
  3444. var color = filter.colors[key] || filter.colors._def_ || options.color;
  3445. var id = name + '_' + (i++);
  3446. h.value+= '\
  3447. <div' + style + ' class="' + filter.input + ' ' + filter.input + '-' + color + '"> \
  3448. <input type="' + filter.input + '" name="' + name + '" id="' + id + '" value="' + key + '"> \
  3449. <label for="' + id + '">' + val + '</label> \
  3450. </div>';
  3451. });
  3452. }
  3453. });
  3454. }, {
  3455. font: 'glyphicons',
  3456. color: 'default'
  3457. });
  3458. /**
  3459. * @class BtSelectpicker
  3460. * @memberof module:plugins
  3461. * @descriptioon Applies Bootstrap Select on filters and operators combo-boxes.
  3462. * @param {object} [options]
  3463. * @param {string} [options.container='body']
  3464. * @param {string} [options.style='btn-inverse btn-xs']
  3465. * @param {int|string} [options.width='auto']
  3466. * @param {boolean} [options.showIcon=false]
  3467. * @throws MissingLibraryError
  3468. */
  3469. QueryBuilder.define('bt-selectpicker', function(options) {
  3470. if (!$.fn.selectpicker || !$.fn.selectpicker.Constructor) {
  3471. Utils.error('MissingLibrary', 'Bootstrap Select is required to use "bt-selectpicker" plugin. Get it here: http://silviomoreto.github.io/bootstrap-select');
  3472. }
  3473. var Selectors = QueryBuilder.selectors;
  3474. // init selectpicker
  3475. this.on('afterCreateRuleFilters', function(e, rule) {
  3476. rule.$el.find(Selectors.rule_filter).removeClass('form-control').selectpicker(options);
  3477. });
  3478. this.on('afterCreateRuleOperators', function(e, rule) {
  3479. rule.$el.find(Selectors.rule_operator).removeClass('form-control').selectpicker(options);
  3480. });
  3481. // update selectpicker on change
  3482. this.on('afterUpdateRuleFilter', function(e, rule) {
  3483. rule.$el.find(Selectors.rule_filter).selectpicker('render');
  3484. });
  3485. this.on('afterUpdateRuleOperator', function(e, rule) {
  3486. rule.$el.find(Selectors.rule_operator).selectpicker('render');
  3487. });
  3488. this.on('beforeDeleteRule', function(e, rule) {
  3489. rule.$el.find(Selectors.rule_filter).selectpicker('destroy');
  3490. rule.$el.find(Selectors.rule_operator).selectpicker('destroy');
  3491. });
  3492. }, {
  3493. container: 'body',
  3494. style: 'btn-inverse btn-xs',
  3495. width: 'auto',
  3496. showIcon: false
  3497. });
  3498. /**
  3499. * @class BtTooltipErrors
  3500. * @memberof module:plugins
  3501. * @description Applies Bootstrap Tooltips on validation error messages.
  3502. * @param {object} [options]
  3503. * @param {string} [options.placement='right']
  3504. * @throws MissingLibraryError
  3505. */
  3506. QueryBuilder.define('bt-tooltip-errors', function(options) {
  3507. if (!$.fn.tooltip || !$.fn.tooltip.Constructor || !$.fn.tooltip.Constructor.prototype.fixTitle) {
  3508. Utils.error('MissingLibrary', 'Bootstrap Tooltip is required to use "bt-tooltip-errors" plugin. Get it here: http://getbootstrap.com');
  3509. }
  3510. var self = this;
  3511. // add BT Tooltip data
  3512. this.on('getRuleTemplate.filter getGroupTemplate.filter', function(h) {
  3513. var $h = $(h.value);
  3514. $h.find(QueryBuilder.selectors.error_container).attr('data-toggle', 'tooltip');
  3515. h.value = $h.prop('outerHTML');
  3516. });
  3517. // init/refresh tooltip when title changes
  3518. this.model.on('update', function(e, node, field) {
  3519. if (field == 'error' && self.settings.display_errors) {
  3520. node.$el.find(QueryBuilder.selectors.error_container).eq(0)
  3521. .tooltip(options)
  3522. .tooltip('hide')
  3523. .tooltip('fixTitle');
  3524. }
  3525. });
  3526. }, {
  3527. placement: 'right'
  3528. });
  3529. /**
  3530. * @class ChangeFilters
  3531. * @memberof module:plugins
  3532. * @description Allows to change available filters after plugin initialization.
  3533. */
  3534. QueryBuilder.extend(/** @lends module:plugins.ChangeFilters.prototype */ {
  3535. /**
  3536. * Change the filters of the builder
  3537. * @param {boolean} [deleteOrphans=false] - delete rules using old filters
  3538. * @param {QueryBuilder[]} filters
  3539. * @fires module:plugins.ChangeFilters.changer:setFilters
  3540. * @fires module:plugins.ChangeFilters.afterSetFilters
  3541. * @throws ChangeFilterError
  3542. */
  3543. setFilters: function(deleteOrphans, filters) {
  3544. var self = this;
  3545. if (filters === undefined) {
  3546. filters = deleteOrphans;
  3547. deleteOrphans = false;
  3548. }
  3549. filters = this.checkFilters(filters);
  3550. /**
  3551. * Modifies the filters before {@link module:plugins.ChangeFilters.setFilters} method
  3552. * @event changer:setFilters
  3553. * @memberof module:plugins.ChangeFilters
  3554. * @param {QueryBuilder.Filter[]} filters
  3555. * @returns {QueryBuilder.Filter[]}
  3556. */
  3557. filters = this.change('setFilters', filters);
  3558. var filtersIds = filters.map(function(filter) {
  3559. return filter.id;
  3560. });
  3561. // check for orphans
  3562. if (!deleteOrphans) {
  3563. (function checkOrphans(node) {
  3564. node.each(
  3565. function(rule) {
  3566. if (rule.filter && filtersIds.indexOf(rule.filter.id) === -1) {
  3567. Utils.error('ChangeFilter', 'A rule is using filter "{0}"', rule.filter.id);
  3568. }
  3569. },
  3570. checkOrphans
  3571. );
  3572. }(this.model.root));
  3573. }
  3574. // replace filters
  3575. this.filters = filters;
  3576. // apply on existing DOM
  3577. (function updateBuilder(node) {
  3578. node.each(true,
  3579. function(rule) {
  3580. if (rule.filter && filtersIds.indexOf(rule.filter.id) === -1) {
  3581. rule.drop();
  3582. self.trigger('rulesChanged');
  3583. }
  3584. else {
  3585. self.createRuleFilters(rule);
  3586. rule.$el.find(QueryBuilder.selectors.rule_filter).val(rule.filter ? rule.filter.id : '-1');
  3587. self.trigger('afterUpdateRuleFilter', rule);
  3588. }
  3589. },
  3590. updateBuilder
  3591. );
  3592. }(this.model.root));
  3593. // update plugins
  3594. if (this.settings.plugins) {
  3595. if (this.settings.plugins['unique-filter']) {
  3596. this.updateDisabledFilters();
  3597. }
  3598. if (this.settings.plugins['bt-selectpicker']) {
  3599. this.$el.find(QueryBuilder.selectors.rule_filter).selectpicker('render');
  3600. }
  3601. }
  3602. // reset the default_filter if does not exist anymore
  3603. if (this.settings.default_filter) {
  3604. try {
  3605. this.getFilterById(this.settings.default_filter);
  3606. }
  3607. catch (e) {
  3608. this.settings.default_filter = null;
  3609. }
  3610. }
  3611. /**
  3612. * After {@link module:plugins.ChangeFilters.setFilters} method
  3613. * @event afterSetFilters
  3614. * @memberof module:plugins.ChangeFilters
  3615. * @param {QueryBuilder.Filter[]} filters
  3616. */
  3617. this.trigger('afterSetFilters', filters);
  3618. },
  3619. /**
  3620. * Adds a new filter to the builder
  3621. * @param {QueryBuilder.Filter|Filter[]} newFilters
  3622. * @param {int|string} [position=#end] - index or '#start' or '#end'
  3623. * @fires module:plugins.ChangeFilters.changer:setFilters
  3624. * @fires module:plugins.ChangeFilters.afterSetFilters
  3625. * @throws ChangeFilterError
  3626. */
  3627. addFilter: function(newFilters, position) {
  3628. if (position === undefined || position == '#end') {
  3629. position = this.filters.length;
  3630. }
  3631. else if (position == '#start') {
  3632. position = 0;
  3633. }
  3634. if (!$.isArray(newFilters)) {
  3635. newFilters = [newFilters];
  3636. }
  3637. var filters = $.extend(true, [], this.filters);
  3638. // numeric position
  3639. if (parseInt(position) == position) {
  3640. Array.prototype.splice.apply(filters, [position, 0].concat(newFilters));
  3641. }
  3642. else {
  3643. // after filter by its id
  3644. if (this.filters.some(function(filter, index) {
  3645. if (filter.id == position) {
  3646. position = index + 1;
  3647. return true;
  3648. }
  3649. })
  3650. ) {
  3651. Array.prototype.splice.apply(filters, [position, 0].concat(newFilters));
  3652. }
  3653. // defaults to end of list
  3654. else {
  3655. Array.prototype.push.apply(filters, newFilters);
  3656. }
  3657. }
  3658. this.setFilters(filters);
  3659. },
  3660. /**
  3661. * Removes a filter from the builder
  3662. * @param {string|string[]} filterIds
  3663. * @param {boolean} [deleteOrphans=false] delete rules using old filters
  3664. * @fires module:plugins.ChangeFilters.changer:setFilters
  3665. * @fires module:plugins.ChangeFilters.afterSetFilters
  3666. * @throws ChangeFilterError
  3667. */
  3668. removeFilter: function(filterIds, deleteOrphans) {
  3669. var filters = $.extend(true, [], this.filters);
  3670. if (typeof filterIds === 'string') {
  3671. filterIds = [filterIds];
  3672. }
  3673. filters = filters.filter(function(filter) {
  3674. return filterIds.indexOf(filter.id) === -1;
  3675. });
  3676. this.setFilters(deleteOrphans, filters);
  3677. }
  3678. });
  3679. /**
  3680. * @class ChosenSelectpicker
  3681. * @memberof module:plugins
  3682. * @descriptioon Applies chosen-js Select on filters and operators combo-boxes.
  3683. * @param {object} [options] Supports all the options for chosen
  3684. * @throws MissingLibraryError
  3685. */
  3686. QueryBuilder.define('chosen-selectpicker', function(options) {
  3687. if (!$.fn.chosen) {
  3688. Utils.error('MissingLibrary', 'chosen is required to use "chosen-selectpicker" plugin. Get it here: https://github.com/harvesthq/chosen');
  3689. }
  3690. if (this.settings.plugins['bt-selectpicker']) {
  3691. Utils.error('Conflict', 'bt-selectpicker is already selected as the dropdown plugin. Please remove chosen-selectpicker from the plugin list');
  3692. }
  3693. var Selectors = QueryBuilder.selectors;
  3694. // init selectpicker
  3695. this.on('afterCreateRuleFilters', function(e, rule) {
  3696. rule.$el.find(Selectors.rule_filter).removeClass('form-control').chosen(options);
  3697. });
  3698. this.on('afterCreateRuleOperators', function(e, rule) {
  3699. rule.$el.find(Selectors.rule_operator).removeClass('form-control').chosen(options);
  3700. });
  3701. // update selectpicker on change
  3702. this.on('afterUpdateRuleFilter', function(e, rule) {
  3703. rule.$el.find(Selectors.rule_filter).trigger('chosen:updated');
  3704. });
  3705. this.on('afterUpdateRuleOperator', function(e, rule) {
  3706. rule.$el.find(Selectors.rule_operator).trigger('chosen:updated');
  3707. });
  3708. this.on('beforeDeleteRule', function(e, rule) {
  3709. rule.$el.find(Selectors.rule_filter).chosen('destroy');
  3710. rule.$el.find(Selectors.rule_operator).chosen('destroy');
  3711. });
  3712. });
  3713. /**
  3714. * @class FilterDescription
  3715. * @memberof module:plugins
  3716. * @description Provides three ways to display a description about a filter: inline, Bootsrap Popover or Bootbox.
  3717. * @param {object} [options]
  3718. * @param {string} [options.icon='glyphicon glyphicon-info-sign']
  3719. * @param {string} [options.mode='popover'] - inline, popover or bootbox
  3720. * @throws ConfigError
  3721. */
  3722. QueryBuilder.define('filter-description', function(options) {
  3723. // INLINE
  3724. if (options.mode === 'inline') {
  3725. this.on('afterUpdateRuleFilter afterUpdateRuleOperator', function(e, rule) {
  3726. var $p = rule.$el.find('p.filter-description');
  3727. var description = e.builder.getFilterDescription(rule.filter, rule);
  3728. if (!description) {
  3729. $p.hide();
  3730. }
  3731. else {
  3732. if ($p.length === 0) {
  3733. $p = $('<p class="filter-description"></p>');
  3734. $p.appendTo(rule.$el);
  3735. }
  3736. else {
  3737. $p.css('display', '');
  3738. }
  3739. $p.html('<i class="' + options.icon + '"></i> ' + description);
  3740. }
  3741. });
  3742. }
  3743. // POPOVER
  3744. else if (options.mode === 'popover') {
  3745. if (!$.fn.popover || !$.fn.popover.Constructor || !$.fn.popover.Constructor.prototype.fixTitle) {
  3746. Utils.error('MissingLibrary', 'Bootstrap Popover is required to use "filter-description" plugin. Get it here: http://getbootstrap.com');
  3747. }
  3748. this.on('afterUpdateRuleFilter afterUpdateRuleOperator', function(e, rule) {
  3749. var $b = rule.$el.find('button.filter-description');
  3750. var description = e.builder.getFilterDescription(rule.filter, rule);
  3751. if (!description) {
  3752. $b.hide();
  3753. if ($b.data('bs.popover')) {
  3754. $b.popover('hide');
  3755. }
  3756. }
  3757. else {
  3758. if ($b.length === 0) {
  3759. $b = $('<button type="button" class="btn btn-xs btn-info filter-description" data-toggle="popover"><i class="' + options.icon + '"></i></button>');
  3760. $b.prependTo(rule.$el.find(QueryBuilder.selectors.rule_actions));
  3761. $b.popover({
  3762. placement: 'left',
  3763. container: 'body',
  3764. html: true
  3765. });
  3766. $b.on('mouseout', function() {
  3767. $b.popover('hide');
  3768. });
  3769. }
  3770. else {
  3771. $b.css('display', '');
  3772. }
  3773. $b.data('bs.popover').options.content = description;
  3774. if ($b.attr('aria-describedby')) {
  3775. $b.popover('show');
  3776. }
  3777. }
  3778. });
  3779. }
  3780. // BOOTBOX
  3781. else if (options.mode === 'bootbox') {
  3782. if (!('bootbox' in window)) {
  3783. Utils.error('MissingLibrary', 'Bootbox is required to use "filter-description" plugin. Get it here: http://bootboxjs.com');
  3784. }
  3785. this.on('afterUpdateRuleFilter afterUpdateRuleOperator', function(e, rule) {
  3786. var $b = rule.$el.find('button.filter-description');
  3787. var description = e.builder.getFilterDescription(rule.filter, rule);
  3788. if (!description) {
  3789. $b.hide();
  3790. }
  3791. else {
  3792. if ($b.length === 0) {
  3793. $b = $('<button type="button" class="btn btn-xs btn-info filter-description" data-toggle="bootbox"><i class="' + options.icon + '"></i></button>');
  3794. $b.prependTo(rule.$el.find(QueryBuilder.selectors.rule_actions));
  3795. $b.on('click', function() {
  3796. bootbox.alert($b.data('description'));
  3797. });
  3798. }
  3799. else {
  3800. $b.css('display', '');
  3801. }
  3802. $b.data('description', description);
  3803. }
  3804. });
  3805. }
  3806. }, {
  3807. icon: 'glyphicon glyphicon-info-sign',
  3808. mode: 'popover'
  3809. });
  3810. QueryBuilder.extend(/** @lends module:plugins.FilterDescription.prototype */ {
  3811. /**
  3812. * Returns the description of a filter for a particular rule (if present)
  3813. * @param {object} filter
  3814. * @param {Rule} [rule]
  3815. * @returns {string}
  3816. * @private
  3817. */
  3818. getFilterDescription: function(filter, rule) {
  3819. if (!filter) {
  3820. return undefined;
  3821. }
  3822. else if (typeof filter.description == 'function') {
  3823. return filter.description.call(this, rule);
  3824. }
  3825. else {
  3826. return filter.description;
  3827. }
  3828. }
  3829. });
  3830. /**
  3831. * @class Invert
  3832. * @memberof module:plugins
  3833. * @description Allows to invert a rule operator, a group condition or the entire builder.
  3834. * @param {object} [options]
  3835. * @param {string} [options.icon='glyphicon glyphicon-random']
  3836. * @param {boolean} [options.recursive=true]
  3837. * @param {boolean} [options.invert_rules=true]
  3838. * @param {boolean} [options.display_rules_button=false]
  3839. * @param {boolean} [options.silent_fail=false]
  3840. */
  3841. QueryBuilder.define('invert', function(options) {
  3842. var self = this;
  3843. var Selectors = QueryBuilder.selectors;
  3844. // Bind events
  3845. this.on('afterInit', function() {
  3846. self.$el.on('click.queryBuilder', '[data-invert=group]', function() {
  3847. var $group = $(this).closest(Selectors.group_container);
  3848. self.invert(self.getModel($group), options);
  3849. });
  3850. if (options.display_rules_button && options.invert_rules) {
  3851. self.$el.on('click.queryBuilder', '[data-invert=rule]', function() {
  3852. var $rule = $(this).closest(Selectors.rule_container);
  3853. self.invert(self.getModel($rule), options);
  3854. });
  3855. }
  3856. });
  3857. // Modify templates
  3858. if (!options.disable_template) {
  3859. this.on('getGroupTemplate.filter', function(h) {
  3860. var $h = $(h.value);
  3861. $h.find(Selectors.condition_container).after(
  3862. '<button type="button" class="btn btn-xs btn-default" data-invert="group">' +
  3863. '<i class="' + options.icon + '"></i> ' + self.translate('invert') +
  3864. '</button>'
  3865. );
  3866. h.value = $h.prop('outerHTML');
  3867. });
  3868. if (options.display_rules_button && options.invert_rules) {
  3869. this.on('getRuleTemplate.filter', function(h) {
  3870. var $h = $(h.value);
  3871. $h.find(Selectors.rule_actions).prepend(
  3872. '<button type="button" class="btn btn-xs btn-default" data-invert="rule">' +
  3873. '<i class="' + options.icon + '"></i> ' + self.translate('invert') +
  3874. '</button>'
  3875. );
  3876. h.value = $h.prop('outerHTML');
  3877. });
  3878. }
  3879. }
  3880. }, {
  3881. icon: 'glyphicon glyphicon-random',
  3882. recursive: true,
  3883. invert_rules: true,
  3884. display_rules_button: false,
  3885. silent_fail: false,
  3886. disable_template: false
  3887. });
  3888. QueryBuilder.defaults({
  3889. operatorOpposites: {
  3890. 'equal': 'not_equal',
  3891. 'not_equal': 'equal',
  3892. 'in': 'not_in',
  3893. 'not_in': 'in',
  3894. 'less': 'greater_or_equal',
  3895. 'less_or_equal': 'greater',
  3896. 'greater': 'less_or_equal',
  3897. 'greater_or_equal': 'less',
  3898. 'between': 'not_between',
  3899. 'not_between': 'between',
  3900. 'begins_with': 'not_begins_with',
  3901. 'not_begins_with': 'begins_with',
  3902. 'contains': 'not_contains',
  3903. 'not_contains': 'contains',
  3904. 'ends_with': 'not_ends_with',
  3905. 'not_ends_with': 'ends_with',
  3906. 'is_empty': 'is_not_empty',
  3907. 'is_not_empty': 'is_empty',
  3908. 'is_null': 'is_not_null',
  3909. 'is_not_null': 'is_null'
  3910. },
  3911. conditionOpposites: {
  3912. 'AND': 'OR',
  3913. 'OR': 'AND'
  3914. }
  3915. });
  3916. QueryBuilder.extend(/** @lends module:plugins.Invert.prototype */ {
  3917. /**
  3918. * Invert a Group, a Rule or the whole builder
  3919. * @param {Node} [node]
  3920. * @param {object} [options] {@link module:plugins.Invert}
  3921. * @fires module:plugins.Invert.afterInvert
  3922. * @throws InvertConditionError, InvertOperatorError
  3923. */
  3924. invert: function(node, options) {
  3925. if (!(node instanceof Node)) {
  3926. if (!this.model.root) return;
  3927. options = node;
  3928. node = this.model.root;
  3929. }
  3930. if (typeof options != 'object') options = {};
  3931. if (options.recursive === undefined) options.recursive = true;
  3932. if (options.invert_rules === undefined) options.invert_rules = true;
  3933. if (options.silent_fail === undefined) options.silent_fail = false;
  3934. if (options.trigger === undefined) options.trigger = true;
  3935. if (node instanceof Group) {
  3936. // invert group condition
  3937. if (this.settings.conditionOpposites[node.condition]) {
  3938. node.condition = this.settings.conditionOpposites[node.condition];
  3939. }
  3940. else if (!options.silent_fail) {
  3941. Utils.error('InvertCondition', 'Unknown inverse of condition "{0}"', node.condition);
  3942. }
  3943. // recursive call
  3944. if (options.recursive) {
  3945. var tempOpts = $.extend({}, options, { trigger: false });
  3946. node.each(function(rule) {
  3947. if (options.invert_rules) {
  3948. this.invert(rule, tempOpts);
  3949. }
  3950. }, function(group) {
  3951. this.invert(group, tempOpts);
  3952. }, this);
  3953. }
  3954. }
  3955. else if (node instanceof Rule) {
  3956. if (node.operator && !node.filter.no_invert) {
  3957. // invert rule operator
  3958. if (this.settings.operatorOpposites[node.operator.type]) {
  3959. var invert = this.settings.operatorOpposites[node.operator.type];
  3960. // check if the invert is "authorized"
  3961. if (!node.filter.operators || node.filter.operators.indexOf(invert) != -1) {
  3962. node.operator = this.getOperatorByType(invert);
  3963. }
  3964. }
  3965. else if (!options.silent_fail) {
  3966. Utils.error('InvertOperator', 'Unknown inverse of operator "{0}"', node.operator.type);
  3967. }
  3968. }
  3969. }
  3970. if (options.trigger) {
  3971. /**
  3972. * After {@link module:plugins.Invert.invert} method
  3973. * @event afterInvert
  3974. * @memberof module:plugins.Invert
  3975. * @param {Node} node - the main group or rule that has been modified
  3976. * @param {object} options
  3977. */
  3978. this.trigger('afterInvert', node, options);
  3979. this.trigger('rulesChanged');
  3980. }
  3981. }
  3982. });
  3983. /**
  3984. * @class MongoDbSupport
  3985. * @memberof module:plugins
  3986. * @description Allows to export rules as a MongoDB find object as well as populating the builder from a MongoDB object.
  3987. */
  3988. QueryBuilder.defaults({
  3989. mongoOperators: {
  3990. // @formatter:off
  3991. equal: function(v) { return v[0]; },
  3992. not_equal: function(v) { return { '$ne': v[0] }; },
  3993. in: function(v) { return { '$in': v }; },
  3994. not_in: function(v) { return { '$nin': v }; },
  3995. less: function(v) { return { '$lt': v[0] }; },
  3996. less_or_equal: function(v) { return { '$lte': v[0] }; },
  3997. greater: function(v) { return { '$gt': v[0] }; },
  3998. greater_or_equal: function(v) { return { '$gte': v[0] }; },
  3999. between: function(v) { return { '$gte': v[0], '$lte': v[1] }; },
  4000. not_between: function(v) { return { '$lt': v[0], '$gt': v[1] }; },
  4001. begins_with: function(v) { return { '$regex': '^' + Utils.escapeRegExp(v[0]) }; },
  4002. not_begins_with: function(v) { return { '$regex': '^(?!' + Utils.escapeRegExp(v[0]) + ')' }; },
  4003. contains: function(v) { return { '$regex': Utils.escapeRegExp(v[0]) }; },
  4004. not_contains: function(v) { return { '$regex': '^((?!' + Utils.escapeRegExp(v[0]) + ').)*$', '$options': 's' }; },
  4005. ends_with: function(v) { return { '$regex': Utils.escapeRegExp(v[0]) + '$' }; },
  4006. not_ends_with: function(v) { return { '$regex': '(?<!' + Utils.escapeRegExp(v[0]) + ')$' }; },
  4007. is_empty: function(v) { return ''; },
  4008. is_not_empty: function(v) { return { '$ne': '' }; },
  4009. is_null: function(v) { return null; },
  4010. is_not_null: function(v) { return { '$ne': null }; }
  4011. // @formatter:on
  4012. },
  4013. mongoRuleOperators: {
  4014. $eq: function(v) {
  4015. return {
  4016. 'val': v,
  4017. 'op': v === null ? 'is_null' : (v === '' ? 'is_empty' : 'equal')
  4018. };
  4019. },
  4020. $ne: function(v) {
  4021. v = v.$ne;
  4022. return {
  4023. 'val': v,
  4024. 'op': v === null ? 'is_not_null' : (v === '' ? 'is_not_empty' : 'not_equal')
  4025. };
  4026. },
  4027. $regex: function(v) {
  4028. v = v.$regex;
  4029. if (v.slice(0, 4) == '^(?!' && v.slice(-1) == ')') {
  4030. return { 'val': v.slice(4, -1), 'op': 'not_begins_with' };
  4031. }
  4032. else if (v.slice(0, 5) == '^((?!' && v.slice(-5) == ').)*$') {
  4033. return { 'val': v.slice(5, -5), 'op': 'not_contains' };
  4034. }
  4035. else if (v.slice(0, 4) == '(?<!' && v.slice(-2) == ')$') {
  4036. return { 'val': v.slice(4, -2), 'op': 'not_ends_with' };
  4037. }
  4038. else if (v.slice(-1) == '$') {
  4039. return { 'val': v.slice(0, -1), 'op': 'ends_with' };
  4040. }
  4041. else if (v.slice(0, 1) == '^') {
  4042. return { 'val': v.slice(1), 'op': 'begins_with' };
  4043. }
  4044. else {
  4045. return { 'val': v, 'op': 'contains' };
  4046. }
  4047. },
  4048. between: function(v) {
  4049. return { 'val': [v.$gte, v.$lte], 'op': 'between' };
  4050. },
  4051. not_between: function(v) {
  4052. return { 'val': [v.$lt, v.$gt], 'op': 'not_between' };
  4053. },
  4054. $in: function(v) {
  4055. return { 'val': v.$in, 'op': 'in' };
  4056. },
  4057. $nin: function(v) {
  4058. return { 'val': v.$nin, 'op': 'not_in' };
  4059. },
  4060. $lt: function(v) {
  4061. return { 'val': v.$lt, 'op': 'less' };
  4062. },
  4063. $lte: function(v) {
  4064. return { 'val': v.$lte, 'op': 'less_or_equal' };
  4065. },
  4066. $gt: function(v) {
  4067. return { 'val': v.$gt, 'op': 'greater' };
  4068. },
  4069. $gte: function(v) {
  4070. return { 'val': v.$gte, 'op': 'greater_or_equal' };
  4071. }
  4072. }
  4073. });
  4074. QueryBuilder.extend(/** @lends module:plugins.MongoDbSupport.prototype */ {
  4075. /**
  4076. * Returns rules as a MongoDB query
  4077. * @param {object} [data] - current rules by default
  4078. * @returns {object}
  4079. * @fires module:plugins.MongoDbSupport.changer:getMongoDBField
  4080. * @fires module:plugins.MongoDbSupport.changer:ruleToMongo
  4081. * @fires module:plugins.MongoDbSupport.changer:groupToMongo
  4082. * @throws UndefinedMongoConditionError, UndefinedMongoOperatorError
  4083. */
  4084. getMongo: function(data) {
  4085. data = (data === undefined) ? this.getRules() : data;
  4086. if (!data) {
  4087. return null;
  4088. }
  4089. var self = this;
  4090. return (function parse(group) {
  4091. if (!group.condition) {
  4092. group.condition = self.settings.default_condition;
  4093. }
  4094. if (['AND', 'OR'].indexOf(group.condition.toUpperCase()) === -1) {
  4095. Utils.error('UndefinedMongoCondition', 'Unable to build MongoDB query with condition "{0}"', group.condition);
  4096. }
  4097. if (!group.rules) {
  4098. return {};
  4099. }
  4100. var parts = [];
  4101. group.rules.forEach(function(rule) {
  4102. if (rule.rules && rule.rules.length > 0) {
  4103. parts.push(parse(rule));
  4104. }
  4105. else {
  4106. var mdb = self.settings.mongoOperators[rule.operator];
  4107. var ope = self.getOperatorByType(rule.operator);
  4108. if (mdb === undefined) {
  4109. Utils.error('UndefinedMongoOperator', 'Unknown MongoDB operation for operator "{0}"', rule.operator);
  4110. }
  4111. if (ope.nb_inputs !== 0) {
  4112. if (!(rule.value instanceof Array)) {
  4113. rule.value = [rule.value];
  4114. }
  4115. }
  4116. /**
  4117. * Modifies the MongoDB field used by a rule
  4118. * @event changer:getMongoDBField
  4119. * @memberof module:plugins.MongoDbSupport
  4120. * @param {string} field
  4121. * @param {Rule} rule
  4122. * @returns {string}
  4123. */
  4124. var field = self.change('getMongoDBField', rule.field, rule);
  4125. var ruleExpression = {};
  4126. ruleExpression[field] = mdb.call(self, rule.value);
  4127. /**
  4128. * Modifies the MongoDB expression generated for a rul
  4129. * @event changer:ruleToMongo
  4130. * @memberof module:plugins.MongoDbSupport
  4131. * @param {object} expression
  4132. * @param {Rule} rule
  4133. * @param {*} value
  4134. * @param {function} valueWrapper - function that takes the value and adds the operator
  4135. * @returns {object}
  4136. */
  4137. parts.push(self.change('ruleToMongo', ruleExpression, rule, rule.value, mdb));
  4138. }
  4139. });
  4140. var groupExpression = {};
  4141. groupExpression['$' + group.condition.toLowerCase()] = parts;
  4142. /**
  4143. * Modifies the MongoDB expression generated for a group
  4144. * @event changer:groupToMongo
  4145. * @memberof module:plugins.MongoDbSupport
  4146. * @param {object} expression
  4147. * @param {Group} group
  4148. * @returns {object}
  4149. */
  4150. return self.change('groupToMongo', groupExpression, group);
  4151. }(data));
  4152. },
  4153. /**
  4154. * Converts a MongoDB query to rules
  4155. * @param {object} query
  4156. * @returns {object}
  4157. * @fires module:plugins.MongoDbSupport.changer:parseMongoNode
  4158. * @fires module:plugins.MongoDbSupport.changer:getMongoDBFieldID
  4159. * @fires module:plugins.MongoDbSupport.changer:mongoToRule
  4160. * @fires module:plugins.MongoDbSupport.changer:mongoToGroup
  4161. * @throws MongoParseError, UndefinedMongoConditionError, UndefinedMongoOperatorError
  4162. */
  4163. getRulesFromMongo: function(query) {
  4164. if (query === undefined || query === null) {
  4165. return null;
  4166. }
  4167. var self = this;
  4168. /**
  4169. * Custom parsing of a MongoDB expression, you can return a sub-part of the expression, or a well formed group or rule JSON
  4170. * @event changer:parseMongoNode
  4171. * @memberof module:plugins.MongoDbSupport
  4172. * @param {object} expression
  4173. * @returns {object} expression, rule or group
  4174. */
  4175. query = self.change('parseMongoNode', query);
  4176. // a plugin returned a group
  4177. if ('rules' in query && 'condition' in query) {
  4178. return query;
  4179. }
  4180. // a plugin returned a rule
  4181. if ('id' in query && 'operator' in query && 'value' in query) {
  4182. return {
  4183. condition: this.settings.default_condition,
  4184. rules: [query]
  4185. };
  4186. }
  4187. var key = self.getMongoCondition(query);
  4188. if (!key) {
  4189. Utils.error('MongoParse', 'Invalid MongoDB query format');
  4190. }
  4191. return (function parse(data, topKey) {
  4192. var rules = data[topKey];
  4193. var parts = [];
  4194. rules.forEach(function(data) {
  4195. // allow plugins to manually parse or handle special cases
  4196. data = self.change('parseMongoNode', data);
  4197. // a plugin returned a group
  4198. if ('rules' in data && 'condition' in data) {
  4199. parts.push(data);
  4200. return;
  4201. }
  4202. // a plugin returned a rule
  4203. if ('id' in data && 'operator' in data && 'value' in data) {
  4204. parts.push(data);
  4205. return;
  4206. }
  4207. var key = self.getMongoCondition(data);
  4208. if (key) {
  4209. parts.push(parse(data, key));
  4210. }
  4211. else {
  4212. var field = Object.keys(data)[0];
  4213. var value = data[field];
  4214. var operator = self.getMongoOperator(value);
  4215. if (operator === undefined) {
  4216. Utils.error('MongoParse', 'Invalid MongoDB query format');
  4217. }
  4218. var mdbrl = self.settings.mongoRuleOperators[operator];
  4219. if (mdbrl === undefined) {
  4220. Utils.error('UndefinedMongoOperator', 'JSON Rule operation unknown for operator "{0}"', operator);
  4221. }
  4222. var opVal = mdbrl.call(self, value);
  4223. var id = self.getMongoDBFieldID(field, value);
  4224. /**
  4225. * Modifies the rule generated from the MongoDB expression
  4226. * @event changer:mongoToRule
  4227. * @memberof module:plugins.MongoDbSupport
  4228. * @param {object} rule
  4229. * @param {object} expression
  4230. * @returns {object}
  4231. */
  4232. var rule = self.change('mongoToRule', {
  4233. id: id,
  4234. field: field,
  4235. operator: opVal.op,
  4236. value: opVal.val
  4237. }, data);
  4238. parts.push(rule);
  4239. }
  4240. });
  4241. /**
  4242. * Modifies the group generated from the MongoDB expression
  4243. * @event changer:mongoToGroup
  4244. * @memberof module:plugins.MongoDbSupport
  4245. * @param {object} group
  4246. * @param {object} expression
  4247. * @returns {object}
  4248. */
  4249. return self.change('mongoToGroup', {
  4250. condition: topKey.replace('$', '').toUpperCase(),
  4251. rules: parts
  4252. }, data);
  4253. }(query, key));
  4254. },
  4255. /**
  4256. * Sets rules a from MongoDB query
  4257. * @see module:plugins.MongoDbSupport.getRulesFromMongo
  4258. */
  4259. setRulesFromMongo: function(query) {
  4260. this.setRules(this.getRulesFromMongo(query));
  4261. },
  4262. /**
  4263. * Returns a filter identifier from the MongoDB field.
  4264. * Automatically use the only one filter with a matching field, fires a changer otherwise.
  4265. * @param {string} field
  4266. * @param {*} value
  4267. * @fires module:plugins.MongoDbSupport:changer:getMongoDBFieldID
  4268. * @returns {string}
  4269. * @private
  4270. */
  4271. getMongoDBFieldID: function(field, value) {
  4272. var matchingFilters = this.filters.filter(function(filter) {
  4273. return filter.field === field;
  4274. });
  4275. var id;
  4276. if (matchingFilters.length === 1) {
  4277. id = matchingFilters[0].id;
  4278. }
  4279. else {
  4280. /**
  4281. * Returns a filter identifier from the MongoDB field
  4282. * @event changer:getMongoDBFieldID
  4283. * @memberof module:plugins.MongoDbSupport
  4284. * @param {string} field
  4285. * @param {*} value
  4286. * @returns {string}
  4287. */
  4288. id = this.change('getMongoDBFieldID', field, value);
  4289. }
  4290. return id;
  4291. },
  4292. /**
  4293. * Finds which operator is used in a MongoDB sub-object
  4294. * @param {*} data
  4295. * @returns {string|undefined}
  4296. * @private
  4297. */
  4298. getMongoOperator: function(data) {
  4299. if (data !== null && typeof data === 'object') {
  4300. if (data.$gte !== undefined && data.$lte !== undefined) {
  4301. return 'between';
  4302. }
  4303. if (data.$lt !== undefined && data.$gt !== undefined) {
  4304. return 'not_between';
  4305. }
  4306. var knownKeys = Object.keys(data).filter(function(key) {
  4307. return !!this.settings.mongoRuleOperators[key];
  4308. }.bind(this));
  4309. if (knownKeys.length === 1) {
  4310. return knownKeys[0];
  4311. }
  4312. }
  4313. else {
  4314. return '$eq';
  4315. }
  4316. },
  4317. /**
  4318. * Returns the key corresponding to "$or" or "$and"
  4319. * @param {object} data
  4320. * @returns {string|undefined}
  4321. * @private
  4322. */
  4323. getMongoCondition: function(data) {
  4324. var keys = Object.keys(data);
  4325. for (var i = 0, l = keys.length; i < l; i++) {
  4326. if (keys[i].toLowerCase() === '$or' || keys[i].toLowerCase() === '$and') {
  4327. return keys[i];
  4328. }
  4329. }
  4330. }
  4331. });
  4332. /**
  4333. * @class NotGroup
  4334. * @memberof module:plugins
  4335. * @description Adds a "Not" checkbox in front of group conditions.
  4336. * @param {object} [options]
  4337. * @param {string} [options.icon_checked='glyphicon glyphicon-checked']
  4338. * @param {string} [options.icon_unchecked='glyphicon glyphicon-unchecked']
  4339. */
  4340. QueryBuilder.define('not-group', function(options) {
  4341. var self = this;
  4342. // Bind events
  4343. this.on('afterInit', function() {
  4344. self.$el.on('click.queryBuilder', '[data-not=group]', function() {
  4345. var $group = $(this).closest(QueryBuilder.selectors.group_container);
  4346. var group = self.getModel($group);
  4347. group.not = !group.not;
  4348. });
  4349. self.model.on('update', function(e, node, field) {
  4350. if (node instanceof Group && field === 'not') {
  4351. self.updateGroupNot(node);
  4352. }
  4353. });
  4354. });
  4355. // Init "not" property
  4356. this.on('afterAddGroup', function(e, group) {
  4357. group.__.not = false;
  4358. });
  4359. // Modify templates
  4360. if (!options.disable_template) {
  4361. this.on('getGroupTemplate.filter', function(h) {
  4362. var $h = $(h.value);
  4363. $h.find(QueryBuilder.selectors.condition_container).prepend(
  4364. '<button type="button" class="btn btn-xs btn-default" data-not="group">' +
  4365. '<i class="' + options.icon_unchecked + '"></i> ' + self.translate('NOT') +
  4366. '</button>'
  4367. );
  4368. h.value = $h.prop('outerHTML');
  4369. });
  4370. }
  4371. // Export "not" to JSON
  4372. this.on('groupToJson.filter', function(e, group) {
  4373. e.value.not = group.not;
  4374. });
  4375. // Read "not" from JSON
  4376. this.on('jsonToGroup.filter', function(e, json) {
  4377. e.value.not = !!json.not;
  4378. });
  4379. // Export "not" to SQL
  4380. this.on('groupToSQL.filter', function(e, group) {
  4381. if (group.not) {
  4382. e.value = 'NOT ( ' + e.value + ' )';
  4383. }
  4384. });
  4385. // Parse "NOT" function from sqlparser
  4386. this.on('parseSQLNode.filter', function(e) {
  4387. if (e.value.name && e.value.name.toUpperCase() == 'NOT') {
  4388. e.value = e.value.arguments.value[0];
  4389. // if the there is no sub-group, create one
  4390. if (['AND', 'OR'].indexOf(e.value.operation.toUpperCase()) === -1) {
  4391. e.value = new SQLParser.nodes.Op(
  4392. self.settings.default_condition,
  4393. e.value,
  4394. null
  4395. );
  4396. }
  4397. e.value.not = true;
  4398. }
  4399. });
  4400. // Request to create sub-group if the "not" flag is set
  4401. this.on('sqlGroupsDistinct.filter', function(e, group, data, i) {
  4402. if (data.not && i > 0) {
  4403. e.value = true;
  4404. }
  4405. });
  4406. // Read "not" from parsed SQL
  4407. this.on('sqlToGroup.filter', function(e, data) {
  4408. e.value.not = !!data.not;
  4409. });
  4410. // Export "not" to Mongo
  4411. this.on('groupToMongo.filter', function(e, group) {
  4412. var key = '$' + group.condition.toLowerCase();
  4413. if (group.not && e.value[key]) {
  4414. e.value = { '$nor': [e.value] };
  4415. }
  4416. });
  4417. // Parse "$nor" operator from Mongo
  4418. this.on('parseMongoNode.filter', function(e) {
  4419. var keys = Object.keys(e.value);
  4420. if (keys[0] == '$nor') {
  4421. e.value = e.value[keys[0]][0];
  4422. e.value.not = true;
  4423. }
  4424. });
  4425. // Read "not" from parsed Mongo
  4426. this.on('mongoToGroup.filter', function(e, data) {
  4427. e.value.not = !!data.not;
  4428. });
  4429. }, {
  4430. icon_unchecked: 'glyphicon glyphicon-unchecked',
  4431. icon_checked: 'glyphicon glyphicon-check',
  4432. disable_template: false
  4433. });
  4434. /**
  4435. * From {@link module:plugins.NotGroup}
  4436. * @name not
  4437. * @member {boolean}
  4438. * @memberof Group
  4439. * @instance
  4440. */
  4441. Utils.defineModelProperties(Group, ['not']);
  4442. QueryBuilder.selectors.group_not = QueryBuilder.selectors.group_header + ' [data-not=group]';
  4443. QueryBuilder.extend(/** @lends module:plugins.NotGroup.prototype */ {
  4444. /**
  4445. * Performs actions when a group's not changes
  4446. * @param {Group} group
  4447. * @fires module:plugins.NotGroup.afterUpdateGroupNot
  4448. * @private
  4449. */
  4450. updateGroupNot: function(group) {
  4451. var options = this.plugins['not-group'];
  4452. group.$el.find('>' + QueryBuilder.selectors.group_not)
  4453. .toggleClass('active', group.not)
  4454. .find('i').attr('class', group.not ? options.icon_checked : options.icon_unchecked);
  4455. /**
  4456. * After the group's not flag has been modified
  4457. * @event afterUpdateGroupNot
  4458. * @memberof module:plugins.NotGroup
  4459. * @param {Group} group
  4460. */
  4461. this.trigger('afterUpdateGroupNot', group);
  4462. this.trigger('rulesChanged');
  4463. }
  4464. });
  4465. /**
  4466. * @class Sortable
  4467. * @memberof module:plugins
  4468. * @description Enables drag & drop sort of rules.
  4469. * @param {object} [options]
  4470. * @param {boolean} [options.inherit_no_drop=true]
  4471. * @param {boolean} [options.inherit_no_sortable=true]
  4472. * @param {string} [options.icon='glyphicon glyphicon-sort']
  4473. * @throws MissingLibraryError, ConfigError
  4474. */
  4475. QueryBuilder.define('sortable', function(options) {
  4476. if (!('interact' in window)) {
  4477. Utils.error('MissingLibrary', 'interact.js is required to use "sortable" plugin. Get it here: http://interactjs.io');
  4478. }
  4479. if (options.default_no_sortable !== undefined) {
  4480. Utils.error(false, 'Config', 'Sortable plugin : "default_no_sortable" options is deprecated, use standard "default_rule_flags" and "default_group_flags" instead');
  4481. this.settings.default_rule_flags.no_sortable = this.settings.default_group_flags.no_sortable = options.default_no_sortable;
  4482. }
  4483. // recompute drop-zones during drag (when a rule is hidden)
  4484. interact.dynamicDrop(true);
  4485. // set move threshold to 10px
  4486. interact.pointerMoveTolerance(10);
  4487. var placeholder;
  4488. var ghost;
  4489. var src;
  4490. var moved;
  4491. // Init drag and drop
  4492. this.on('afterAddRule afterAddGroup', function(e, node) {
  4493. if (node == placeholder) {
  4494. return;
  4495. }
  4496. var self = e.builder;
  4497. // Inherit flags
  4498. if (options.inherit_no_sortable && node.parent && node.parent.flags.no_sortable) {
  4499. node.flags.no_sortable = true;
  4500. }
  4501. if (options.inherit_no_drop && node.parent && node.parent.flags.no_drop) {
  4502. node.flags.no_drop = true;
  4503. }
  4504. // Configure drag
  4505. if (!node.flags.no_sortable) {
  4506. interact(node.$el[0])
  4507. .draggable({
  4508. allowFrom: QueryBuilder.selectors.drag_handle,
  4509. onstart: function(event) {
  4510. moved = false;
  4511. // get model of dragged element
  4512. src = self.getModel(event.target);
  4513. // create ghost
  4514. ghost = src.$el.clone()
  4515. .appendTo(src.$el.parent())
  4516. .width(src.$el.outerWidth())
  4517. .addClass('dragging');
  4518. // create drop placeholder
  4519. var ph = $('<div class="rule-placeholder">&nbsp;</div>')
  4520. .height(src.$el.outerHeight());
  4521. placeholder = src.parent.addRule(ph, src.getPos());
  4522. // hide dragged element
  4523. src.$el.hide();
  4524. },
  4525. onmove: function(event) {
  4526. // make the ghost follow the cursor
  4527. ghost[0].style.top = event.clientY - 15 + 'px';
  4528. ghost[0].style.left = event.clientX - 15 + 'px';
  4529. },
  4530. onend: function(event) {
  4531. // starting from Interact 1.3.3, onend is called before ondrop
  4532. if (event.dropzone) {
  4533. moveSortableToTarget(src, $(event.relatedTarget), self);
  4534. moved = true;
  4535. }
  4536. // remove ghost
  4537. ghost.remove();
  4538. ghost = undefined;
  4539. // remove placeholder
  4540. placeholder.drop();
  4541. placeholder = undefined;
  4542. // show element
  4543. src.$el.css('display', '');
  4544. /**
  4545. * After a node has been moved with {@link module:plugins.Sortable}
  4546. * @event afterMove
  4547. * @memberof module:plugins.Sortable
  4548. * @param {Node} node
  4549. */
  4550. self.trigger('afterMove', src);
  4551. self.trigger('rulesChanged');
  4552. }
  4553. });
  4554. }
  4555. if (!node.flags.no_drop) {
  4556. // Configure drop on groups and rules
  4557. interact(node.$el[0])
  4558. .dropzone({
  4559. accept: QueryBuilder.selectors.rule_and_group_containers,
  4560. ondragenter: function(event) {
  4561. moveSortableToTarget(placeholder, $(event.target), self);
  4562. },
  4563. ondrop: function(event) {
  4564. if (!moved) {
  4565. moveSortableToTarget(src, $(event.target), self);
  4566. }
  4567. }
  4568. });
  4569. // Configure drop on group headers
  4570. if (node instanceof Group) {
  4571. interact(node.$el.find(QueryBuilder.selectors.group_header)[0])
  4572. .dropzone({
  4573. accept: QueryBuilder.selectors.rule_and_group_containers,
  4574. ondragenter: function(event) {
  4575. moveSortableToTarget(placeholder, $(event.target), self);
  4576. },
  4577. ondrop: function(event) {
  4578. if (!moved) {
  4579. moveSortableToTarget(src, $(event.target), self);
  4580. }
  4581. }
  4582. });
  4583. }
  4584. }
  4585. });
  4586. // Detach interactables
  4587. this.on('beforeDeleteRule beforeDeleteGroup', function(e, node) {
  4588. if (!e.isDefaultPrevented()) {
  4589. interact(node.$el[0]).unset();
  4590. if (node instanceof Group) {
  4591. interact(node.$el.find(QueryBuilder.selectors.group_header)[0]).unset();
  4592. }
  4593. }
  4594. });
  4595. // Remove drag handle from non-sortable items
  4596. this.on('afterApplyRuleFlags afterApplyGroupFlags', function(e, node) {
  4597. if (node.flags.no_sortable) {
  4598. node.$el.find('.drag-handle').remove();
  4599. }
  4600. });
  4601. // Modify templates
  4602. if (!options.disable_template) {
  4603. this.on('getGroupTemplate.filter', function(h, level) {
  4604. if (level > 1) {
  4605. var $h = $(h.value);
  4606. $h.find(QueryBuilder.selectors.condition_container).after('<div class="drag-handle"><i class="' + options.icon + '"></i></div>');
  4607. h.value = $h.prop('outerHTML');
  4608. }
  4609. });
  4610. this.on('getRuleTemplate.filter', function(h) {
  4611. var $h = $(h.value);
  4612. $h.find(QueryBuilder.selectors.rule_header).after('<div class="drag-handle"><i class="' + options.icon + '"></i></div>');
  4613. h.value = $h.prop('outerHTML');
  4614. });
  4615. }
  4616. }, {
  4617. inherit_no_sortable: true,
  4618. inherit_no_drop: true,
  4619. icon: 'glyphicon glyphicon-sort',
  4620. disable_template: false
  4621. });
  4622. QueryBuilder.selectors.rule_and_group_containers = QueryBuilder.selectors.rule_container + ', ' + QueryBuilder.selectors.group_container;
  4623. QueryBuilder.selectors.drag_handle = '.drag-handle';
  4624. QueryBuilder.defaults({
  4625. default_rule_flags: {
  4626. no_sortable: false,
  4627. no_drop: false
  4628. },
  4629. default_group_flags: {
  4630. no_sortable: false,
  4631. no_drop: false
  4632. }
  4633. });
  4634. /**
  4635. * Moves an element (placeholder or actual object) depending on active target
  4636. * @memberof module:plugins.Sortable
  4637. * @param {Node} node
  4638. * @param {jQuery} target
  4639. * @param {QueryBuilder} [builder]
  4640. * @private
  4641. */
  4642. function moveSortableToTarget(node, target, builder) {
  4643. var parent, method;
  4644. var Selectors = QueryBuilder.selectors;
  4645. // on rule
  4646. parent = target.closest(Selectors.rule_container);
  4647. if (parent.length) {
  4648. method = 'moveAfter';
  4649. }
  4650. // on group header
  4651. if (!method) {
  4652. parent = target.closest(Selectors.group_header);
  4653. if (parent.length) {
  4654. parent = target.closest(Selectors.group_container);
  4655. method = 'moveAtBegin';
  4656. }
  4657. }
  4658. // on group
  4659. if (!method) {
  4660. parent = target.closest(Selectors.group_container);
  4661. if (parent.length) {
  4662. method = 'moveAtEnd';
  4663. }
  4664. }
  4665. if (method) {
  4666. node[method](builder.getModel(parent));
  4667. // refresh radio value
  4668. if (builder && node instanceof Rule) {
  4669. builder.setRuleInputValue(node, node.value);
  4670. }
  4671. }
  4672. }
  4673. /**
  4674. * @class SqlSupport
  4675. * @memberof module:plugins
  4676. * @description Allows to export rules as a SQL WHERE statement as well as populating the builder from an SQL query.
  4677. * @param {object} [options]
  4678. * @param {boolean} [options.boolean_as_integer=true] - `true` to convert boolean values to integer in the SQL output
  4679. */
  4680. QueryBuilder.define('sql-support', function(options) {
  4681. }, {
  4682. boolean_as_integer: true
  4683. });
  4684. QueryBuilder.defaults({
  4685. // operators for internal -> SQL conversion
  4686. sqlOperators: {
  4687. equal: { op: '= ?' },
  4688. not_equal: { op: '!= ?' },
  4689. in: { op: 'IN(?)', sep: ', ' },
  4690. not_in: { op: 'NOT IN(?)', sep: ', ' },
  4691. less: { op: '< ?' },
  4692. less_or_equal: { op: '<= ?' },
  4693. greater: { op: '> ?' },
  4694. greater_or_equal: { op: '>= ?' },
  4695. between: { op: 'BETWEEN ?', sep: ' AND ' },
  4696. not_between: { op: 'NOT BETWEEN ?', sep: ' AND ' },
  4697. begins_with: { op: 'LIKE(?)', mod: '{0}%' },
  4698. not_begins_with: { op: 'NOT LIKE(?)', mod: '{0}%' },
  4699. contains: { op: 'LIKE(?)', mod: '%{0}%' },
  4700. not_contains: { op: 'NOT LIKE(?)', mod: '%{0}%' },
  4701. ends_with: { op: 'LIKE(?)', mod: '%{0}' },
  4702. not_ends_with: { op: 'NOT LIKE(?)', mod: '%{0}' },
  4703. is_empty: { op: '= \'\'' },
  4704. is_not_empty: { op: '!= \'\'' },
  4705. is_null: { op: 'IS NULL' },
  4706. is_not_null: { op: 'IS NOT NULL' }
  4707. },
  4708. // operators for SQL -> internal conversion
  4709. sqlRuleOperator: {
  4710. '=': function(v) {
  4711. return {
  4712. val: v,
  4713. op: v === '' ? 'is_empty' : 'equal'
  4714. };
  4715. },
  4716. '!=': function(v) {
  4717. return {
  4718. val: v,
  4719. op: v === '' ? 'is_not_empty' : 'not_equal'
  4720. };
  4721. },
  4722. 'LIKE': function(v) {
  4723. if (v.slice(0, 1) == '%' && v.slice(-1) == '%') {
  4724. return {
  4725. val: v.slice(1, -1),
  4726. op: 'contains'
  4727. };
  4728. }
  4729. else if (v.slice(0, 1) == '%') {
  4730. return {
  4731. val: v.slice(1),
  4732. op: 'ends_with'
  4733. };
  4734. }
  4735. else if (v.slice(-1) == '%') {
  4736. return {
  4737. val: v.slice(0, -1),
  4738. op: 'begins_with'
  4739. };
  4740. }
  4741. else {
  4742. Utils.error('SQLParse', 'Invalid value for LIKE operator "{0}"', v);
  4743. }
  4744. },
  4745. 'NOT LIKE': function(v) {
  4746. if (v.slice(0, 1) == '%' && v.slice(-1) == '%') {
  4747. return {
  4748. val: v.slice(1, -1),
  4749. op: 'not_contains'
  4750. };
  4751. }
  4752. else if (v.slice(0, 1) == '%') {
  4753. return {
  4754. val: v.slice(1),
  4755. op: 'not_ends_with'
  4756. };
  4757. }
  4758. else if (v.slice(-1) == '%') {
  4759. return {
  4760. val: v.slice(0, -1),
  4761. op: 'not_begins_with'
  4762. };
  4763. }
  4764. else {
  4765. Utils.error('SQLParse', 'Invalid value for NOT LIKE operator "{0}"', v);
  4766. }
  4767. },
  4768. 'IN': function(v) {
  4769. return { val: v, op: 'in' };
  4770. },
  4771. 'NOT IN': function(v) {
  4772. return { val: v, op: 'not_in' };
  4773. },
  4774. '<': function(v) {
  4775. return { val: v, op: 'less' };
  4776. },
  4777. '<=': function(v) {
  4778. return { val: v, op: 'less_or_equal' };
  4779. },
  4780. '>': function(v) {
  4781. return { val: v, op: 'greater' };
  4782. },
  4783. '>=': function(v) {
  4784. return { val: v, op: 'greater_or_equal' };
  4785. },
  4786. 'BETWEEN': function(v) {
  4787. return { val: v, op: 'between' };
  4788. },
  4789. 'NOT BETWEEN': function(v) {
  4790. return { val: v, op: 'not_between' };
  4791. },
  4792. 'IS': function(v) {
  4793. if (v !== null) {
  4794. Utils.error('SQLParse', 'Invalid value for IS operator');
  4795. }
  4796. return { val: null, op: 'is_null' };
  4797. },
  4798. 'IS NOT': function(v) {
  4799. if (v !== null) {
  4800. Utils.error('SQLParse', 'Invalid value for IS operator');
  4801. }
  4802. return { val: null, op: 'is_not_null' };
  4803. }
  4804. },
  4805. // statements for internal -> SQL conversion
  4806. sqlStatements: {
  4807. 'question_mark': function() {
  4808. var params = [];
  4809. return {
  4810. add: function(rule, value) {
  4811. params.push(value);
  4812. return '?';
  4813. },
  4814. run: function() {
  4815. return params;
  4816. }
  4817. };
  4818. },
  4819. 'numbered': function(char) {
  4820. if (!char || char.length > 1) char = '$';
  4821. var index = 0;
  4822. var params = [];
  4823. return {
  4824. add: function(rule, value) {
  4825. params.push(value);
  4826. index++;
  4827. return char + index;
  4828. },
  4829. run: function() {
  4830. return params;
  4831. }
  4832. };
  4833. },
  4834. 'named': function(char) {
  4835. if (!char || char.length > 1) char = ':';
  4836. var indexes = {};
  4837. var params = {};
  4838. return {
  4839. add: function(rule, value) {
  4840. if (!indexes[rule.field]) indexes[rule.field] = 1;
  4841. var key = rule.field + '_' + (indexes[rule.field]++);
  4842. params[key] = value;
  4843. return char + key;
  4844. },
  4845. run: function() {
  4846. return params;
  4847. }
  4848. };
  4849. }
  4850. },
  4851. // statements for SQL -> internal conversion
  4852. sqlRuleStatement: {
  4853. 'question_mark': function(values) {
  4854. var index = 0;
  4855. return {
  4856. parse: function(v) {
  4857. return v == '?' ? values[index++] : v;
  4858. },
  4859. esc: function(sql) {
  4860. return sql.replace(/\?/g, '\'?\'');
  4861. }
  4862. };
  4863. },
  4864. 'numbered': function(values, char) {
  4865. if (!char || char.length > 1) char = '$';
  4866. var regex1 = new RegExp('^\\' + char + '[0-9]+$');
  4867. var regex2 = new RegExp('\\' + char + '([0-9]+)', 'g');
  4868. return {
  4869. parse: function(v) {
  4870. return regex1.test(v) ? values[v.slice(1) - 1] : v;
  4871. },
  4872. esc: function(sql) {
  4873. return sql.replace(regex2, '\'' + (char == '$' ? '$$' : char) + '$1\'');
  4874. }
  4875. };
  4876. },
  4877. 'named': function(values, char) {
  4878. if (!char || char.length > 1) char = ':';
  4879. var regex1 = new RegExp('^\\' + char);
  4880. var regex2 = new RegExp('\\' + char + '(' + Object.keys(values).join('|') + ')', 'g');
  4881. return {
  4882. parse: function(v) {
  4883. return regex1.test(v) ? values[v.slice(1)] : v;
  4884. },
  4885. esc: function(sql) {
  4886. return sql.replace(regex2, '\'' + (char == '$' ? '$$' : char) + '$1\'');
  4887. }
  4888. };
  4889. }
  4890. }
  4891. });
  4892. /**
  4893. * @typedef {object} SqlQuery
  4894. * @memberof module:plugins.SqlSupport
  4895. * @property {string} sql
  4896. * @property {object} params
  4897. */
  4898. QueryBuilder.extend(/** @lends module:plugins.SqlSupport.prototype */ {
  4899. /**
  4900. * Returns rules as a SQL query
  4901. * @param {boolean|string} [stmt] - use prepared statements: false, 'question_mark', 'numbered', 'numbered(@)', 'named', 'named(@)'
  4902. * @param {boolean} [nl=false] output with new lines
  4903. * @param {object} [data] - current rules by default
  4904. * @returns {module:plugins.SqlSupport.SqlQuery}
  4905. * @fires module:plugins.SqlSupport.changer:getSQLField
  4906. * @fires module:plugins.SqlSupport.changer:ruleToSQL
  4907. * @fires module:plugins.SqlSupport.changer:groupToSQL
  4908. * @throws UndefinedSQLConditionError, UndefinedSQLOperatorError
  4909. */
  4910. getSQL: function(stmt, nl, data) {
  4911. data = (data === undefined) ? this.getRules() : data;
  4912. if (!data) {
  4913. return null;
  4914. }
  4915. nl = !!nl ? '\n' : ' ';
  4916. var boolean_as_integer = this.getPluginOptions('sql-support', 'boolean_as_integer');
  4917. if (stmt === true) {
  4918. stmt = 'question_mark';
  4919. }
  4920. if (typeof stmt == 'string') {
  4921. var config = getStmtConfig(stmt);
  4922. stmt = this.settings.sqlStatements[config[1]](config[2]);
  4923. }
  4924. var self = this;
  4925. var sql = (function parse(group) {
  4926. if (!group.condition) {
  4927. group.condition = self.settings.default_condition;
  4928. }
  4929. if (['AND', 'OR'].indexOf(group.condition.toUpperCase()) === -1) {
  4930. Utils.error('UndefinedSQLCondition', 'Unable to build SQL query with condition "{0}"', group.condition);
  4931. }
  4932. if (!group.rules) {
  4933. return '';
  4934. }
  4935. var parts = [];
  4936. group.rules.forEach(function(rule) {
  4937. if (rule.rules && rule.rules.length > 0) {
  4938. parts.push('(' + nl + parse(rule) + nl + ')' + nl);
  4939. }
  4940. else {
  4941. var sql = self.settings.sqlOperators[rule.operator];
  4942. var ope = self.getOperatorByType(rule.operator);
  4943. var value = '';
  4944. if (sql === undefined) {
  4945. Utils.error('UndefinedSQLOperator', 'Unknown SQL operation for operator "{0}"', rule.operator);
  4946. }
  4947. if (ope.nb_inputs !== 0) {
  4948. if (!(rule.value instanceof Array)) {
  4949. rule.value = [rule.value];
  4950. }
  4951. rule.value.forEach(function(v, i) {
  4952. if (i > 0) {
  4953. value += sql.sep;
  4954. }
  4955. if (rule.type == 'boolean' && boolean_as_integer) {
  4956. v = v ? 1 : 0;
  4957. }
  4958. else if (!stmt && rule.type !== 'integer' && rule.type !== 'double' && rule.type !== 'boolean') {
  4959. v = Utils.escapeString(v);
  4960. }
  4961. if (sql.mod) {
  4962. v = Utils.fmt(sql.mod, v);
  4963. }
  4964. if (stmt) {
  4965. value += stmt.add(rule, v);
  4966. }
  4967. else {
  4968. if (typeof v == 'string') {
  4969. v = '\'' + v + '\'';
  4970. }
  4971. value += v;
  4972. }
  4973. });
  4974. }
  4975. var sqlFn = function(v) {
  4976. return sql.op.replace('?', function() {
  4977. return v;
  4978. });
  4979. };
  4980. /**
  4981. * Modifies the SQL field used by a rule
  4982. * @event changer:getSQLField
  4983. * @memberof module:plugins.SqlSupport
  4984. * @param {string} field
  4985. * @param {Rule} rule
  4986. * @returns {string}
  4987. */
  4988. var field = self.change('getSQLField', rule.field, rule);
  4989. var ruleExpression = field + ' ' + sqlFn(value);
  4990. /**
  4991. * Modifies the SQL generated for a rule
  4992. * @event changer:ruleToSQL
  4993. * @memberof module:plugins.SqlSupport
  4994. * @param {string} expression
  4995. * @param {Rule} rule
  4996. * @param {*} value
  4997. * @param {function} valueWrapper - function that takes the value and adds the operator
  4998. * @returns {string}
  4999. */
  5000. parts.push(self.change('ruleToSQL', ruleExpression, rule, value, sqlFn));
  5001. }
  5002. });
  5003. var groupExpression = parts.join(' ' + group.condition + nl);
  5004. /**
  5005. * Modifies the SQL generated for a group
  5006. * @event changer:groupToSQL
  5007. * @memberof module:plugins.SqlSupport
  5008. * @param {string} expression
  5009. * @param {Group} group
  5010. * @returns {string}
  5011. */
  5012. return self.change('groupToSQL', groupExpression, group);
  5013. }(data));
  5014. if (stmt) {
  5015. return {
  5016. sql: sql,
  5017. params: stmt.run()
  5018. };
  5019. }
  5020. else {
  5021. return {
  5022. sql: sql
  5023. };
  5024. }
  5025. },
  5026. /**
  5027. * Convert a SQL query to rules
  5028. * @param {string|module:plugins.SqlSupport.SqlQuery} query
  5029. * @param {boolean|string} stmt
  5030. * @returns {object}
  5031. * @fires module:plugins.SqlSupport.changer:parseSQLNode
  5032. * @fires module:plugins.SqlSupport.changer:getSQLFieldID
  5033. * @fires module:plugins.SqlSupport.changer:sqlToRule
  5034. * @fires module:plugins.SqlSupport.changer:sqlToGroup
  5035. * @throws MissingLibraryError, SQLParseError, UndefinedSQLOperatorError
  5036. */
  5037. getRulesFromSQL: function(query, stmt) {
  5038. if (!('SQLParser' in window)) {
  5039. Utils.error('MissingLibrary', 'SQLParser is required to parse SQL queries. Get it here https://github.com/mistic100/sql-parser');
  5040. }
  5041. var self = this;
  5042. if (typeof query == 'string') {
  5043. query = { sql: query };
  5044. }
  5045. if (stmt === true) stmt = 'question_mark';
  5046. if (typeof stmt == 'string') {
  5047. var config = getStmtConfig(stmt);
  5048. stmt = this.settings.sqlRuleStatement[config[1]](query.params, config[2]);
  5049. }
  5050. if (stmt) {
  5051. query.sql = stmt.esc(query.sql);
  5052. }
  5053. if (query.sql.toUpperCase().indexOf('SELECT') !== 0) {
  5054. query.sql = 'SELECT * FROM table WHERE ' + query.sql;
  5055. }
  5056. var parsed = SQLParser.parse(query.sql);
  5057. if (!parsed.where) {
  5058. Utils.error('SQLParse', 'No WHERE clause found');
  5059. }
  5060. /**
  5061. * Custom parsing of an AST node generated by SQLParser, you can return a sub-part of the tree, or a well formed group or rule JSON
  5062. * @event changer:parseSQLNode
  5063. * @memberof module:plugins.SqlSupport
  5064. * @param {object} AST node
  5065. * @returns {object} tree, rule or group
  5066. */
  5067. var data = self.change('parseSQLNode', parsed.where.conditions);
  5068. // a plugin returned a group
  5069. if ('rules' in data && 'condition' in data) {
  5070. return data;
  5071. }
  5072. // a plugin returned a rule
  5073. if ('id' in data && 'operator' in data && 'value' in data) {
  5074. return {
  5075. condition: this.settings.default_condition,
  5076. rules: [data]
  5077. };
  5078. }
  5079. // create root group
  5080. var out = self.change('sqlToGroup', {
  5081. condition: this.settings.default_condition,
  5082. rules: []
  5083. }, data);
  5084. // keep track of current group
  5085. var curr = out;
  5086. (function flatten(data, i) {
  5087. if (data === null) {
  5088. return;
  5089. }
  5090. // allow plugins to manually parse or handle special cases
  5091. data = self.change('parseSQLNode', data);
  5092. // a plugin returned a group
  5093. if ('rules' in data && 'condition' in data) {
  5094. curr.rules.push(data);
  5095. return;
  5096. }
  5097. // a plugin returned a rule
  5098. if ('id' in data && 'operator' in data && 'value' in data) {
  5099. curr.rules.push(data);
  5100. return;
  5101. }
  5102. // data must be a SQL parser node
  5103. if (!('left' in data) || !('right' in data) || !('operation' in data)) {
  5104. Utils.error('SQLParse', 'Unable to parse WHERE clause');
  5105. }
  5106. // it's a node
  5107. if (['AND', 'OR'].indexOf(data.operation.toUpperCase()) !== -1) {
  5108. // create a sub-group if the condition is not the same and it's not the first level
  5109. /**
  5110. * Given an existing group and an AST node, determines if a sub-group must be created
  5111. * @event changer:sqlGroupsDistinct
  5112. * @memberof module:plugins.SqlSupport
  5113. * @param {boolean} create - true by default if the group condition is different
  5114. * @param {object} group
  5115. * @param {object} AST
  5116. * @param {int} current group level
  5117. * @returns {boolean}
  5118. */
  5119. var createGroup = self.change('sqlGroupsDistinct', i > 0 && curr.condition != data.operation.toUpperCase(), curr, data, i);
  5120. if (createGroup) {
  5121. /**
  5122. * Modifies the group generated from the SQL expression (this is called before the group is filled with rules)
  5123. * @event changer:sqlToGroup
  5124. * @memberof module:plugins.SqlSupport
  5125. * @param {object} group
  5126. * @param {object} AST
  5127. * @returns {object}
  5128. */
  5129. var group = self.change('sqlToGroup', {
  5130. condition: self.settings.default_condition,
  5131. rules: []
  5132. }, data);
  5133. curr.rules.push(group);
  5134. curr = group;
  5135. }
  5136. curr.condition = data.operation.toUpperCase();
  5137. i++;
  5138. // some magic !
  5139. var next = curr;
  5140. flatten(data.left, i);
  5141. curr = next;
  5142. flatten(data.right, i);
  5143. }
  5144. // it's a leaf
  5145. else {
  5146. if ($.isPlainObject(data.right.value)) {
  5147. Utils.error('SQLParse', 'Value format not supported for {0}.', data.left.value);
  5148. }
  5149. // convert array
  5150. var value;
  5151. if ($.isArray(data.right.value)) {
  5152. value = data.right.value.map(function(v) {
  5153. return v.value;
  5154. });
  5155. }
  5156. else {
  5157. value = data.right.value;
  5158. }
  5159. // get actual values
  5160. if (stmt) {
  5161. if ($.isArray(value)) {
  5162. value = value.map(stmt.parse);
  5163. }
  5164. else {
  5165. value = stmt.parse(value);
  5166. }
  5167. }
  5168. // convert operator
  5169. var operator = data.operation.toUpperCase();
  5170. if (operator == '<>') {
  5171. operator = '!=';
  5172. }
  5173. var sqlrl = self.settings.sqlRuleOperator[operator];
  5174. if (sqlrl === undefined) {
  5175. Utils.error('UndefinedSQLOperator', 'Invalid SQL operation "{0}".', data.operation);
  5176. }
  5177. var opVal = sqlrl.call(this, value, data.operation);
  5178. // find field name
  5179. var field;
  5180. if ('values' in data.left) {
  5181. field = data.left.values.join('.');
  5182. }
  5183. else if ('value' in data.left) {
  5184. field = data.left.value;
  5185. }
  5186. else {
  5187. Utils.error('SQLParse', 'Cannot find field name in {0}', JSON.stringify(data.left));
  5188. }
  5189. var id = self.getSQLFieldID(field, value);
  5190. /**
  5191. * Modifies the rule generated from the SQL expression
  5192. * @event changer:sqlToRule
  5193. * @memberof module:plugins.SqlSupport
  5194. * @param {object} rule
  5195. * @param {object} AST
  5196. * @returns {object}
  5197. */
  5198. var rule = self.change('sqlToRule', {
  5199. id: id,
  5200. field: field,
  5201. operator: opVal.op,
  5202. value: opVal.val
  5203. }, data);
  5204. curr.rules.push(rule);
  5205. }
  5206. }(data, 0));
  5207. return out;
  5208. },
  5209. /**
  5210. * Sets the builder's rules from a SQL query
  5211. * @see module:plugins.SqlSupport.getRulesFromSQL
  5212. */
  5213. setRulesFromSQL: function(query, stmt) {
  5214. this.setRules(this.getRulesFromSQL(query, stmt));
  5215. },
  5216. /**
  5217. * Returns a filter identifier from the SQL field.
  5218. * Automatically use the only one filter with a matching field, fires a changer otherwise.
  5219. * @param {string} field
  5220. * @param {*} value
  5221. * @fires module:plugins.SqlSupport:changer:getSQLFieldID
  5222. * @returns {string}
  5223. * @private
  5224. */
  5225. getSQLFieldID: function(field, value) {
  5226. var matchingFilters = this.filters.filter(function(filter) {
  5227. return filter.field.toLowerCase() === field.toLowerCase();
  5228. });
  5229. var id;
  5230. if (matchingFilters.length === 1) {
  5231. id = matchingFilters[0].id;
  5232. }
  5233. else {
  5234. /**
  5235. * Returns a filter identifier from the SQL field
  5236. * @event changer:getSQLFieldID
  5237. * @memberof module:plugins.SqlSupport
  5238. * @param {string} field
  5239. * @param {*} value
  5240. * @returns {string}
  5241. */
  5242. id = this.change('getSQLFieldID', field, value);
  5243. }
  5244. return id;
  5245. }
  5246. });
  5247. /**
  5248. * Parses the statement configuration
  5249. * @memberof module:plugins.SqlSupport
  5250. * @param {string} stmt
  5251. * @returns {Array} null, mode, option
  5252. * @private
  5253. */
  5254. function getStmtConfig(stmt) {
  5255. var config = stmt.match(/(question_mark|numbered|named)(?:\((.)\))?/);
  5256. if (!config) config = [null, 'question_mark', undefined];
  5257. return config;
  5258. }
  5259. /**
  5260. * @class UniqueFilter
  5261. * @memberof module:plugins
  5262. * @description Allows to define some filters as "unique": ie which can be used for only one rule, globally or in the same group.
  5263. */
  5264. QueryBuilder.define('unique-filter', function() {
  5265. this.status.used_filters = {};
  5266. this.on('afterUpdateRuleFilter', this.updateDisabledFilters);
  5267. this.on('afterDeleteRule', this.updateDisabledFilters);
  5268. this.on('afterCreateRuleFilters', this.applyDisabledFilters);
  5269. this.on('afterReset', this.clearDisabledFilters);
  5270. this.on('afterClear', this.clearDisabledFilters);
  5271. // Ensure that the default filter is not already used if unique
  5272. this.on('getDefaultFilter.filter', function(e, model) {
  5273. var self = e.builder;
  5274. self.updateDisabledFilters();
  5275. if (e.value.id in self.status.used_filters) {
  5276. var found = self.filters.some(function(filter) {
  5277. if (!(filter.id in self.status.used_filters) || self.status.used_filters[filter.id].length > 0 && self.status.used_filters[filter.id].indexOf(model.parent) === -1) {
  5278. e.value = filter;
  5279. return true;
  5280. }
  5281. });
  5282. if (!found) {
  5283. Utils.error(false, 'UniqueFilter', 'No more non-unique filters available');
  5284. e.value = undefined;
  5285. }
  5286. }
  5287. });
  5288. });
  5289. QueryBuilder.extend(/** @lends module:plugins.UniqueFilter.prototype */ {
  5290. /**
  5291. * Updates the list of used filters
  5292. * @param {$.Event} [e]
  5293. * @private
  5294. */
  5295. updateDisabledFilters: function(e) {
  5296. var self = e ? e.builder : this;
  5297. self.status.used_filters = {};
  5298. if (!self.model) {
  5299. return;
  5300. }
  5301. // get used filters
  5302. (function walk(group) {
  5303. group.each(function(rule) {
  5304. if (rule.filter && rule.filter.unique) {
  5305. if (!self.status.used_filters[rule.filter.id]) {
  5306. self.status.used_filters[rule.filter.id] = [];
  5307. }
  5308. if (rule.filter.unique == 'group') {
  5309. self.status.used_filters[rule.filter.id].push(rule.parent);
  5310. }
  5311. }
  5312. }, function(group) {
  5313. walk(group);
  5314. });
  5315. }(self.model.root));
  5316. self.applyDisabledFilters(e);
  5317. },
  5318. /**
  5319. * Clear the list of used filters
  5320. * @param {$.Event} [e]
  5321. * @private
  5322. */
  5323. clearDisabledFilters: function(e) {
  5324. var self = e ? e.builder : this;
  5325. self.status.used_filters = {};
  5326. self.applyDisabledFilters(e);
  5327. },
  5328. /**
  5329. * Disabled filters depending on the list of used ones
  5330. * @param {$.Event} [e]
  5331. * @private
  5332. */
  5333. applyDisabledFilters: function(e) {
  5334. var self = e ? e.builder : this;
  5335. // re-enable everything
  5336. self.$el.find(QueryBuilder.selectors.filter_container + ' option').prop('disabled', false);
  5337. // disable some
  5338. $.each(self.status.used_filters, function(filterId, groups) {
  5339. if (groups.length === 0) {
  5340. self.$el.find(QueryBuilder.selectors.filter_container + ' option[value="' + filterId + '"]:not(:selected)').prop('disabled', true);
  5341. }
  5342. else {
  5343. groups.forEach(function(group) {
  5344. group.each(function(rule) {
  5345. rule.$el.find(QueryBuilder.selectors.filter_container + ' option[value="' + filterId + '"]:not(:selected)').prop('disabled', true);
  5346. });
  5347. });
  5348. }
  5349. });
  5350. // update Selectpicker
  5351. if (self.settings.plugins && self.settings.plugins['bt-selectpicker']) {
  5352. self.$el.find(QueryBuilder.selectors.rule_filter).selectpicker('render');
  5353. }
  5354. }
  5355. });
  5356. /*!
  5357. * jQuery QueryBuilder 2.5.2
  5358. * Locale: English (en)
  5359. * Author: Damien "Mistic" Sorel, http://www.strangeplanet.fr
  5360. * Licensed under MIT (https://opensource.org/licenses/MIT)
  5361. */
  5362. QueryBuilder.regional['en'] = {
  5363. "__locale": "English (en)",
  5364. "__author": "Damien \"Mistic\" Sorel, http://www.strangeplanet.fr",
  5365. "add_rule": "Add rule",
  5366. "add_group": "Add group",
  5367. "delete_rule": "Delete",
  5368. "delete_group": "Delete",
  5369. "conditions": {
  5370. "AND": "AND",
  5371. "OR": "OR"
  5372. },
  5373. "operators": {
  5374. "equal": "equal",
  5375. "not_equal": "not equal",
  5376. "in": "in",
  5377. "not_in": "not in",
  5378. "less": "less",
  5379. "less_or_equal": "less or equal",
  5380. "greater": "greater",
  5381. "greater_or_equal": "greater or equal",
  5382. "between": "between",
  5383. "not_between": "not between",
  5384. "begins_with": "begins with",
  5385. "not_begins_with": "doesn't begin with",
  5386. "contains": "contains",
  5387. "not_contains": "doesn't contain",
  5388. "ends_with": "ends with",
  5389. "not_ends_with": "doesn't end with",
  5390. "is_empty": "is empty",
  5391. "is_not_empty": "is not empty",
  5392. "is_null": "is null",
  5393. "is_not_null": "is not null"
  5394. },
  5395. "errors": {
  5396. "no_filter": "No filter selected",
  5397. "empty_group": "The group is empty",
  5398. "radio_empty": "No value selected",
  5399. "checkbox_empty": "No value selected",
  5400. "select_empty": "No value selected",
  5401. "string_empty": "Empty value",
  5402. "string_exceed_min_length": "Must contain at least {0} characters",
  5403. "string_exceed_max_length": "Must not contain more than {0} characters",
  5404. "string_invalid_format": "Invalid format ({0})",
  5405. "number_nan": "Not a number",
  5406. "number_not_integer": "Not an integer",
  5407. "number_not_double": "Not a real number",
  5408. "number_exceed_min": "Must be greater than {0}",
  5409. "number_exceed_max": "Must be lower than {0}",
  5410. "number_wrong_step": "Must be a multiple of {0}",
  5411. "number_between_invalid": "Invalid values, {0} is greater than {1}",
  5412. "datetime_empty": "Empty value",
  5413. "datetime_invalid": "Invalid date format ({0})",
  5414. "datetime_exceed_min": "Must be after {0}",
  5415. "datetime_exceed_max": "Must be before {0}",
  5416. "datetime_between_invalid": "Invalid values, {0} is greater than {1}",
  5417. "boolean_not_valid": "Not a boolean",
  5418. "operator_not_multiple": "Operator \"{1}\" cannot accept multiple values"
  5419. },
  5420. "invert": "Invert",
  5421. "NOT": "NOT"
  5422. };
  5423. QueryBuilder.defaults({ lang_code: 'en' });
  5424. return QueryBuilder;
  5425. }));