bootstrap-editable.js 180 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738273927402741274227432744274527462747274827492750275127522753275427552756275727582759276027612762276327642765276627672768276927702771277227732774277527762777277827792780278127822783278427852786278727882789279027912792279327942795279627972798279928002801280228032804280528062807280828092810281128122813281428152816281728182819282028212822282328242825282628272828282928302831283228332834283528362837283828392840284128422843284428452846284728482849285028512852285328542855285628572858285928602861286228632864286528662867286828692870287128722873287428752876287728782879288028812882288328842885288628872888288928902891289228932894289528962897289828992900290129022903290429052906290729082909291029112912291329142915291629172918291929202921292229232924292529262927292829292930293129322933293429352936293729382939294029412942294329442945294629472948294929502951295229532954295529562957295829592960296129622963296429652966296729682969297029712972297329742975297629772978297929802981298229832984298529862987298829892990299129922993299429952996299729982999300030013002300330043005300630073008300930103011301230133014301530163017301830193020302130223023302430253026302730283029303030313032303330343035303630373038303930403041304230433044304530463047304830493050305130523053305430553056305730583059306030613062306330643065306630673068306930703071307230733074307530763077307830793080308130823083308430853086308730883089309030913092309330943095309630973098309931003101310231033104310531063107310831093110311131123113311431153116311731183119312031213122312331243125312631273128312931303131313231333134313531363137313831393140314131423143314431453146314731483149315031513152315331543155315631573158315931603161316231633164316531663167316831693170317131723173317431753176317731783179318031813182318331843185318631873188318931903191319231933194319531963197319831993200320132023203320432053206320732083209321032113212321332143215321632173218321932203221322232233224322532263227322832293230323132323233323432353236323732383239324032413242324332443245324632473248324932503251325232533254325532563257325832593260326132623263326432653266326732683269327032713272327332743275327632773278327932803281328232833284328532863287328832893290329132923293329432953296329732983299330033013302330333043305330633073308330933103311331233133314331533163317331833193320332133223323332433253326332733283329333033313332333333343335333633373338333933403341334233433344334533463347334833493350335133523353335433553356335733583359336033613362336333643365336633673368336933703371337233733374337533763377337833793380338133823383338433853386338733883389339033913392339333943395339633973398339934003401340234033404340534063407340834093410341134123413341434153416341734183419342034213422342334243425342634273428342934303431343234333434343534363437343834393440344134423443344434453446344734483449345034513452345334543455345634573458345934603461346234633464346534663467346834693470347134723473347434753476347734783479348034813482348334843485348634873488348934903491349234933494349534963497349834993500350135023503350435053506350735083509351035113512351335143515351635173518351935203521352235233524352535263527352835293530353135323533353435353536353735383539354035413542354335443545354635473548354935503551355235533554355535563557355835593560356135623563356435653566356735683569357035713572357335743575357635773578357935803581358235833584358535863587358835893590359135923593359435953596359735983599360036013602360336043605360636073608360936103611361236133614361536163617361836193620362136223623362436253626362736283629363036313632363336343635363636373638363936403641364236433644364536463647364836493650365136523653365436553656365736583659366036613662366336643665366636673668366936703671367236733674367536763677367836793680368136823683368436853686368736883689369036913692369336943695369636973698369937003701370237033704370537063707370837093710371137123713371437153716371737183719372037213722372337243725372637273728372937303731373237333734373537363737373837393740374137423743374437453746374737483749375037513752375337543755375637573758375937603761376237633764376537663767376837693770377137723773377437753776377737783779378037813782378337843785378637873788378937903791379237933794379537963797379837993800380138023803380438053806380738083809381038113812381338143815381638173818381938203821382238233824382538263827382838293830383138323833383438353836383738383839384038413842384338443845384638473848384938503851385238533854385538563857385838593860386138623863386438653866386738683869387038713872387338743875387638773878387938803881388238833884388538863887388838893890389138923893389438953896389738983899390039013902390339043905390639073908390939103911391239133914391539163917391839193920392139223923392439253926392739283929393039313932393339343935393639373938393939403941394239433944394539463947394839493950395139523953395439553956395739583959396039613962396339643965396639673968396939703971397239733974397539763977397839793980398139823983398439853986398739883989399039913992399339943995399639973998399940004001400240034004400540064007400840094010401140124013401440154016401740184019402040214022402340244025402640274028402940304031403240334034403540364037403840394040404140424043404440454046404740484049405040514052405340544055405640574058405940604061406240634064406540664067406840694070407140724073407440754076407740784079408040814082408340844085408640874088408940904091409240934094409540964097409840994100410141024103410441054106410741084109411041114112411341144115411641174118411941204121412241234124412541264127412841294130413141324133413441354136413741384139414041414142414341444145414641474148414941504151415241534154415541564157415841594160416141624163416441654166416741684169417041714172417341744175417641774178417941804181418241834184418541864187418841894190419141924193419441954196419741984199420042014202420342044205420642074208420942104211421242134214421542164217421842194220422142224223422442254226422742284229423042314232423342344235423642374238423942404241424242434244424542464247424842494250425142524253425442554256425742584259426042614262426342644265426642674268426942704271427242734274427542764277427842794280428142824283428442854286428742884289429042914292429342944295429642974298429943004301430243034304430543064307430843094310431143124313431443154316431743184319432043214322432343244325432643274328432943304331433243334334433543364337433843394340434143424343434443454346434743484349435043514352435343544355435643574358435943604361436243634364436543664367436843694370437143724373437443754376437743784379438043814382438343844385438643874388438943904391439243934394439543964397439843994400440144024403440444054406440744084409441044114412441344144415441644174418441944204421442244234424442544264427442844294430443144324433443444354436443744384439444044414442444344444445444644474448444944504451445244534454445544564457445844594460446144624463446444654466446744684469447044714472447344744475447644774478447944804481448244834484448544864487448844894490449144924493449444954496449744984499450045014502450345044505450645074508450945104511451245134514451545164517451845194520452145224523452445254526452745284529453045314532453345344535453645374538453945404541454245434544454545464547454845494550455145524553455445554556455745584559456045614562456345644565456645674568456945704571457245734574457545764577457845794580458145824583458445854586458745884589459045914592459345944595459645974598459946004601460246034604460546064607460846094610461146124613461446154616461746184619462046214622462346244625462646274628462946304631463246334634463546364637463846394640464146424643464446454646464746484649465046514652465346544655465646574658465946604661466246634664466546664667466846694670467146724673467446754676467746784679468046814682468346844685468646874688468946904691469246934694469546964697469846994700470147024703470447054706470747084709471047114712471347144715471647174718471947204721472247234724472547264727472847294730473147324733473447354736473747384739474047414742474347444745474647474748474947504751475247534754475547564757475847594760476147624763476447654766476747684769477047714772477347744775477647774778477947804781478247834784478547864787478847894790479147924793479447954796479747984799480048014802480348044805480648074808480948104811481248134814481548164817481848194820482148224823482448254826482748284829483048314832483348344835483648374838483948404841484248434844484548464847484848494850485148524853485448554856485748584859486048614862486348644865486648674868486948704871487248734874487548764877487848794880488148824883488448854886488748884889489048914892489348944895489648974898489949004901490249034904490549064907490849094910491149124913491449154916491749184919492049214922492349244925492649274928492949304931493249334934493549364937493849394940494149424943494449454946494749484949495049514952495349544955495649574958495949604961496249634964496549664967496849694970497149724973497449754976497749784979498049814982498349844985498649874988498949904991499249934994499549964997499849995000500150025003500450055006500750085009501050115012501350145015501650175018501950205021502250235024502550265027502850295030503150325033503450355036503750385039504050415042504350445045504650475048504950505051505250535054505550565057505850595060506150625063506450655066506750685069507050715072507350745075507650775078507950805081508250835084508550865087508850895090509150925093509450955096509750985099510051015102510351045105510651075108510951105111511251135114511551165117511851195120512151225123512451255126512751285129513051315132513351345135513651375138513951405141514251435144514551465147514851495150515151525153515451555156515751585159516051615162516351645165516651675168516951705171517251735174517551765177517851795180518151825183518451855186518751885189519051915192519351945195519651975198519952005201520252035204520552065207520852095210521152125213521452155216521752185219522052215222522352245225522652275228522952305231523252335234523552365237523852395240524152425243524452455246524752485249
  1. /*! X-editable - v1.5.1
  2. * In-place editing with Twitter Bootstrap, jQuery UI or pure jQuery
  3. * http://github.com/vitalets/x-editable
  4. * Copyright (c) 2013 Vitaliy Potapov; Licensed MIT */
  5. /**
  6. Form with single input element, two buttons and two states: normal/loading.
  7. Applied as jQuery method to DIV tag (not to form tag!). This is because form can be in loading state when spinner shown.
  8. Editableform is linked with one of input types, e.g. 'text', 'select' etc.
  9. @class editableform
  10. @uses text
  11. @uses textarea
  12. **/
  13. (function ($) {
  14. "use strict";
  15. var EditableForm = function (div, options) {
  16. this.options = $.extend({}, $.fn.editableform.defaults, options);
  17. this.$div = $(div); //div, containing form. Not form tag. Not editable-element.
  18. if(!this.options.scope) {
  19. this.options.scope = this;
  20. }
  21. //nothing shown after init
  22. };
  23. EditableForm.prototype = {
  24. constructor: EditableForm,
  25. initInput: function() { //called once
  26. //take input from options (as it is created in editable-element)
  27. this.input = this.options.input;
  28. //set initial value
  29. //todo: may be add check: typeof str === 'string' ?
  30. this.value = this.input.str2value(this.options.value);
  31. //prerender: get input.$input
  32. this.input.prerender();
  33. },
  34. initTemplate: function() {
  35. this.$form = $($.fn.editableform.template);
  36. },
  37. initButtons: function() {
  38. var $btn = this.$form.find('.editable-buttons');
  39. $btn.append($.fn.editableform.buttons);
  40. if(this.options.showbuttons === 'bottom') {
  41. $btn.addClass('editable-buttons-bottom');
  42. }
  43. },
  44. /**
  45. Renders editableform
  46. @method render
  47. **/
  48. render: function() {
  49. //init loader
  50. this.$loading = $($.fn.editableform.loading);
  51. this.$div.empty().append(this.$loading);
  52. //init form template and buttons
  53. this.initTemplate();
  54. if(this.options.showbuttons) {
  55. this.initButtons();
  56. } else {
  57. this.$form.find('.editable-buttons').remove();
  58. }
  59. //show loading state
  60. this.showLoading();
  61. //flag showing is form now saving value to server.
  62. //It is needed to wait when closing form.
  63. this.isSaving = false;
  64. /**
  65. Fired when rendering starts
  66. @event rendering
  67. @param {Object} event event object
  68. **/
  69. this.$div.triggerHandler('rendering');
  70. //init input
  71. this.initInput();
  72. //append input to form
  73. this.$form.find('div.editable-input').append(this.input.$tpl);
  74. //append form to container
  75. this.$div.append(this.$form);
  76. //render input
  77. $.when(this.input.render())
  78. .then($.proxy(function () {
  79. //setup input to submit automatically when no buttons shown
  80. if(!this.options.showbuttons) {
  81. this.input.autosubmit();
  82. }
  83. //attach 'cancel' handler
  84. this.$form.find('.editable-cancel').click($.proxy(this.cancel, this));
  85. if(this.input.error) {
  86. this.error(this.input.error);
  87. this.$form.find('.editable-submit').attr('disabled', true);
  88. this.input.$input.attr('disabled', true);
  89. //prevent form from submitting
  90. this.$form.submit(function(e){ e.preventDefault(); });
  91. } else {
  92. this.error(false);
  93. this.input.$input.removeAttr('disabled');
  94. this.$form.find('.editable-submit').removeAttr('disabled');
  95. var value = (this.value === null || this.value === undefined || this.value === '') ? this.options.defaultValue : this.value;
  96. this.input.value2input(value);
  97. //attach submit handler
  98. this.$form.submit($.proxy(this.submit, this));
  99. }
  100. /**
  101. Fired when form is rendered
  102. @event rendered
  103. @param {Object} event event object
  104. **/
  105. this.$div.triggerHandler('rendered');
  106. this.showForm();
  107. //call postrender method to perform actions required visibility of form
  108. if(this.input.postrender) {
  109. this.input.postrender();
  110. }
  111. }, this));
  112. },
  113. cancel: function() {
  114. /**
  115. Fired when form was cancelled by user
  116. @event cancel
  117. @param {Object} event event object
  118. **/
  119. this.$div.triggerHandler('cancel');
  120. },
  121. showLoading: function() {
  122. var w, h;
  123. if(this.$form) {
  124. //set loading size equal to form
  125. w = this.$form.outerWidth();
  126. h = this.$form.outerHeight();
  127. if(w) {
  128. this.$loading.width(w);
  129. }
  130. if(h) {
  131. this.$loading.height(h);
  132. }
  133. this.$form.hide();
  134. } else {
  135. //stretch loading to fill container width
  136. w = this.$loading.parent().width();
  137. if(w) {
  138. this.$loading.width(w);
  139. }
  140. }
  141. this.$loading.show();
  142. },
  143. showForm: function(activate) {
  144. this.$loading.hide();
  145. this.$form.show();
  146. if(activate !== false) {
  147. this.input.activate();
  148. }
  149. /**
  150. Fired when form is shown
  151. @event show
  152. @param {Object} event event object
  153. **/
  154. this.$div.triggerHandler('show');
  155. },
  156. error: function(msg) {
  157. var $group = this.$form.find('.control-group'),
  158. $block = this.$form.find('.editable-error-block'),
  159. lines;
  160. if(msg === false) {
  161. $group.removeClass($.fn.editableform.errorGroupClass);
  162. $block.removeClass($.fn.editableform.errorBlockClass).empty().hide();
  163. } else {
  164. //convert newline to <br> for more pretty error display
  165. if(msg) {
  166. lines = (''+msg).split('\n');
  167. for (var i = 0; i < lines.length; i++) {
  168. lines[i] = $('<div>').text(lines[i]).html();
  169. }
  170. msg = lines.join('<br>');
  171. }
  172. $group.addClass($.fn.editableform.errorGroupClass);
  173. $block.addClass($.fn.editableform.errorBlockClass).html(msg).show();
  174. }
  175. },
  176. submit: function(e) {
  177. e.stopPropagation();
  178. e.preventDefault();
  179. //get new value from input
  180. var newValue = this.input.input2value();
  181. //validation: if validate returns string or truthy value - means error
  182. //if returns object like {newValue: '...'} => submitted value is reassigned to it
  183. var error = this.validate(newValue);
  184. if ($.type(error) === 'object' && error.newValue !== undefined) {
  185. newValue = error.newValue;
  186. this.input.value2input(newValue);
  187. if(typeof error.msg === 'string') {
  188. this.error(error.msg);
  189. this.showForm();
  190. return;
  191. }
  192. } else if (error) {
  193. this.error(error);
  194. this.showForm();
  195. return;
  196. }
  197. //if value not changed --> trigger 'nochange' event and return
  198. /*jslint eqeq: true*/
  199. if (!this.options.savenochange && this.input.value2str(newValue) == this.input.value2str(this.value)) {
  200. /*jslint eqeq: false*/
  201. /**
  202. Fired when value not changed but form is submitted. Requires savenochange = false.
  203. @event nochange
  204. @param {Object} event event object
  205. **/
  206. this.$div.triggerHandler('nochange');
  207. return;
  208. }
  209. //convert value for submitting to server
  210. var submitValue = this.input.value2submit(newValue);
  211. this.isSaving = true;
  212. //sending data to server
  213. $.when(this.save(submitValue))
  214. .done($.proxy(function(response) {
  215. this.isSaving = false;
  216. //run success callback
  217. var res = typeof this.options.success === 'function' ? this.options.success.call(this.options.scope, response, newValue) : null;
  218. //if success callback returns false --> keep form open and do not activate input
  219. if(res === false) {
  220. this.error(false);
  221. this.showForm(false);
  222. return;
  223. }
  224. //if success callback returns string --> keep form open, show error and activate input
  225. if(typeof res === 'string') {
  226. this.error(res);
  227. this.showForm();
  228. return;
  229. }
  230. //if success callback returns object like {newValue: <something>} --> use that value instead of submitted
  231. //it is usefull if you want to chnage value in url-function
  232. if(res && typeof res === 'object' && res.hasOwnProperty('newValue')) {
  233. newValue = res.newValue;
  234. }
  235. //clear error message
  236. this.error(false);
  237. this.value = newValue;
  238. /**
  239. Fired when form is submitted
  240. @event save
  241. @param {Object} event event object
  242. @param {Object} params additional params
  243. @param {mixed} params.newValue raw new value
  244. @param {mixed} params.submitValue submitted value as string
  245. @param {Object} params.response ajax response
  246. @example
  247. $('#form-div').on('save'), function(e, params){
  248. if(params.newValue === 'username') {...}
  249. });
  250. **/
  251. this.$div.triggerHandler('save', {newValue: newValue, submitValue: submitValue, response: response});
  252. }, this))
  253. .fail($.proxy(function(xhr) {
  254. this.isSaving = false;
  255. var msg;
  256. if(typeof this.options.error === 'function') {
  257. msg = this.options.error.call(this.options.scope, xhr, newValue);
  258. } else {
  259. msg = typeof xhr === 'string' ? xhr : xhr.responseText || xhr.statusText || 'Unknown error!';
  260. }
  261. this.error(msg);
  262. this.showForm();
  263. }, this));
  264. },
  265. save: function(submitValue) {
  266. //try parse composite pk defined as json string in data-pk
  267. this.options.pk = $.fn.editableutils.tryParseJson(this.options.pk, true);
  268. var pk = (typeof this.options.pk === 'function') ? this.options.pk.call(this.options.scope) : this.options.pk,
  269. /*
  270. send on server in following cases:
  271. 1. url is function
  272. 2. url is string AND (pk defined OR send option = always)
  273. */
  274. send = !!(typeof this.options.url === 'function' || (this.options.url && ((this.options.send === 'always') || (this.options.send === 'auto' && pk !== null && pk !== undefined)))),
  275. params;
  276. if (send) { //send to server
  277. this.showLoading();
  278. //standard params
  279. params = {
  280. name: this.options.name || '',
  281. value: submitValue,
  282. pk: pk
  283. };
  284. //additional params
  285. if(typeof this.options.params === 'function') {
  286. params = this.options.params.call(this.options.scope, params);
  287. } else {
  288. //try parse json in single quotes (from data-params attribute)
  289. this.options.params = $.fn.editableutils.tryParseJson(this.options.params, true);
  290. $.extend(params, this.options.params);
  291. }
  292. if(typeof this.options.url === 'function') { //user's function
  293. return this.options.url.call(this.options.scope, params);
  294. } else {
  295. //send ajax to server and return deferred object
  296. return $.ajax($.extend({
  297. url : this.options.url,
  298. data : params,
  299. type : 'POST'
  300. }, this.options.ajaxOptions));
  301. }
  302. }
  303. },
  304. validate: function (value) {
  305. if (value === undefined) {
  306. value = this.value;
  307. }
  308. if (typeof this.options.validate === 'function') {
  309. return this.options.validate.call(this.options.scope, value);
  310. }
  311. },
  312. option: function(key, value) {
  313. if(key in this.options) {
  314. this.options[key] = value;
  315. }
  316. if(key === 'value') {
  317. this.setValue(value);
  318. }
  319. //do not pass option to input as it is passed in editable-element
  320. },
  321. setValue: function(value, convertStr) {
  322. if(convertStr) {
  323. this.value = this.input.str2value(value);
  324. } else {
  325. this.value = value;
  326. }
  327. //if form is visible, update input
  328. if(this.$form && this.$form.is(':visible')) {
  329. this.input.value2input(this.value);
  330. }
  331. }
  332. };
  333. /*
  334. Initialize editableform. Applied to jQuery object.
  335. @method $().editableform(options)
  336. @params {Object} options
  337. @example
  338. var $form = $('&lt;div&gt;').editableform({
  339. type: 'text',
  340. name: 'username',
  341. url: '/post',
  342. value: 'vitaliy'
  343. });
  344. //to display form you should call 'render' method
  345. $form.editableform('render');
  346. */
  347. $.fn.editableform = function (option) {
  348. var args = arguments;
  349. return this.each(function () {
  350. var $this = $(this),
  351. data = $this.data('editableform'),
  352. options = typeof option === 'object' && option;
  353. if (!data) {
  354. $this.data('editableform', (data = new EditableForm(this, options)));
  355. }
  356. if (typeof option === 'string') { //call method
  357. data[option].apply(data, Array.prototype.slice.call(args, 1));
  358. }
  359. });
  360. };
  361. //keep link to constructor to allow inheritance
  362. $.fn.editableform.Constructor = EditableForm;
  363. //defaults
  364. $.fn.editableform.defaults = {
  365. /* see also defaults for input */
  366. /**
  367. Type of input. Can be <code>text|textarea|select|date|checklist</code>
  368. @property type
  369. @type string
  370. @default 'text'
  371. **/
  372. type: 'text',
  373. /**
  374. Url for submit, e.g. <code>'/post'</code>
  375. If function - it will be called instead of ajax. Function should return deferred object to run fail/done callbacks.
  376. @property url
  377. @type string|function
  378. @default null
  379. @example
  380. url: function(params) {
  381. var d = new $.Deferred;
  382. if(params.value === 'abc') {
  383. return d.reject('error message'); //returning error via deferred object
  384. } else {
  385. //async saving data in js model
  386. someModel.asyncSaveMethod({
  387. ...,
  388. success: function(){
  389. d.resolve();
  390. }
  391. });
  392. return d.promise();
  393. }
  394. }
  395. **/
  396. url:null,
  397. /**
  398. Additional params for submit. If defined as <code>object</code> - it is **appended** to original ajax data (pk, name and value).
  399. If defined as <code>function</code> - returned object **overwrites** original ajax data.
  400. @example
  401. params: function(params) {
  402. //originally params contain pk, name and value
  403. params.a = 1;
  404. return params;
  405. }
  406. @property params
  407. @type object|function
  408. @default null
  409. **/
  410. params:null,
  411. /**
  412. Name of field. Will be submitted on server. Can be taken from <code>id</code> attribute
  413. @property name
  414. @type string
  415. @default null
  416. **/
  417. name: null,
  418. /**
  419. Primary key of editable object (e.g. record id in database). For composite keys use object, e.g. <code>{id: 1, lang: 'en'}</code>.
  420. Can be calculated dynamically via function.
  421. @property pk
  422. @type string|object|function
  423. @default null
  424. **/
  425. pk: null,
  426. /**
  427. Initial value. If not defined - will be taken from element's content.
  428. For __select__ type should be defined (as it is ID of shown text).
  429. @property value
  430. @type string|object
  431. @default null
  432. **/
  433. value: null,
  434. /**
  435. Value that will be displayed in input if original field value is empty (`null|undefined|''`).
  436. @property defaultValue
  437. @type string|object
  438. @default null
  439. @since 1.4.6
  440. **/
  441. defaultValue: null,
  442. /**
  443. Strategy for sending data on server. Can be `auto|always|never`.
  444. When 'auto' data will be sent on server **only if pk and url defined**, otherwise new value will be stored locally.
  445. @property send
  446. @type string
  447. @default 'auto'
  448. **/
  449. send: 'auto',
  450. /**
  451. Function for client-side validation. If returns string - means validation not passed and string showed as error.
  452. Since 1.5.1 you can modify submitted value by returning object from `validate`:
  453. `{newValue: '...'}` or `{newValue: '...', msg: '...'}`
  454. @property validate
  455. @type function
  456. @default null
  457. @example
  458. validate: function(value) {
  459. if($.trim(value) == '') {
  460. return 'This field is required';
  461. }
  462. }
  463. **/
  464. validate: null,
  465. /**
  466. Success callback. Called when value successfully sent on server and **response status = 200**.
  467. Usefull to work with json response. For example, if your backend response can be <code>{success: true}</code>
  468. or <code>{success: false, msg: "server error"}</code> you can check it inside this callback.
  469. If it returns **string** - means error occured and string is shown as error message.
  470. If it returns **object like** <code>{newValue: &lt;something&gt;}</code> - it overwrites value, submitted by user.
  471. Otherwise newValue simply rendered into element.
  472. @property success
  473. @type function
  474. @default null
  475. @example
  476. success: function(response, newValue) {
  477. if(!response.success) return response.msg;
  478. }
  479. **/
  480. success: null,
  481. /**
  482. Error callback. Called when request failed (response status != 200).
  483. Usefull when you want to parse error response and display a custom message.
  484. Must return **string** - the message to be displayed in the error block.
  485. @property error
  486. @type function
  487. @default null
  488. @since 1.4.4
  489. @example
  490. error: function(response, newValue) {
  491. if(response.status === 500) {
  492. return 'Service unavailable. Please try later.';
  493. } else {
  494. return response.responseText;
  495. }
  496. }
  497. **/
  498. error: null,
  499. /**
  500. Additional options for submit ajax request.
  501. List of values: http://api.jquery.com/jQuery.ajax
  502. @property ajaxOptions
  503. @type object
  504. @default null
  505. @since 1.1.1
  506. @example
  507. ajaxOptions: {
  508. type: 'put',
  509. dataType: 'json'
  510. }
  511. **/
  512. ajaxOptions: null,
  513. /**
  514. Where to show buttons: left(true)|bottom|false
  515. Form without buttons is auto-submitted.
  516. @property showbuttons
  517. @type boolean|string
  518. @default true
  519. @since 1.1.1
  520. **/
  521. showbuttons: true,
  522. /**
  523. Scope for callback methods (success, validate).
  524. If <code>null</code> means editableform instance itself.
  525. @property scope
  526. @type DOMElement|object
  527. @default null
  528. @since 1.2.0
  529. @private
  530. **/
  531. scope: null,
  532. /**
  533. Whether to save or cancel value when it was not changed but form was submitted
  534. @property savenochange
  535. @type boolean
  536. @default false
  537. @since 1.2.0
  538. **/
  539. savenochange: false
  540. };
  541. /*
  542. Note: following params could redefined in engine: bootstrap or jqueryui:
  543. Classes 'control-group' and 'editable-error-block' must always present!
  544. */
  545. $.fn.editableform.template = '<form class="form-inline editableform">'+
  546. '<div class="control-group">' +
  547. '<div><div class="editable-input"></div><div class="editable-buttons"></div></div>'+
  548. '<div class="editable-error-block"></div>' +
  549. '</div>' +
  550. '</form>';
  551. //loading div
  552. $.fn.editableform.loading = '<div class="editableform-loading"></div>';
  553. //buttons
  554. $.fn.editableform.buttons = '<button type="submit" class="editable-submit">ok</button>'+
  555. '<button type="button" class="editable-cancel">cancel</button>';
  556. //error class attached to control-group
  557. $.fn.editableform.errorGroupClass = null;
  558. //error class attached to editable-error-block
  559. $.fn.editableform.errorBlockClass = 'editable-error';
  560. //engine
  561. $.fn.editableform.engine = 'jquery';
  562. }(window.jQuery));
  563. /**
  564. * EditableForm utilites
  565. */
  566. (function ($) {
  567. "use strict";
  568. //utils
  569. $.fn.editableutils = {
  570. /**
  571. * classic JS inheritance function
  572. */
  573. inherit: function (Child, Parent) {
  574. var F = function() { };
  575. F.prototype = Parent.prototype;
  576. Child.prototype = new F();
  577. Child.prototype.constructor = Child;
  578. Child.superclass = Parent.prototype;
  579. },
  580. /**
  581. * set caret position in input
  582. * see http://stackoverflow.com/questions/499126/jquery-set-cursor-position-in-text-area
  583. */
  584. setCursorPosition: function(elem, pos) {
  585. if (elem.setSelectionRange) {
  586. elem.setSelectionRange(pos, pos);
  587. } else if (elem.createTextRange) {
  588. var range = elem.createTextRange();
  589. range.collapse(true);
  590. range.moveEnd('character', pos);
  591. range.moveStart('character', pos);
  592. range.select();
  593. }
  594. },
  595. /**
  596. * function to parse JSON in *single* quotes. (jquery automatically parse only double quotes)
  597. * That allows such code as: <a data-source="{'a': 'b', 'c': 'd'}">
  598. * safe = true --> means no exception will be thrown
  599. * for details see http://stackoverflow.com/questions/7410348/how-to-set-json-format-to-html5-data-attributes-in-the-jquery
  600. */
  601. tryParseJson: function(s, safe) {
  602. if (typeof s === 'string' && s.length && s.match(/^[\{\[].*[\}\]]$/)) {
  603. if (safe) {
  604. try {
  605. /*jslint evil: true*/
  606. s = (new Function('return ' + s))();
  607. /*jslint evil: false*/
  608. } catch (e) {} finally {
  609. return s;
  610. }
  611. } else {
  612. /*jslint evil: true*/
  613. s = (new Function('return ' + s))();
  614. /*jslint evil: false*/
  615. }
  616. }
  617. return s;
  618. },
  619. /**
  620. * slice object by specified keys
  621. */
  622. sliceObj: function(obj, keys, caseSensitive /* default: false */) {
  623. var key, keyLower, newObj = {};
  624. if (!$.isArray(keys) || !keys.length) {
  625. return newObj;
  626. }
  627. for (var i = 0; i < keys.length; i++) {
  628. key = keys[i];
  629. if (obj.hasOwnProperty(key)) {
  630. newObj[key] = obj[key];
  631. }
  632. if(caseSensitive === true) {
  633. continue;
  634. }
  635. //when getting data-* attributes via $.data() it's converted to lowercase.
  636. //details: http://stackoverflow.com/questions/7602565/using-data-attributes-with-jquery
  637. //workaround is code below.
  638. keyLower = key.toLowerCase();
  639. if (obj.hasOwnProperty(keyLower)) {
  640. newObj[key] = obj[keyLower];
  641. }
  642. }
  643. return newObj;
  644. },
  645. /*
  646. exclude complex objects from $.data() before pass to config
  647. */
  648. getConfigData: function($element) {
  649. var data = {};
  650. $.each($element.data(), function(k, v) {
  651. if(typeof v !== 'object' || (v && typeof v === 'object' && (v.constructor === Object || v.constructor === Array))) {
  652. data[k] = v;
  653. }
  654. });
  655. return data;
  656. },
  657. /*
  658. returns keys of object
  659. */
  660. objectKeys: function(o) {
  661. if (Object.keys) {
  662. return Object.keys(o);
  663. } else {
  664. if (o !== Object(o)) {
  665. throw new TypeError('Object.keys called on a non-object');
  666. }
  667. var k=[], p;
  668. for (p in o) {
  669. if (Object.prototype.hasOwnProperty.call(o,p)) {
  670. k.push(p);
  671. }
  672. }
  673. return k;
  674. }
  675. },
  676. /**
  677. method to escape html.
  678. **/
  679. escape: function(str) {
  680. return $('<div>').text(str).html();
  681. },
  682. /*
  683. returns array items from sourceData having value property equal or inArray of 'value'
  684. */
  685. itemsByValue: function(value, sourceData, valueProp) {
  686. if(!sourceData || value === null) {
  687. return [];
  688. }
  689. if (typeof(valueProp) !== "function") {
  690. var idKey = valueProp || 'value';
  691. valueProp = function (e) { return e[idKey]; };
  692. }
  693. var isValArray = $.isArray(value),
  694. result = [],
  695. that = this;
  696. $.each(sourceData, function(i, o) {
  697. if(o.children) {
  698. result = result.concat(that.itemsByValue(value, o.children, valueProp));
  699. } else {
  700. /*jslint eqeq: true*/
  701. if(isValArray) {
  702. if($.grep(value, function(v){ return v == (o && typeof o === 'object' ? valueProp(o) : o); }).length) {
  703. result.push(o);
  704. }
  705. } else {
  706. var itemValue = (o && (typeof o === 'object')) ? valueProp(o) : o;
  707. if(value == itemValue) {
  708. result.push(o);
  709. }
  710. }
  711. /*jslint eqeq: false*/
  712. }
  713. });
  714. return result;
  715. },
  716. /*
  717. Returns input by options: type, mode.
  718. */
  719. createInput: function(options) {
  720. var TypeConstructor, typeOptions, input,
  721. type = options.type;
  722. //`date` is some kind of virtual type that is transformed to one of exact types
  723. //depending on mode and core lib
  724. if(type === 'date') {
  725. //inline
  726. if(options.mode === 'inline') {
  727. if($.fn.editabletypes.datefield) {
  728. type = 'datefield';
  729. } else if($.fn.editabletypes.dateuifield) {
  730. type = 'dateuifield';
  731. }
  732. //popup
  733. } else {
  734. if($.fn.editabletypes.date) {
  735. type = 'date';
  736. } else if($.fn.editabletypes.dateui) {
  737. type = 'dateui';
  738. }
  739. }
  740. //if type still `date` and not exist in types, replace with `combodate` that is base input
  741. if(type === 'date' && !$.fn.editabletypes.date) {
  742. type = 'combodate';
  743. }
  744. }
  745. //`datetime` should be datetimefield in 'inline' mode
  746. if(type === 'datetime' && options.mode === 'inline') {
  747. type = 'datetimefield';
  748. }
  749. //change wysihtml5 to textarea for jquery UI and plain versions
  750. if(type === 'wysihtml5' && !$.fn.editabletypes[type]) {
  751. type = 'textarea';
  752. }
  753. //create input of specified type. Input will be used for converting value, not in form
  754. if(typeof $.fn.editabletypes[type] === 'function') {
  755. TypeConstructor = $.fn.editabletypes[type];
  756. typeOptions = this.sliceObj(options, this.objectKeys(TypeConstructor.defaults));
  757. input = new TypeConstructor(typeOptions);
  758. return input;
  759. } else {
  760. $.error('Unknown type: '+ type);
  761. return false;
  762. }
  763. },
  764. //see http://stackoverflow.com/questions/7264899/detect-css-transitions-using-javascript-and-without-modernizr
  765. supportsTransitions: function () {
  766. var b = document.body || document.documentElement,
  767. s = b.style,
  768. p = 'transition',
  769. v = ['Moz', 'Webkit', 'Khtml', 'O', 'ms'];
  770. if(typeof s[p] === 'string') {
  771. return true;
  772. }
  773. // Tests for vendor specific prop
  774. p = p.charAt(0).toUpperCase() + p.substr(1);
  775. for(var i=0; i<v.length; i++) {
  776. if(typeof s[v[i] + p] === 'string') {
  777. return true;
  778. }
  779. }
  780. return false;
  781. }
  782. };
  783. }(window.jQuery));
  784. /**
  785. Attaches stand-alone container with editable-form to HTML element. Element is used only for positioning, value is not stored anywhere.<br>
  786. This method applied internally in <code>$().editable()</code>. You should subscribe on it's events (save / cancel) to get profit of it.<br>
  787. Final realization can be different: bootstrap-popover, jqueryui-tooltip, poshytip, inline-div. It depends on which js file you include.<br>
  788. Applied as jQuery method.
  789. @class editableContainer
  790. @uses editableform
  791. **/
  792. (function ($) {
  793. "use strict";
  794. var Popup = function (element, options) {
  795. this.init(element, options);
  796. };
  797. var Inline = function (element, options) {
  798. this.init(element, options);
  799. };
  800. //methods
  801. Popup.prototype = {
  802. containerName: null, //method to call container on element
  803. containerDataName: null, //object name in element's .data()
  804. innerCss: null, //tbd in child class
  805. containerClass: 'editable-container editable-popup', //css class applied to container element
  806. defaults: {}, //container itself defaults
  807. init: function(element, options) {
  808. this.$element = $(element);
  809. //since 1.4.1 container do not use data-* directly as they already merged into options.
  810. this.options = $.extend({}, $.fn.editableContainer.defaults, options);
  811. this.splitOptions();
  812. //set scope of form callbacks to element
  813. this.formOptions.scope = this.$element[0];
  814. this.initContainer();
  815. //flag to hide container, when saving value will finish
  816. this.delayedHide = false;
  817. //bind 'destroyed' listener to destroy container when element is removed from dom
  818. this.$element.on('destroyed', $.proxy(function(){
  819. this.destroy();
  820. }, this));
  821. //attach document handler to close containers on click / escape
  822. if(!$(document).data('editable-handlers-attached')) {
  823. //close all on escape
  824. $(document).on('keyup.editable', function (e) {
  825. if (e.which === 27) {
  826. $('.editable-open').editableContainer('hide');
  827. //todo: return focus on element
  828. }
  829. });
  830. //close containers when click outside
  831. //(mousedown could be better than click, it closes everything also on drag drop)
  832. $(document).on('click.editable', function(e) {
  833. var $target = $(e.target), i,
  834. exclude_classes = ['.editable-container',
  835. '.ui-datepicker-header',
  836. '.datepicker', //in inline mode datepicker is rendered into body
  837. '.modal-backdrop',
  838. '.bootstrap-wysihtml5-insert-image-modal',
  839. '.bootstrap-wysihtml5-insert-link-modal'
  840. ];
  841. //select2 has extra body click in IE
  842. //see: https://github.com/ivaynberg/select2/issues/1058
  843. if ($('.select2-drop-mask').is(':visible')) {
  844. return;//ACE
  845. }
  846. //check if element is detached. It occurs when clicking in bootstrap datepicker
  847. if (!$.contains(document.documentElement, e.target)) {
  848. return;
  849. }
  850. //for some reason FF 20 generates extra event (click) in select2 widget with e.target = document
  851. //we need to filter it via construction below. See https://github.com/vitalets/x-editable/issues/199
  852. //Possibly related to http://stackoverflow.com/questions/10119793/why-does-firefox-react-differently-from-webkit-and-ie-to-click-event-on-selec
  853. if($target.is(document)) {
  854. return;
  855. }
  856. //if click inside one of exclude classes --> no nothing
  857. for(i=0; i<exclude_classes.length; i++) {
  858. if($target.is(exclude_classes[i]) || $target.parents(exclude_classes[i]).length) {
  859. return;
  860. }
  861. }
  862. //close all open containers (except one - target)
  863. Popup.prototype.closeOthers(e.target);
  864. });
  865. $(document).data('editable-handlers-attached', true);
  866. }
  867. },
  868. //split options on containerOptions and formOptions
  869. splitOptions: function() {
  870. this.containerOptions = {};
  871. this.formOptions = {};
  872. if(!$.fn[this.containerName]) {
  873. throw new Error(this.containerName + ' not found. Have you included corresponding js file?');
  874. }
  875. //keys defined in container defaults go to container, others go to form
  876. for(var k in this.options) {
  877. if(k in this.defaults) {
  878. this.containerOptions[k] = this.options[k];
  879. } else {
  880. this.formOptions[k] = this.options[k];
  881. }
  882. }
  883. },
  884. /*
  885. Returns jquery object of container
  886. @method tip()
  887. */
  888. tip: function() {
  889. return this.container() ? this.container().$tip : null;
  890. },
  891. /* returns container object */
  892. container: function() {
  893. var container;
  894. //first, try get it by `containerDataName`
  895. if(this.containerDataName) {
  896. if(container = this.$element.data(this.containerDataName)) {
  897. return container;
  898. }
  899. }
  900. //second, try `containerName`
  901. container = this.$element.data(this.containerName);
  902. return container;
  903. },
  904. /* call native method of underlying container, e.g. this.$element.popover('method') */
  905. call: function() {
  906. this.$element[this.containerName].apply(this.$element, arguments);
  907. },
  908. initContainer: function(){
  909. this.call(this.containerOptions);
  910. },
  911. renderForm: function() {
  912. this.$form
  913. .editableform(this.formOptions)
  914. .on({
  915. save: $.proxy(this.save, this), //click on submit button (value changed)
  916. nochange: $.proxy(function(){ this.hide('nochange'); }, this), //click on submit button (value NOT changed)
  917. cancel: $.proxy(function(){ this.hide('cancel'); }, this), //click on calcel button
  918. show: $.proxy(function() {
  919. if(this.delayedHide) {
  920. this.hide(this.delayedHide.reason);
  921. this.delayedHide = false;
  922. } else {
  923. this.setPosition();
  924. }
  925. }, this), //re-position container every time form is shown (occurs each time after loading state)
  926. rendering: $.proxy(this.setPosition, this), //this allows to place container correctly when loading shown
  927. resize: $.proxy(this.setPosition, this), //this allows to re-position container when form size is changed
  928. rendered: $.proxy(function(){
  929. /**
  930. Fired when container is shown and form is rendered (for select will wait for loading dropdown options).
  931. **Note:** Bootstrap popover has own `shown` event that now cannot be separated from x-editable's one.
  932. The workaround is to check `arguments.length` that is always `2` for x-editable.
  933. @event shown
  934. @param {Object} event event object
  935. @example
  936. $('#username').on('shown', function(e, editable) {
  937. editable.input.$input.val('overwriting value of input..');
  938. });
  939. **/
  940. /*
  941. TODO: added second param mainly to distinguish from bootstrap's shown event. It's a hotfix that will be solved in future versions via namespaced events.
  942. */
  943. this.$element.triggerHandler('shown', $(this.options.scope).data('editable'));
  944. }, this)
  945. })
  946. .editableform('render');
  947. },
  948. /**
  949. Shows container with form
  950. @method show()
  951. @param {boolean} closeAll Whether to close all other editable containers when showing this one. Default true.
  952. **/
  953. /* Note: poshytip owerwrites this method totally! */
  954. show: function (closeAll) {
  955. this.$element.addClass('editable-open');
  956. if(closeAll !== false) {
  957. //close all open containers (except this)
  958. this.closeOthers(this.$element[0]);
  959. }
  960. //show container itself
  961. this.innerShow();
  962. this.tip().addClass(this.containerClass);
  963. /*
  964. Currently, form is re-rendered on every show.
  965. The main reason is that we dont know, what will container do with content when closed:
  966. remove(), detach() or just hide() - it depends on container.
  967. Detaching form itself before hide and re-insert before show is good solution,
  968. but visually it looks ugly --> container changes size before hide.
  969. */
  970. //if form already exist - delete previous data
  971. if(this.$form) {
  972. //todo: destroy prev data!
  973. //this.$form.destroy();
  974. }
  975. this.$form = $('<div>');
  976. //insert form into container body
  977. if(this.tip().is(this.innerCss)) {
  978. //for inline container
  979. this.tip().append(this.$form);
  980. } else {
  981. this.tip().find(this.innerCss).append(this.$form);
  982. }
  983. //render form
  984. this.renderForm();
  985. },
  986. /**
  987. Hides container with form
  988. @method hide()
  989. @param {string} reason Reason caused hiding. Can be <code>save|cancel|onblur|nochange|undefined (=manual)</code>
  990. **/
  991. hide: function(reason) {
  992. if(!this.tip() || !this.tip().is(':visible') || !this.$element.hasClass('editable-open')) {
  993. return;
  994. }
  995. //if form is saving value, schedule hide
  996. if(this.$form.data('editableform').isSaving) {
  997. this.delayedHide = {reason: reason};
  998. return;
  999. } else {
  1000. this.delayedHide = false;
  1001. }
  1002. this.$element.removeClass('editable-open');
  1003. this.innerHide();
  1004. /**
  1005. Fired when container was hidden. It occurs on both save or cancel.
  1006. **Note:** Bootstrap popover has own `hidden` event that now cannot be separated from x-editable's one.
  1007. The workaround is to check `arguments.length` that is always `2` for x-editable.
  1008. @event hidden
  1009. @param {object} event event object
  1010. @param {string} reason Reason caused hiding. Can be <code>save|cancel|onblur|nochange|manual</code>
  1011. @example
  1012. $('#username').on('hidden', function(e, reason) {
  1013. if(reason === 'save' || reason === 'cancel') {
  1014. //auto-open next editable
  1015. $(this).closest('tr').next().find('.editable').editable('show');
  1016. }
  1017. });
  1018. **/
  1019. this.$element.triggerHandler('hidden', reason || 'manual');
  1020. },
  1021. /* internal show method. To be overwritten in child classes */
  1022. innerShow: function () {
  1023. },
  1024. /* internal hide method. To be overwritten in child classes */
  1025. innerHide: function () {
  1026. },
  1027. /**
  1028. Toggles container visibility (show / hide)
  1029. @method toggle()
  1030. @param {boolean} closeAll Whether to close all other editable containers when showing this one. Default true.
  1031. **/
  1032. toggle: function(closeAll) {
  1033. if(this.container() && this.tip() && this.tip().is(':visible')) {
  1034. this.hide();
  1035. } else {
  1036. this.show(closeAll);
  1037. }
  1038. },
  1039. /*
  1040. Updates the position of container when content changed.
  1041. @method setPosition()
  1042. */
  1043. setPosition: function() {
  1044. //tbd in child class
  1045. },
  1046. save: function(e, params) {
  1047. /**
  1048. Fired when new value was submitted. You can use <code>$(this).data('editableContainer')</code> inside handler to access to editableContainer instance
  1049. @event save
  1050. @param {Object} event event object
  1051. @param {Object} params additional params
  1052. @param {mixed} params.newValue submitted value
  1053. @param {Object} params.response ajax response
  1054. @example
  1055. $('#username').on('save', function(e, params) {
  1056. //assuming server response: '{success: true}'
  1057. var pk = $(this).data('editableContainer').options.pk;
  1058. if(params.response && params.response.success) {
  1059. alert('value: ' + params.newValue + ' with pk: ' + pk + ' saved!');
  1060. } else {
  1061. alert('error!');
  1062. }
  1063. });
  1064. **/
  1065. this.$element.triggerHandler('save', params);
  1066. //hide must be after trigger, as saving value may require methods of plugin, applied to input
  1067. this.hide('save');
  1068. },
  1069. /**
  1070. Sets new option
  1071. @method option(key, value)
  1072. @param {string} key
  1073. @param {mixed} value
  1074. **/
  1075. option: function(key, value) {
  1076. this.options[key] = value;
  1077. if(key in this.containerOptions) {
  1078. this.containerOptions[key] = value;
  1079. this.setContainerOption(key, value);
  1080. } else {
  1081. this.formOptions[key] = value;
  1082. if(this.$form) {
  1083. this.$form.editableform('option', key, value);
  1084. }
  1085. }
  1086. },
  1087. setContainerOption: function(key, value) {
  1088. this.call('option', key, value);
  1089. },
  1090. /**
  1091. Destroys the container instance
  1092. @method destroy()
  1093. **/
  1094. destroy: function() {
  1095. this.hide();
  1096. this.innerDestroy();
  1097. this.$element.off('destroyed');
  1098. this.$element.removeData('editableContainer');
  1099. },
  1100. /* to be overwritten in child classes */
  1101. innerDestroy: function() {
  1102. },
  1103. /*
  1104. Closes other containers except one related to passed element.
  1105. Other containers can be cancelled or submitted (depends on onblur option)
  1106. */
  1107. closeOthers: function(element) {
  1108. $('.editable-open').each(function(i, el){
  1109. //do nothing with passed element and it's children
  1110. if(el === element || $(el).find(element).length) {
  1111. return;
  1112. }
  1113. //otherwise cancel or submit all open containers
  1114. var $el = $(el),
  1115. ec = $el.data('editableContainer');
  1116. if(!ec) {
  1117. return;
  1118. }
  1119. if(ec.options.onblur === 'cancel') {
  1120. $el.data('editableContainer').hide('onblur');
  1121. } else if(ec.options.onblur === 'submit') {
  1122. $el.data('editableContainer').tip().find('form').submit();
  1123. }
  1124. });
  1125. },
  1126. /**
  1127. Activates input of visible container (e.g. set focus)
  1128. @method activate()
  1129. **/
  1130. activate: function() {
  1131. if(this.tip && this.tip().is(':visible') && this.$form) {
  1132. this.$form.data('editableform').input.activate();
  1133. }
  1134. }
  1135. };
  1136. /**
  1137. jQuery method to initialize editableContainer.
  1138. @method $().editableContainer(options)
  1139. @params {Object} options
  1140. @example
  1141. $('#edit').editableContainer({
  1142. type: 'text',
  1143. url: '/post',
  1144. pk: 1,
  1145. value: 'hello'
  1146. });
  1147. **/
  1148. $.fn.editableContainer = function (option) {
  1149. var args = arguments;
  1150. return this.each(function () {
  1151. var $this = $(this),
  1152. dataKey = 'editableContainer',
  1153. data = $this.data(dataKey),
  1154. options = typeof option === 'object' && option,
  1155. Constructor = (options.mode === 'inline') ? Inline : Popup;
  1156. if (!data) {
  1157. $this.data(dataKey, (data = new Constructor(this, options)));
  1158. }
  1159. if (typeof option === 'string') { //call method
  1160. data[option].apply(data, Array.prototype.slice.call(args, 1));
  1161. }
  1162. });
  1163. };
  1164. //store constructors
  1165. $.fn.editableContainer.Popup = Popup;
  1166. $.fn.editableContainer.Inline = Inline;
  1167. //defaults
  1168. $.fn.editableContainer.defaults = {
  1169. /**
  1170. Initial value of form input
  1171. @property value
  1172. @type mixed
  1173. @default null
  1174. @private
  1175. **/
  1176. value: null,
  1177. /**
  1178. Placement of container relative to element. Can be <code>top|right|bottom|left</code>. Not used for inline container.
  1179. @property placement
  1180. @type string
  1181. @default 'top'
  1182. **/
  1183. placement: 'top',
  1184. /**
  1185. Whether to hide container on save/cancel.
  1186. @property autohide
  1187. @type boolean
  1188. @default true
  1189. @private
  1190. **/
  1191. autohide: true,
  1192. /**
  1193. Action when user clicks outside the container. Can be <code>cancel|submit|ignore</code>.
  1194. Setting <code>ignore</code> allows to have several containers open.
  1195. @property onblur
  1196. @type string
  1197. @default 'cancel'
  1198. @since 1.1.1
  1199. **/
  1200. onblur: 'cancel',
  1201. /**
  1202. Animation speed (inline mode only)
  1203. @property anim
  1204. @type string
  1205. @default false
  1206. **/
  1207. anim: false,
  1208. /**
  1209. Mode of editable, can be `popup` or `inline`
  1210. @property mode
  1211. @type string
  1212. @default 'popup'
  1213. @since 1.4.0
  1214. **/
  1215. mode: 'popup'
  1216. };
  1217. /*
  1218. * workaround to have 'destroyed' event to destroy popover when element is destroyed
  1219. * see http://stackoverflow.com/questions/2200494/jquery-trigger-event-when-an-element-is-removed-from-the-dom
  1220. */
  1221. jQuery.event.special.destroyed = {
  1222. remove: function(o) {
  1223. if (o.handler) {
  1224. o.handler();
  1225. }
  1226. }
  1227. };
  1228. }(window.jQuery));
  1229. /**
  1230. * Editable Inline
  1231. * ---------------------
  1232. */
  1233. (function ($) {
  1234. "use strict";
  1235. //copy prototype from EditableContainer
  1236. //extend methods
  1237. $.extend($.fn.editableContainer.Inline.prototype, $.fn.editableContainer.Popup.prototype, {
  1238. containerName: 'editableform',
  1239. innerCss: '.editable-inline',
  1240. containerClass: 'editable-container editable-inline', //css class applied to container element
  1241. initContainer: function(){
  1242. //container is <span> element
  1243. this.$tip = $('<span></span>');
  1244. //convert anim to miliseconds (int)
  1245. if(!this.options.anim) {
  1246. this.options.anim = 0;
  1247. }
  1248. },
  1249. splitOptions: function() {
  1250. //all options are passed to form
  1251. this.containerOptions = {};
  1252. this.formOptions = this.options;
  1253. },
  1254. tip: function() {
  1255. return this.$tip;
  1256. },
  1257. innerShow: function () {
  1258. this.$element.hide();
  1259. this.tip().insertAfter(this.$element).show();
  1260. },
  1261. innerHide: function () {
  1262. this.$tip.hide(this.options.anim, $.proxy(function() {
  1263. this.$element.show();
  1264. this.innerDestroy();
  1265. }, this));
  1266. },
  1267. innerDestroy: function() {
  1268. if(this.tip()) {
  1269. this.tip().empty().remove();
  1270. }
  1271. }
  1272. });
  1273. }(window.jQuery));
  1274. /**
  1275. Makes editable any HTML element on the page. Applied as jQuery method.
  1276. @class editable
  1277. @uses editableContainer
  1278. **/
  1279. (function ($) {
  1280. "use strict";
  1281. var Editable = function (element, options) {
  1282. this.$element = $(element);
  1283. //data-* has more priority over js options: because dynamically created elements may change data-*
  1284. this.options = $.extend({}, $.fn.editable.defaults, options, $.fn.editableutils.getConfigData(this.$element));
  1285. if(this.options.selector) {
  1286. this.initLive();
  1287. } else {
  1288. this.init();
  1289. }
  1290. //check for transition support
  1291. if(this.options.highlight && !$.fn.editableutils.supportsTransitions()) {
  1292. this.options.highlight = false;
  1293. }
  1294. };
  1295. Editable.prototype = {
  1296. constructor: Editable,
  1297. init: function () {
  1298. var isValueByText = false,
  1299. doAutotext, finalize;
  1300. //name
  1301. this.options.name = this.options.name || this.$element.attr('id');
  1302. //create input of specified type. Input needed already here to convert value for initial display (e.g. show text by id for select)
  1303. //also we set scope option to have access to element inside input specific callbacks (e. g. source as function)
  1304. this.options.scope = this.$element[0];
  1305. this.input = $.fn.editableutils.createInput(this.options);
  1306. if(!this.input) {
  1307. return;
  1308. }
  1309. //set value from settings or by element's text
  1310. if (this.options.value === undefined || this.options.value === null) {
  1311. this.value = this.input.html2value($.trim(this.$element.html()));
  1312. isValueByText = true;
  1313. } else {
  1314. /*
  1315. value can be string when received from 'data-value' attribute
  1316. for complext objects value can be set as json string in data-value attribute,
  1317. e.g. data-value="{city: 'Moscow', street: 'Lenina'}"
  1318. */
  1319. this.options.value = $.fn.editableutils.tryParseJson(this.options.value, true);
  1320. if(typeof this.options.value === 'string') {
  1321. this.value = this.input.str2value(this.options.value);
  1322. } else {
  1323. this.value = this.options.value;
  1324. }
  1325. }
  1326. //add 'editable' class to every editable element
  1327. this.$element.addClass('editable');
  1328. //specifically for "textarea" add class .editable-pre-wrapped to keep linebreaks
  1329. if(this.input.type === 'textarea') {
  1330. this.$element.addClass('editable-pre-wrapped');
  1331. }
  1332. //attach handler activating editable. In disabled mode it just prevent default action (useful for links)
  1333. if(this.options.toggle !== 'manual') {
  1334. this.$element.addClass('editable-click');
  1335. this.$element.on(this.options.toggle + '.editable', $.proxy(function(e){
  1336. //prevent following link if editable enabled
  1337. if(!this.options.disabled) {
  1338. e.preventDefault();
  1339. }
  1340. //stop propagation not required because in document click handler it checks event target
  1341. //e.stopPropagation();
  1342. if(this.options.toggle === 'mouseenter') {
  1343. //for hover only show container
  1344. this.show();
  1345. } else {
  1346. //when toggle='click' we should not close all other containers as they will be closed automatically in document click listener
  1347. var closeAll = (this.options.toggle !== 'click');
  1348. this.toggle(closeAll);
  1349. }
  1350. }, this));
  1351. } else {
  1352. this.$element.attr('tabindex', -1); //do not stop focus on element when toggled manually
  1353. }
  1354. //if display is function it's far more convinient to have autotext = always to render correctly on init
  1355. //see https://github.com/vitalets/x-editable-yii/issues/34
  1356. if(typeof this.options.display === 'function') {
  1357. this.options.autotext = 'always';
  1358. }
  1359. //check conditions for autotext:
  1360. switch(this.options.autotext) {
  1361. case 'always':
  1362. doAutotext = true;
  1363. break;
  1364. case 'auto':
  1365. //if element text is empty and value is defined and value not generated by text --> run autotext
  1366. doAutotext = !$.trim(this.$element.text()).length && this.value !== null && this.value !== undefined && !isValueByText;
  1367. break;
  1368. default:
  1369. doAutotext = false;
  1370. }
  1371. //depending on autotext run render() or just finilize init
  1372. $.when(doAutotext ? this.render() : true).then($.proxy(function() {
  1373. if(this.options.disabled) {
  1374. this.disable();
  1375. } else {
  1376. this.enable();
  1377. }
  1378. /**
  1379. Fired when element was initialized by `$().editable()` method.
  1380. Please note that you should setup `init` handler **before** applying `editable`.
  1381. @event init
  1382. @param {Object} event event object
  1383. @param {Object} editable editable instance (as here it cannot accessed via data('editable'))
  1384. @since 1.2.0
  1385. @example
  1386. $('#username').on('init', function(e, editable) {
  1387. alert('initialized ' + editable.options.name);
  1388. });
  1389. $('#username').editable();
  1390. **/
  1391. this.$element.triggerHandler('init', this);
  1392. }, this));
  1393. },
  1394. /*
  1395. Initializes parent element for live editables
  1396. */
  1397. initLive: function() {
  1398. //store selector
  1399. var selector = this.options.selector;
  1400. //modify options for child elements
  1401. this.options.selector = false;
  1402. this.options.autotext = 'never';
  1403. //listen toggle events
  1404. this.$element.on(this.options.toggle + '.editable', selector, $.proxy(function(e){
  1405. var $target = $(e.target);
  1406. if(!$target.data('editable')) {
  1407. //if delegated element initially empty, we need to clear it's text (that was manually set to `empty` by user)
  1408. //see https://github.com/vitalets/x-editable/issues/137
  1409. if($target.hasClass(this.options.emptyclass)) {
  1410. $target.empty();
  1411. }
  1412. $target.editable(this.options).trigger(e);
  1413. }
  1414. }, this));
  1415. },
  1416. /*
  1417. Renders value into element's text.
  1418. Can call custom display method from options.
  1419. Can return deferred object.
  1420. @method render()
  1421. @param {mixed} response server response (if exist) to pass into display function
  1422. */
  1423. render: function(response) {
  1424. //do not display anything
  1425. if(this.options.display === false) {
  1426. return;
  1427. }
  1428. //if input has `value2htmlFinal` method, we pass callback in third param to be called when source is loaded
  1429. if(this.input.value2htmlFinal) {
  1430. return this.input.value2html(this.value, this.$element[0], this.options.display, response);
  1431. //if display method defined --> use it
  1432. } else if(typeof this.options.display === 'function') {
  1433. return this.options.display.call(this.$element[0], this.value, response);
  1434. //else use input's original value2html() method
  1435. } else {
  1436. return this.input.value2html(this.value, this.$element[0]);
  1437. }
  1438. },
  1439. /**
  1440. Enables editable
  1441. @method enable()
  1442. **/
  1443. enable: function() {
  1444. this.options.disabled = false;
  1445. this.$element.removeClass('editable-disabled');
  1446. this.handleEmpty(this.isEmpty);
  1447. if(this.options.toggle !== 'manual') {
  1448. if(this.$element.attr('tabindex') === '-1') {
  1449. this.$element.removeAttr('tabindex');
  1450. }
  1451. }
  1452. },
  1453. /**
  1454. Disables editable
  1455. @method disable()
  1456. **/
  1457. disable: function() {
  1458. this.options.disabled = true;
  1459. this.hide();
  1460. this.$element.addClass('editable-disabled');
  1461. this.handleEmpty(this.isEmpty);
  1462. //do not stop focus on this element
  1463. this.$element.attr('tabindex', -1);
  1464. },
  1465. /**
  1466. Toggles enabled / disabled state of editable element
  1467. @method toggleDisabled()
  1468. **/
  1469. toggleDisabled: function() {
  1470. if(this.options.disabled) {
  1471. this.enable();
  1472. } else {
  1473. this.disable();
  1474. }
  1475. },
  1476. /**
  1477. Sets new option
  1478. @method option(key, value)
  1479. @param {string|object} key option name or object with several options
  1480. @param {mixed} value option new value
  1481. @example
  1482. $('.editable').editable('option', 'pk', 2);
  1483. **/
  1484. option: function(key, value) {
  1485. //set option(s) by object
  1486. if(key && typeof key === 'object') {
  1487. $.each(key, $.proxy(function(k, v){
  1488. this.option($.trim(k), v);
  1489. }, this));
  1490. return;
  1491. }
  1492. //set option by string
  1493. this.options[key] = value;
  1494. //disabled
  1495. if(key === 'disabled') {
  1496. return value ? this.disable() : this.enable();
  1497. }
  1498. //value
  1499. if(key === 'value') {
  1500. this.setValue(value);
  1501. }
  1502. //transfer new option to container!
  1503. if(this.container) {
  1504. this.container.option(key, value);
  1505. }
  1506. //pass option to input directly (as it points to the same in form)
  1507. if(this.input.option) {
  1508. this.input.option(key, value);
  1509. }
  1510. },
  1511. /*
  1512. * set emptytext if element is empty
  1513. */
  1514. handleEmpty: function (isEmpty) {
  1515. //do not handle empty if we do not display anything
  1516. if(this.options.display === false) {
  1517. return;
  1518. }
  1519. /*
  1520. isEmpty may be set directly as param of method.
  1521. It is required when we enable/disable field and can't rely on content
  1522. as node content is text: "Empty" that is not empty %)
  1523. */
  1524. if(isEmpty !== undefined) {
  1525. this.isEmpty = isEmpty;
  1526. } else {
  1527. //detect empty
  1528. //for some inputs we need more smart check
  1529. //e.g. wysihtml5 may have <br>, <p></p>, <img>
  1530. if(typeof(this.input.isEmpty) === 'function') {
  1531. this.isEmpty = this.input.isEmpty(this.$element);
  1532. } else {
  1533. this.isEmpty = $.trim(this.$element.html()) === '';
  1534. }
  1535. }
  1536. //emptytext shown only for enabled
  1537. if(!this.options.disabled) {
  1538. if (this.isEmpty) {
  1539. this.$element.html(this.options.emptytext);
  1540. if(this.options.emptyclass) {
  1541. this.$element.addClass(this.options.emptyclass);
  1542. }
  1543. } else if(this.options.emptyclass) {
  1544. this.$element.removeClass(this.options.emptyclass);
  1545. }
  1546. } else {
  1547. //below required if element disable property was changed
  1548. if(this.isEmpty) {
  1549. this.$element.empty();
  1550. if(this.options.emptyclass) {
  1551. this.$element.removeClass(this.options.emptyclass);
  1552. }
  1553. }
  1554. }
  1555. },
  1556. /**
  1557. Shows container with form
  1558. @method show()
  1559. @param {boolean} closeAll Whether to close all other editable containers when showing this one. Default true.
  1560. **/
  1561. show: function (closeAll) {
  1562. if(this.options.disabled) {
  1563. return;
  1564. }
  1565. //init editableContainer: popover, tooltip, inline, etc..
  1566. if(!this.container) {
  1567. var containerOptions = $.extend({}, this.options, {
  1568. value: this.value,
  1569. input: this.input //pass input to form (as it is already created)
  1570. });
  1571. this.$element.editableContainer(containerOptions);
  1572. //listen `save` event
  1573. this.$element.on("save.internal", $.proxy(this.save, this));
  1574. this.container = this.$element.data('editableContainer');
  1575. } else if(this.container.tip().is(':visible')) {
  1576. return;
  1577. }
  1578. //show container
  1579. this.container.show(closeAll);
  1580. },
  1581. /**
  1582. Hides container with form
  1583. @method hide()
  1584. **/
  1585. hide: function () {
  1586. if(this.container) {
  1587. this.container.hide();
  1588. }
  1589. },
  1590. /**
  1591. Toggles container visibility (show / hide)
  1592. @method toggle()
  1593. @param {boolean} closeAll Whether to close all other editable containers when showing this one. Default true.
  1594. **/
  1595. toggle: function(closeAll) {
  1596. if(this.container && this.container.tip().is(':visible')) {
  1597. this.hide();
  1598. } else {
  1599. this.show(closeAll);
  1600. }
  1601. },
  1602. /*
  1603. * called when form was submitted
  1604. */
  1605. save: function(e, params) {
  1606. //mark element with unsaved class if needed
  1607. if(this.options.unsavedclass) {
  1608. /*
  1609. Add unsaved css to element if:
  1610. - url is not user's function
  1611. - value was not sent to server
  1612. - params.response === undefined, that means data was not sent
  1613. - value changed
  1614. */
  1615. var sent = false;
  1616. sent = sent || typeof this.options.url === 'function';
  1617. sent = sent || this.options.display === false;
  1618. sent = sent || params.response !== undefined;
  1619. sent = sent || (this.options.savenochange && this.input.value2str(this.value) !== this.input.value2str(params.newValue));
  1620. if(sent) {
  1621. this.$element.removeClass(this.options.unsavedclass);
  1622. } else {
  1623. this.$element.addClass(this.options.unsavedclass);
  1624. }
  1625. }
  1626. //highlight when saving
  1627. if(this.options.highlight) {
  1628. var $e = this.$element,
  1629. bgColor = $e.css('background-color');
  1630. $e.css('background-color', this.options.highlight);
  1631. setTimeout(function(){
  1632. if(bgColor === 'transparent') {
  1633. bgColor = '';
  1634. }
  1635. $e.css('background-color', bgColor);
  1636. $e.addClass('editable-bg-transition');
  1637. setTimeout(function(){
  1638. $e.removeClass('editable-bg-transition');
  1639. }, 1700);
  1640. }, 10);
  1641. }
  1642. //set new value
  1643. this.setValue(params.newValue, false, params.response);
  1644. /**
  1645. Fired when new value was submitted. You can use <code>$(this).data('editable')</code> to access to editable instance
  1646. @event save
  1647. @param {Object} event event object
  1648. @param {Object} params additional params
  1649. @param {mixed} params.newValue submitted value
  1650. @param {Object} params.response ajax response
  1651. @example
  1652. $('#username').on('save', function(e, params) {
  1653. alert('Saved value: ' + params.newValue);
  1654. });
  1655. **/
  1656. //event itself is triggered by editableContainer. Description here is only for documentation
  1657. },
  1658. validate: function () {
  1659. if (typeof this.options.validate === 'function') {
  1660. return this.options.validate.call(this, this.value);
  1661. }
  1662. },
  1663. /**
  1664. Sets new value of editable
  1665. @method setValue(value, convertStr)
  1666. @param {mixed} value new value
  1667. @param {boolean} convertStr whether to convert value from string to internal format
  1668. **/
  1669. setValue: function(value, convertStr, response) {
  1670. if(convertStr) {
  1671. this.value = this.input.str2value(value);
  1672. } else {
  1673. this.value = value;
  1674. }
  1675. if(this.container) {
  1676. this.container.option('value', this.value);
  1677. }
  1678. $.when(this.render(response))
  1679. .then($.proxy(function() {
  1680. this.handleEmpty();
  1681. }, this));
  1682. },
  1683. /**
  1684. Activates input of visible container (e.g. set focus)
  1685. @method activate()
  1686. **/
  1687. activate: function() {
  1688. if(this.container) {
  1689. this.container.activate();
  1690. }
  1691. },
  1692. /**
  1693. Removes editable feature from element
  1694. @method destroy()
  1695. **/
  1696. destroy: function() {
  1697. this.disable();
  1698. if(this.container) {
  1699. this.container.destroy();
  1700. }
  1701. this.input.destroy();
  1702. if(this.options.toggle !== 'manual') {
  1703. this.$element.removeClass('editable-click');
  1704. this.$element.off(this.options.toggle + '.editable');
  1705. }
  1706. this.$element.off("save.internal");
  1707. this.$element.removeClass('editable editable-open editable-disabled');
  1708. this.$element.removeData('editable');
  1709. }
  1710. };
  1711. /* EDITABLE PLUGIN DEFINITION
  1712. * ======================= */
  1713. /**
  1714. jQuery method to initialize editable element.
  1715. @method $().editable(options)
  1716. @params {Object} options
  1717. @example
  1718. $('#username').editable({
  1719. type: 'text',
  1720. url: '/post',
  1721. pk: 1
  1722. });
  1723. **/
  1724. $.fn.editable = function (option) {
  1725. //special API methods returning non-jquery object
  1726. var result = {}, args = arguments, datakey = 'editable';
  1727. switch (option) {
  1728. /**
  1729. Runs client-side validation for all matched editables
  1730. @method validate()
  1731. @returns {Object} validation errors map
  1732. @example
  1733. $('#username, #fullname').editable('validate');
  1734. // possible result:
  1735. {
  1736. username: "username is required",
  1737. fullname: "fullname should be minimum 3 letters length"
  1738. }
  1739. **/
  1740. case 'validate':
  1741. this.each(function () {
  1742. var $this = $(this), data = $this.data(datakey), error;
  1743. if (data && (error = data.validate())) {
  1744. result[data.options.name] = error;
  1745. }
  1746. });
  1747. return result;
  1748. /**
  1749. Returns current values of editable elements.
  1750. Note that it returns an **object** with name-value pairs, not a value itself. It allows to get data from several elements.
  1751. If value of some editable is `null` or `undefined` it is excluded from result object.
  1752. When param `isSingle` is set to **true** - it is supposed you have single element and will return value of editable instead of object.
  1753. @method getValue()
  1754. @param {bool} isSingle whether to return just value of single element
  1755. @returns {Object} object of element names and values
  1756. @example
  1757. $('#username, #fullname').editable('getValue');
  1758. //result:
  1759. {
  1760. username: "superuser",
  1761. fullname: "John"
  1762. }
  1763. //isSingle = true
  1764. $('#username').editable('getValue', true);
  1765. //result "superuser"
  1766. **/
  1767. case 'getValue':
  1768. if(arguments.length === 2 && arguments[1] === true) { //isSingle = true
  1769. result = this.eq(0).data(datakey).value;
  1770. } else {
  1771. this.each(function () {
  1772. var $this = $(this), data = $this.data(datakey);
  1773. if (data && data.value !== undefined && data.value !== null) {
  1774. result[data.options.name] = data.input.value2submit(data.value);
  1775. }
  1776. });
  1777. }
  1778. return result;
  1779. /**
  1780. This method collects values from several editable elements and submit them all to server.
  1781. Internally it runs client-side validation for all fields and submits only in case of success.
  1782. See <a href="#newrecord">creating new records</a> for details.
  1783. Since 1.5.1 `submit` can be applied to single element to send data programmatically. In that case
  1784. `url`, `success` and `error` is taken from initial options and you can just call `$('#username').editable('submit')`.
  1785. @method submit(options)
  1786. @param {object} options
  1787. @param {object} options.url url to submit data
  1788. @param {object} options.data additional data to submit
  1789. @param {object} options.ajaxOptions additional ajax options
  1790. @param {function} options.error(obj) error handler
  1791. @param {function} options.success(obj,config) success handler
  1792. @returns {Object} jQuery object
  1793. **/
  1794. case 'submit': //collects value, validate and submit to server for creating new record
  1795. var config = arguments[1] || {},
  1796. $elems = this,
  1797. errors = this.editable('validate');
  1798. // validation ok
  1799. if($.isEmptyObject(errors)) {
  1800. var ajaxOptions = {};
  1801. // for single element use url, success etc from options
  1802. if($elems.length === 1) {
  1803. var editable = $elems.data('editable');
  1804. //standard params
  1805. var params = {
  1806. name: editable.options.name || '',
  1807. value: editable.input.value2submit(editable.value),
  1808. pk: (typeof editable.options.pk === 'function') ?
  1809. editable.options.pk.call(editable.options.scope) :
  1810. editable.options.pk
  1811. };
  1812. //additional params
  1813. if(typeof editable.options.params === 'function') {
  1814. params = editable.options.params.call(editable.options.scope, params);
  1815. } else {
  1816. //try parse json in single quotes (from data-params attribute)
  1817. editable.options.params = $.fn.editableutils.tryParseJson(editable.options.params, true);
  1818. $.extend(params, editable.options.params);
  1819. }
  1820. ajaxOptions = {
  1821. url: editable.options.url,
  1822. data: params,
  1823. type: 'POST'
  1824. };
  1825. // use success / error from options
  1826. config.success = config.success || editable.options.success;
  1827. config.error = config.error || editable.options.error;
  1828. // multiple elements
  1829. } else {
  1830. var values = this.editable('getValue');
  1831. ajaxOptions = {
  1832. url: config.url,
  1833. data: values,
  1834. type: 'POST'
  1835. };
  1836. }
  1837. // ajax success callabck (response 200 OK)
  1838. ajaxOptions.success = typeof config.success === 'function' ? function(response) {
  1839. config.success.call($elems, response, config);
  1840. } : $.noop;
  1841. // ajax error callabck
  1842. ajaxOptions.error = typeof config.error === 'function' ? function() {
  1843. config.error.apply($elems, arguments);
  1844. } : $.noop;
  1845. // extend ajaxOptions
  1846. if(config.ajaxOptions) {
  1847. $.extend(ajaxOptions, config.ajaxOptions);
  1848. }
  1849. // extra data
  1850. if(config.data) {
  1851. $.extend(ajaxOptions.data, config.data);
  1852. }
  1853. // perform ajax request
  1854. $.ajax(ajaxOptions);
  1855. } else { //client-side validation error
  1856. if(typeof config.error === 'function') {
  1857. config.error.call($elems, errors);
  1858. }
  1859. }
  1860. return this;
  1861. }
  1862. //return jquery object
  1863. return this.each(function () {
  1864. var $this = $(this),
  1865. data = $this.data(datakey),
  1866. options = typeof option === 'object' && option;
  1867. //for delegated targets do not store `editable` object for element
  1868. //it's allows several different selectors.
  1869. //see: https://github.com/vitalets/x-editable/issues/312
  1870. if(options && options.selector) {
  1871. data = new Editable(this, options);
  1872. return;
  1873. }
  1874. if (!data) {
  1875. $this.data(datakey, (data = new Editable(this, options)));
  1876. }
  1877. if (typeof option === 'string') { //call method
  1878. data[option].apply(data, Array.prototype.slice.call(args, 1));
  1879. }
  1880. });
  1881. };
  1882. $.fn.editable.defaults = {
  1883. /**
  1884. Type of input. Can be <code>text|textarea|select|date|checklist</code> and more
  1885. @property type
  1886. @type string
  1887. @default 'text'
  1888. **/
  1889. type: 'text',
  1890. /**
  1891. Sets disabled state of editable
  1892. @property disabled
  1893. @type boolean
  1894. @default false
  1895. **/
  1896. disabled: false,
  1897. /**
  1898. How to toggle editable. Can be <code>click|dblclick|mouseenter|manual</code>.
  1899. When set to <code>manual</code> you should manually call <code>show/hide</code> methods of editable.
  1900. **Note**: if you call <code>show</code> or <code>toggle</code> inside **click** handler of some DOM element,
  1901. you need to apply <code>e.stopPropagation()</code> because containers are being closed on any click on document.
  1902. @example
  1903. $('#edit-button').click(function(e) {
  1904. e.stopPropagation();
  1905. $('#username').editable('toggle');
  1906. });
  1907. @property toggle
  1908. @type string
  1909. @default 'click'
  1910. **/
  1911. toggle: 'click',
  1912. /**
  1913. Text shown when element is empty.
  1914. @property emptytext
  1915. @type string
  1916. @default 'Empty'
  1917. **/
  1918. emptytext: 'Empty',
  1919. /**
  1920. Allows to automatically set element's text based on it's value. Can be <code>auto|always|never</code>. Useful for select and date.
  1921. For example, if dropdown list is <code>{1: 'a', 2: 'b'}</code> and element's value set to <code>1</code>, it's html will be automatically set to <code>'a'</code>.
  1922. <code>auto</code> - text will be automatically set only if element is empty.
  1923. <code>always|never</code> - always(never) try to set element's text.
  1924. @property autotext
  1925. @type string
  1926. @default 'auto'
  1927. **/
  1928. autotext: 'auto',
  1929. /**
  1930. Initial value of input. If not set, taken from element's text.
  1931. Note, that if element's text is empty - text is automatically generated from value and can be customized (see `autotext` option).
  1932. For example, to display currency sign:
  1933. @example
  1934. <a id="price" data-type="text" data-value="100"></a>
  1935. <script>
  1936. $('#price').editable({
  1937. ...
  1938. display: function(value) {
  1939. $(this).text(value + '$');
  1940. }
  1941. })
  1942. </script>
  1943. @property value
  1944. @type mixed
  1945. @default element's text
  1946. **/
  1947. value: null,
  1948. /**
  1949. Callback to perform custom displaying of value in element's text.
  1950. If `null`, default input's display used.
  1951. If `false`, no displaying methods will be called, element's text will never change.
  1952. Runs under element's scope.
  1953. _**Parameters:**_
  1954. * `value` current value to be displayed
  1955. * `response` server response (if display called after ajax submit), since 1.4.0
  1956. For _inputs with source_ (select, checklist) parameters are different:
  1957. * `value` current value to be displayed
  1958. * `sourceData` array of items for current input (e.g. dropdown items)
  1959. * `response` server response (if display called after ajax submit), since 1.4.0
  1960. To get currently selected items use `$.fn.editableutils.itemsByValue(value, sourceData)`.
  1961. @property display
  1962. @type function|boolean
  1963. @default null
  1964. @since 1.2.0
  1965. @example
  1966. display: function(value, sourceData) {
  1967. //display checklist as comma-separated values
  1968. var html = [],
  1969. checked = $.fn.editableutils.itemsByValue(value, sourceData);
  1970. if(checked.length) {
  1971. $.each(checked, function(i, v) { html.push($.fn.editableutils.escape(v.text)); });
  1972. $(this).html(html.join(', '));
  1973. } else {
  1974. $(this).empty();
  1975. }
  1976. }
  1977. **/
  1978. display: null,
  1979. /**
  1980. Css class applied when editable text is empty.
  1981. @property emptyclass
  1982. @type string
  1983. @since 1.4.1
  1984. @default editable-empty
  1985. **/
  1986. emptyclass: 'editable-empty',
  1987. /**
  1988. Css class applied when value was stored but not sent to server (`pk` is empty or `send = 'never'`).
  1989. You may set it to `null` if you work with editables locally and submit them together.
  1990. @property unsavedclass
  1991. @type string
  1992. @since 1.4.1
  1993. @default editable-unsaved
  1994. **/
  1995. unsavedclass: 'editable-unsaved',
  1996. /**
  1997. If selector is provided, editable will be delegated to the specified targets.
  1998. Usefull for dynamically generated DOM elements.
  1999. **Please note**, that delegated targets can't be initialized with `emptytext` and `autotext` options,
  2000. as they actually become editable only after first click.
  2001. You should manually set class `editable-click` to these elements.
  2002. Also, if element originally empty you should add class `editable-empty`, set `data-value=""` and write emptytext into element:
  2003. @property selector
  2004. @type string
  2005. @since 1.4.1
  2006. @default null
  2007. @example
  2008. <div id="user">
  2009. <!-- empty -->
  2010. <a href="#" data-name="username" data-type="text" class="editable-click editable-empty" data-value="" title="Username">Empty</a>
  2011. <!-- non-empty -->
  2012. <a href="#" data-name="group" data-type="select" data-source="/groups" data-value="1" class="editable-click" title="Group">Operator</a>
  2013. </div>
  2014. <script>
  2015. $('#user').editable({
  2016. selector: 'a',
  2017. url: '/post',
  2018. pk: 1
  2019. });
  2020. </script>
  2021. **/
  2022. selector: null,
  2023. /**
  2024. Color used to highlight element after update. Implemented via CSS3 transition, works in modern browsers.
  2025. @property highlight
  2026. @type string|boolean
  2027. @since 1.4.5
  2028. @default #FFFF80
  2029. **/
  2030. highlight: '#FFFF80'
  2031. };
  2032. }(window.jQuery));
  2033. /**
  2034. AbstractInput - base class for all editable inputs.
  2035. It defines interface to be implemented by any input type.
  2036. To create your own input you can inherit from this class.
  2037. @class abstractinput
  2038. **/
  2039. (function ($) {
  2040. "use strict";
  2041. //types
  2042. $.fn.editabletypes = {};
  2043. var AbstractInput = function () { };
  2044. AbstractInput.prototype = {
  2045. /**
  2046. Initializes input
  2047. @method init()
  2048. **/
  2049. init: function(type, options, defaults) {
  2050. this.type = type;
  2051. this.options = $.extend({}, defaults, options);
  2052. },
  2053. /*
  2054. this method called before render to init $tpl that is inserted in DOM
  2055. */
  2056. prerender: function() {
  2057. this.$tpl = $(this.options.tpl); //whole tpl as jquery object
  2058. this.$input = this.$tpl; //control itself, can be changed in render method
  2059. this.$clear = null; //clear button
  2060. this.error = null; //error message, if input cannot be rendered
  2061. },
  2062. /**
  2063. Renders input from tpl. Can return jQuery deferred object.
  2064. Can be overwritten in child objects
  2065. @method render()
  2066. **/
  2067. render: function() {
  2068. },
  2069. /**
  2070. Sets element's html by value.
  2071. @method value2html(value, element)
  2072. @param {mixed} value
  2073. @param {DOMElement} element
  2074. **/
  2075. value2html: function(value, element) {
  2076. $(element)[this.options.escape ? 'text' : 'html']($.trim(value));
  2077. },
  2078. /**
  2079. Converts element's html to value
  2080. @method html2value(html)
  2081. @param {string} html
  2082. @returns {mixed}
  2083. **/
  2084. html2value: function(html) {
  2085. return $('<div>').html(html).text();
  2086. },
  2087. /**
  2088. Converts value to string (for internal compare). For submitting to server used value2submit().
  2089. @method value2str(value)
  2090. @param {mixed} value
  2091. @returns {string}
  2092. **/
  2093. value2str: function(value) {
  2094. return value;
  2095. },
  2096. /**
  2097. Converts string received from server into value. Usually from `data-value` attribute.
  2098. @method str2value(str)
  2099. @param {string} str
  2100. @returns {mixed}
  2101. **/
  2102. str2value: function(str) {
  2103. return str;
  2104. },
  2105. /**
  2106. Converts value for submitting to server. Result can be string or object.
  2107. @method value2submit(value)
  2108. @param {mixed} value
  2109. @returns {mixed}
  2110. **/
  2111. value2submit: function(value) {
  2112. return value;
  2113. },
  2114. /**
  2115. Sets value of input.
  2116. @method value2input(value)
  2117. @param {mixed} value
  2118. **/
  2119. value2input: function(value) {
  2120. this.$input.val(value);
  2121. },
  2122. /**
  2123. Returns value of input. Value can be object (e.g. datepicker)
  2124. @method input2value()
  2125. **/
  2126. input2value: function() {
  2127. return this.$input.val();
  2128. },
  2129. /**
  2130. Activates input. For text it sets focus.
  2131. @method activate()
  2132. **/
  2133. activate: function() {
  2134. if(this.$input.is(':visible')) {
  2135. this.$input.focus();
  2136. }
  2137. },
  2138. /**
  2139. Creates input.
  2140. @method clear()
  2141. **/
  2142. clear: function() {
  2143. this.$input.val(null);
  2144. },
  2145. /**
  2146. method to escape html.
  2147. **/
  2148. escape: function(str) {
  2149. return $('<div>').text(str).html();
  2150. },
  2151. /**
  2152. attach handler to automatically submit form when value changed (useful when buttons not shown)
  2153. **/
  2154. autosubmit: function() {
  2155. },
  2156. /**
  2157. Additional actions when destroying element
  2158. **/
  2159. destroy: function() {
  2160. },
  2161. // -------- helper functions --------
  2162. setClass: function() {
  2163. if(this.options.inputclass) {
  2164. this.$input.addClass(this.options.inputclass);
  2165. }
  2166. },
  2167. setAttr: function(attr) {
  2168. if (this.options[attr] !== undefined && this.options[attr] !== null) {
  2169. this.$input.attr(attr, this.options[attr]);
  2170. }
  2171. },
  2172. option: function(key, value) {
  2173. this.options[key] = value;
  2174. }
  2175. };
  2176. AbstractInput.defaults = {
  2177. /**
  2178. HTML template of input. Normally you should not change it.
  2179. @property tpl
  2180. @type string
  2181. @default ''
  2182. **/
  2183. tpl: '',
  2184. /**
  2185. CSS class automatically applied to input
  2186. @property inputclass
  2187. @type string
  2188. @default null
  2189. **/
  2190. inputclass: null,
  2191. /**
  2192. If `true` - html will be escaped in content of element via $.text() method.
  2193. If `false` - html will not be escaped, $.html() used.
  2194. When you use own `display` function, this option obviosly has no effect.
  2195. @property escape
  2196. @type boolean
  2197. @since 1.5.0
  2198. @default true
  2199. **/
  2200. escape: true,
  2201. //scope for external methods (e.g. source defined as function)
  2202. //for internal use only
  2203. scope: null,
  2204. //need to re-declare showbuttons here to get it's value from common config (passed only options existing in defaults)
  2205. showbuttons: true
  2206. };
  2207. $.extend($.fn.editabletypes, {abstractinput: AbstractInput});
  2208. }(window.jQuery));
  2209. /**
  2210. List - abstract class for inputs that have source option loaded from js array or via ajax
  2211. @class list
  2212. @extends abstractinput
  2213. **/
  2214. (function ($) {
  2215. "use strict";
  2216. var List = function (options) {
  2217. };
  2218. $.fn.editableutils.inherit(List, $.fn.editabletypes.abstractinput);
  2219. $.extend(List.prototype, {
  2220. render: function () {
  2221. var deferred = $.Deferred();
  2222. this.error = null;
  2223. this.onSourceReady(function () {
  2224. this.renderList();
  2225. deferred.resolve();
  2226. }, function () {
  2227. this.error = this.options.sourceError;
  2228. deferred.resolve();
  2229. });
  2230. return deferred.promise();
  2231. },
  2232. html2value: function (html) {
  2233. return null; //can't set value by text
  2234. },
  2235. value2html: function (value, element, display, response) {
  2236. var deferred = $.Deferred(),
  2237. success = function () {
  2238. if(typeof display === 'function') {
  2239. //custom display method
  2240. display.call(element, value, this.sourceData, response);
  2241. } else {
  2242. this.value2htmlFinal(value, element);
  2243. }
  2244. deferred.resolve();
  2245. };
  2246. //for null value just call success without loading source
  2247. if(value === null) {
  2248. success.call(this);
  2249. } else {
  2250. this.onSourceReady(success, function () { deferred.resolve(); });
  2251. }
  2252. return deferred.promise();
  2253. },
  2254. // ------------- additional functions ------------
  2255. onSourceReady: function (success, error) {
  2256. //run source if it function
  2257. var source;
  2258. if ($.isFunction(this.options.source)) {
  2259. source = this.options.source.call(this.options.scope);
  2260. this.sourceData = null;
  2261. //note: if function returns the same source as URL - sourceData will be taken from cahce and no extra request performed
  2262. } else {
  2263. source = this.options.source;
  2264. }
  2265. //if allready loaded just call success
  2266. if(this.options.sourceCache && $.isArray(this.sourceData)) {
  2267. success.call(this);
  2268. return;
  2269. }
  2270. //try parse json in single quotes (for double quotes jquery does automatically)
  2271. try {
  2272. source = $.fn.editableutils.tryParseJson(source, false);
  2273. } catch (e) {
  2274. error.call(this);
  2275. return;
  2276. }
  2277. //loading from url
  2278. if (typeof source === 'string') {
  2279. //try to get sourceData from cache
  2280. if(this.options.sourceCache) {
  2281. var cacheID = source,
  2282. cache;
  2283. if (!$(document).data(cacheID)) {
  2284. $(document).data(cacheID, {});
  2285. }
  2286. cache = $(document).data(cacheID);
  2287. //check for cached data
  2288. if (cache.loading === false && cache.sourceData) { //take source from cache
  2289. this.sourceData = cache.sourceData;
  2290. this.doPrepend();
  2291. success.call(this);
  2292. return;
  2293. } else if (cache.loading === true) { //cache is loading, put callback in stack to be called later
  2294. cache.callbacks.push($.proxy(function () {
  2295. this.sourceData = cache.sourceData;
  2296. this.doPrepend();
  2297. success.call(this);
  2298. }, this));
  2299. //also collecting error callbacks
  2300. cache.err_callbacks.push($.proxy(error, this));
  2301. return;
  2302. } else { //no cache yet, activate it
  2303. cache.loading = true;
  2304. cache.callbacks = [];
  2305. cache.err_callbacks = [];
  2306. }
  2307. }
  2308. //ajaxOptions for source. Can be overwritten bt options.sourceOptions
  2309. var ajaxOptions = $.extend({
  2310. url: source,
  2311. type: 'get',
  2312. cache: false,
  2313. dataType: 'json',
  2314. success: $.proxy(function (data) {
  2315. if(cache) {
  2316. cache.loading = false;
  2317. }
  2318. this.sourceData = this.makeArray(data);
  2319. if($.isArray(this.sourceData)) {
  2320. if(cache) {
  2321. //store result in cache
  2322. cache.sourceData = this.sourceData;
  2323. //run success callbacks for other fields waiting for this source
  2324. $.each(cache.callbacks, function () { this.call(); });
  2325. }
  2326. this.doPrepend();
  2327. success.call(this);
  2328. } else {
  2329. error.call(this);
  2330. if(cache) {
  2331. //run error callbacks for other fields waiting for this source
  2332. $.each(cache.err_callbacks, function () { this.call(); });
  2333. }
  2334. }
  2335. }, this),
  2336. error: $.proxy(function () {
  2337. error.call(this);
  2338. if(cache) {
  2339. cache.loading = false;
  2340. //run error callbacks for other fields
  2341. $.each(cache.err_callbacks, function () { this.call(); });
  2342. }
  2343. }, this)
  2344. }, this.options.sourceOptions);
  2345. //loading sourceData from server
  2346. $.ajax(ajaxOptions);
  2347. } else { //options as json/array
  2348. this.sourceData = this.makeArray(source);
  2349. if($.isArray(this.sourceData)) {
  2350. this.doPrepend();
  2351. success.call(this);
  2352. } else {
  2353. error.call(this);
  2354. }
  2355. }
  2356. },
  2357. doPrepend: function () {
  2358. if(this.options.prepend === null || this.options.prepend === undefined) {
  2359. return;
  2360. }
  2361. if(!$.isArray(this.prependData)) {
  2362. //run prepend if it is function (once)
  2363. if ($.isFunction(this.options.prepend)) {
  2364. this.options.prepend = this.options.prepend.call(this.options.scope);
  2365. }
  2366. //try parse json in single quotes
  2367. this.options.prepend = $.fn.editableutils.tryParseJson(this.options.prepend, true);
  2368. //convert prepend from string to object
  2369. if (typeof this.options.prepend === 'string') {
  2370. this.options.prepend = {'': this.options.prepend};
  2371. }
  2372. this.prependData = this.makeArray(this.options.prepend);
  2373. }
  2374. if($.isArray(this.prependData) && $.isArray(this.sourceData)) {
  2375. this.sourceData = this.prependData.concat(this.sourceData);
  2376. }
  2377. },
  2378. /*
  2379. renders input list
  2380. */
  2381. renderList: function() {
  2382. // this method should be overwritten in child class
  2383. },
  2384. /*
  2385. set element's html by value
  2386. */
  2387. value2htmlFinal: function(value, element) {
  2388. // this method should be overwritten in child class
  2389. },
  2390. /**
  2391. * convert data to array suitable for sourceData, e.g. [{value: 1, text: 'abc'}, {...}]
  2392. */
  2393. makeArray: function(data) {
  2394. var count, obj, result = [], item, iterateItem;
  2395. if(!data || typeof data === 'string') {
  2396. return null;
  2397. }
  2398. if($.isArray(data)) { //array
  2399. /*
  2400. function to iterate inside item of array if item is object.
  2401. Caclulates count of keys in item and store in obj.
  2402. */
  2403. iterateItem = function (k, v) {
  2404. obj = {value: k, text: v};
  2405. if(count++ >= 2) {
  2406. return false;// exit from `each` if item has more than one key.
  2407. }
  2408. };
  2409. for(var i = 0; i < data.length; i++) {
  2410. item = data[i];
  2411. if(typeof item === 'object') {
  2412. count = 0; //count of keys inside item
  2413. $.each(item, iterateItem);
  2414. //case: [{val1: 'text1'}, {val2: 'text2} ...]
  2415. if(count === 1) {
  2416. result.push(obj);
  2417. //case: [{value: 1, text: 'text1'}, {value: 2, text: 'text2'}, ...]
  2418. } else if(count > 1) {
  2419. //removed check of existance: item.hasOwnProperty('value') && item.hasOwnProperty('text')
  2420. if(item.children) {
  2421. item.children = this.makeArray(item.children);
  2422. }
  2423. result.push(item);
  2424. }
  2425. } else {
  2426. //case: ['text1', 'text2' ...]
  2427. result.push({value: item, text: item});
  2428. }
  2429. }
  2430. } else { //case: {val1: 'text1', val2: 'text2, ...}
  2431. $.each(data, function (k, v) {
  2432. result.push({value: k, text: v});
  2433. });
  2434. }
  2435. return result;
  2436. },
  2437. option: function(key, value) {
  2438. this.options[key] = value;
  2439. if(key === 'source') {
  2440. this.sourceData = null;
  2441. }
  2442. if(key === 'prepend') {
  2443. this.prependData = null;
  2444. }
  2445. }
  2446. });
  2447. List.defaults = $.extend({}, $.fn.editabletypes.abstractinput.defaults, {
  2448. /**
  2449. Source data for list.
  2450. If **array** - it should be in format: `[{value: 1, text: "text1"}, {value: 2, text: "text2"}, ...]`
  2451. For compability, object format is also supported: `{"1": "text1", "2": "text2" ...}` but it does not guarantee elements order.
  2452. If **string** - considered ajax url to load items. In that case results will be cached for fields with the same source and name. See also `sourceCache` option.
  2453. If **function**, it should return data in format above (since 1.4.0).
  2454. Since 1.4.1 key `children` supported to render OPTGROUP (for **select** input only).
  2455. `[{text: "group1", children: [{value: 1, text: "text1"}, {value: 2, text: "text2"}]}, ...]`
  2456. @property source
  2457. @type string | array | object | function
  2458. @default null
  2459. **/
  2460. source: null,
  2461. /**
  2462. Data automatically prepended to the beginning of dropdown list.
  2463. @property prepend
  2464. @type string | array | object | function
  2465. @default false
  2466. **/
  2467. prepend: false,
  2468. /**
  2469. Error message when list cannot be loaded (e.g. ajax error)
  2470. @property sourceError
  2471. @type string
  2472. @default Error when loading list
  2473. **/
  2474. sourceError: 'Error when loading list',
  2475. /**
  2476. if <code>true</code> and source is **string url** - results will be cached for fields with the same source.
  2477. Usefull for editable column in grid to prevent extra requests.
  2478. @property sourceCache
  2479. @type boolean
  2480. @default true
  2481. @since 1.2.0
  2482. **/
  2483. sourceCache: true,
  2484. /**
  2485. Additional ajax options to be used in $.ajax() when loading list from server.
  2486. Useful to send extra parameters (`data` key) or change request method (`type` key).
  2487. @property sourceOptions
  2488. @type object|function
  2489. @default null
  2490. @since 1.5.0
  2491. **/
  2492. sourceOptions: null
  2493. });
  2494. $.fn.editabletypes.list = List;
  2495. }(window.jQuery));
  2496. /**
  2497. Text input
  2498. @class text
  2499. @extends abstractinput
  2500. @final
  2501. @example
  2502. <a href="#" id="username" data-type="text" data-pk="1">awesome</a>
  2503. <script>
  2504. $(function(){
  2505. $('#username').editable({
  2506. url: '/post',
  2507. title: 'Enter username'
  2508. });
  2509. });
  2510. </script>
  2511. **/
  2512. (function ($) {
  2513. "use strict";
  2514. var Text = function (options) {
  2515. this.init('text', options, Text.defaults);
  2516. };
  2517. $.fn.editableutils.inherit(Text, $.fn.editabletypes.abstractinput);
  2518. $.extend(Text.prototype, {
  2519. render: function() {
  2520. this.renderClear();
  2521. this.setClass();
  2522. this.setAttr('placeholder');
  2523. },
  2524. activate: function() {
  2525. if(this.$input.is(':visible')) {
  2526. this.$input.focus();
  2527. $.fn.editableutils.setCursorPosition(this.$input.get(0), this.$input.val().length);
  2528. if(this.toggleClear) {
  2529. this.toggleClear();
  2530. }
  2531. }
  2532. },
  2533. //render clear button
  2534. renderClear: function() {
  2535. if (this.options.clear) {
  2536. this.$clear = $('<span class="editable-clear-x"></span>');
  2537. this.$input.after(this.$clear)
  2538. .css('padding-right', 24)
  2539. .keyup($.proxy(function(e) {
  2540. //arrows, enter, tab, etc
  2541. if(~$.inArray(e.keyCode, [40,38,9,13,27])) {
  2542. return;
  2543. }
  2544. clearTimeout(this.t);
  2545. var that = this;
  2546. this.t = setTimeout(function() {
  2547. that.toggleClear(e);
  2548. }, 100);
  2549. }, this))
  2550. .parent().css('position', 'relative');
  2551. this.$clear.click($.proxy(this.clear, this));
  2552. }
  2553. },
  2554. postrender: function() {
  2555. /*
  2556. //now `clear` is positioned via css
  2557. if(this.$clear) {
  2558. //can position clear button only here, when form is shown and height can be calculated
  2559. // var h = this.$input.outerHeight(true) || 20,
  2560. var h = this.$clear.parent().height(),
  2561. delta = (h - this.$clear.height()) / 2;
  2562. //this.$clear.css({bottom: delta, right: delta});
  2563. }
  2564. */
  2565. },
  2566. //show / hide clear button
  2567. toggleClear: function(e) {
  2568. if(!this.$clear) {
  2569. return;
  2570. }
  2571. var len = this.$input.val().length,
  2572. visible = this.$clear.is(':visible');
  2573. if(len && !visible) {
  2574. this.$clear.show();
  2575. }
  2576. if(!len && visible) {
  2577. this.$clear.hide();
  2578. }
  2579. },
  2580. clear: function() {
  2581. this.$clear.hide();
  2582. this.$input.val('').focus();
  2583. }
  2584. });
  2585. Text.defaults = $.extend({}, $.fn.editabletypes.abstractinput.defaults, {
  2586. /**
  2587. @property tpl
  2588. @default <input type="text">
  2589. **/
  2590. tpl: '<input type="text">',
  2591. /**
  2592. Placeholder attribute of input. Shown when input is empty.
  2593. @property placeholder
  2594. @type string
  2595. @default null
  2596. **/
  2597. placeholder: null,
  2598. /**
  2599. Whether to show `clear` button
  2600. @property clear
  2601. @type boolean
  2602. @default true
  2603. **/
  2604. clear: true
  2605. });
  2606. $.fn.editabletypes.text = Text;
  2607. }(window.jQuery));
  2608. /**
  2609. Textarea input
  2610. @class textarea
  2611. @extends abstractinput
  2612. @final
  2613. @example
  2614. <a href="#" id="comments" data-type="textarea" data-pk="1">awesome comment!</a>
  2615. <script>
  2616. $(function(){
  2617. $('#comments').editable({
  2618. url: '/post',
  2619. title: 'Enter comments',
  2620. rows: 10
  2621. });
  2622. });
  2623. </script>
  2624. **/
  2625. (function ($) {
  2626. "use strict";
  2627. var Textarea = function (options) {
  2628. this.init('textarea', options, Textarea.defaults);
  2629. };
  2630. $.fn.editableutils.inherit(Textarea, $.fn.editabletypes.abstractinput);
  2631. $.extend(Textarea.prototype, {
  2632. render: function () {
  2633. this.setClass();
  2634. this.setAttr('placeholder');
  2635. this.setAttr('rows');
  2636. //ctrl + enter
  2637. this.$input.keydown(function (e) {
  2638. if (e.ctrlKey && e.which === 13) {
  2639. $(this).closest('form').submit();
  2640. }
  2641. });
  2642. },
  2643. //using `white-space: pre-wrap` solves \n <--> BR conversion very elegant!
  2644. /*
  2645. value2html: function(value, element) {
  2646. var html = '', lines;
  2647. if(value) {
  2648. lines = value.split("\n");
  2649. for (var i = 0; i < lines.length; i++) {
  2650. lines[i] = $('<div>').text(lines[i]).html();
  2651. }
  2652. html = lines.join('<br>');
  2653. }
  2654. $(element).html(html);
  2655. },
  2656. html2value: function(html) {
  2657. if(!html) {
  2658. return '';
  2659. }
  2660. var regex = new RegExp(String.fromCharCode(10), 'g');
  2661. var lines = html.split(/<br\s*\/?>/i);
  2662. for (var i = 0; i < lines.length; i++) {
  2663. var text = $('<div>').html(lines[i]).text();
  2664. // Remove newline characters (\n) to avoid them being converted by value2html() method
  2665. // thus adding extra <br> tags
  2666. text = text.replace(regex, '');
  2667. lines[i] = text;
  2668. }
  2669. return lines.join("\n");
  2670. },
  2671. */
  2672. activate: function() {
  2673. $.fn.editabletypes.text.prototype.activate.call(this);
  2674. }
  2675. });
  2676. Textarea.defaults = $.extend({}, $.fn.editabletypes.abstractinput.defaults, {
  2677. /**
  2678. @property tpl
  2679. @default <textarea></textarea>
  2680. **/
  2681. tpl:'<textarea></textarea>',
  2682. /**
  2683. @property inputclass
  2684. @default input-large
  2685. **/
  2686. inputclass: 'input-large',
  2687. /**
  2688. Placeholder attribute of input. Shown when input is empty.
  2689. @property placeholder
  2690. @type string
  2691. @default null
  2692. **/
  2693. placeholder: null,
  2694. /**
  2695. Number of rows in textarea
  2696. @property rows
  2697. @type integer
  2698. @default 7
  2699. **/
  2700. rows: 7
  2701. });
  2702. $.fn.editabletypes.textarea = Textarea;
  2703. }(window.jQuery));
  2704. /**
  2705. Select (dropdown)
  2706. @class select
  2707. @extends list
  2708. @final
  2709. @example
  2710. <a href="#" id="status" data-type="select" data-pk="1" data-url="/post" data-title="Select status"></a>
  2711. <script>
  2712. $(function(){
  2713. $('#status').editable({
  2714. value: 2,
  2715. source: [
  2716. {value: 1, text: 'Active'},
  2717. {value: 2, text: 'Blocked'},
  2718. {value: 3, text: 'Deleted'}
  2719. ]
  2720. });
  2721. });
  2722. </script>
  2723. **/
  2724. (function ($) {
  2725. "use strict";
  2726. var Select = function (options) {
  2727. this.init('select', options, Select.defaults);
  2728. };
  2729. $.fn.editableutils.inherit(Select, $.fn.editabletypes.list);
  2730. $.extend(Select.prototype, {
  2731. renderList: function() {
  2732. this.$input.empty();
  2733. var fillItems = function($el, data) {
  2734. var attr;
  2735. if($.isArray(data)) {
  2736. for(var i=0; i<data.length; i++) {
  2737. attr = {};
  2738. if(data[i].children) {
  2739. attr.label = data[i].text;
  2740. $el.append(fillItems($('<optgroup>', attr), data[i].children));
  2741. } else {
  2742. attr.value = data[i].value;
  2743. if(data[i].disabled) {
  2744. attr.disabled = true;
  2745. }
  2746. $el.append($('<option>', attr).text(data[i].text));
  2747. }
  2748. }
  2749. }
  2750. return $el;
  2751. };
  2752. fillItems(this.$input, this.sourceData);
  2753. this.setClass();
  2754. //enter submit
  2755. this.$input.on('keydown.editable', function (e) {
  2756. if (e.which === 13) {
  2757. $(this).closest('form').submit();
  2758. }
  2759. });
  2760. },
  2761. value2htmlFinal: function(value, element) {
  2762. var text = '',
  2763. items = $.fn.editableutils.itemsByValue(value, this.sourceData);
  2764. if(items.length) {
  2765. text = items[0].text;
  2766. }
  2767. //$(element).text(text);
  2768. $.fn.editabletypes.abstractinput.prototype.value2html.call(this, text, element);
  2769. },
  2770. autosubmit: function() {
  2771. this.$input.off('keydown.editable').on('change.editable', function(){
  2772. $(this).closest('form').submit();
  2773. });
  2774. }
  2775. });
  2776. Select.defaults = $.extend({}, $.fn.editabletypes.list.defaults, {
  2777. /**
  2778. @property tpl
  2779. @default <select></select>
  2780. **/
  2781. tpl:'<select></select>'
  2782. });
  2783. $.fn.editabletypes.select = Select;
  2784. }(window.jQuery));
  2785. /**
  2786. List of checkboxes.
  2787. Internally value stored as javascript array of values.
  2788. @class checklist
  2789. @extends list
  2790. @final
  2791. @example
  2792. <a href="#" id="options" data-type="checklist" data-pk="1" data-url="/post" data-title="Select options"></a>
  2793. <script>
  2794. $(function(){
  2795. $('#options').editable({
  2796. value: [2, 3],
  2797. source: [
  2798. {value: 1, text: 'option1'},
  2799. {value: 2, text: 'option2'},
  2800. {value: 3, text: 'option3'}
  2801. ]
  2802. });
  2803. });
  2804. </script>
  2805. **/
  2806. (function ($) {
  2807. "use strict";
  2808. var Checklist = function (options) {
  2809. this.init('checklist', options, Checklist.defaults);
  2810. };
  2811. $.fn.editableutils.inherit(Checklist, $.fn.editabletypes.list);
  2812. $.extend(Checklist.prototype, {
  2813. renderList: function() {
  2814. var $label, $div;
  2815. this.$tpl.empty();
  2816. if(!$.isArray(this.sourceData)) {
  2817. return;
  2818. }
  2819. for(var i=0; i<this.sourceData.length; i++) {
  2820. $label = $('<label>').append($('<input>', {
  2821. type: 'checkbox',
  2822. value: this.sourceData[i].value
  2823. }))
  2824. .append($('<span>').text(' '+this.sourceData[i].text));
  2825. $('<div>').append($label).appendTo(this.$tpl);
  2826. }
  2827. this.$input = this.$tpl.find('input[type="checkbox"]');
  2828. this.setClass();
  2829. },
  2830. value2str: function(value) {
  2831. return $.isArray(value) ? value.sort().join($.trim(this.options.separator)) : '';
  2832. },
  2833. //parse separated string
  2834. str2value: function(str) {
  2835. var reg, value = null;
  2836. if(typeof str === 'string' && str.length) {
  2837. reg = new RegExp('\\s*'+$.trim(this.options.separator)+'\\s*');
  2838. value = str.split(reg);
  2839. } else if($.isArray(str)) {
  2840. value = str;
  2841. } else {
  2842. value = [str];
  2843. }
  2844. return value;
  2845. },
  2846. //set checked on required checkboxes
  2847. value2input: function(value) {
  2848. this.$input.prop('checked', false);
  2849. if($.isArray(value) && value.length) {
  2850. this.$input.each(function(i, el) {
  2851. var $el = $(el);
  2852. // cannot use $.inArray as it performs strict comparison
  2853. $.each(value, function(j, val){
  2854. /*jslint eqeq: true*/
  2855. if($el.val() == val) {
  2856. /*jslint eqeq: false*/
  2857. $el.prop('checked', true);
  2858. }
  2859. });
  2860. });
  2861. }
  2862. },
  2863. input2value: function() {
  2864. var checked = [];
  2865. this.$input.filter(':checked').each(function(i, el) {
  2866. checked.push($(el).val());
  2867. });
  2868. return checked;
  2869. },
  2870. //collect text of checked boxes
  2871. value2htmlFinal: function(value, element) {
  2872. var html = [],
  2873. checked = $.fn.editableutils.itemsByValue(value, this.sourceData),
  2874. escape = this.options.escape;
  2875. if(checked.length) {
  2876. $.each(checked, function(i, v) {
  2877. var text = escape ? $.fn.editableutils.escape(v.text) : v.text;
  2878. html.push(text);
  2879. });
  2880. $(element).html(html.join('<br>'));
  2881. } else {
  2882. $(element).empty();
  2883. }
  2884. },
  2885. activate: function() {
  2886. this.$input.first().focus();
  2887. },
  2888. autosubmit: function() {
  2889. this.$input.on('keydown', function(e){
  2890. if (e.which === 13) {
  2891. $(this).closest('form').submit();
  2892. }
  2893. });
  2894. }
  2895. });
  2896. Checklist.defaults = $.extend({}, $.fn.editabletypes.list.defaults, {
  2897. /**
  2898. @property tpl
  2899. @default <div></div>
  2900. **/
  2901. tpl:'<div class="editable-checklist"></div>',
  2902. /**
  2903. @property inputclass
  2904. @type string
  2905. @default null
  2906. **/
  2907. inputclass: null,
  2908. /**
  2909. Separator of values when reading from `data-value` attribute
  2910. @property separator
  2911. @type string
  2912. @default ','
  2913. **/
  2914. separator: ','
  2915. });
  2916. $.fn.editabletypes.checklist = Checklist;
  2917. }(window.jQuery));
  2918. /**
  2919. HTML5 input types.
  2920. Following types are supported:
  2921. * password
  2922. * email
  2923. * url
  2924. * tel
  2925. * number
  2926. * range
  2927. * time
  2928. Learn more about html5 inputs:
  2929. http://www.w3.org/wiki/HTML5_form_additions
  2930. To check browser compatibility please see:
  2931. https://developer.mozilla.org/en-US/docs/HTML/Element/Input
  2932. @class html5types
  2933. @extends text
  2934. @final
  2935. @since 1.3.0
  2936. @example
  2937. <a href="#" id="email" data-type="email" data-pk="1">admin@example.com</a>
  2938. <script>
  2939. $(function(){
  2940. $('#email').editable({
  2941. url: '/post',
  2942. title: 'Enter email'
  2943. });
  2944. });
  2945. </script>
  2946. **/
  2947. /**
  2948. @property tpl
  2949. @default depends on type
  2950. **/
  2951. /*
  2952. Password
  2953. */
  2954. (function ($) {
  2955. "use strict";
  2956. var Password = function (options) {
  2957. this.init('password', options, Password.defaults);
  2958. };
  2959. $.fn.editableutils.inherit(Password, $.fn.editabletypes.text);
  2960. $.extend(Password.prototype, {
  2961. //do not display password, show '[hidden]' instead
  2962. value2html: function(value, element) {
  2963. if(value) {
  2964. $(element).text('[hidden]');
  2965. } else {
  2966. $(element).empty();
  2967. }
  2968. },
  2969. //as password not displayed, should not set value by html
  2970. html2value: function(html) {
  2971. return null;
  2972. }
  2973. });
  2974. Password.defaults = $.extend({}, $.fn.editabletypes.text.defaults, {
  2975. tpl: '<input type="password">'
  2976. });
  2977. $.fn.editabletypes.password = Password;
  2978. }(window.jQuery));
  2979. /*
  2980. Email
  2981. */
  2982. (function ($) {
  2983. "use strict";
  2984. var Email = function (options) {
  2985. this.init('email', options, Email.defaults);
  2986. };
  2987. $.fn.editableutils.inherit(Email, $.fn.editabletypes.text);
  2988. Email.defaults = $.extend({}, $.fn.editabletypes.text.defaults, {
  2989. tpl: '<input type="email">'
  2990. });
  2991. $.fn.editabletypes.email = Email;
  2992. }(window.jQuery));
  2993. /*
  2994. Url
  2995. */
  2996. (function ($) {
  2997. "use strict";
  2998. var Url = function (options) {
  2999. this.init('url', options, Url.defaults);
  3000. };
  3001. $.fn.editableutils.inherit(Url, $.fn.editabletypes.text);
  3002. Url.defaults = $.extend({}, $.fn.editabletypes.text.defaults, {
  3003. tpl: '<input type="url">'
  3004. });
  3005. $.fn.editabletypes.url = Url;
  3006. }(window.jQuery));
  3007. /*
  3008. Tel
  3009. */
  3010. (function ($) {
  3011. "use strict";
  3012. var Tel = function (options) {
  3013. this.init('tel', options, Tel.defaults);
  3014. };
  3015. $.fn.editableutils.inherit(Tel, $.fn.editabletypes.text);
  3016. Tel.defaults = $.extend({}, $.fn.editabletypes.text.defaults, {
  3017. tpl: '<input type="tel">'
  3018. });
  3019. $.fn.editabletypes.tel = Tel;
  3020. }(window.jQuery));
  3021. /*
  3022. Number
  3023. */
  3024. (function ($) {
  3025. "use strict";
  3026. var NumberInput = function (options) {
  3027. this.init('number', options, NumberInput.defaults);
  3028. };
  3029. $.fn.editableutils.inherit(NumberInput, $.fn.editabletypes.text);
  3030. $.extend(NumberInput.prototype, {
  3031. render: function () {
  3032. NumberInput.superclass.render.call(this);
  3033. this.setAttr('min');
  3034. this.setAttr('max');
  3035. this.setAttr('step');
  3036. },
  3037. postrender: function() {
  3038. if(this.$clear) {
  3039. //increase right ffset for up/down arrows
  3040. this.$clear.css({right: 24});
  3041. /*
  3042. //can position clear button only here, when form is shown and height can be calculated
  3043. var h = this.$input.outerHeight(true) || 20,
  3044. delta = (h - this.$clear.height()) / 2;
  3045. //add 12px to offset right for up/down arrows
  3046. this.$clear.css({top: delta, right: delta + 16});
  3047. */
  3048. }
  3049. }
  3050. });
  3051. NumberInput.defaults = $.extend({}, $.fn.editabletypes.text.defaults, {
  3052. tpl: '<input type="number">',
  3053. inputclass: 'input-mini',
  3054. min: null,
  3055. max: null,
  3056. step: null
  3057. });
  3058. $.fn.editabletypes.number = NumberInput;
  3059. }(window.jQuery));
  3060. /*
  3061. Range (inherit from number)
  3062. */
  3063. (function ($) {
  3064. "use strict";
  3065. var Range = function (options) {
  3066. this.init('range', options, Range.defaults);
  3067. };
  3068. $.fn.editableutils.inherit(Range, $.fn.editabletypes.number);
  3069. $.extend(Range.prototype, {
  3070. render: function () {
  3071. this.$input = this.$tpl.filter('input');
  3072. this.setClass();
  3073. this.setAttr('min');
  3074. this.setAttr('max');
  3075. this.setAttr('step');
  3076. this.$input.on('input', function(){
  3077. $(this).siblings('output').text($(this).val());
  3078. });
  3079. },
  3080. activate: function() {
  3081. this.$input.focus();
  3082. }
  3083. });
  3084. Range.defaults = $.extend({}, $.fn.editabletypes.number.defaults, {
  3085. tpl: '<input type="range"><output style="width: 30px; display: inline-block"></output>',
  3086. inputclass: 'input-medium'
  3087. });
  3088. $.fn.editabletypes.range = Range;
  3089. }(window.jQuery));
  3090. /*
  3091. Time
  3092. */
  3093. (function ($) {
  3094. "use strict";
  3095. var Time = function (options) {
  3096. this.init('time', options, Time.defaults);
  3097. };
  3098. //inherit from abstract, as inheritance from text gives selection error.
  3099. $.fn.editableutils.inherit(Time, $.fn.editabletypes.abstractinput);
  3100. $.extend(Time.prototype, {
  3101. render: function() {
  3102. this.setClass();
  3103. }
  3104. });
  3105. Time.defaults = $.extend({}, $.fn.editabletypes.abstractinput.defaults, {
  3106. tpl: '<input type="time">'
  3107. });
  3108. $.fn.editabletypes.time = Time;
  3109. }(window.jQuery));
  3110. /**
  3111. Select2 input. Based on amazing work of Igor Vaynberg https://github.com/ivaynberg/select2.
  3112. Please see [original select2 docs](http://ivaynberg.github.com/select2) for detailed description and options.
  3113. You should manually download and include select2 distributive:
  3114. <link href="select2/select2.css" rel="stylesheet" type="text/css"></link>
  3115. <script src="select2/select2.js"></script>
  3116. To make it **bootstrap-styled** you can use css from [here](https://github.com/t0m/select2-bootstrap-css):
  3117. <link href="select2-bootstrap.css" rel="stylesheet" type="text/css"></link>
  3118. **Note:** currently `autotext` feature does not work for select2 with `ajax` remote source.
  3119. You need initially put both `data-value` and element's text youself:
  3120. <a href="#" data-type="select2" data-value="1">Text1</a>
  3121. @class select2
  3122. @extends abstractinput
  3123. @since 1.4.1
  3124. @final
  3125. @example
  3126. <a href="#" id="country" data-type="select2" data-pk="1" data-value="ru" data-url="/post" data-title="Select country"></a>
  3127. <script>
  3128. $(function(){
  3129. //local source
  3130. $('#country').editable({
  3131. source: [
  3132. {id: 'gb', text: 'Great Britain'},
  3133. {id: 'us', text: 'United States'},
  3134. {id: 'ru', text: 'Russia'}
  3135. ],
  3136. select2: {
  3137. multiple: true
  3138. }
  3139. });
  3140. //remote source (simple)
  3141. $('#country').editable({
  3142. source: '/getCountries',
  3143. select2: {
  3144. placeholder: 'Select Country',
  3145. minimumInputLength: 1
  3146. }
  3147. });
  3148. //remote source (advanced)
  3149. $('#country').editable({
  3150. select2: {
  3151. placeholder: 'Select Country',
  3152. allowClear: true,
  3153. minimumInputLength: 3,
  3154. id: function (item) {
  3155. return item.CountryId;
  3156. },
  3157. ajax: {
  3158. url: '/getCountries',
  3159. dataType: 'json',
  3160. data: function (term, page) {
  3161. return { query: term };
  3162. },
  3163. results: function (data, page) {
  3164. return { results: data };
  3165. }
  3166. },
  3167. formatResult: function (item) {
  3168. return item.CountryName;
  3169. },
  3170. formatSelection: function (item) {
  3171. return item.CountryName;
  3172. },
  3173. initSelection: function (element, callback) {
  3174. return $.get('/getCountryById', { query: element.val() }, function (data) {
  3175. callback(data);
  3176. });
  3177. }
  3178. }
  3179. });
  3180. });
  3181. </script>
  3182. **/
  3183. (function ($) {
  3184. "use strict";
  3185. var Constructor = function (options) {
  3186. this.init('select2', options, Constructor.defaults);
  3187. options.select2 = options.select2 || {};
  3188. this.sourceData = null;
  3189. //placeholder
  3190. if(options.placeholder) {
  3191. options.select2.placeholder = options.placeholder;
  3192. }
  3193. //if not `tags` mode, use source
  3194. if(!options.select2.tags && options.source) {
  3195. var source = options.source;
  3196. //if source is function, call it (once!)
  3197. if ($.isFunction(options.source)) {
  3198. source = options.source.call(options.scope);
  3199. }
  3200. if (typeof source === 'string') {
  3201. options.select2.ajax = options.select2.ajax || {};
  3202. //some default ajax params
  3203. if(!options.select2.ajax.data) {
  3204. options.select2.ajax.data = function(term) {return { query:term };};
  3205. }
  3206. if(!options.select2.ajax.results) {
  3207. options.select2.ajax.results = function(data) { return {results:data };};
  3208. }
  3209. options.select2.ajax.url = source;
  3210. } else {
  3211. //check format and convert x-editable format to select2 format (if needed)
  3212. this.sourceData = this.convertSource(source);
  3213. options.select2.data = this.sourceData;
  3214. }
  3215. }
  3216. //overriding objects in config (as by default jQuery extend() is not recursive)
  3217. this.options.select2 = $.extend({}, Constructor.defaults.select2, options.select2);
  3218. //detect whether it is multi-valued
  3219. this.isMultiple = this.options.select2.tags || this.options.select2.multiple;
  3220. this.isRemote = ('ajax' in this.options.select2);
  3221. //store function returning ID of item
  3222. //should be here as used inautotext for local source
  3223. this.idFunc = this.options.select2.id;
  3224. if (typeof(this.idFunc) !== "function") {
  3225. var idKey = this.idFunc || 'id';
  3226. this.idFunc = function (e) { return e[idKey]; };
  3227. }
  3228. //store function that renders text in select2
  3229. this.formatSelection = this.options.select2.formatSelection;
  3230. if (typeof(this.formatSelection) !== "function") {
  3231. this.formatSelection = function (e) { return e.text; };
  3232. }
  3233. };
  3234. $.fn.editableutils.inherit(Constructor, $.fn.editabletypes.abstractinput);
  3235. $.extend(Constructor.prototype, {
  3236. render: function() {
  3237. this.setClass();
  3238. //can not apply select2 here as it calls initSelection
  3239. //over input that does not have correct value yet.
  3240. //apply select2 only in value2input
  3241. //this.$input.select2(this.options.select2);
  3242. //when data is loaded via ajax, we need to know when it's done to populate listData
  3243. if(this.isRemote) {
  3244. //listen to loaded event to populate data
  3245. this.$input.on('select2-loaded', $.proxy(function(e) {
  3246. this.sourceData = e.items.results;
  3247. }, this));
  3248. }
  3249. //trigger resize of editableform to re-position container in multi-valued mode
  3250. if(this.isMultiple) {
  3251. this.$input.on('change', function() {
  3252. $(this).closest('form').parent().triggerHandler('resize');
  3253. });
  3254. }
  3255. },
  3256. value2html: function(value, element) {
  3257. var text = '', data,
  3258. that = this;
  3259. if(this.options.select2.tags) { //in tags mode just assign value
  3260. data = value;
  3261. //data = $.fn.editableutils.itemsByValue(value, this.options.select2.tags, this.idFunc);
  3262. } else if(this.sourceData) {
  3263. data = $.fn.editableutils.itemsByValue(value, this.sourceData, this.idFunc);
  3264. } else {
  3265. //can not get list of possible values
  3266. //(e.g. autotext for select2 with ajax source)
  3267. }
  3268. //data may be array (when multiple values allowed)
  3269. if($.isArray(data)) {
  3270. //collect selected data and show with separator
  3271. text = [];
  3272. $.each(data, function(k, v){
  3273. text.push(v && typeof v === 'object' ? that.formatSelection(v) : v);
  3274. });
  3275. } else if(data) {
  3276. text = that.formatSelection(data);
  3277. }
  3278. text = $.isArray(text) ? text.join(this.options.viewseparator) : text;
  3279. //$(element).text(text);
  3280. Constructor.superclass.value2html.call(this, text, element);
  3281. },
  3282. html2value: function(html) {
  3283. return this.options.select2.tags ? this.str2value(html, this.options.viewseparator) : null;
  3284. },
  3285. value2input: function(value) {
  3286. // if value array => join it anyway
  3287. if($.isArray(value)) {
  3288. value = value.join(this.getSeparator());
  3289. }
  3290. //for remote source just set value, text is updated by initSelection
  3291. if(!this.$input.data('select2')) {
  3292. this.$input.val(value);
  3293. this.$input.select2(this.options.select2);
  3294. } else {
  3295. //second argument needed to separate initial change from user's click (for autosubmit)
  3296. this.$input.val(value).trigger('change', true);
  3297. //Uncaught Error: cannot call val() if initSelection() is not defined
  3298. //this.$input.select2('val', value);
  3299. }
  3300. // if defined remote source AND no multiple mode AND no user's initSelection provided -->
  3301. // we should somehow get text for provided id.
  3302. // The solution is to use element's text as text for that id (exclude empty)
  3303. if(this.isRemote && !this.isMultiple && !this.options.select2.initSelection) {
  3304. // customId and customText are methods to extract `id` and `text` from data object
  3305. // we can use this workaround only if user did not define these methods
  3306. // otherwise we cant construct data object
  3307. var customId = this.options.select2.id,
  3308. customText = this.options.select2.formatSelection;
  3309. if(!customId && !customText) {
  3310. var $el = $(this.options.scope);
  3311. if (!$el.data('editable').isEmpty) {
  3312. var data = {id: value, text: $el.text()};
  3313. this.$input.select2('data', data);
  3314. }
  3315. }
  3316. }
  3317. },
  3318. input2value: function() {
  3319. return this.$input.select2('val');
  3320. },
  3321. str2value: function(str, separator) {
  3322. if(typeof str !== 'string' || !this.isMultiple) {
  3323. return str;
  3324. }
  3325. separator = separator || this.getSeparator();
  3326. var val, i, l;
  3327. if (str === null || str.length < 1) {
  3328. return null;
  3329. }
  3330. val = str.split(separator);
  3331. for (i = 0, l = val.length; i < l; i = i + 1) {
  3332. val[i] = $.trim(val[i]);
  3333. }
  3334. return val;
  3335. },
  3336. autosubmit: function() {
  3337. this.$input.on('change', function(e, isInitial){
  3338. if(!isInitial) {
  3339. $(this).closest('form').submit();
  3340. }
  3341. });
  3342. },
  3343. getSeparator: function() {
  3344. return this.options.select2.separator || $.fn.select2.defaults.separator;
  3345. },
  3346. /*
  3347. Converts source from x-editable format: {value: 1, text: "1"} to
  3348. select2 format: {id: 1, text: "1"}
  3349. */
  3350. convertSource: function(source) {
  3351. if($.isArray(source) && source.length && source[0].value !== undefined) {
  3352. for(var i = 0; i<source.length; i++) {
  3353. if(source[i].value !== undefined) {
  3354. source[i].id = source[i].value;
  3355. delete source[i].value;
  3356. }
  3357. }
  3358. }
  3359. return source;
  3360. },
  3361. destroy: function() {
  3362. if(this.$input.data('select2')) {
  3363. this.$input.select2('destroy');
  3364. }
  3365. }
  3366. });
  3367. Constructor.defaults = $.extend({}, $.fn.editabletypes.abstractinput.defaults, {
  3368. /**
  3369. @property tpl
  3370. @default <input type="hidden">
  3371. **/
  3372. tpl:'<input type="hidden">',
  3373. /**
  3374. Configuration of select2. [Full list of options](http://ivaynberg.github.com/select2).
  3375. @property select2
  3376. @type object
  3377. @default null
  3378. **/
  3379. select2: null,
  3380. /**
  3381. Placeholder attribute of select
  3382. @property placeholder
  3383. @type string
  3384. @default null
  3385. **/
  3386. placeholder: null,
  3387. /**
  3388. Source data for select. It will be assigned to select2 `data` property and kept here just for convenience.
  3389. Please note, that format is different from simple `select` input: use 'id' instead of 'value'.
  3390. E.g. `[{id: 1, text: "text1"}, {id: 2, text: "text2"}, ...]`.
  3391. @property source
  3392. @type array|string|function
  3393. @default null
  3394. **/
  3395. source: null,
  3396. /**
  3397. Separator used to display tags.
  3398. @property viewseparator
  3399. @type string
  3400. @default ', '
  3401. **/
  3402. viewseparator: ', '
  3403. });
  3404. $.fn.editabletypes.select2 = Constructor;
  3405. }(window.jQuery));
  3406. /**
  3407. * Combodate - 1.0.5
  3408. * Dropdown date and time picker.
  3409. * Converts text input into dropdowns to pick day, month, year, hour, minute and second.
  3410. * Uses momentjs as datetime library http://momentjs.com.
  3411. * For i18n include corresponding file from https://github.com/timrwood/moment/tree/master/lang
  3412. *
  3413. * Confusion at noon and midnight - see http://en.wikipedia.org/wiki/12-hour_clock#Confusion_at_noon_and_midnight
  3414. * In combodate:
  3415. * 12:00 pm --> 12:00 (24-h format, midday)
  3416. * 12:00 am --> 00:00 (24-h format, midnight, start of day)
  3417. *
  3418. * Differs from momentjs parse rules:
  3419. * 00:00 pm, 12:00 pm --> 12:00 (24-h format, day not change)
  3420. * 00:00 am, 12:00 am --> 00:00 (24-h format, day not change)
  3421. *
  3422. *
  3423. * Author: Vitaliy Potapov
  3424. * Project page: http://github.com/vitalets/combodate
  3425. * Copyright (c) 2012 Vitaliy Potapov. Released under MIT License.
  3426. **/
  3427. (function ($) {
  3428. var Combodate = function (element, options) {
  3429. this.$element = $(element);
  3430. if(!this.$element.is('input')) {
  3431. $.error('Combodate should be applied to INPUT element');
  3432. return;
  3433. }
  3434. this.options = $.extend({}, $.fn.combodate.defaults, options, this.$element.data());
  3435. this.init();
  3436. };
  3437. Combodate.prototype = {
  3438. constructor: Combodate,
  3439. init: function () {
  3440. this.map = {
  3441. //key regexp moment.method
  3442. day: ['D', 'date'],
  3443. month: ['M', 'month'],
  3444. year: ['Y', 'year'],
  3445. hour: ['[Hh]', 'hours'],
  3446. minute: ['m', 'minutes'],
  3447. second: ['s', 'seconds'],
  3448. ampm: ['[Aa]', '']
  3449. };
  3450. this.$widget = $('<span class="combodate"></span>').html(this.getTemplate());
  3451. this.initCombos();
  3452. //update original input on change
  3453. this.$widget.on('change', 'select', $.proxy(function(e) {
  3454. this.$element.val(this.getValue()).change();
  3455. // update days count if month or year changes
  3456. if (this.options.smartDays) {
  3457. if ($(e.target).is('.month') || $(e.target).is('.year')) {
  3458. this.fillCombo('day');
  3459. }
  3460. }
  3461. }, this));
  3462. this.$widget.find('select').css('width', 'auto');
  3463. // hide original input and insert widget
  3464. this.$element.hide().after(this.$widget);
  3465. // set initial value
  3466. this.setValue(this.$element.val() || this.options.value);
  3467. },
  3468. /*
  3469. Replace tokens in template with <select> elements
  3470. */
  3471. getTemplate: function() {
  3472. var tpl = this.options.template;
  3473. //first pass
  3474. $.each(this.map, function(k, v) {
  3475. v = v[0];
  3476. var r = new RegExp(v+'+'),
  3477. token = v.length > 1 ? v.substring(1, 2) : v;
  3478. tpl = tpl.replace(r, '{'+token+'}');
  3479. });
  3480. //replace spaces with &nbsp;
  3481. tpl = tpl.replace(/ /g, '&nbsp;');
  3482. //second pass
  3483. $.each(this.map, function(k, v) {
  3484. v = v[0];
  3485. var token = v.length > 1 ? v.substring(1, 2) : v;
  3486. tpl = tpl.replace('{'+token+'}', '<select class="'+k+'"></select>');
  3487. });
  3488. return tpl;
  3489. },
  3490. /*
  3491. Initialize combos that presents in template
  3492. */
  3493. initCombos: function() {
  3494. for (var k in this.map) {
  3495. var $c = this.$widget.find('.'+k);
  3496. // set properties like this.$day, this.$month etc.
  3497. this['$'+k] = $c.length ? $c : null;
  3498. // fill with items
  3499. this.fillCombo(k);
  3500. }
  3501. },
  3502. /*
  3503. Fill combo with items
  3504. */
  3505. fillCombo: function(k) {
  3506. var $combo = this['$'+k];
  3507. if (!$combo) {
  3508. return;
  3509. }
  3510. // define method name to fill items, e.g `fillDays`
  3511. var f = 'fill' + k.charAt(0).toUpperCase() + k.slice(1);
  3512. var items = this[f]();
  3513. var value = $combo.val();
  3514. $combo.empty();
  3515. for(var i=0; i<items.length; i++) {
  3516. $combo.append('<option value="'+items[i][0]+'">'+items[i][1]+'</option>');
  3517. }
  3518. $combo.val(value);
  3519. },
  3520. /*
  3521. Initialize items of combos. Handles `firstItem` option
  3522. */
  3523. fillCommon: function(key) {
  3524. var values = [],
  3525. relTime;
  3526. if(this.options.firstItem === 'name') {
  3527. //need both to support moment ver < 2 and >= 2
  3528. relTime = moment.relativeTime || moment.langData()._relativeTime;
  3529. var header = typeof relTime[key] === 'function' ? relTime[key](1, true, key, false) : relTime[key];
  3530. //take last entry (see momentjs lang files structure)
  3531. header = header.split(' ').reverse()[0];
  3532. values.push(['', header]);
  3533. } else if(this.options.firstItem === 'empty') {
  3534. values.push(['', '']);
  3535. }
  3536. return values;
  3537. },
  3538. /*
  3539. fill day
  3540. */
  3541. fillDay: function() {
  3542. var items = this.fillCommon('d'), name, i,
  3543. twoDigit = this.options.template.indexOf('DD') !== -1,
  3544. daysCount = 31;
  3545. // detect days count (depends on month and year)
  3546. // originally https://github.com/vitalets/combodate/pull/7
  3547. if (this.options.smartDays && this.$month && this.$year) {
  3548. var month = parseInt(this.$month.val(), 10);
  3549. var year = parseInt(this.$year.val(), 10);
  3550. if (!isNaN(month) && !isNaN(year)) {
  3551. daysCount = moment([year, month]).daysInMonth();
  3552. }
  3553. }
  3554. for (i = 1; i <= daysCount; i++) {
  3555. name = twoDigit ? this.leadZero(i) : i;
  3556. items.push([i, name]);
  3557. }
  3558. return items;
  3559. },
  3560. /*
  3561. fill month
  3562. */
  3563. fillMonth: function() {
  3564. var items = this.fillCommon('M'), name, i,
  3565. longNames = this.options.template.indexOf('MMMM') !== -1,
  3566. shortNames = this.options.template.indexOf('MMM') !== -1,
  3567. twoDigit = this.options.template.indexOf('MM') !== -1;
  3568. for(i=0; i<=11; i++) {
  3569. if(longNames) {
  3570. //see https://github.com/timrwood/momentjs.com/pull/36
  3571. name = moment().date(1).month(i).format('MMMM');
  3572. } else if(shortNames) {
  3573. name = moment().date(1).month(i).format('MMM');
  3574. } else if(twoDigit) {
  3575. name = this.leadZero(i+1);
  3576. } else {
  3577. name = i+1;
  3578. }
  3579. items.push([i, name]);
  3580. }
  3581. return items;
  3582. },
  3583. /*
  3584. fill year
  3585. */
  3586. fillYear: function() {
  3587. var items = [], name, i,
  3588. longNames = this.options.template.indexOf('YYYY') !== -1;
  3589. for(i=this.options.maxYear; i>=this.options.minYear; i--) {
  3590. name = longNames ? i : (i+'').substring(2);
  3591. items[this.options.yearDescending ? 'push' : 'unshift']([i, name]);
  3592. }
  3593. items = this.fillCommon('y').concat(items);
  3594. return items;
  3595. },
  3596. /*
  3597. fill hour
  3598. */
  3599. fillHour: function() {
  3600. var items = this.fillCommon('h'), name, i,
  3601. h12 = this.options.template.indexOf('h') !== -1,
  3602. h24 = this.options.template.indexOf('H') !== -1,
  3603. twoDigit = this.options.template.toLowerCase().indexOf('hh') !== -1,
  3604. min = h12 ? 1 : 0,
  3605. max = h12 ? 12 : 23;
  3606. for(i=min; i<=max; i++) {
  3607. name = twoDigit ? this.leadZero(i) : i;
  3608. items.push([i, name]);
  3609. }
  3610. return items;
  3611. },
  3612. /*
  3613. fill minute
  3614. */
  3615. fillMinute: function() {
  3616. var items = this.fillCommon('m'), name, i,
  3617. twoDigit = this.options.template.indexOf('mm') !== -1;
  3618. for(i=0; i<=59; i+= this.options.minuteStep) {
  3619. name = twoDigit ? this.leadZero(i) : i;
  3620. items.push([i, name]);
  3621. }
  3622. return items;
  3623. },
  3624. /*
  3625. fill second
  3626. */
  3627. fillSecond: function() {
  3628. var items = this.fillCommon('s'), name, i,
  3629. twoDigit = this.options.template.indexOf('ss') !== -1;
  3630. for(i=0; i<=59; i+= this.options.secondStep) {
  3631. name = twoDigit ? this.leadZero(i) : i;
  3632. items.push([i, name]);
  3633. }
  3634. return items;
  3635. },
  3636. /*
  3637. fill ampm
  3638. */
  3639. fillAmpm: function() {
  3640. var ampmL = this.options.template.indexOf('a') !== -1,
  3641. ampmU = this.options.template.indexOf('A') !== -1,
  3642. items = [
  3643. ['am', ampmL ? 'am' : 'AM'],
  3644. ['pm', ampmL ? 'pm' : 'PM']
  3645. ];
  3646. return items;
  3647. },
  3648. /*
  3649. Returns current date value from combos.
  3650. If format not specified - `options.format` used.
  3651. If format = `null` - Moment object returned.
  3652. */
  3653. getValue: function(format) {
  3654. var dt, values = {},
  3655. that = this,
  3656. notSelected = false;
  3657. //getting selected values
  3658. $.each(this.map, function(k, v) {
  3659. if(k === 'ampm') {
  3660. return;
  3661. }
  3662. var def = k === 'day' ? 1 : 0;
  3663. values[k] = that['$'+k] ? parseInt(that['$'+k].val(), 10) : def;
  3664. if(isNaN(values[k])) {
  3665. notSelected = true;
  3666. return false;
  3667. }
  3668. });
  3669. //if at least one visible combo not selected - return empty string
  3670. if(notSelected) {
  3671. return '';
  3672. }
  3673. //convert hours 12h --> 24h
  3674. if(this.$ampm) {
  3675. //12:00 pm --> 12:00 (24-h format, midday), 12:00 am --> 00:00 (24-h format, midnight, start of day)
  3676. if(values.hour === 12) {
  3677. values.hour = this.$ampm.val() === 'am' ? 0 : 12;
  3678. } else {
  3679. values.hour = this.$ampm.val() === 'am' ? values.hour : values.hour+12;
  3680. }
  3681. }
  3682. dt = moment([values.year, values.month, values.day, values.hour, values.minute, values.second]);
  3683. //highlight invalid date
  3684. this.highlight(dt);
  3685. format = format === undefined ? this.options.format : format;
  3686. if(format === null) {
  3687. return dt.isValid() ? dt : null;
  3688. } else {
  3689. return dt.isValid() ? dt.format(format) : '';
  3690. }
  3691. },
  3692. setValue: function(value) {
  3693. if(!value) {
  3694. return;
  3695. }
  3696. var dt = typeof value === 'string' ? moment(value, this.options.format) : moment(value),
  3697. that = this,
  3698. values = {};
  3699. //function to find nearest value in select options
  3700. function getNearest($select, value) {
  3701. var delta = {};
  3702. $select.children('option').each(function(i, opt){
  3703. var optValue = $(opt).attr('value'),
  3704. distance;
  3705. if(optValue === '') return;
  3706. distance = Math.abs(optValue - value);
  3707. if(typeof delta.distance === 'undefined' || distance < delta.distance) {
  3708. delta = {value: optValue, distance: distance};
  3709. }
  3710. });
  3711. return delta.value;
  3712. }
  3713. if(dt.isValid()) {
  3714. //read values from date object
  3715. $.each(this.map, function(k, v) {
  3716. if(k === 'ampm') {
  3717. return;
  3718. }
  3719. values[k] = dt[v[1]]();
  3720. });
  3721. if(this.$ampm) {
  3722. //12:00 pm --> 12:00 (24-h format, midday), 12:00 am --> 00:00 (24-h format, midnight, start of day)
  3723. if(values.hour >= 12) {
  3724. values.ampm = 'pm';
  3725. if(values.hour > 12) {
  3726. values.hour -= 12;
  3727. }
  3728. } else {
  3729. values.ampm = 'am';
  3730. if(values.hour === 0) {
  3731. values.hour = 12;
  3732. }
  3733. }
  3734. }
  3735. $.each(values, function(k, v) {
  3736. //call val() for each existing combo, e.g. this.$hour.val()
  3737. if(that['$'+k]) {
  3738. if(k === 'minute' && that.options.minuteStep > 1 && that.options.roundTime) {
  3739. v = getNearest(that['$'+k], v);
  3740. }
  3741. if(k === 'second' && that.options.secondStep > 1 && that.options.roundTime) {
  3742. v = getNearest(that['$'+k], v);
  3743. }
  3744. that['$'+k].val(v);
  3745. }
  3746. });
  3747. // update days count
  3748. if (this.options.smartDays) {
  3749. this.fillCombo('day');
  3750. }
  3751. this.$element.val(dt.format(this.options.format)).change();
  3752. }
  3753. },
  3754. /*
  3755. highlight combos if date is invalid
  3756. */
  3757. highlight: function(dt) {
  3758. if(!dt.isValid()) {
  3759. if(this.options.errorClass) {
  3760. this.$widget.addClass(this.options.errorClass);
  3761. } else {
  3762. //store original border color
  3763. if(!this.borderColor) {
  3764. this.borderColor = this.$widget.find('select').css('border-color');
  3765. }
  3766. this.$widget.find('select').css('border-color', 'red');
  3767. }
  3768. } else {
  3769. if(this.options.errorClass) {
  3770. this.$widget.removeClass(this.options.errorClass);
  3771. } else {
  3772. this.$widget.find('select').css('border-color', this.borderColor);
  3773. }
  3774. }
  3775. },
  3776. leadZero: function(v) {
  3777. return v <= 9 ? '0' + v : v;
  3778. },
  3779. destroy: function() {
  3780. this.$widget.remove();
  3781. this.$element.removeData('combodate').show();
  3782. }
  3783. //todo: clear method
  3784. };
  3785. $.fn.combodate = function ( option ) {
  3786. var d, args = Array.apply(null, arguments);
  3787. args.shift();
  3788. //getValue returns date as string / object (not jQuery object)
  3789. if(option === 'getValue' && this.length && (d = this.eq(0).data('combodate'))) {
  3790. return d.getValue.apply(d, args);
  3791. }
  3792. return this.each(function () {
  3793. var $this = $(this),
  3794. data = $this.data('combodate'),
  3795. options = typeof option == 'object' && option;
  3796. if (!data) {
  3797. $this.data('combodate', (data = new Combodate(this, options)));
  3798. }
  3799. if (typeof option == 'string' && typeof data[option] == 'function') {
  3800. data[option].apply(data, args);
  3801. }
  3802. });
  3803. };
  3804. $.fn.combodate.defaults = {
  3805. //in this format value stored in original input
  3806. format: 'DD-MM-YYYY HH:mm',
  3807. //in this format items in dropdowns are displayed
  3808. template: 'D / MMM / YYYY H : mm',
  3809. //initial value, can be `new Date()`
  3810. value: null,
  3811. minYear: 1970,
  3812. maxYear: 2015,
  3813. yearDescending: true,
  3814. minuteStep: 5,
  3815. secondStep: 1,
  3816. firstItem: 'empty', //'name', 'empty', 'none'
  3817. errorClass: null,
  3818. roundTime: true, // whether to round minutes and seconds if step > 1
  3819. smartDays: false // whether days in combo depend on selected month: 31, 30, 28
  3820. };
  3821. }(window.jQuery));
  3822. /**
  3823. Combodate input - dropdown date and time picker.
  3824. Based on [combodate](http://vitalets.github.com/combodate) plugin (included). To use it you should manually include [momentjs](http://momentjs.com).
  3825. <script src="js/moment.min.js"></script>
  3826. Allows to input:
  3827. * only date
  3828. * only time
  3829. * both date and time
  3830. Please note, that format is taken from momentjs and **not compatible** with bootstrap-datepicker / jquery UI datepicker.
  3831. Internally value stored as `momentjs` object.
  3832. @class combodate
  3833. @extends abstractinput
  3834. @final
  3835. @since 1.4.0
  3836. @example
  3837. <a href="#" id="dob" data-type="combodate" data-pk="1" data-url="/post" data-value="1984-05-15" data-title="Select date"></a>
  3838. <script>
  3839. $(function(){
  3840. $('#dob').editable({
  3841. format: 'YYYY-MM-DD',
  3842. viewformat: 'DD.MM.YYYY',
  3843. template: 'D / MMMM / YYYY',
  3844. combodate: {
  3845. minYear: 2000,
  3846. maxYear: 2015,
  3847. minuteStep: 1
  3848. }
  3849. }
  3850. });
  3851. });
  3852. </script>
  3853. **/
  3854. /*global moment*/
  3855. (function ($) {
  3856. "use strict";
  3857. var Constructor = function (options) {
  3858. this.init('combodate', options, Constructor.defaults);
  3859. //by default viewformat equals to format
  3860. if(!this.options.viewformat) {
  3861. this.options.viewformat = this.options.format;
  3862. }
  3863. //try parse combodate config defined as json string in data-combodate
  3864. options.combodate = $.fn.editableutils.tryParseJson(options.combodate, true);
  3865. //overriding combodate config (as by default jQuery extend() is not recursive)
  3866. this.options.combodate = $.extend({}, Constructor.defaults.combodate, options.combodate, {
  3867. format: this.options.format,
  3868. template: this.options.template
  3869. });
  3870. };
  3871. $.fn.editableutils.inherit(Constructor, $.fn.editabletypes.abstractinput);
  3872. $.extend(Constructor.prototype, {
  3873. render: function () {
  3874. this.$input.combodate(this.options.combodate);
  3875. if($.fn.editableform.engine === 'bs3') {
  3876. this.$input.siblings().find('select').addClass('form-control');
  3877. }
  3878. if(this.options.inputclass) {
  3879. this.$input.siblings().find('select').addClass(this.options.inputclass);
  3880. }
  3881. //"clear" link
  3882. /*
  3883. if(this.options.clear) {
  3884. this.$clear = $('<a href="#"></a>').html(this.options.clear).click($.proxy(function(e){
  3885. e.preventDefault();
  3886. e.stopPropagation();
  3887. this.clear();
  3888. }, this));
  3889. this.$tpl.parent().append($('<div class="editable-clear">').append(this.$clear));
  3890. }
  3891. */
  3892. },
  3893. value2html: function(value, element) {
  3894. var text = value ? value.format(this.options.viewformat) : '';
  3895. //$(element).text(text);
  3896. Constructor.superclass.value2html.call(this, text, element);
  3897. },
  3898. html2value: function(html) {
  3899. return html ? moment(html, this.options.viewformat) : null;
  3900. },
  3901. value2str: function(value) {
  3902. return value ? value.format(this.options.format) : '';
  3903. },
  3904. str2value: function(str) {
  3905. return str ? moment(str, this.options.format) : null;
  3906. },
  3907. value2submit: function(value) {
  3908. return this.value2str(value);
  3909. },
  3910. value2input: function(value) {
  3911. this.$input.combodate('setValue', value);
  3912. },
  3913. input2value: function() {
  3914. return this.$input.combodate('getValue', null);
  3915. },
  3916. activate: function() {
  3917. this.$input.siblings('.combodate').find('select').eq(0).focus();
  3918. },
  3919. /*
  3920. clear: function() {
  3921. this.$input.data('datepicker').date = null;
  3922. this.$input.find('.active').removeClass('active');
  3923. },
  3924. */
  3925. autosubmit: function() {
  3926. }
  3927. });
  3928. Constructor.defaults = $.extend({}, $.fn.editabletypes.abstractinput.defaults, {
  3929. /**
  3930. @property tpl
  3931. @default <input type="text">
  3932. **/
  3933. tpl:'<input type="text">',
  3934. /**
  3935. @property inputclass
  3936. @default null
  3937. **/
  3938. inputclass: null,
  3939. /**
  3940. Format used for sending value to server. Also applied when converting date from <code>data-value</code> attribute.<br>
  3941. See list of tokens in [momentjs docs](http://momentjs.com/docs/#/parsing/string-format)
  3942. @property format
  3943. @type string
  3944. @default YYYY-MM-DD
  3945. **/
  3946. format:'YYYY-MM-DD',
  3947. /**
  3948. Format used for displaying date. Also applied when converting date from element's text on init.
  3949. If not specified equals to `format`.
  3950. @property viewformat
  3951. @type string
  3952. @default null
  3953. **/
  3954. viewformat: null,
  3955. /**
  3956. Template used for displaying dropdowns.
  3957. @property template
  3958. @type string
  3959. @default D / MMM / YYYY
  3960. **/
  3961. template: 'D / MMM / YYYY',
  3962. /**
  3963. Configuration of combodate.
  3964. Full list of options: http://vitalets.github.com/combodate/#docs
  3965. @property combodate
  3966. @type object
  3967. @default null
  3968. **/
  3969. combodate: null
  3970. /*
  3971. (not implemented yet)
  3972. Text shown as clear date button.
  3973. If <code>false</code> clear button will not be rendered.
  3974. @property clear
  3975. @type boolean|string
  3976. @default 'x clear'
  3977. */
  3978. //clear: '&times; clear'
  3979. });
  3980. $.fn.editabletypes.combodate = Constructor;
  3981. }(window.jQuery));
  3982. /*
  3983. Editableform based on Twitter Bootstrap 3
  3984. */
  3985. (function ($) {
  3986. "use strict";
  3987. //store parent methods
  3988. var pInitInput = $.fn.editableform.Constructor.prototype.initInput;
  3989. $.extend($.fn.editableform.Constructor.prototype, {
  3990. initTemplate: function() {
  3991. this.$form = $($.fn.editableform.template);
  3992. this.$form.find('.control-group').addClass('form-group');
  3993. this.$form.find('.editable-error-block').addClass('help-block');
  3994. },
  3995. initInput: function() {
  3996. pInitInput.apply(this);
  3997. //for bs3 set default class `input-sm` to standard inputs
  3998. var emptyInputClass = this.input.options.inputclass === null || this.input.options.inputclass === false;
  3999. var defaultClass = 'input-sm';
  4000. //bs3 add `form-control` class to standard inputs
  4001. var stdtypes = 'text,select,textarea,password,email,url,tel,number,range,time,typeaheadjs'.split(',');
  4002. if(~$.inArray(this.input.type, stdtypes)) {
  4003. this.input.$input.addClass('form-control');
  4004. if(emptyInputClass) {
  4005. this.input.options.inputclass = defaultClass;
  4006. this.input.$input.addClass(defaultClass);
  4007. }
  4008. }
  4009. //apply bs3 size class also to buttons (to fit size of control)
  4010. var $btn = this.$form.find('.editable-buttons');
  4011. var classes = emptyInputClass ? [defaultClass] : this.input.options.inputclass.split(' ');
  4012. for(var i=0; i<classes.length; i++) {
  4013. // `btn-sm` is default now
  4014. /*
  4015. if(classes[i].toLowerCase() === 'input-sm') {
  4016. $btn.find('button').addClass('btn-sm');
  4017. }
  4018. */
  4019. if(classes[i].toLowerCase() === 'input-lg') {
  4020. $btn.find('button').removeClass('btn-sm').addClass('btn-lg');
  4021. }
  4022. }
  4023. }
  4024. });
  4025. //buttons
  4026. $.fn.editableform.buttons =
  4027. '<button type="submit" class="btn btn-primary btn-sm editable-submit">'+
  4028. '<i class="glyphicon glyphicon-ok"></i>'+
  4029. '</button>'+
  4030. '<button type="button" class="btn btn-default btn-sm editable-cancel">'+
  4031. '<i class="glyphicon glyphicon-remove"></i>'+
  4032. '</button>';
  4033. //error classes
  4034. $.fn.editableform.errorGroupClass = 'has-error';
  4035. $.fn.editableform.errorBlockClass = null;
  4036. //engine
  4037. $.fn.editableform.engine = 'bs3';
  4038. }(window.jQuery));
  4039. /**
  4040. * Editable Popover3 (for Bootstrap 3)
  4041. * ---------------------
  4042. * requires bootstrap-popover.js
  4043. */
  4044. (function ($) {
  4045. "use strict";
  4046. //extend methods
  4047. $.extend($.fn.editableContainer.Popup.prototype, {
  4048. containerName: 'popover',
  4049. containerDataName: 'bs.popover',
  4050. innerCss: '.popover-content',
  4051. defaults: $.fn.popover.Constructor.DEFAULTS,
  4052. initContainer: function(){
  4053. $.extend(this.containerOptions, {
  4054. trigger: 'manual',
  4055. selector: false,
  4056. content: ' ',
  4057. template: this.defaults.template
  4058. });
  4059. //as template property is used in inputs, hide it from popover
  4060. var t;
  4061. if(this.$element.data('template')) {
  4062. t = this.$element.data('template');
  4063. this.$element.removeData('template');
  4064. }
  4065. this.call(this.containerOptions);
  4066. if(t) {
  4067. //restore data('template')
  4068. this.$element.data('template', t);
  4069. }
  4070. },
  4071. /* show */
  4072. innerShow: function () {
  4073. this.call('show');
  4074. },
  4075. /* hide */
  4076. innerHide: function () {
  4077. this.call('hide');
  4078. },
  4079. /* destroy */
  4080. innerDestroy: function() {
  4081. this.call('destroy');
  4082. },
  4083. setContainerOption: function(key, value) {
  4084. this.container().options[key] = value;
  4085. },
  4086. /**
  4087. * move popover to new position. This function mainly copied from bootstrap-popover.
  4088. */
  4089. /*jshint laxcomma: true, eqeqeq: false*/
  4090. setPosition: function () {
  4091. (function() {
  4092. /*
  4093. var $tip = this.tip()
  4094. , inside
  4095. , pos
  4096. , actualWidth
  4097. , actualHeight
  4098. , placement
  4099. , tp
  4100. , tpt
  4101. , tpb
  4102. , tpl
  4103. , tpr;
  4104. placement = typeof this.options.placement === 'function' ?
  4105. this.options.placement.call(this, $tip[0], this.$element[0]) :
  4106. this.options.placement;
  4107. inside = /in/.test(placement);
  4108. $tip
  4109. // .detach()
  4110. //vitalets: remove any placement class because otherwise they dont influence on re-positioning of visible popover
  4111. .removeClass('top right bottom left')
  4112. .css({ top: 0, left: 0, display: 'block' });
  4113. // .insertAfter(this.$element);
  4114. pos = this.getPosition(inside);
  4115. actualWidth = $tip[0].offsetWidth;
  4116. actualHeight = $tip[0].offsetHeight;
  4117. placement = inside ? placement.split(' ')[1] : placement;
  4118. tpb = {top: pos.top + pos.height, left: pos.left + pos.width / 2 - actualWidth / 2};
  4119. tpt = {top: pos.top - actualHeight, left: pos.left + pos.width / 2 - actualWidth / 2};
  4120. tpl = {top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left - actualWidth};
  4121. tpr = {top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left + pos.width};
  4122. switch (placement) {
  4123. case 'bottom':
  4124. if ((tpb.top + actualHeight) > ($(window).scrollTop() + $(window).height())) {
  4125. if (tpt.top > $(window).scrollTop()) {
  4126. placement = 'top';
  4127. } else if ((tpr.left + actualWidth) < ($(window).scrollLeft() + $(window).width())) {
  4128. placement = 'right';
  4129. } else if (tpl.left > $(window).scrollLeft()) {
  4130. placement = 'left';
  4131. } else {
  4132. placement = 'right';
  4133. }
  4134. }
  4135. break;
  4136. case 'top':
  4137. if (tpt.top < $(window).scrollTop()) {
  4138. if ((tpb.top + actualHeight) < ($(window).scrollTop() + $(window).height())) {
  4139. placement = 'bottom';
  4140. } else if ((tpr.left + actualWidth) < ($(window).scrollLeft() + $(window).width())) {
  4141. placement = 'right';
  4142. } else if (tpl.left > $(window).scrollLeft()) {
  4143. placement = 'left';
  4144. } else {
  4145. placement = 'right';
  4146. }
  4147. }
  4148. break;
  4149. case 'left':
  4150. if (tpl.left < $(window).scrollLeft()) {
  4151. if ((tpr.left + actualWidth) < ($(window).scrollLeft() + $(window).width())) {
  4152. placement = 'right';
  4153. } else if (tpt.top > $(window).scrollTop()) {
  4154. placement = 'top';
  4155. } else if (tpt.top > $(window).scrollTop()) {
  4156. placement = 'bottom';
  4157. } else {
  4158. placement = 'right';
  4159. }
  4160. }
  4161. break;
  4162. case 'right':
  4163. if ((tpr.left + actualWidth) > ($(window).scrollLeft() + $(window).width())) {
  4164. if (tpl.left > $(window).scrollLeft()) {
  4165. placement = 'left';
  4166. } else if (tpt.top > $(window).scrollTop()) {
  4167. placement = 'top';
  4168. } else if (tpt.top > $(window).scrollTop()) {
  4169. placement = 'bottom';
  4170. }
  4171. }
  4172. break;
  4173. }
  4174. switch (placement) {
  4175. case 'bottom':
  4176. tp = tpb;
  4177. break;
  4178. case 'top':
  4179. tp = tpt;
  4180. break;
  4181. case 'left':
  4182. tp = tpl;
  4183. break;
  4184. case 'right':
  4185. tp = tpr;
  4186. break;
  4187. }
  4188. $tip
  4189. .offset(tp)
  4190. .addClass(placement)
  4191. .addClass('in');
  4192. */
  4193. var $tip = this.tip();
  4194. var placement = typeof this.options.placement == 'function' ?
  4195. this.options.placement.call(this, $tip[0], this.$element[0]) :
  4196. this.options.placement;
  4197. var autoToken = /\s?auto?\s?/i;
  4198. var autoPlace = autoToken.test(placement);
  4199. if (autoPlace) {
  4200. placement = placement.replace(autoToken, '') || 'top';
  4201. }
  4202. var pos = this.getPosition();
  4203. var actualWidth = $tip[0].offsetWidth;
  4204. var actualHeight = $tip[0].offsetHeight;
  4205. if (autoPlace) {
  4206. var $parent = this.$element.parent();
  4207. var orgPlacement = placement;
  4208. var docScroll = document.documentElement.scrollTop || document.body.scrollTop;
  4209. var parentWidth = this.options.container == 'body' ? window.innerWidth : $parent.outerWidth();
  4210. var parentHeight = this.options.container == 'body' ? window.innerHeight : $parent.outerHeight();
  4211. var parentLeft = this.options.container == 'body' ? 0 : $parent.offset().left;
  4212. placement = placement == 'bottom' && pos.top + pos.height + actualHeight - docScroll > parentHeight ? 'top' :
  4213. placement == 'top' && pos.top - docScroll - actualHeight < 0 ? 'bottom' :
  4214. placement == 'right' && pos.right + actualWidth > parentWidth ? 'left' :
  4215. placement == 'left' && pos.left - actualWidth < parentLeft ? 'right' :
  4216. placement;
  4217. $tip
  4218. .removeClass(orgPlacement)
  4219. .addClass(placement);
  4220. }
  4221. var calculatedOffset = this.getCalculatedOffset(placement, pos, actualWidth, actualHeight);
  4222. this.applyPlacement(calculatedOffset, placement);
  4223. }).call(this.container());
  4224. /*jshint laxcomma: false, eqeqeq: true*/
  4225. }
  4226. });
  4227. }(window.jQuery));
  4228. /**
  4229. Bootstrap-datetimepicker.
  4230. Based on [smalot bootstrap-datetimepicker plugin](https://github.com/smalot/bootstrap-datetimepicker).
  4231. Before usage you should manually include dependent js and css:
  4232. <link href="css/datetimepicker.css" rel="stylesheet" type="text/css"></link>
  4233. <script src="js/bootstrap-datetimepicker.js"></script>
  4234. For **i18n** you should include js file from here: https://github.com/smalot/bootstrap-datetimepicker/tree/master/js/locales
  4235. and set `language` option.
  4236. @class datetime
  4237. @extends abstractinput
  4238. @final
  4239. @since 1.4.4
  4240. @example
  4241. <a href="#" id="last_seen" data-type="datetime" data-pk="1" data-url="/post" title="Select date & time">15/03/2013 12:45</a>
  4242. <script>
  4243. $(function(){
  4244. $('#last_seen').editable({
  4245. format: 'yyyy-mm-dd hh:ii',
  4246. viewformat: 'dd/mm/yyyy hh:ii',
  4247. datetimepicker: {
  4248. weekStart: 1
  4249. }
  4250. }
  4251. });
  4252. });
  4253. </script>
  4254. **/
  4255. (function ($) {
  4256. "use strict";
  4257. var DateTime = function (options) {
  4258. this.init('datetime', options, DateTime.defaults);
  4259. this.initPicker(options, DateTime.defaults);
  4260. };
  4261. $.fn.editableutils.inherit(DateTime, $.fn.editabletypes.abstractinput);
  4262. $.extend(DateTime.prototype, {
  4263. initPicker: function(options, defaults) {
  4264. //'format' is set directly from settings or data-* attributes
  4265. //by default viewformat equals to format
  4266. if(!this.options.viewformat) {
  4267. this.options.viewformat = this.options.format;
  4268. }
  4269. //try parse datetimepicker config defined as json string in data-datetimepicker
  4270. options.datetimepicker = $.fn.editableutils.tryParseJson(options.datetimepicker, true);
  4271. //overriding datetimepicker config (as by default jQuery extend() is not recursive)
  4272. //since 1.4 datetimepicker internally uses viewformat instead of format. Format is for submit only
  4273. this.options.datetimepicker = $.extend({}, defaults.datetimepicker, options.datetimepicker, {
  4274. format: this.options.viewformat
  4275. });
  4276. //language
  4277. this.options.datetimepicker.language = this.options.datetimepicker.language || 'en';
  4278. //store DPglobal
  4279. this.dpg = $.fn.datetimepicker.DPGlobal;
  4280. //store parsed formats
  4281. this.parsedFormat = this.dpg.parseFormat(this.options.format, this.options.formatType);
  4282. this.parsedViewFormat = this.dpg.parseFormat(this.options.viewformat, this.options.formatType);
  4283. },
  4284. render: function () {
  4285. this.$input.datetimepicker(this.options.datetimepicker);
  4286. //adjust container position when viewMode changes
  4287. //see https://github.com/smalot/bootstrap-datetimepicker/pull/80
  4288. this.$input.on('changeMode', function(e) {
  4289. var f = $(this).closest('form').parent();
  4290. //timeout here, otherwise container changes position before form has new size
  4291. setTimeout(function(){
  4292. f.triggerHandler('resize');
  4293. }, 0);
  4294. });
  4295. //"clear" link
  4296. if(this.options.clear) {
  4297. this.$clear = $('<a href="#"></a>').html(this.options.clear).click($.proxy(function(e){
  4298. e.preventDefault();
  4299. e.stopPropagation();
  4300. this.clear();
  4301. }, this));
  4302. this.$tpl.parent().append($('<div class="editable-clear">').append(this.$clear));
  4303. }
  4304. },
  4305. value2html: function(value, element) {
  4306. //formatDate works with UTCDate!
  4307. var text = value ? this.dpg.formatDate(this.toUTC(value), this.parsedViewFormat, this.options.datetimepicker.language, this.options.formatType) : '';
  4308. if(element) {
  4309. DateTime.superclass.value2html.call(this, text, element);
  4310. } else {
  4311. return text;
  4312. }
  4313. },
  4314. html2value: function(html) {
  4315. //parseDate return utc date!
  4316. var value = this.parseDate(html, this.parsedViewFormat);
  4317. return value ? this.fromUTC(value) : null;
  4318. },
  4319. value2str: function(value) {
  4320. //formatDate works with UTCDate!
  4321. return value ? this.dpg.formatDate(this.toUTC(value), this.parsedFormat, this.options.datetimepicker.language, this.options.formatType) : '';
  4322. },
  4323. str2value: function(str) {
  4324. //parseDate return utc date!
  4325. var value = this.parseDate(str, this.parsedFormat);
  4326. return value ? this.fromUTC(value) : null;
  4327. },
  4328. value2submit: function(value) {
  4329. return this.value2str(value);
  4330. },
  4331. value2input: function(value) {
  4332. if(value) {
  4333. this.$input.data('datetimepicker').setDate(value);
  4334. }
  4335. },
  4336. input2value: function() {
  4337. //date may be cleared, in that case getDate() triggers error
  4338. var dt = this.$input.data('datetimepicker');
  4339. return dt.date ? dt.getDate() : null;
  4340. },
  4341. activate: function() {
  4342. },
  4343. clear: function() {
  4344. this.$input.data('datetimepicker').date = null;
  4345. this.$input.find('.active').removeClass('active');
  4346. if(!this.options.showbuttons) {
  4347. this.$input.closest('form').submit();
  4348. }
  4349. },
  4350. autosubmit: function() {
  4351. this.$input.on('mouseup', '.minute', function(e){
  4352. var $form = $(this).closest('form');
  4353. setTimeout(function() {
  4354. $form.submit();
  4355. }, 200);
  4356. });
  4357. },
  4358. //convert date from local to utc
  4359. toUTC: function(value) {
  4360. return value ? new Date(value.valueOf() - value.getTimezoneOffset() * 60000) : value;
  4361. },
  4362. //convert date from utc to local
  4363. fromUTC: function(value) {
  4364. return value ? new Date(value.valueOf() + value.getTimezoneOffset() * 60000) : value;
  4365. },
  4366. /*
  4367. For incorrect date bootstrap-datetimepicker returns current date that is not suitable
  4368. for datetimefield.
  4369. This function returns null for incorrect date.
  4370. */
  4371. parseDate: function(str, format) {
  4372. var date = null, formattedBack;
  4373. if(str) {
  4374. date = this.dpg.parseDate(str, format, this.options.datetimepicker.language, this.options.formatType);
  4375. if(typeof str === 'string') {
  4376. formattedBack = this.dpg.formatDate(date, format, this.options.datetimepicker.language, this.options.formatType);
  4377. if(str !== formattedBack) {
  4378. date = null;
  4379. }
  4380. }
  4381. }
  4382. return date;
  4383. }
  4384. });
  4385. DateTime.defaults = $.extend({}, $.fn.editabletypes.abstractinput.defaults, {
  4386. /**
  4387. @property tpl
  4388. @default <div></div>
  4389. **/
  4390. tpl:'<div class="editable-date well"></div>',
  4391. /**
  4392. @property inputclass
  4393. @default null
  4394. **/
  4395. inputclass: null,
  4396. /**
  4397. Format used for sending value to server. Also applied when converting date from <code>data-value</code> attribute.<br>
  4398. Possible tokens are: <code>d, dd, m, mm, yy, yyyy, h, i</code>
  4399. @property format
  4400. @type string
  4401. @default yyyy-mm-dd hh:ii
  4402. **/
  4403. format:'yyyy-mm-dd hh:ii',
  4404. formatType:'standard',
  4405. /**
  4406. Format used for displaying date. Also applied when converting date from element's text on init.
  4407. If not specified equals to <code>format</code>
  4408. @property viewformat
  4409. @type string
  4410. @default null
  4411. **/
  4412. viewformat: null,
  4413. /**
  4414. Configuration of datetimepicker.
  4415. Full list of options: https://github.com/smalot/bootstrap-datetimepicker
  4416. @property datetimepicker
  4417. @type object
  4418. @default { }
  4419. **/
  4420. datetimepicker:{
  4421. todayHighlight: false,
  4422. autoclose: false
  4423. },
  4424. /**
  4425. Text shown as clear date button.
  4426. If <code>false</code> clear button will not be rendered.
  4427. @property clear
  4428. @type boolean|string
  4429. @default 'x clear'
  4430. **/
  4431. clear: '&times; clear'
  4432. });
  4433. $.fn.editabletypes.datetime = DateTime;
  4434. }(window.jQuery));
  4435. /**
  4436. Bootstrap datetimefield input - datetime input for inline mode.
  4437. Shows normal <input type="text"> and binds popup datetimepicker.
  4438. Automatically shown in inline mode.
  4439. @class datetimefield
  4440. @extends datetime
  4441. **/
  4442. (function ($) {
  4443. "use strict";
  4444. var DateTimeField = function (options) {
  4445. this.init('datetimefield', options, DateTimeField.defaults);
  4446. this.initPicker(options, DateTimeField.defaults);
  4447. };
  4448. $.fn.editableutils.inherit(DateTimeField, $.fn.editabletypes.datetime);
  4449. $.extend(DateTimeField.prototype, {
  4450. render: function () {
  4451. this.$input = this.$tpl.find('input');
  4452. this.setClass();
  4453. this.setAttr('placeholder');
  4454. this.$tpl.datetimepicker(this.options.datetimepicker);
  4455. //need to disable original event handlers
  4456. this.$input.off('focus keydown');
  4457. //update value of datepicker
  4458. this.$input.keyup($.proxy(function(){
  4459. this.$tpl.removeData('date');
  4460. this.$tpl.datetimepicker('update');
  4461. }, this));
  4462. },
  4463. value2input: function(value) {
  4464. this.$input.val(this.value2html(value));
  4465. this.$tpl.datetimepicker('update');
  4466. },
  4467. input2value: function() {
  4468. return this.html2value(this.$input.val());
  4469. },
  4470. activate: function() {
  4471. $.fn.editabletypes.text.prototype.activate.call(this);
  4472. },
  4473. autosubmit: function() {
  4474. //reset autosubmit to empty
  4475. }
  4476. });
  4477. DateTimeField.defaults = $.extend({}, $.fn.editabletypes.datetime.defaults, {
  4478. /**
  4479. @property tpl
  4480. **/
  4481. tpl:'<div class="input-group date"><input type="text"/><span class="input-group-addon"><i class="fa fa-th"></i></span></div>',
  4482. /**
  4483. @property inputclass
  4484. @default 'input-medium'
  4485. **/
  4486. inputclass: 'input-medium',
  4487. /* datetimepicker config */
  4488. datetimepicker:{
  4489. todayHighlight: false,
  4490. autoclose: true
  4491. }
  4492. });
  4493. $.fn.editabletypes.datetimefield = DateTimeField;
  4494. }(window.jQuery));