123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661266226632664266526662667266826692670267126722673267426752676267726782679268026812682268326842685268626872688268926902691269226932694269526962697269826992700270127022703270427052706270727082709271027112712271327142715271627172718271927202721272227232724272527262727272827292730273127322733273427352736273727382739274027412742274327442745274627472748274927502751275227532754275527562757275827592760276127622763276427652766276727682769277027712772277327742775277627772778277927802781278227832784278527862787278827892790279127922793279427952796279727982799280028012802280328042805280628072808280928102811281228132814281528162817281828192820282128222823282428252826282728282829283028312832283328342835283628372838283928402841284228432844284528462847284828492850285128522853285428552856285728582859286028612862286328642865286628672868286928702871287228732874287528762877287828792880288128822883288428852886288728882889289028912892289328942895289628972898289929002901290229032904290529062907290829092910291129122913291429152916291729182919292029212922292329242925292629272928292929302931293229332934293529362937293829392940294129422943294429452946294729482949295029512952295329542955295629572958295929602961296229632964296529662967296829692970297129722973297429752976297729782979298029812982298329842985298629872988298929902991299229932994299529962997299829993000300130023003300430053006300730083009301030113012301330143015301630173018301930203021302230233024302530263027302830293030303130323033303430353036303730383039304030413042304330443045304630473048304930503051305230533054305530563057305830593060306130623063306430653066306730683069307030713072307330743075307630773078307930803081308230833084308530863087308830893090309130923093309430953096309730983099310031013102310331043105310631073108310931103111311231133114311531163117311831193120312131223123312431253126312731283129313031313132313331343135313631373138313931403141314231433144314531463147314831493150315131523153315431553156315731583159316031613162316331643165316631673168316931703171317231733174317531763177317831793180318131823183318431853186318731883189319031913192319331943195319631973198319932003201320232033204320532063207320832093210321132123213321432153216321732183219322032213222322332243225322632273228322932303231323232333234323532363237323832393240324132423243324432453246324732483249325032513252325332543255325632573258325932603261326232633264326532663267326832693270327132723273327432753276327732783279328032813282328332843285328632873288328932903291329232933294329532963297329832993300330133023303330433053306330733083309331033113312331333143315331633173318331933203321332233233324332533263327332833293330333133323333333433353336333733383339334033413342334333443345334633473348334933503351335233533354335533563357335833593360336133623363336433653366336733683369337033713372337333743375337633773378337933803381338233833384338533863387338833893390339133923393339433953396339733983399340034013402340334043405340634073408340934103411341234133414341534163417341834193420342134223423342434253426342734283429343034313432343334343435343634373438343934403441344234433444344534463447344834493450345134523453345434553456345734583459346034613462346334643465346634673468346934703471347234733474347534763477347834793480348134823483348434853486348734883489349034913492349334943495349634973498349935003501350235033504350535063507350835093510351135123513351435153516351735183519352035213522352335243525352635273528352935303531353235333534353535363537353835393540354135423543354435453546354735483549355035513552355335543555355635573558355935603561356235633564356535663567356835693570357135723573357435753576357735783579358035813582358335843585358635873588358935903591359235933594359535963597359835993600360136023603360436053606360736083609361036113612361336143615361636173618361936203621362236233624362536263627362836293630363136323633363436353636363736383639364036413642364336443645364636473648364936503651365236533654365536563657365836593660366136623663366436653666366736683669367036713672367336743675367636773678367936803681368236833684368536863687368836893690369136923693369436953696369736983699370037013702370337043705370637073708370937103711371237133714371537163717371837193720372137223723372437253726372737283729373037313732373337343735373637373738373937403741374237433744374537463747374837493750375137523753375437553756375737583759376037613762376337643765376637673768376937703771377237733774377537763777377837793780378137823783378437853786378737883789379037913792379337943795379637973798379938003801380238033804380538063807380838093810381138123813381438153816381738183819382038213822382338243825382638273828382938303831383238333834383538363837383838393840384138423843384438453846384738483849385038513852385338543855385638573858385938603861386238633864386538663867386838693870387138723873387438753876387738783879388038813882388338843885388638873888388938903891389238933894389538963897389838993900390139023903390439053906390739083909391039113912391339143915391639173918391939203921392239233924392539263927392839293930393139323933393439353936393739383939394039413942394339443945394639473948394939503951395239533954395539563957395839593960396139623963396439653966396739683969397039713972397339743975397639773978397939803981398239833984398539863987398839893990399139923993399439953996399739983999400040014002400340044005400640074008400940104011401240134014401540164017401840194020402140224023402440254026402740284029403040314032403340344035403640374038403940404041404240434044404540464047404840494050405140524053405440554056405740584059406040614062406340644065406640674068406940704071407240734074407540764077407840794080408140824083408440854086408740884089409040914092409340944095409640974098409941004101410241034104410541064107410841094110411141124113411441154116411741184119412041214122412341244125412641274128412941304131413241334134413541364137413841394140414141424143414441454146414741484149415041514152415341544155415641574158415941604161416241634164416541664167416841694170417141724173417441754176417741784179418041814182418341844185418641874188418941904191419241934194419541964197419841994200420142024203420442054206420742084209421042114212421342144215421642174218421942204221422242234224422542264227422842294230423142324233423442354236423742384239424042414242424342444245424642474248424942504251425242534254425542564257425842594260426142624263426442654266426742684269427042714272427342744275427642774278427942804281428242834284428542864287428842894290429142924293429442954296429742984299430043014302430343044305430643074308430943104311431243134314431543164317431843194320432143224323432443254326432743284329433043314332433343344335433643374338433943404341434243434344434543464347434843494350435143524353435443554356435743584359436043614362436343644365436643674368436943704371437243734374437543764377437843794380438143824383438443854386438743884389439043914392439343944395439643974398439944004401440244034404440544064407440844094410441144124413441444154416441744184419442044214422442344244425442644274428442944304431443244334434443544364437443844394440444144424443444444454446444744484449445044514452445344544455445644574458445944604461446244634464446544664467446844694470447144724473447444754476447744784479448044814482448344844485448644874488448944904491449244934494449544964497449844994500450145024503450445054506450745084509451045114512451345144515451645174518451945204521452245234524452545264527452845294530453145324533453445354536453745384539454045414542454345444545454645474548454945504551455245534554455545564557455845594560456145624563456445654566456745684569457045714572457345744575457645774578457945804581458245834584458545864587458845894590459145924593459445954596459745984599460046014602460346044605460646074608460946104611461246134614461546164617461846194620462146224623462446254626462746284629463046314632463346344635463646374638463946404641464246434644464546464647464846494650465146524653465446554656465746584659466046614662466346644665466646674668466946704671467246734674467546764677467846794680468146824683468446854686468746884689469046914692469346944695469646974698469947004701470247034704470547064707470847094710471147124713471447154716471747184719472047214722472347244725472647274728472947304731473247334734473547364737473847394740474147424743474447454746474747484749475047514752475347544755475647574758475947604761476247634764476547664767476847694770477147724773477447754776477747784779478047814782478347844785478647874788478947904791479247934794479547964797479847994800480148024803480448054806480748084809481048114812481348144815481648174818481948204821482248234824482548264827482848294830483148324833483448354836483748384839484048414842484348444845484648474848484948504851485248534854485548564857485848594860486148624863486448654866486748684869487048714872487348744875487648774878487948804881488248834884488548864887488848894890489148924893489448954896489748984899490049014902490349044905490649074908490949104911491249134914491549164917491849194920492149224923492449254926492749284929493049314932493349344935493649374938493949404941494249434944494549464947494849494950495149524953495449554956495749584959496049614962496349644965496649674968496949704971497249734974497549764977497849794980498149824983498449854986498749884989499049914992499349944995499649974998499950005001500250035004500550065007500850095010501150125013501450155016501750185019502050215022502350245025502650275028502950305031503250335034503550365037503850395040504150425043504450455046504750485049505050515052505350545055505650575058505950605061506250635064506550665067506850695070507150725073507450755076507750785079508050815082508350845085508650875088508950905091509250935094509550965097509850995100510151025103510451055106510751085109511051115112511351145115511651175118511951205121512251235124512551265127512851295130513151325133513451355136513751385139514051415142514351445145514651475148514951505151515251535154515551565157515851595160516151625163516451655166516751685169517051715172517351745175517651775178517951805181518251835184518551865187518851895190519151925193519451955196519751985199520052015202520352045205520652075208520952105211521252135214521552165217521852195220522152225223522452255226522752285229523052315232523352345235523652375238523952405241524252435244 |
- (function($){var lm={"config":{},"container":{},"controls":{},"errors":{},"items":{},"utils":{}};
- lm.utils.F = function() {
- };
- lm.utils.extend = function( subClass, superClass ) {
- subClass.prototype = lm.utils.createObject( superClass.prototype );
- subClass.prototype.contructor = subClass;
- };
- lm.utils.createObject = function( prototype ) {
- if( typeof Object.create === 'function' ) {
- return Object.create( prototype );
- } else {
- lm.utils.F.prototype = prototype;
- return new lm.utils.F();
- }
- };
- lm.utils.objectKeys = function( object ) {
- var keys, key;
- if( typeof Object.keys === 'function' ) {
- return Object.keys( object );
- } else {
- keys = [];
- for( key in object ) {
- keys.push( key );
- }
- return keys;
- }
- };
- lm.utils.getHashValue = function( key ) {
- var matches = location.hash.match( new RegExp( key + '=([^&]*)' ) );
- return matches ? matches[ 1 ] : null;
- };
- lm.utils.getQueryStringParam = function( param ) {
- if( window.location.hash ) {
- return lm.utils.getHashValue( param );
- } else if( !window.location.search ) {
- return null;
- }
- var keyValuePairs = window.location.search.substr( 1 ).split( '&' ),
- params = {},
- pair,
- i;
- for( i = 0; i < keyValuePairs.length; i++ ) {
- pair = keyValuePairs[ i ].split( '=' );
- params[ pair[ 0 ] ] = pair[ 1 ];
- }
- return params[ param ] || null;
- };
- lm.utils.copy = function( target, source ) {
- for( var key in source ) {
- target[ key ] = source[ key ];
- }
- return target;
- };
- /**
- * This is based on Paul Irish's shim, but looks quite odd in comparison. Why?
- * Because
- * a) it shouldn't affect the global requestAnimationFrame function
- * b) it shouldn't pass on the time that has passed
- *
- * @param {Function} fn
- *
- * @returns {void}
- */
- lm.utils.animFrame = function( fn ) {
- return ( window.requestAnimationFrame ||
- window.webkitRequestAnimationFrame ||
- window.mozRequestAnimationFrame ||
- function( callback ) {
- window.setTimeout( callback, 1000 / 60 );
- })( function() {
- fn();
- } );
- };
- lm.utils.indexOf = function( needle, haystack ) {
- if( !( haystack instanceof Array ) ) {
- throw new Error( 'Haystack is not an Array' );
- }
- if( haystack.indexOf ) {
- return haystack.indexOf( needle );
- } else {
- for( var i = 0; i < haystack.length; i++ ) {
- if( haystack[ i ] === needle ) {
- return i;
- }
- }
- return -1;
- }
- };
- if( typeof /./ != 'function' && typeof Int8Array != 'object' ) {
- lm.utils.isFunction = function( obj ) {
- return typeof obj == 'function' || false;
- };
- } else {
- lm.utils.isFunction = function( obj ) {
- return toString.call( obj ) === '[object Function]';
- };
- }
- lm.utils.fnBind = function( fn, context, boundArgs ) {
- if( Function.prototype.bind !== undefined ) {
- return Function.prototype.bind.apply( fn, [ context ].concat( boundArgs || [] ) );
- }
- var bound = function() {
- // Join the already applied arguments to the now called ones (after converting to an array again).
- var args = ( boundArgs || [] ).concat( Array.prototype.slice.call( arguments, 0 ) );
- // If not being called as a constructor
- if( !(this instanceof bound) ) {
- // return the result of the function called bound to target and partially applied.
- return fn.apply( context, args );
- }
- // If being called as a constructor, apply the function bound to self.
- fn.apply( this, args );
- };
- // Attach the prototype of the function to our newly created function.
- bound.prototype = fn.prototype;
- return bound;
- };
- lm.utils.removeFromArray = function( item, array ) {
- var index = lm.utils.indexOf( item, array );
- if( index === -1 ) {
- throw new Error( 'Can\'t remove item from array. Item is not in the array' );
- }
- array.splice( index, 1 );
- };
- lm.utils.now = function() {
- if( typeof Date.now === 'function' ) {
- return Date.now();
- } else {
- return ( new Date() ).getTime();
- }
- };
- lm.utils.getUniqueId = function() {
- return ( Math.random() * 1000000000000000 )
- .toString( 36 )
- .replace( '.', '' );
- };
- /**
- * A basic XSS filter. It is ultimately up to the
- * implementing developer to make sure their particular
- * applications and usecases are save from cross site scripting attacks
- *
- * @param {String} input
- * @param {Boolean} keepTags
- *
- * @returns {String} filtered input
- */
- lm.utils.filterXss = function( input, keepTags ) {
- var output = input
- .replace( /javascript/gi, 'javascript' )
- .replace( /expression/gi, 'expression' )
- .replace( /onload/gi, 'onload' )
- .replace( /script/gi, 'script' )
- .replace( /onerror/gi, 'onerror' );
- if( keepTags === true ) {
- return output;
- } else {
- return output
- .replace( />/g, '>' )
- .replace( /</g, '<' );
- }
- };
- /**
- * Removes html tags from a string
- *
- * @param {String} input
- *
- * @returns {String} input without tags
- */
- lm.utils.stripTags = function( input ) {
- return $.trim( input.replace( /(<([^>]+)>)/ig, '' ) );
- };
- /**
- * A generic and very fast EventEmitter
- * implementation. On top of emitting the
- * actual event it emits an
- *
- * lm.utils.EventEmitter.ALL_EVENT
- *
- * event for every event triggered. This allows
- * to hook into it and proxy events forwards
- *
- * @constructor
- */
- lm.utils.EventEmitter = function() {
- this._mSubscriptions = {};
- this._mSubscriptions[ lm.utils.EventEmitter.ALL_EVENT ] = [];
- /**
- * Listen for events
- *
- * @param {String} sEvent The name of the event to listen to
- * @param {Function} fCallback The callback to execute when the event occurs
- * @param {[Object]} oContext The value of the this pointer within the callback function
- *
- * @returns {void}
- */
- this.on = function( sEvent, fCallback, oContext ) {
- if( !lm.utils.isFunction( fCallback ) ) {
- throw new Error( 'Tried to listen to event ' + sEvent + ' with non-function callback ' + fCallback );
- }
- if( !this._mSubscriptions[ sEvent ] ) {
- this._mSubscriptions[ sEvent ] = [];
- }
- this._mSubscriptions[ sEvent ].push( { fn: fCallback, ctx: oContext } );
- };
- /**
- * Emit an event and notify listeners
- *
- * @param {String} sEvent The name of the event
- * @param {Mixed} various additional arguments that will be passed to the listener
- *
- * @returns {void}
- */
- this.emit = function( sEvent ) {
- var i, ctx, args;
- args = Array.prototype.slice.call( arguments, 1 );
- if( this._mSubscriptions[ sEvent ] ) {
- for( i = 0; i < this._mSubscriptions[ sEvent ].length; i++ ) {
- ctx = this._mSubscriptions[ sEvent ][ i ].ctx || {};
- this._mSubscriptions[ sEvent ][ i ].fn.apply( ctx, args );
- }
- }
- args.unshift( sEvent );
- for( i = 0; i < this._mSubscriptions[ lm.utils.EventEmitter.ALL_EVENT ].length; i++ ) {
- ctx = this._mSubscriptions[ lm.utils.EventEmitter.ALL_EVENT ][ i ].ctx || {};
- this._mSubscriptions[ lm.utils.EventEmitter.ALL_EVENT ][ i ].fn.apply( ctx, args );
- }
- };
- /**
- * Removes a listener for an event, or all listeners if no callback and context is provided.
- *
- * @param {String} sEvent The name of the event
- * @param {Function} fCallback The previously registered callback method (optional)
- * @param {Object} oContext The previously registered context (optional)
- *
- * @returns {void}
- */
- this.unbind = function( sEvent, fCallback, oContext ) {
- if( !this._mSubscriptions[ sEvent ] ) {
- throw new Error( 'No subscribtions to unsubscribe for event ' + sEvent );
- }
- var i, bUnbound = false;
- for( i = 0; i < this._mSubscriptions[ sEvent ].length; i++ ) {
- if
- (
- ( !fCallback || this._mSubscriptions[ sEvent ][ i ].fn === fCallback ) &&
- ( !oContext || oContext === this._mSubscriptions[ sEvent ][ i ].ctx )
- ) {
- this._mSubscriptions[ sEvent ].splice( i, 1 );
- bUnbound = true;
- }
- }
- if( bUnbound === false ) {
- throw new Error( 'Nothing to unbind for ' + sEvent );
- }
- };
- /**
- * Alias for unbind
- */
- this.off = this.unbind;
- /**
- * Alias for emit
- */
- this.trigger = this.emit;
- };
- /**
- * The name of the event that's triggered for every other event
- *
- * usage
- *
- * myEmitter.on( lm.utils.EventEmitter.ALL_EVENT, function( eventName, argsArray ){
- * //do stuff
- * });
- *
- * @type {String}
- */
- lm.utils.EventEmitter.ALL_EVENT = '__all';
- lm.utils.DragListener = function( eElement, nButtonCode ) {
- lm.utils.EventEmitter.call( this );
- this._eElement = $( eElement );
- this._oDocument = $( document );
- this._eBody = $( document.body );
- this._nButtonCode = nButtonCode || 0;
- /**
- * The delay after which to start the drag in milliseconds
- */
- this._nDelay = 200;
- /**
- * The distance the mouse needs to be moved to qualify as a drag
- */
- this._nDistance = 10;//TODO - works better with delay only
- this._nX = 0;
- this._nY = 0;
- this._nOriginalX = 0;
- this._nOriginalY = 0;
- this._bDragging = false;
- this._fMove = lm.utils.fnBind( this.onMouseMove, this );
- this._fUp = lm.utils.fnBind( this.onMouseUp, this );
- this._fDown = lm.utils.fnBind( this.onMouseDown, this );
- this._eElement.on( 'mousedown touchstart', this._fDown );
- };
- lm.utils.DragListener.timeout = null;
- lm.utils.copy( lm.utils.DragListener.prototype, {
- destroy: function() {
- this._eElement.unbind( 'mousedown touchstart', this._fDown );
- },
- onMouseDown: function( oEvent ) {
- oEvent.preventDefault();
- if( oEvent.button == 0 || oEvent.type === "touchstart" ) {
- var coordinates = this._getCoordinates( oEvent );
- this._nOriginalX = coordinates.x;
- this._nOriginalY = coordinates.y;
- this._oDocument.on( 'mousemove touchmove', this._fMove );
- this._oDocument.one( 'mouseup touchend', this._fUp );
- this._timeout = setTimeout( lm.utils.fnBind( this._startDrag, this ), this._nDelay );
- }
- },
- onMouseMove: function( oEvent ) {
- if( this._timeout != null ) {
- oEvent.preventDefault();
- var coordinates = this._getCoordinates( oEvent );
- this._nX = coordinates.x - this._nOriginalX;
- this._nY = coordinates.y - this._nOriginalY;
- if( this._bDragging === false ) {
- if(
- Math.abs( this._nX ) > this._nDistance ||
- Math.abs( this._nY ) > this._nDistance
- ) {
- clearTimeout( this._timeout );
- this._startDrag();
- }
- }
- if( this._bDragging ) {
- this.emit( 'drag', this._nX, this._nY, oEvent );
- }
- }
- },
- onMouseUp: function( oEvent ) {
- if( this._timeout != null ) {
- clearTimeout( this._timeout );
- this._eBody.removeClass( 'lm_dragging' );
- this._eElement.removeClass( 'lm_dragging' );
- this._oDocument.find( 'iframe' ).css( 'pointer-events', '' );
- this._oDocument.unbind( 'mousemove touchmove', this._fMove );
- if( this._bDragging === true ) {
- this._bDragging = false;
- this.emit( 'dragStop', oEvent, this._nOriginalX + this._nX );
- }
- }
- },
- _startDrag: function() {
- this._bDragging = true;
- this._eBody.addClass( 'lm_dragging' );
- this._eElement.addClass( 'lm_dragging' );
- this._oDocument.find( 'iframe' ).css( 'pointer-events', 'none' );
- this.emit( 'dragStart', this._nOriginalX, this._nOriginalY );
- },
- _getCoordinates: function( event ) {
- event = event.originalEvent && event.originalEvent.touches ? event.originalEvent.touches[ 0 ] : event;
- return {
- x: event.pageX,
- y: event.pageY
- };
- }
- } );
- /**
- * The main class that will be exposed as GoldenLayout.
- *
- * @public
- * @constructor
- * @param {GoldenLayout config} config
- * @param {[DOM element container]} container Can be a jQuery selector string or a Dom element. Defaults to body
- *
- * @returns {VOID}
- */
- lm.LayoutManager = function( config, container ) {
- if( !$ || typeof $.noConflict !== 'function' ) {
- var errorMsg = 'jQuery is missing as dependency for GoldenLayout. ';
- errorMsg += 'Please either expose $ on GoldenLayout\'s scope (e.g. window) or add "jquery" to ';
- errorMsg += 'your paths when using RequireJS/AMD';
- throw new Error( errorMsg );
- }
- lm.utils.EventEmitter.call( this );
- this.isInitialised = false;
- this._isFullPage = false;
- this._resizeTimeoutId = null;
- this._components = { 'lm-react-component': lm.utils.ReactComponentHandler };
- this._itemAreas = [];
- this._resizeFunction = lm.utils.fnBind( this._onResize, this );
- this._unloadFunction = lm.utils.fnBind( this._onUnload, this );
- this._maximisedItem = null;
- this._maximisePlaceholder = $( '<div class="lm_maximise_place"></div>' );
- this._creationTimeoutPassed = false;
- this._subWindowsCreated = false;
- this._dragSources = [];
- this._updatingColumnsResponsive = false;
- this._firstLoad = true;
- this.width = null;
- this.height = null;
- this.root = null;
- this.openPopouts = [];
- this.selectedItem = null;
- this.isSubWindow = false;
- this.eventHub = new lm.utils.EventHub( this );
- this.config = this._createConfig( config );
- this.container = container;
- this.dropTargetIndicator = null;
- this.transitionIndicator = null;
- this.tabDropPlaceholder = $( '<div class="lm_drop_tab_placeholder"></div>' );
- if( this.isSubWindow === true ) {
- $( 'body' ).css( 'visibility', 'hidden' );
- }
- this._typeToItem = {
- 'column': lm.utils.fnBind( lm.items.RowOrColumn, this, [ true ] ),
- 'row': lm.utils.fnBind( lm.items.RowOrColumn, this, [ false ] ),
- 'stack': lm.items.Stack,
- 'component': lm.items.Component
- };
- };
- /**
- * Hook that allows to access private classes
- */
- lm.LayoutManager.__lm = lm;
- /**
- * Takes a GoldenLayout configuration object and
- * replaces its keys and values recursively with
- * one letter codes
- *
- * @static
- * @public
- * @param {Object} config A GoldenLayout config object
- *
- * @returns {Object} minified config
- */
- lm.LayoutManager.minifyConfig = function( config ) {
- return ( new lm.utils.ConfigMinifier() ).minifyConfig( config );
- };
- /**
- * Takes a configuration Object that was previously minified
- * using minifyConfig and returns its original version
- *
- * @static
- * @public
- * @param {Object} minifiedConfig
- *
- * @returns {Object} the original configuration
- */
- lm.LayoutManager.unminifyConfig = function( config ) {
- return ( new lm.utils.ConfigMinifier() ).unminifyConfig( config );
- };
- lm.utils.copy( lm.LayoutManager.prototype, {
- /**
- * Register a component with the layout manager. If a configuration node
- * of type component is reached it will look up componentName and create the
- * associated component
- *
- * {
- * type: "component",
- * componentName: "EquityNewsFeed",
- * componentState: { "feedTopic": "us-bluechips" }
- * }
- *
- * @public
- * @param {String} name
- * @param {Function} constructor
- *
- * @returns {void}
- */
- registerComponent: function( name, constructor ) {
- if( typeof constructor !== 'function' ) {
- throw new Error( 'Please register a constructor function' );
- }
- if( this._components[ name ] !== undefined ) {
- throw new Error( 'Component ' + name + ' is already registered' );
- }
- this._components[ name ] = constructor;
- },
- /**
- * Creates a layout configuration object based on the the current state
- *
- * @public
- * @returns {Object} GoldenLayout configuration
- */
- toConfig: function( root ) {
- var config, next, i;
- if( this.isInitialised === false ) {
- throw new Error( 'Can\'t create config, layout not yet initialised' );
- }
- if( root && !( root instanceof lm.items.AbstractContentItem ) ) {
- throw new Error( 'Root must be a ContentItem' );
- }
- /*
- * settings & labels
- */
- config = {
- settings: lm.utils.copy( {}, this.config.settings ),
- dimensions: lm.utils.copy( {}, this.config.dimensions ),
- labels: lm.utils.copy( {}, this.config.labels )
- };
- /*
- * Content
- */
- config.content = [];
- next = function( configNode, item ) {
- var key, i;
- for( key in item.config ) {
- if( key !== 'content' ) {
- configNode[ key ] = item.config[ key ];
- }
- }
- if( item.contentItems.length ) {
- configNode.content = [];
- for( i = 0; i < item.contentItems.length; i++ ) {
- configNode.content[ i ] = {};
- next( configNode.content[ i ], item.contentItems[ i ] );
- }
- }
- };
- if( root ) {
- next( config, { contentItems: [ root ] } );
- } else {
- next( config, this.root );
- }
- /*
- * Retrieve config for subwindows
- */
- this._$reconcilePopoutWindows();
- config.openPopouts = [];
- for( i = 0; i < this.openPopouts.length; i++ ) {
- config.openPopouts.push( this.openPopouts[ i ].toConfig() );
- }
- /*
- * Add maximised item
- */
- config.maximisedItemId = this._maximisedItem ? '__glMaximised' : null;
- return config;
- },
- /**
- * Returns a previously registered component
- *
- * @public
- * @param {String} name The name used
- *
- * @returns {Function}
- */
- getComponent: function( name ) {
- if( this._components[ name ] === undefined ) {
- throw new lm.errors.ConfigurationError( 'Unknown component "' + name + '"' );
- }
- return this._components[ name ];
- },
- /**
- * Creates the actual layout. Must be called after all initial components
- * are registered. Recurses through the configuration and sets up
- * the item tree.
- *
- * If called before the document is ready it adds itself as a listener
- * to the document.ready event
- *
- * @public
- *
- * @returns {void}
- */
- init: function() {
- /**
- * Create the popout windows straight away. If popouts are blocked
- * an error is thrown on the same 'thread' rather than a timeout and can
- * be caught. This also prevents any further initilisation from taking place.
- */
- if( this._subWindowsCreated === false ) {
- this._createSubWindows();
- this._subWindowsCreated = true;
- }
- /**
- * If the document isn't ready yet, wait for it.
- */
- if( document.readyState === 'loading' || document.body === null ) {
- $( document ).ready( lm.utils.fnBind( this.init, this ) );
- return;
- }
- /**
- * If this is a subwindow, wait a few milliseconds for the original
- * page's js calls to be executed, then replace the bodies content
- * with GoldenLayout
- */
- if( this.isSubWindow === true && this._creationTimeoutPassed === false ) {
- setTimeout( lm.utils.fnBind( this.init, this ), 7 );
- this._creationTimeoutPassed = true;
- return;
- }
- if( this.isSubWindow === true ) {
- this._adjustToWindowMode();
- }
- this._setContainer();
- this.dropTargetIndicator = new lm.controls.DropTargetIndicator( this.container );
- this.transitionIndicator = new lm.controls.TransitionIndicator();
- this.updateSize();
- this._create( this.config );
- this._bindEvents();
- this.isInitialised = true;
- this._adjustColumnsResponsive();
- this.emit( 'initialised' );
- },
- /**
- * Updates the layout managers size
- *
- * @public
- * @param {[int]} width height in pixels
- * @param {[int]} height width in pixels
- *
- * @returns {void}
- */
- updateSize: function( width, height ) {
- if( arguments.length === 2 ) {
- this.width = width;
- this.height = height;
- } else {
- this.width = this.container.width();
- this.height = this.container.height();
- }
- if( this.isInitialised === true ) {
- this.root.callDownwards( 'setSize', [ this.width, this.height ] );
- if( this._maximisedItem ) {
- this._maximisedItem.element.width( this.container.width() );
- this._maximisedItem.element.height( this.container.height() );
- this._maximisedItem.callDownwards( 'setSize' );
- }
- this._adjustColumnsResponsive();
- }
- },
- /**
- * Destroys the LayoutManager instance itself as well as every ContentItem
- * within it. After this is called nothing should be left of the LayoutManager.
- *
- * @public
- * @returns {void}
- */
- destroy: function() {
- if( this.isInitialised === false ) {
- return;
- }
- this._onUnload();
- $( window ).off( 'resize', this._resizeFunction );
- $( window ).off( 'unload beforeunload', this._unloadFunction );
- this.root.callDownwards( '_$destroy', [], true );
- this.root.contentItems = [];
- this.tabDropPlaceholder.remove();
- this.dropTargetIndicator.destroy();
- this.transitionIndicator.destroy();
- this.eventHub.destroy();
- this._dragSources.forEach( function( dragSource ) {
- dragSource._dragListener.destroy();
- dragSource._element = null;
- dragSource._itemConfig = null;
- dragSource._dragListener = null;
- } );
- this._dragSources = [];
- },
- /**
- * Recursively creates new item tree structures based on a provided
- * ItemConfiguration object
- *
- * @public
- * @param {Object} config ItemConfig
- * @param {[ContentItem]} parent The item the newly created item should be a child of
- *
- * @returns {lm.items.ContentItem}
- */
- createContentItem: function( config, parent ) {
- var typeErrorMsg, contentItem;
- if( typeof config.type !== 'string' ) {
- throw new lm.errors.ConfigurationError( 'Missing parameter \'type\'', config );
- }
- if( config.type === 'react-component' ) {
- config.type = 'component';
- config.componentName = 'lm-react-component';
- }
- if( !this._typeToItem[ config.type ] ) {
- typeErrorMsg = 'Unknown type \'' + config.type + '\'. ' +
- 'Valid types are ' + lm.utils.objectKeys( this._typeToItem ).join( ',' );
- throw new lm.errors.ConfigurationError( typeErrorMsg );
- }
- /**
- * We add an additional stack around every component that's not within a stack anyways.
- */
- if(
- // If this is a component
- config.type === 'component' &&
- // and it's not already within a stack
- !( parent instanceof lm.items.Stack ) &&
- // and we have a parent
- !!parent &&
- // and it's not the topmost item in a new window
- !( this.isSubWindow === true && parent instanceof lm.items.Root )
- ) {
- config = {
- type: 'stack',
- width: config.width,
- height: config.height,
- content: [ config ]
- };
- }
- contentItem = new this._typeToItem[ config.type ]( this, config, parent );
- return contentItem;
- },
- /**
- * Creates a popout window with the specified content and dimensions
- *
- * @param {Object|lm.itemsAbstractContentItem} configOrContentItem
- * @param {[Object]} dimensions A map with width, height, left and top
- * @param {[String]} parentId the id of the element this item will be appended to
- * when popIn is called
- * @param {[Number]} indexInParent The position of this item within its parent element
- * @returns {lm.controls.BrowserPopout}
- */
- createPopout: function( configOrContentItem, dimensions, parentId, indexInParent ) {
- var config = configOrContentItem,
- isItem = configOrContentItem instanceof lm.items.AbstractContentItem,
- self = this,
- windowLeft,
- windowTop,
- offset,
- parent,
- child,
- browserPopout;
- parentId = parentId || null;
- if( isItem ) {
- config = this.toConfig( configOrContentItem ).content;
- parentId = lm.utils.getUniqueId();
- /**
- * If the item is the only component within a stack or for some
- * other reason the only child of its parent the parent will be destroyed
- * when the child is removed.
- *
- * In order to support this we move up the tree until we find something
- * that will remain after the item is being popped out
- */
- parent = configOrContentItem.parent;
- child = configOrContentItem;
- while( parent.contentItems.length === 1 && !parent.isRoot ) {
- parent = parent.parent;
- child = child.parent;
- }
- parent.addId( parentId );
- if( isNaN( indexInParent ) ) {
- indexInParent = lm.utils.indexOf( child, parent.contentItems );
- }
- } else {
- if( !( config instanceof Array ) ) {
- config = [ config ];
- }
- }
- if( !dimensions && isItem ) {
- windowLeft = window.screenX || window.screenLeft;
- windowTop = window.screenY || window.screenTop;
- offset = configOrContentItem.element.offset();
- dimensions = {
- left: windowLeft + offset.left,
- top: windowTop + offset.top,
- width: configOrContentItem.element.width(),
- height: configOrContentItem.element.height()
- };
- }
- if( !dimensions && !isItem ) {
- dimensions = {
- left: window.screenX || window.screenLeft + 20,
- top: window.screenY || window.screenTop + 20,
- width: 500,
- height: 309
- };
- }
- if( isItem ) {
- configOrContentItem.remove();
- }
- browserPopout = new lm.controls.BrowserPopout( config, dimensions, parentId, indexInParent, this );
- browserPopout.on( 'initialised', function() {
- self.emit( 'windowOpened', browserPopout );
- } );
- browserPopout.on( 'closed', function() {
- self._$reconcilePopoutWindows();
- } );
- this.openPopouts.push( browserPopout );
- return browserPopout;
- },
- /**
- * Attaches DragListener to any given DOM element
- * and turns it into a way of creating new ContentItems
- * by 'dragging' the DOM element into the layout
- *
- * @param {jQuery DOM element} element
- * @param {Object|Function} itemConfig for the new item to be created, or a function which will provide it
- *
- * @returns {void}
- */
- createDragSource: function( element, itemConfig ) {
- this.config.settings.constrainDragToContainer = false;
- var dragSource = new lm.controls.DragSource( $( element ), itemConfig, this );
- this._dragSources.push( dragSource );
- return dragSource;
- },
- /**
- * Programmatically selects an item. This deselects
- * the currently selected item, selects the specified item
- * and emits a selectionChanged event
- *
- * @param {lm.item.AbstractContentItem} item#
- * @param {[Boolean]} _$silent Wheather to notify the item of its selection
- * @event selectionChanged
- *
- * @returns {VOID}
- */
- selectItem: function( item, _$silent ) {
- if( this.config.settings.selectionEnabled !== true ) {
- throw new Error( 'Please set selectionEnabled to true to use this feature' );
- }
- if( item === this.selectedItem ) {
- return;
- }
- if( this.selectedItem !== null ) {
- this.selectedItem.deselect();
- }
- if( item && _$silent !== true ) {
- item.select();
- }
- this.selectedItem = item;
- this.emit( 'selectionChanged', item );
- },
- /*************************
- * PACKAGE PRIVATE
- *************************/
- _$maximiseItem: function( contentItem ) {
- if( this._maximisedItem !== null ) {
- this._$minimiseItem( this._maximisedItem );
- }
- this._maximisedItem = contentItem;
- this._maximisedItem.addId( '__glMaximised' );
- contentItem.element.addClass( 'lm_maximised' );
- contentItem.element.after( this._maximisePlaceholder );
- this.root.element.prepend( contentItem.element );
- contentItem.element.width( this.container.width() );
- contentItem.element.height( this.container.height() );
- contentItem.callDownwards( 'setSize' );
- this._maximisedItem.emit( 'maximised' );
- this.emit( 'stateChanged' );
- },
- _$minimiseItem: function( contentItem ) {
- contentItem.element.removeClass( 'lm_maximised' );
- contentItem.removeId( '__glMaximised' );
- this._maximisePlaceholder.after( contentItem.element );
- this._maximisePlaceholder.remove();
- contentItem.parent.callDownwards( 'setSize' );
- this._maximisedItem = null;
- contentItem.emit( 'minimised' );
- this.emit( 'stateChanged' );
- },
- /**
- * This method is used to get around sandboxed iframe restrictions.
- * If 'allow-top-navigation' is not specified in the iframe's 'sandbox' attribute
- * (as is the case with codepens) the parent window is forbidden from calling certain
- * methods on the child, such as window.close() or setting document.location.href.
- *
- * This prevented GoldenLayout popouts from popping in in codepens. The fix is to call
- * _$closeWindow on the child window's gl instance which (after a timeout to disconnect
- * the invoking method from the close call) closes itself.
- *
- * @packagePrivate
- *
- * @returns {void}
- */
- _$closeWindow: function() {
- window.setTimeout( function() {
- window.close();
- }, 1 );
- },
- _$getArea: function( x, y ) {
- var i, area, smallestSurface = Infinity, mathingArea = null;
- for( i = 0; i < this._itemAreas.length; i++ ) {
- area = this._itemAreas[ i ];
- if(
- x > area.x1 &&
- x < area.x2 &&
- y > area.y1 &&
- y < area.y2 &&
- smallestSurface > area.surface
- ) {
- smallestSurface = area.surface;
- mathingArea = area;
- }
- }
- return mathingArea;
- },
- _$createRootItemAreas: function() {
- var areaSize = 50;
- var sides = { y2: 0, x2: 0, y1: 'y2', x1: 'x2' };
- for( side in sides ) {
- var area = this.root._$getArea();
- area.side = side;
- if( sides [ side ] )
- area[ side ] = area[ sides [ side ] ] - areaSize;
- else
- area[ side ] = areaSize;
- with( area )
- surface = ( x2 - x1 ) * ( y2 - y1 );
- this._itemAreas.push( area );
- }
- },
- _$calculateItemAreas: function() {
- var i, area, allContentItems = this._getAllContentItems();
- this._itemAreas = [];
- /**
- * If the last item is dragged out, highlight the entire container size to
- * allow to re-drop it. allContentItems[ 0 ] === this.root at this point
- *
- * Don't include root into the possible drop areas though otherwise since it
- * will used for every gap in the layout, e.g. splitters
- */
- if( allContentItems.length === 1 ) {
- this._itemAreas.push( this.root._$getArea() );
- return;
- }
- this._$createRootItemAreas();
- for( i = 0; i < allContentItems.length; i++ ) {
- if( !( allContentItems[ i ].isStack ) ) {
- continue;
- }
- area = allContentItems[ i ]._$getArea();
- if( area === null ) {
- continue;
- } else if( area instanceof Array ) {
- this._itemAreas = this._itemAreas.concat( area );
- } else {
- this._itemAreas.push( area );
- var header = {};
- lm.utils.copy( header, area );
- lm.utils.copy( header, area.contentItem._contentAreaDimensions.header.highlightArea );
- with( header )
- surface = ( x2 - x1 ) * ( y2 - y1 );
- this._itemAreas.push( header );
- }
- }
- },
- /**
- * Takes a contentItem or a configuration and optionally a parent
- * item and returns an initialised instance of the contentItem.
- * If the contentItem is a function, it is first called
- *
- * @packagePrivate
- *
- * @param {lm.items.AbtractContentItem|Object|Function} contentItemOrConfig
- * @param {lm.items.AbtractContentItem} parent Only necessary when passing in config
- *
- * @returns {lm.items.AbtractContentItem}
- */
- _$normalizeContentItem: function( contentItemOrConfig, parent ) {
- if( !contentItemOrConfig ) {
- throw new Error( 'No content item defined' );
- }
- if( lm.utils.isFunction( contentItemOrConfig ) ) {
- contentItemOrConfig = contentItemOrConfig();
- }
- if( contentItemOrConfig instanceof lm.items.AbstractContentItem ) {
- return contentItemOrConfig;
- }
- if( $.isPlainObject( contentItemOrConfig ) && contentItemOrConfig.type ) {
- var newContentItem = this.createContentItem( contentItemOrConfig, parent );
- newContentItem.callDownwards( '_$init' );
- return newContentItem;
- } else {
- throw new Error( 'Invalid contentItem' );
- }
- },
- /**
- * Iterates through the array of open popout windows and removes the ones
- * that are effectively closed. This is necessary due to the lack of reliably
- * listening for window.close / unload events in a cross browser compatible fashion.
- *
- * @packagePrivate
- *
- * @returns {void}
- */
- _$reconcilePopoutWindows: function() {
- var openPopouts = [], i;
- for( i = 0; i < this.openPopouts.length; i++ ) {
- if( this.openPopouts[ i ].getWindow().closed === false ) {
- openPopouts.push( this.openPopouts[ i ] );
- } else {
- this.emit( 'windowClosed', this.openPopouts[ i ] );
- }
- }
- if( this.openPopouts.length !== openPopouts.length ) {
- this.emit( 'stateChanged' );
- this.openPopouts = openPopouts;
- }
- },
- /***************************
- * PRIVATE
- ***************************/
- /**
- * Returns a flattened array of all content items,
- * regardles of level or type
- *
- * @private
- *
- * @returns {void}
- */
- _getAllContentItems: function() {
- var allContentItems = [];
- var addChildren = function( contentItem ) {
- allContentItems.push( contentItem );
- if( contentItem.contentItems instanceof Array ) {
- for( var i = 0; i < contentItem.contentItems.length; i++ ) {
- addChildren( contentItem.contentItems[ i ] );
- }
- }
- };
- addChildren( this.root );
- return allContentItems;
- },
- /**
- * Binds to DOM/BOM events on init
- *
- * @private
- *
- * @returns {void}
- */
- _bindEvents: function() {
- if( this._isFullPage ) {
- $( window ).resize( this._resizeFunction );
- }
- $( window ).on( 'unload beforeunload', this._unloadFunction );
- },
- /**
- * Debounces resize events
- *
- * @private
- *
- * @returns {void}
- */
- _onResize: function() {
- clearTimeout( this._resizeTimeoutId );
- this._resizeTimeoutId = setTimeout( lm.utils.fnBind( this.updateSize, this ), 100 );
- },
- /**
- * Extends the default config with the user specific settings and applies
- * derivations. Please note that there's a seperate method (AbstractContentItem._extendItemNode)
- * that deals with the extension of item configs
- *
- * @param {Object} config
- * @static
- * @returns {Object} config
- */
- _createConfig: function( config ) {
- var windowConfigKey = lm.utils.getQueryStringParam( 'gl-window' );
- if( windowConfigKey ) {
- this.isSubWindow = true;
- config = localStorage.getItem( windowConfigKey );
- config = JSON.parse( config );
- config = ( new lm.utils.ConfigMinifier() ).unminifyConfig( config );
- localStorage.removeItem( windowConfigKey );
- }
- config = $.extend( true, {}, lm.config.defaultConfig, config );
- var nextNode = function( node ) {
- for( var key in node ) {
- if( key !== 'props' && typeof node[ key ] === 'object' ) {
- nextNode( node[ key ] );
- }
- else if( key === 'type' && node[ key ] === 'react-component' ) {
- node.type = 'component';
- node.componentName = 'lm-react-component';
- }
- }
- }
- nextNode( config );
- if( config.settings.hasHeaders === false ) {
- config.dimensions.headerHeight = 0;
- }
- return config;
- },
- /**
- * This is executed when GoldenLayout detects that it is run
- * within a previously opened popout window.
- *
- * @private
- *
- * @returns {void}
- */
- _adjustToWindowMode: function() {
- var popInButton = $( '<div class="lm_popin" title="' + this.config.labels.popin + '">' +
- '<div class="lm_icon"></div>' +
- '<div class="lm_bg"></div>' +
- '</div>' );
- popInButton.click( lm.utils.fnBind( function() {
- this.emit( 'popIn' );
- }, this ) );
- document.title = lm.utils.stripTags( this.config.content[ 0 ].title );
- $( 'head' ).append( $( 'body link, body style, template, .gl_keep' ) );
- this.container = $( 'body' )
- .html( '' )
- .css( 'visibility', 'visible' )
- .append( popInButton );
- /*
- * This seems a bit pointless, but actually causes a reflow/re-evaluation getting around
- * slickgrid's "Cannot find stylesheet." bug in chrome
- */
- var x = document.body.offsetHeight; // jshint ignore:line
- /*
- * Expose this instance on the window object
- * to allow the opening window to interact with
- * it
- */
- window.__glInstance = this;
- },
- /**
- * Creates Subwindows (if there are any). Throws an error
- * if popouts are blocked.
- *
- * @returns {void}
- */
- _createSubWindows: function() {
- var i, popout;
- for( i = 0; i < this.config.openPopouts.length; i++ ) {
- popout = this.config.openPopouts[ i ];
- this.createPopout(
- popout.content,
- popout.dimensions,
- popout.parentId,
- popout.indexInParent
- );
- }
- },
- /**
- * Determines what element the layout will be created in
- *
- * @private
- *
- * @returns {void}
- */
- _setContainer: function() {
- var container = $( this.container || 'body' );
- if( container.length === 0 ) {
- throw new Error( 'GoldenLayout container not found' );
- }
- if( container.length > 1 ) {
- throw new Error( 'GoldenLayout more than one container element specified' );
- }
- if( container[ 0 ] === document.body ) {
- this._isFullPage = true;
- $( 'html, body' ).css( {
- height: '100%',
- margin: 0,
- padding: 0,
- overflow: 'hidden'
- } );
- }
- this.container = container;
- },
- /**
- * Kicks of the initial, recursive creation chain
- *
- * @param {Object} config GoldenLayout Config
- *
- * @returns {void}
- */
- _create: function( config ) {
- var errorMsg;
- if( !( config.content instanceof Array ) ) {
- if( config.content === undefined ) {
- errorMsg = 'Missing setting \'content\' on top level of configuration';
- } else {
- errorMsg = 'Configuration parameter \'content\' must be an array';
- }
- throw new lm.errors.ConfigurationError( errorMsg, config );
- }
- if( config.content.length > 1 ) {
- errorMsg = 'Top level content can\'t contain more then one element.';
- throw new lm.errors.ConfigurationError( errorMsg, config );
- }
- this.root = new lm.items.Root( this, { content: config.content }, this.container );
- this.root.callDownwards( '_$init' );
- if( config.maximisedItemId === '__glMaximised' ) {
- this.root.getItemsById( config.maximisedItemId )[ 0 ].toggleMaximise();
- }
- },
- /**
- * Called when the window is closed or the user navigates away
- * from the page
- *
- * @returns {void}
- */
- _onUnload: function() {
- if( this.config.settings.closePopoutsOnUnload === true ) {
- for( var i = 0; i < this.openPopouts.length; i++ ) {
- this.openPopouts[ i ].close();
- }
- }
- },
- /**
- * Adjusts the number of columns to be lower to fit the screen and still maintain minItemWidth.
- *
- * @returns {void}
- */
- _adjustColumnsResponsive: function() {
- // If there is no min width set, or not content items, do nothing.
- if( !this._useResponsiveLayout() || this._updatingColumnsResponsive || !this.config.dimensions || !this.config.dimensions.minItemWidth || this.root.contentItems.length === 0 || !this.root.contentItems[ 0 ].isRow ) {
- this._firstLoad = false;
- return;
- }
- this._firstLoad = false;
- // If there is only one column, do nothing.
- var columnCount = this.root.contentItems[ 0 ].contentItems.length;
- if( columnCount <= 1 ) {
- return;
- }
- // If they all still fit, do nothing.
- var minItemWidth = this.config.dimensions.minItemWidth;
- var totalMinWidth = columnCount * minItemWidth;
- if( totalMinWidth <= this.width ) {
- return;
- }
- // Prevent updates while it is already happening.
- this._updatingColumnsResponsive = true;
- // Figure out how many columns to stack, and put them all in the first stack container.
- var finalColumnCount = Math.max( Math.floor( this.width / minItemWidth ), 1 );
- var stackColumnCount = columnCount - finalColumnCount;
- var rootContentItem = this.root.contentItems[ 0 ];
- var firstStackContainer = this._findAllStackContainers()[ 0 ];
- for( var i = 0; i < stackColumnCount; i++ ) {
- // Stack from right.
- var column = rootContentItem.contentItems[ rootContentItem.contentItems.length - 1 ];
- rootContentItem.removeChild( column );
- this._addChildContentItemsToContainer( firstStackContainer, column );
- }
- this._updatingColumnsResponsive = false;
- },
- /**
- * Determines if responsive layout should be used.
- *
- * @returns {bool} - True if responsive layout should be used; otherwise false.
- */
- _useResponsiveLayout: function() {
- return this.config.settings && ( this.config.settings.responsiveMode == 'always' || ( this.config.settings.responsiveMode == 'onload' && this._firstLoad ) );
- },
- /**
- * Adds all children of a node to another container recursively.
- * @param {object} container - Container to add child content items to.
- * @param {object} node - Node to search for content items.
- * @returns {void}
- */
- _addChildContentItemsToContainer: function( container, node ) {
- if( node.type === 'stack' ) {
- node.contentItems.forEach( function( item ) {
- container.addChild( item );
- } );
- }
- else {
- node.contentItems.forEach( lm.utils.fnBind( function( item ) {
- this._addChildContentItemsToContainer( container, item );
- }, this ) );
- }
- },
- /**
- * Finds all the stack containers.
- * @returns {array} - The found stack containers.
- */
- _findAllStackContainers: function() {
- var stackContainers = [];
- this._findAllStackContainersRecursive( stackContainers, this.root );
- return stackContainers;
- },
- /**
- * Finds all the stack containers.
- *
- * @param {array} - Set of containers to populate.
- * @param {object} - Current node to process.
- *
- * @returns {void}
- */
- _findAllStackContainersRecursive: function( stackContainers, node ) {
- node.contentItems.forEach( lm.utils.fnBind( function( item ) {
- if( item.type == 'stack' ) {
- stackContainers.push( item );
- }
- else if( !item.isComponent ) {
- this._findAllStackContainersRecursive( stackContainers, item );
- }
- }, this ) );
- }
- } );
- /**
- * Expose the Layoutmanager as the single entrypoint using UMD
- */
- (function() {
- /* global define */
- if( typeof define === 'function' && define.amd ) {
- define( [ 'jquery' ], function( jquery ) {
- $ = jquery;
- return lm.LayoutManager;
- } ); // jshint ignore:line
- } else if( typeof exports === 'object' ) {
- module.exports = lm.LayoutManager;
- } else {
- window.GoldenLayout = lm.LayoutManager;
- }
- })();
- lm.config.itemDefaultConfig = {
- isClosable: true,
- reorderEnabled: true,
- title: ''
- };
- lm.config.defaultConfig = {
- openPopouts: [],
- settings: {
- hasHeaders: true,
- constrainDragToContainer: true,
- constrainDragToHeader: true,
- reorderEnabled: true,
- selectionEnabled: false,
- popoutWholeStack: false,
- blockedPopoutsThrowError: true,
- closePopoutsOnUnload: true,
- showPopoutIcon: true,
- showMaximiseIcon: true,
- showCloseIcon: true,
- responsiveMode: 'onload' // Can be onload, always, or none.
- },
- dimensions: {
- borderWidth: 5,
- minItemHeight: 10,
- minItemWidth: 10,
- headerHeight: 20,
- dragProxyWidth: 300,
- dragProxyHeight: 200
- },
- labels: {
- close: 'close',
- maximise: 'maximise',
- minimise: 'minimise',
- popout: 'open in new window',
- popin: 'pop in',
- tabDropdown: 'additional tabs'
- }
- };
- lm.container.ItemContainer = function( config, parent, layoutManager ) {
- lm.utils.EventEmitter.call( this );
- this.width = null;
- this.height = null;
- this.title = config.componentName;
- this.parent = parent;
- this.layoutManager = layoutManager;
- this.isHidden = false;
- this._config = config;
- this._element = $( [
- '<div class="lm_item_container">',
- '<div class="lm_content"></div>',
- '</div>'
- ].join( '' ) );
- this._contentElement = this._element.find( '.lm_content' );
- };
- lm.utils.copy( lm.container.ItemContainer.prototype, {
- /**
- * Get the inner DOM element the container's content
- * is intended to live in
- *
- * @returns {DOM element}
- */
- getElement: function() {
- return this._contentElement;
- },
- /**
- * Hide the container. Notifies the containers content first
- * and then hides the DOM node. If the container is already hidden
- * this should have no effect
- *
- * @returns {void}
- */
- hide: function() {
- this.emit( 'hide' );
- this.isHidden = true;
- this._element.hide();
- },
- /**
- * Shows a previously hidden container. Notifies the
- * containers content first and then shows the DOM element.
- * If the container is already visible this has no effect.
- *
- * @returns {void}
- */
- show: function() {
- this.emit( 'show' );
- this.isHidden = false;
- this._element.show();
- // call shown only if the container has a valid size
- if( this.height != 0 || this.width != 0 ) {
- this.emit( 'shown' );
- }
- },
- /**
- * Set the size from within the container. Traverses up
- * the item tree until it finds a row or column element
- * and resizes its items accordingly.
- *
- * If this container isn't a descendant of a row or column
- * it returns false
- * @todo Rework!!!
- * @param {Number} width The new width in pixel
- * @param {Number} height The new height in pixel
- *
- * @returns {Boolean} resizeSuccesful
- */
- setSize: function( width, height ) {
- var rowOrColumn = this.parent,
- rowOrColumnChild = this,
- totalPixel,
- percentage,
- direction,
- newSize,
- delta,
- i;
- while( !rowOrColumn.isColumn && !rowOrColumn.isRow ) {
- rowOrColumnChild = rowOrColumn;
- rowOrColumn = rowOrColumn.parent;
- /**
- * No row or column has been found
- */
- if( rowOrColumn.isRoot ) {
- return false;
- }
- }
- direction = rowOrColumn.isColumn ? "height" : "width";
- newSize = direction === "height" ? height : width;
- totalPixel = this[ direction ] * ( 1 / ( rowOrColumnChild.config[ direction ] / 100 ) );
- percentage = ( newSize / totalPixel ) * 100;
- delta = ( rowOrColumnChild.config[ direction ] - percentage ) / (rowOrColumn.contentItems.length - 1);
- for( i = 0; i < rowOrColumn.contentItems.length; i++ ) {
- if( rowOrColumn.contentItems[ i ] === rowOrColumnChild ) {
- rowOrColumn.contentItems[ i ].config[ direction ] = percentage;
- } else {
- rowOrColumn.contentItems[ i ].config[ direction ] += delta;
- }
- }
- rowOrColumn.callDownwards( 'setSize' );
- return true;
- },
- /**
- * Closes the container if it is closable. Can be called by
- * both the component within at as well as the contentItem containing
- * it. Emits a close event before the container itself is closed.
- *
- * @returns {void}
- */
- close: function() {
- if( this._config.isClosable ) {
- this.emit( 'close' );
- this.parent.close();
- }
- },
- /**
- * Returns the current state object
- *
- * @returns {Object} state
- */
- getState: function() {
- return this._config.componentState;
- },
- /**
- * Merges the provided state into the current one
- *
- * @param {Object} state
- *
- * @returns {void}
- */
- extendState: function( state ) {
- this.setState( $.extend( true, this.getState(), state ) );
- },
- /**
- * Notifies the layout manager of a stateupdate
- *
- * @param {serialisable} state
- */
- setState: function( state ) {
- this._config.componentState = state;
- this.parent.emitBubblingEvent( 'stateChanged' );
- },
- /**
- * Set's the components title
- *
- * @param {String} title
- */
- setTitle: function( title ) {
- this.parent.setTitle( title );
- },
- /**
- * Set's the containers size. Called by the container's component.
- * To set the size programmatically from within the container please
- * use the public setSize method
- *
- * @param {[Int]} width in px
- * @param {[Int]} height in px
- *
- * @returns {void}
- */
- _$setSize: function( width, height ) {
- if( width !== this.width || height !== this.height ) {
- this.width = width;
- this.height = height;
- this._contentElement.width( this.width ).height( this.height );
- this.emit( 'resize' );
- }
- }
- } );
- /**
- * Pops a content item out into a new browser window.
- * This is achieved by
- *
- * - Creating a new configuration with the content item as root element
- * - Serializing and minifying the configuration
- * - Opening the current window's URL with the configuration as a GET parameter
- * - GoldenLayout when opened in the new window will look for the GET parameter
- * and use it instead of the provided configuration
- *
- * @param {Object} config GoldenLayout item config
- * @param {Object} dimensions A map with width, height, top and left
- * @param {String} parentId The id of the element the item will be appended to on popIn
- * @param {Number} indexInParent The position of this element within its parent
- * @param {lm.LayoutManager} layoutManager
- */
- lm.controls.BrowserPopout = function( config, dimensions, parentId, indexInParent, layoutManager ) {
- lm.utils.EventEmitter.call( this );
- this.isInitialised = false;
- this._config = config;
- this._dimensions = dimensions;
- this._parentId = parentId;
- this._indexInParent = indexInParent;
- this._layoutManager = layoutManager;
- this._popoutWindow = null;
- this._id = null;
- this._createWindow();
- };
- lm.utils.copy( lm.controls.BrowserPopout.prototype, {
- toConfig: function() {
- if( this.isInitialised === false ) {
- throw new Error( 'Can\'t create config, layout not yet initialised' );
- return;
- }
- return {
- dimensions: {
- width: this.getGlInstance().width,
- height: this.getGlInstance().height,
- left: this._popoutWindow.screenX || this._popoutWindow.screenLeft,
- top: this._popoutWindow.screenY || this._popoutWindow.screenTop
- },
- content: this.getGlInstance().toConfig().content,
- parentId: this._parentId,
- indexInParent: this._indexInParent
- };
- },
- getGlInstance: function() {
- return this._popoutWindow.__glInstance;
- },
- getWindow: function() {
- return this._popoutWindow;
- },
- close: function() {
- if( this.getGlInstance() ) {
- this.getGlInstance()._$closeWindow();
- } else {
- try {
- this.getWindow().close();
- } catch( e ) {
- }
- }
- },
- /**
- * Returns the popped out item to its original position. If the original
- * parent isn't available anymore it falls back to the layout's topmost element
- */
- popIn: function() {
- var childConfig,
- parentItem,
- index = this._indexInParent;
- if( this._parentId ) {
- /*
- * The $.extend call seems a bit pointless, but it's crucial to
- * copy the config returned by this.getGlInstance().toConfig()
- * onto a new object. Internet Explorer keeps the references
- * to objects on the child window, resulting in the following error
- * once the child window is closed:
- *
- * The callee (server [not server application]) is not available and disappeared
- */
- childConfig = $.extend( true, {}, this.getGlInstance().toConfig() ).content[ 0 ];
- parentItem = this._layoutManager.root.getItemsById( this._parentId )[ 0 ];
- /*
- * Fallback if parentItem is not available. Either add it to the topmost
- * item or make it the topmost item if the layout is empty
- */
- if( !parentItem ) {
- if( this._layoutManager.root.contentItems.length > 0 ) {
- parentItem = this._layoutManager.root.contentItems[ 0 ];
- } else {
- parentItem = this._layoutManager.root;
- }
- index = 0;
- }
- }
- parentItem.addChild( childConfig, this._indexInParent );
- this.close();
- },
- /**
- * Creates the URL and window parameter
- * and opens a new window
- *
- * @private
- *
- * @returns {void}
- */
- _createWindow: function() {
- var checkReadyInterval,
- url = this._createUrl(),
- /**
- * Bogus title to prevent re-usage of existing window with the
- * same title. The actual title will be set by the new window's
- * GoldenLayout instance if it detects that it is in subWindowMode
- */
- title = Math.floor( Math.random() * 1000000 ).toString( 36 ),
- /**
- * The options as used in the window.open string
- */
- options = this._serializeWindowOptions( {
- width: this._dimensions.width,
- height: this._dimensions.height,
- innerWidth: this._dimensions.width,
- innerHeight: this._dimensions.height,
- menubar: 'no',
- toolbar: 'no',
- location: 'no',
- personalbar: 'no',
- resizable: 'yes',
- scrollbars: 'no',
- status: 'no'
- } );
- this._popoutWindow = window.open( url, title, options );
- if( !this._popoutWindow ) {
- if( this._layoutManager.config.settings.blockedPopoutsThrowError === true ) {
- var error = new Error( 'Popout blocked' );
- error.type = 'popoutBlocked';
- throw error;
- } else {
- return;
- }
- }
- $( this._popoutWindow )
- .on( 'load', lm.utils.fnBind( this._positionWindow, this ) )
- .on( 'unload beforeunload', lm.utils.fnBind( this._onClose, this ) );
- /**
- * Polling the childwindow to find out if GoldenLayout has been initialised
- * doesn't seem optimal, but the alternatives - adding a callback to the parent
- * window or raising an event on the window object - both would introduce knowledge
- * about the parent to the child window which we'd rather avoid
- */
- checkReadyInterval = setInterval( lm.utils.fnBind( function() {
- if( this._popoutWindow.__glInstance && this._popoutWindow.__glInstance.isInitialised ) {
- this._onInitialised();
- clearInterval( checkReadyInterval );
- }
- }, this ), 10 );
- },
- /**
- * Serialises a map of key:values to a window options string
- *
- * @param {Object} windowOptions
- *
- * @returns {String} serialised window options
- */
- _serializeWindowOptions: function( windowOptions ) {
- var windowOptionsString = [], key;
- for( key in windowOptions ) {
- windowOptionsString.push( key + '=' + windowOptions[ key ] );
- }
- return windowOptionsString.join( ',' );
- },
- /**
- * Creates the URL for the new window, including the
- * config GET parameter
- *
- * @returns {String} URL
- */
- _createUrl: function() {
- var config = { content: this._config },
- storageKey = 'gl-window-config-' + lm.utils.getUniqueId(),
- urlParts;
- config = ( new lm.utils.ConfigMinifier() ).minifyConfig( config );
- try {
- localStorage.setItem( storageKey, JSON.stringify( config ) );
- } catch( e ) {
- throw new Error( 'Error while writing to localStorage ' + e.toString() );
- }
- urlParts = document.location.href.split( '?' );
- // URL doesn't contain GET-parameters
- if( urlParts.length === 1 ) {
- return urlParts[ 0 ] + '?gl-window=' + storageKey;
- // URL contains GET-parameters
- } else {
- return document.location.href + '&gl-window=' + storageKey;
- }
- },
- /**
- * Move the newly created window roughly to
- * where the component used to be.
- *
- * @private
- *
- * @returns {void}
- */
- _positionWindow: function() {
- this._popoutWindow.moveTo( this._dimensions.left, this._dimensions.top );
- this._popoutWindow.focus();
- },
- /**
- * Callback when the new window is opened and the GoldenLayout instance
- * within it is initialised
- *
- * @returns {void}
- */
- _onInitialised: function() {
- this.isInitialised = true;
- this.getGlInstance().on( 'popIn', this.popIn, this );
- this.emit( 'initialised' );
- },
- /**
- * Invoked 50ms after the window unload event
- *
- * @private
- *
- * @returns {void}
- */
- _onClose: function() {
- setTimeout( lm.utils.fnBind( this.emit, this, [ 'closed' ] ), 50 );
- }
- } );
- /**
- * This class creates a temporary container
- * for the component whilst it is being dragged
- * and handles drag events
- *
- * @constructor
- * @private
- *
- * @param {Number} x The initial x position
- * @param {Number} y The initial y position
- * @param {lm.utils.DragListener} dragListener
- * @param {lm.LayoutManager} layoutManager
- * @param {lm.item.AbstractContentItem} contentItem
- * @param {lm.item.AbstractContentItem} originalParent
- */
- lm.controls.DragProxy = function( x, y, dragListener, layoutManager, contentItem, originalParent ) {
- lm.utils.EventEmitter.call( this );
- this._dragListener = dragListener;
- this._layoutManager = layoutManager;
- this._contentItem = contentItem;
- this._originalParent = originalParent;
- this._area = null;
- this._lastValidArea = null;
- this._dragListener.on( 'drag', this._onDrag, this );
- this._dragListener.on( 'dragStop', this._onDrop, this );
- this.element = $( lm.controls.DragProxy._template );
- if( originalParent && originalParent._side ) {
- this._sided = originalParent._sided;
- this.element.addClass( 'lm_' + originalParent._side );
- if( [ 'right', 'bottom' ].indexOf( originalParent._side ) >= 0 )
- this.element.find( '.lm_content' ).after( this.element.find( '.lm_header' ) );
- }
- this.element.css( { left: x, top: y } );
- this.element.find( '.lm_tab' ).attr( 'title', lm.utils.stripTags( this._contentItem.config.title ) );
- this.element.find( '.lm_title' ).html( this._contentItem.config.title );
- this.childElementContainer = this.element.find( '.lm_content' );
- this.childElementContainer.append( contentItem.element );
- this._updateTree();
- this._layoutManager._$calculateItemAreas();
- this._setDimensions();
- $( document.body ).append( this.element );
- var offset = this._layoutManager.container.offset();
- this._minX = offset.left;
- this._minY = offset.top;
- this._maxX = this._layoutManager.container.width() + this._minX;
- this._maxY = this._layoutManager.container.height() + this._minY;
- this._width = this.element.width();
- this._height = this.element.height();
- this._setDropPosition( x, y );
- };
- lm.controls.DragProxy._template = '<div class="lm_dragProxy">' +
- '<div class="lm_header">' +
- '<ul class="lm_tabs">' +
- '<li class="lm_tab lm_active"><i class="lm_left"></i>' +
- '<span class="lm_title"></span>' +
- '<i class="lm_right"></i></li>' +
- '</ul>' +
- '</div>' +
- '<div class="lm_content"></div>' +
- '</div>';
- lm.utils.copy( lm.controls.DragProxy.prototype, {
- /**
- * Callback on every mouseMove event during a drag. Determines if the drag is
- * still within the valid drag area and calls the layoutManager to highlight the
- * current drop area
- *
- * @param {Number} offsetX The difference from the original x position in px
- * @param {Number} offsetY The difference from the original y position in px
- * @param {jQuery DOM event} event
- *
- * @private
- *
- * @returns {void}
- */
- _onDrag: function( offsetX, offsetY, event ) {
- event = event.originalEvent && event.originalEvent.touches ? event.originalEvent.touches[ 0 ] : event;
- var x = event.pageX,
- y = event.pageY,
- isWithinContainer = x > this._minX && x < this._maxX && y > this._minY && y < this._maxY,
- isWithinHeader = x > this._area.x1 && x < this._area.x2 && y > this._area.y1 && y < this._area.y2;
- if( !isWithinContainer && this._layoutManager.config.settings.constrainDragToContainer === true ) {
- return;
- }
- if( !isWithinHeader && this._layoutManager.config.settings.constrainDragToHeader === true ) {
- return;
- }
- this._setDropPosition( x, y );
- },
- /**
- * Sets the target position, highlighting the appropriate area
- *
- * @param {Number} x The x position in px
- * @param {Number} y The y position in px
- *
- * @private
- *
- * @returns {void}
- */
- _setDropPosition: function( x, y ) {
- this.element.css( { left: x, top: y } );
- this._area = this._layoutManager._$getArea( x, y );
- if( this._area !== null ) {
- this._lastValidArea = this._area;
- this._area.contentItem._$highlightDropZone( x, y, this._area );
- }
- },
- /**
- * Callback when the drag has finished. Determines the drop area
- * and adds the child to it
- *
- * @private
- *
- * @returns {void}
- */
- _onDrop: function() {
- this._layoutManager.dropTargetIndicator.hide();
- /*
- * Valid drop area found
- */
- if( this._area !== null ) {
- this._area.contentItem._$onDrop( this._contentItem, this._area );
- /**
- * No valid drop area available at present, but one has been found before.
- * Use it
- */
- } else if( this._lastValidArea !== null ) {
- this._lastValidArea.contentItem._$onDrop( this._contentItem, this._lastValidArea );
- /**
- * No valid drop area found during the duration of the drag. Return
- * content item to its original position if a original parent is provided.
- * (Which is not the case if the drag had been initiated by createDragSource)
- */
- } else if( this._originalParent ) {
- this._originalParent.addChild( this._contentItem );
- /**
- * The drag didn't ultimately end up with adding the content item to
- * any container. In order to ensure clean up happens, destroy the
- * content item.
- */
- } else {
- this._contentItem._$destroy();
- }
- this.element.remove();
- this._layoutManager.emit( 'itemDropped', this._contentItem );
- },
- /**
- * Removes the item from its original position within the tree
- *
- * @private
- *
- * @returns {void}
- */
- _updateTree: function() {
- /**
- * parent is null if the drag had been initiated by a external drag source
- */
- if( this._contentItem.parent ) {
- this._contentItem.parent.removeChild( this._contentItem, true );
- }
- this._contentItem._$setParent( this );
- },
- /**
- * Updates the Drag Proxie's dimensions
- *
- * @private
- *
- * @returns {void}
- */
- _setDimensions: function() {
- var dimensions = this._layoutManager.config.dimensions,
- width = dimensions.dragProxyWidth,
- height = dimensions.dragProxyHeight;
- this.element.width( width );
- this.element.height( height );
- width -= ( this._sided ? dimensions.headerHeight : 0 );
- height -= ( !this._sided ? dimensions.headerHeight : 0 );
- this.childElementContainer.width( width );
- this.childElementContainer.height( height );
- this._contentItem.element.width( width );
- this._contentItem.element.height( height );
- this._contentItem.callDownwards( '_$show' );
- this._contentItem.callDownwards( 'setSize' );
- }
- } );
- /**
- * Allows for any DOM item to create a component on drag
- * start tobe dragged into the Layout
- *
- * @param {jQuery element} element
- * @param {Object} itemConfig the configuration for the contentItem that will be created
- * @param {LayoutManager} layoutManager
- *
- * @constructor
- */
- lm.controls.DragSource = function( element, itemConfig, layoutManager ) {
- this._element = element;
- this._itemConfig = itemConfig;
- this._layoutManager = layoutManager;
- this._dragListener = null;
- this._createDragListener();
- };
- lm.utils.copy( lm.controls.DragSource.prototype, {
- /**
- * Called initially and after every drag
- *
- * @returns {void}
- */
- _createDragListener: function() {
- if( this._dragListener !== null ) {
- this._dragListener.destroy();
- }
- this._dragListener = new lm.utils.DragListener( this._element );
- this._dragListener.on( 'dragStart', this._onDragStart, this );
- this._dragListener.on( 'dragStop', this._createDragListener, this );
- },
- /**
- * Callback for the DragListener's dragStart event
- *
- * @param {int} x the x position of the mouse on dragStart
- * @param {int} y the x position of the mouse on dragStart
- *
- * @returns {void}
- */
- _onDragStart: function( x, y ) {
- var itemConfig = this._itemConfig;
- if( lm.utils.isFunction( itemConfig ) ) {
- itemConfig = itemConfig();
- }
- var contentItem = this._layoutManager._$normalizeContentItem( $.extend( true, {}, itemConfig ) ),
- dragProxy = new lm.controls.DragProxy( x, y, this._dragListener, this._layoutManager, contentItem, null );
- this._layoutManager.transitionIndicator.transitionElements( this._element, dragProxy.element );
- }
- } );
- lm.controls.DropTargetIndicator = function() {
- this.element = $( lm.controls.DropTargetIndicator._template );
- $( document.body ).append( this.element );
- };
- lm.controls.DropTargetIndicator._template = '<div class="lm_dropTargetIndicator"><div class="lm_inner"></div></div>';
- lm.utils.copy( lm.controls.DropTargetIndicator.prototype, {
- destroy: function() {
- this.element.remove();
- },
- highlight: function( x1, y1, x2, y2 ) {
- this.highlightArea( { x1: x1, y1: y1, x2: x2, y2: y2 } );
- },
- highlightArea: function( area ) {
- this.element.css( {
- left: area.x1,
- top: area.y1,
- width: area.x2 - area.x1,
- height: area.y2 - area.y1
- } ).show();
- },
- hide: function() {
- this.element.hide();
- }
- } );
- /**
- * This class represents a header above a Stack ContentItem.
- *
- * @param {lm.LayoutManager} layoutManager
- * @param {lm.item.AbstractContentItem} parent
- */
- lm.controls.Header = function( layoutManager, parent ) {
- lm.utils.EventEmitter.call( this );
- this.layoutManager = layoutManager;
- this.element = $( lm.controls.Header._template );
- if( this.layoutManager.config.settings.selectionEnabled === true ) {
- this.element.addClass( 'lm_selectable' );
- this.element.on( 'click touchstart', lm.utils.fnBind( this._onHeaderClick, this ) );
- }
- this.tabsContainer = this.element.find( '.lm_tabs' );
- this.tabDropdownContainer = this.element.find( '.lm_tabdropdown_list' );
- this.tabDropdownContainer.hide();
- this.controlsContainer = this.element.find( '.lm_controls' );
- this.parent = parent;
- this.parent.on( 'resize', this._updateTabSizes, this );
- this.tabs = [];
- this.activeContentItem = null;
- this.closeButton = null;
- this.tabDropdownButton = null;
- //$( document ).mouseup( lm.utils.fnBind( this._hideAdditionalTabsDropdown, this ) );
- this._lastVisibleTabIndex = -1;
- this._tabControlOffset = 10;
- this._createControls();
- };
- lm.controls.Header._template = [
- '<div class="lm_header">',
- '<ul class="lm_tabs"></ul>',
- '<ul class="lm_controls"></ul>',
- '<ul class="lm_tabdropdown_list"></ul>',
- '</div>'
- ].join( '' );
- lm.utils.copy( lm.controls.Header.prototype, {
- /**
- * Creates a new tab and associates it with a contentItem
- *
- * @param {lm.item.AbstractContentItem} contentItem
- * @param {Integer} index The position of the tab
- *
- * @returns {void}
- */
- createTab: function( contentItem, index ) {
- var tab, i;
- //If there's already a tab relating to the
- //content item, don't do anything
- for( i = 0; i < this.tabs.length; i++ ) {
- if( this.tabs[ i ].contentItem === contentItem ) {
- return;
- }
- }
- tab = new lm.controls.Tab( this, contentItem );
- if( this.tabs.length === 0 ) {
- this.tabs.push( tab );
- this.tabsContainer.append( tab.element );
- return;
- }
- if( index === undefined ) {
- index = this.tabs.length;
- }
- if( index > 0 ) {
- this.tabs[ index - 1 ].element.after( tab.element );
- } else {
- this.tabs[ 0 ].element.before( tab.element );
- }
- this.tabs.splice( index, 0, tab );
- this._updateTabSizes();
- },
- /**
- * Finds a tab based on the contentItem its associated with and removes it.
- *
- * @param {lm.item.AbstractContentItem} contentItem
- *
- * @returns {void}
- */
- removeTab: function( contentItem ) {
- for( var i = 0; i < this.tabs.length; i++ ) {
- if( this.tabs[ i ].contentItem === contentItem ) {
- this.tabs[ i ]._$destroy();
- this.tabs.splice( i, 1 );
- return;
- }
- }
- throw new Error( 'contentItem is not controlled by this header' );
- },
- /**
- * The programmatical equivalent of clicking a Tab.
- *
- * @param {lm.item.AbstractContentItem} contentItem
- */
- setActiveContentItem: function( contentItem ) {
- var i, j, isActive, activeTab;
- for( i = 0; i < this.tabs.length; i++ ) {
- isActive = this.tabs[ i ].contentItem === contentItem;
- this.tabs[ i ].setActive( isActive );
- if( isActive === true ) {
- this.activeContentItem = contentItem;
- this.parent.config.activeItemIndex = i;
- }
- }
- /**
- * If the tab selected was in the dropdown, move everything down one to make way for this one to be the first.
- * This will make sure the most used tabs stay visible.
- */
- if( this._lastVisibleTabIndex !== -1 && this.parent.config.activeItemIndex > this._lastVisibleTabIndex ) {
- activeTab = this.tabs[ this.parent.config.activeItemIndex ];
- for( j = this.parent.config.activeItemIndex; j > 0; j-- ) {
- this.tabs[ j ] = this.tabs[ j - 1 ];
- }
- this.tabs[ 0 ] = activeTab;
- this.parent.config.activeItemIndex = 0;
- }
- this._updateTabSizes();
- this.parent.emitBubblingEvent( 'stateChanged' );
- },
- /**
- * Programmatically operate with header position.
- *
- * @param {string} position one of ('top','left','right','bottom') to set or empty to get it.
- *
- * @returns {string} previous header position
- */
- position: function( position ) {
- var previous = this.parent._header.show;
- if( previous && !this.parent._side )
- previous = 'top';
- if( position !== undefined && this.parent._header.show != position ) {
- this.parent._header.show = position;
- this.parent._setupHeaderPosition();
- }
- return previous;
- },
- /**
- * Programmatically set closability.
- *
- * @package private
- * @param {Boolean} isClosable Whether to enable/disable closability.
- *
- * @returns {Boolean} Whether the action was successful
- */
- _$setClosable: function( isClosable ) {
- if( this.closeButton && this._isClosable() ) {
- this.closeButton.element[ isClosable ? "show" : "hide" ]();
- return true;
- }
- return false;
- },
- /**
- * Destroys the entire header
- *
- * @package private
- *
- * @returns {void}
- */
- _$destroy: function() {
- this.emit( 'destroy', this );
- for( var i = 0; i < this.tabs.length; i++ ) {
- this.tabs[ i ]._$destroy();
- }
- this.element.remove();
- },
- /**
- * get settings from header
- *
- * @returns {string} when exists
- */
- _getHeaderSetting: function( name ) {
- if( name in this.parent._header )
- return this.parent._header[ name ];
- },
- /**
- * Creates the popout, maximise and close buttons in the header's top right corner
- *
- * @returns {void}
- */
- _createControls: function() {
- var closeStack,
- popout,
- label,
- maximiseLabel,
- minimiseLabel,
- maximise,
- maximiseButton,
- tabDropdownLabel,
- showTabDropdown;
- /**
- * Dropdown to show additional tabs.
- */
- showTabDropdown = lm.utils.fnBind( this._toggleAdditionalTabsDropdownVisiblity, this );
- tabDropdownLabel = this.layoutManager.config.labels.tabDropdown;
- this.tabDropdownButton = new lm.controls.HeaderButton( this, tabDropdownLabel, 'lm_tabdropdown', showTabDropdown );
- this.tabDropdownButton.element.hide();
- /**
- * Popout control to launch component in new window.
- */
- if( this._getHeaderSetting( 'popout' ) ) {
- popout = lm.utils.fnBind( this._onPopoutClick, this );
- label = this._getHeaderSetting( 'popout' );
- new lm.controls.HeaderButton( this, label, 'lm_popout', popout );
- }
- /**
- * Maximise control - set the component to the full size of the layout
- */
- if( this._getHeaderSetting( 'maximise' ) ) {
- maximise = lm.utils.fnBind( this.parent.toggleMaximise, this.parent );
- maximiseLabel = this._getHeaderSetting( 'maximise' );
- minimiseLabel = this._getHeaderSetting( 'minimise' );
- maximiseButton = new lm.controls.HeaderButton( this, maximiseLabel, 'lm_maximise', maximise );
- this.parent.on( 'maximised', function() {
- maximiseButton.element.attr( 'title', minimiseLabel );
- } );
- this.parent.on( 'minimised', function() {
- maximiseButton.element.attr( 'title', maximiseLabel );
- } );
- }
- /**
- * Close button
- */
- if( this._isClosable() ) {
- closeStack = lm.utils.fnBind( this.parent.remove, this.parent );
- label = this._getHeaderSetting( 'close' );
- this.closeButton = new lm.controls.HeaderButton( this, label, 'lm_close', closeStack );
- }
- },
- /**
- * Toggle visibility of the additional tabs's drop down for when there are too many to display.
- *
- * @returns {void}
- */
- _toggleAdditionalTabsDropdownVisiblity: function() {
- if (this.tabDropdownContainer.is(":visible"))
- this.tabDropdownContainer.hide();
- else
- this.tabDropdownContainer.show();
- },
- /**
- * Checks whether the header is closable based on the parent config and
- * the global config.
- *
- * @returns {Boolean} Whether the header is closable.
- */
- _isClosable: function() {
- return this.parent.config.isClosable && this.layoutManager.config.settings.showCloseIcon;
- },
- _onPopoutClick: function() {
- if( this.layoutManager.config.settings.popoutWholeStack === true ) {
- this.parent.popout();
- } else {
- this.activeContentItem.popout();
- }
- },
- /**
- * Invoked when the header's background is clicked (not it's tabs or controls)
- *
- * @param {jQuery DOM event} event
- *
- * @returns {void}
- */
- _onHeaderClick: function( event ) {
- if( event.target === this.element[ 0 ] ) {
- this.parent.select();
- }
- },
- /**
- * Pushes the tabs to the tab dropdown if the available space is not sufficient
- *
- * @returns {void}
- */
- _updateTabSizes: function() {
- if( this.tabs.length === 0 ) {
- return;
- }
- var size = function( val ) {
- return val ? 'width' : 'height';
- }
- this.element.css( size( !this.parent._sided ), '' );
- this.element[ size( this.parent._sided ) ]( this.layoutManager.config.dimensions.headerHeight );
- var availableWidth = this.element.outerWidth() - this.controlsContainer.outerWidth() - this._tabControlOffset,
- totalTabWidth = 0,
- tabElement,
- i,
- showTabDropdown,
- swapTab,
- tabWidth,
- hasVisibleTab = false;
- if( this.parent._sided )
- availableWidth = this.element.outerHeight() - this.controlsContainer.outerHeight() - this._tabControlOffset;
- this._lastVisibleTabIndex = -1;
- for( i = 0; i < this.tabs.length; i++ ) {
- tabElement = this.tabs[ i ].element;
- /*
- * Retain tab width when hidden so it can be restored.
- */
- tabWidth = tabElement.data( 'lastTabWidth' );
- if( !tabWidth ) {
- tabWidth = tabElement.outerWidth() + parseInt( tabElement.css( 'margin-right' ), 10 );
- }
- totalTabWidth += tabWidth;
- // If the tab won't fit, put it in the dropdown for tabs, making sure there is always at least one tab visible.
- if( totalTabWidth > availableWidth && hasVisibleTab ) {
- tabElement.data( 'lastTabWidth', tabWidth );
- this.tabDropdownContainer.append( tabElement );
- }
- else {
- hasVisibleTab = true;
- this._lastVisibleTabIndex = i;
- tabElement.removeData( 'lastTabWidth' );
- this.tabsContainer.append( tabElement );
- }
- }
- /*
- * Show the tab dropdown icon if not all tabs fit.
- */
- showTabDropdown = totalTabWidth > availableWidth;
- this.tabDropdownButton.element[ showTabDropdown ? 'show' : 'hide' ]();
- }
- } );
- lm.controls.HeaderButton = function( header, label, cssClass, action ) {
- this._header = header;
- this.element = $( '<li class="' + cssClass + '" title="' + label + '"></li>' );
- this._header.on( 'destroy', this._$destroy, this );
- this._action = action;
- this.element.on( 'click touchstart', this._action );
- this._header.controlsContainer.append( this.element );
- };
- lm.utils.copy( lm.controls.HeaderButton.prototype, {
- _$destroy: function() {
- this.element.off();
- this.element.remove();
- }
- } );
- lm.controls.Splitter = function( isVertical, size ) {
- this._isVertical = isVertical;
- this._size = size;
- this.element = this._createElement();
- this._dragListener = new lm.utils.DragListener( this.element );
- };
- lm.utils.copy( lm.controls.Splitter.prototype, {
- on: function( event, callback, context ) {
- this._dragListener.on( event, callback, context );
- },
- _$destroy: function() {
- this.element.remove();
- },
- _createElement: function() {
- var element = $( '<div class="lm_splitter"><div class="lm_drag_handle"></div></div>' );
- element.addClass( 'lm_' + ( this._isVertical ? 'vertical' : 'horizontal' ) );
- element[ this._isVertical ? 'height' : 'width' ]( this._size );
- return element;
- }
- } );
- /**
- * Represents an individual tab within a Stack's header
- *
- * @param {lm.controls.Header} header
- * @param {lm.items.AbstractContentItem} contentItem
- *
- * @constructor
- */
- lm.controls.Tab = function( header, contentItem ) {
- this.header = header;
- this.contentItem = contentItem;
- this.element = $( lm.controls.Tab._template );
- this.titleElement = this.element.find( '.lm_title' );
- this.closeElement = this.element.find( '.lm_close_tab' );
- this.closeElement[ contentItem.config.isClosable ? 'show' : 'hide' ]();
- this.isActive = false;
- this.setTitle( contentItem.config.title );
- this.contentItem.on( 'titleChanged', this.setTitle, this );
- this._layoutManager = this.contentItem.layoutManager;
- if(
- this._layoutManager.config.settings.reorderEnabled === true &&
- contentItem.config.reorderEnabled === true
- ) {
- this._dragListener = new lm.utils.DragListener( this.titleElement );
- this._dragListener.on( 'dragStart', this._onDragStart, this );
- }
- this._onTabClickFn = lm.utils.fnBind( this._onTabClick, this );
- this._onCloseClickFn = lm.utils.fnBind( this._onCloseClick, this );
- this.titleElement.on( 'mousedown touchstart', this._onTabClickFn );
- if( this.contentItem.config.isClosable ) {
- this.closeElement.on( 'click touchstart', this._onCloseClickFn );
- } else {
- this.closeElement.remove();
- }
- this.contentItem.tab = this;
- this.contentItem.emit( 'tab', this );
- this.contentItem.layoutManager.emit( 'tabCreated', this );
- if( this.contentItem.isComponent ) {
- this.contentItem.container.tab = this;
- this.contentItem.container.emit( 'tab', this );
- }
- };
- /**
- * The tab's html template
- *
- * @type {String}
- */
- lm.controls.Tab._template = '<li class="lm_tab"><i class="lm_left"></i>' +
- '<span class="lm_title"></span><div class="lm_close_tab"></div>' +
- '<i class="lm_right"></i></li>';
- lm.utils.copy( lm.controls.Tab.prototype, {
- /**
- * Sets the tab's title to the provided string and sets
- * its title attribute to a pure text representation (without
- * html tags) of the same string.
- *
- * @public
- * @param {String} title can contain html
- */
- setTitle: function( title ) {
- this.element.attr( 'title', lm.utils.stripTags( title ) );
- this.titleElement.html( title );
- },
- /**
- * Sets this tab's active state. To programmatically
- * switch tabs, use header.setActiveContentItem( item ) instead.
- *
- * @public
- * @param {Boolean} isActive
- */
- setActive: function( isActive ) {
- if( isActive === this.isActive ) {
- return;
- }
- this.isActive = isActive;
- if( isActive ) {
- this.element.addClass( 'lm_active' );
- } else {
- this.element.removeClass( 'lm_active' );
- }
- },
- /**
- * Destroys the tab
- *
- * @private
- * @returns {void}
- */
- _$destroy: function() {
- this.titleElement.off( 'mousedown touchstart', this._onTabClickFn );
- this.closeElement.off( 'click touchstart', this._onCloseClickFn );
- if( this._dragListener ) {
- this._dragListener.off( 'dragStart', this._onDragStart );
- this._dragListener = null;
- }
- this.element.remove();
- },
- /**
- * Callback for the DragListener
- *
- * @param {Number} x The tabs absolute x position
- * @param {Number} y The tabs absolute y position
- *
- * @private
- * @returns {void}
- */
- _onDragStart: function( x, y ) {
- if( this.contentItem.parent.isMaximised === true ) {
- this.contentItem.parent.toggleMaximise();
- }
- new lm.controls.DragProxy(
- x,
- y,
- this._dragListener,
- this._layoutManager,
- this.contentItem,
- this.header.parent
- );
- },
- /**
- * Callback when the tab is clicked
- *
- * @param {jQuery DOM event} event
- *
- * @private
- * @returns {void}
- */
- _onTabClick: function( event ) {
- // left mouse button or tap
- if( event.button === 0 || event.type === 'touchstart' ) {
- var activeContentItem = this.header.parent.getActiveContentItem();
- if( this.contentItem !== activeContentItem ) {
- this.header.parent.setActiveContentItem( this.contentItem );
- }
- // middle mouse button
- } else if( event.button === 1 && this.contentItem.config.isClosable ) {
- this._onCloseClick( event );
- }
- },
- onClose : function() { return true; },
- /**
- * Callback when the tab's close button is
- * clicked
- *
- * @param {jQuery DOM event} event
- *
- * @private
- * @returns {void}
- */
- _onCloseClick: function( event ) {
- event.stopPropagation();
- if( !this.onClose() ) return;
- this.header.parent.removeChild( this.contentItem );
- }
- } );
- lm.controls.TransitionIndicator = function() {
- this._element = $( '<div class="lm_transition_indicator"></div>' );
- $( document.body ).append( this._element );
- this._toElement = null;
- this._fromDimensions = null;
- this._totalAnimationDuration = 200;
- this._animationStartTime = null;
- };
- lm.utils.copy( lm.controls.TransitionIndicator.prototype, {
- destroy: function() {
- this._element.remove();
- },
- transitionElements: function( fromElement, toElement ) {
- /**
- * TODO - This is not quite as cool as expected. Review.
- */
- return;
- this._toElement = toElement;
- this._animationStartTime = lm.utils.now();
- this._fromDimensions = this._measure( fromElement );
- this._fromDimensions.opacity = 0.8;
- this._element.show().css( this._fromDimensions );
- lm.utils.animFrame( lm.utils.fnBind( this._nextAnimationFrame, this ) );
- },
- _nextAnimationFrame: function() {
- var toDimensions = this._measure( this._toElement ),
- animationProgress = ( lm.utils.now() - this._animationStartTime ) / this._totalAnimationDuration,
- currentFrameStyles = {},
- cssProperty;
- if( animationProgress >= 1 ) {
- this._element.hide();
- return;
- }
- toDimensions.opacity = 0;
- for( cssProperty in this._fromDimensions ) {
- currentFrameStyles[ cssProperty ] = this._fromDimensions[ cssProperty ] +
- ( toDimensions[ cssProperty ] - this._fromDimensions[ cssProperty ] ) *
- animationProgress;
- }
- this._element.css( currentFrameStyles );
- lm.utils.animFrame( lm.utils.fnBind( this._nextAnimationFrame, this ) );
- },
- _measure: function( element ) {
- var offset = element.offset();
- return {
- left: offset.left,
- top: offset.top,
- width: element.outerWidth(),
- height: element.outerHeight()
- };
- }
- } );
- lm.errors.ConfigurationError = function( message, node ) {
- Error.call( this );
- this.name = 'Configuration Error';
- this.message = message;
- this.node = node;
- };
- lm.errors.ConfigurationError.prototype = new Error();
- /**
- * This is the baseclass that all content items inherit from.
- * Most methods provide a subset of what the sub-classes do.
- *
- * It also provides a number of functions for tree traversal
- *
- * @param {lm.LayoutManager} layoutManager
- * @param {item node configuration} config
- * @param {lm.item} parent
- *
- * @event stateChanged
- * @event beforeItemDestroyed
- * @event itemDestroyed
- * @event itemCreated
- * @event componentCreated
- * @event rowCreated
- * @event columnCreated
- * @event stackCreated
- *
- * @constructor
- */
- lm.items.AbstractContentItem = function( layoutManager, config, parent ) {
- lm.utils.EventEmitter.call( this );
- this.config = this._extendItemNode( config );
- this.type = config.type;
- this.contentItems = [];
- this.parent = parent;
- this.isInitialised = false;
- this.isMaximised = false;
- this.isRoot = false;
- this.isRow = false;
- this.isColumn = false;
- this.isStack = false;
- this.isComponent = false;
- this.layoutManager = layoutManager;
- this._pendingEventPropagations = {};
- this._throttledEvents = [ 'stateChanged' ];
- this.on( lm.utils.EventEmitter.ALL_EVENT, this._propagateEvent, this );
- if( config.content ) {
- this._createContentItems( config );
- }
- };
- lm.utils.copy( lm.items.AbstractContentItem.prototype, {
- /**
- * Set the size of the component and its children, called recursively
- *
- * @abstract
- * @returns void
- */
- setSize: function() {
- throw new Error( 'Abstract Method' );
- },
- /**
- * Calls a method recursively downwards on the tree
- *
- * @param {String} functionName the name of the function to be called
- * @param {[Array]}functionArguments optional arguments that are passed to every function
- * @param {[bool]} bottomUp Call methods from bottom to top, defaults to false
- * @param {[bool]} skipSelf Don't invoke the method on the class that calls it, defaults to false
- *
- * @returns {void}
- */
- callDownwards: function( functionName, functionArguments, bottomUp, skipSelf ) {
- var i;
- if( bottomUp !== true && skipSelf !== true ) {
- this[ functionName ].apply( this, functionArguments || [] );
- }
- for( i = 0; i < this.contentItems.length; i++ ) {
- this.contentItems[ i ].callDownwards( functionName, functionArguments, bottomUp );
- }
- if( bottomUp === true && skipSelf !== true ) {
- this[ functionName ].apply( this, functionArguments || [] );
- }
- },
- /**
- * Removes a child node (and its children) from the tree
- *
- * @param {lm.items.ContentItem} contentItem
- *
- * @returns {void}
- */
- removeChild: function( contentItem, keepChild ) {
- /*
- * Get the position of the item that's to be removed within all content items this node contains
- */
- var index = lm.utils.indexOf( contentItem, this.contentItems );
- /*
- * Make sure the content item to be removed is actually a child of this item
- */
- if( index === -1 ) {
- throw new Error( 'Can\'t remove child item. Unknown content item' );
- }
- /**
- * Call ._$destroy on the content item. This also calls ._$destroy on all its children
- */
- if( keepChild !== true ) {
- this.contentItems[ index ]._$destroy();
- }
- /**
- * Remove the content item from this nodes array of children
- */
- this.contentItems.splice( index, 1 );
- /**
- * Remove the item from the configuration
- */
- this.config.content.splice( index, 1 );
- /**
- * If this node still contains other content items, adjust their size
- */
- if( this.contentItems.length > 0 ) {
- this.callDownwards( 'setSize' );
- /**
- * If this was the last content item, remove this node as well
- */
- } else if( !(this instanceof lm.items.Root) && this.config.isClosable === true ) {
- this.parent.removeChild( this );
- }
- },
- /**
- * Sets up the tree structure for the newly added child
- * The responsibility for the actual DOM manipulations lies
- * with the concrete item
- *
- * @param {lm.items.AbstractContentItem} contentItem
- * @param {[Int]} index If omitted item will be appended
- */
- addChild: function( contentItem, index ) {
- if( index === undefined ) {
- index = this.contentItems.length;
- }
- this.contentItems.splice( index, 0, contentItem );
- if( this.config.content === undefined ) {
- this.config.content = [];
- }
- this.config.content.splice( index, 0, contentItem.config );
- contentItem.parent = this;
- if( contentItem.parent.isInitialised === true && contentItem.isInitialised === false ) {
- contentItem._$init();
- }
- },
- /**
- * Replaces oldChild with newChild. This used to use jQuery.replaceWith... which for
- * some reason removes all event listeners, so isn't really an option.
- *
- * @param {lm.item.AbstractContentItem} oldChild
- * @param {lm.item.AbstractContentItem} newChild
- *
- * @returns {void}
- */
- replaceChild: function( oldChild, newChild, _$destroyOldChild ) {
- newChild = this.layoutManager._$normalizeContentItem( newChild );
- var index = lm.utils.indexOf( oldChild, this.contentItems ),
- parentNode = oldChild.element[ 0 ].parentNode;
- if( index === -1 ) {
- throw new Error( 'Can\'t replace child. oldChild is not child of this' );
- }
- parentNode.replaceChild( newChild.element[ 0 ], oldChild.element[ 0 ] );
- /*
- * Optionally destroy the old content item
- */
- if( _$destroyOldChild === true ) {
- oldChild.parent = null;
- oldChild._$destroy();
- }
- /*
- * Wire the new contentItem into the tree
- */
- this.contentItems[ index ] = newChild;
- newChild.parent = this;
- /*
- * Update tab reference
- */
- if( this.isStack ) {
- this.header.tabs[ index ].contentItem = newChild;
- }
- //TODO This doesn't update the config... refactor to leave item nodes untouched after creation
- if( newChild.parent.isInitialised === true && newChild.isInitialised === false ) {
- newChild._$init();
- }
- this.callDownwards( 'setSize' );
- },
- /**
- * Convenience method.
- * Shorthand for this.parent.removeChild( this )
- *
- * @returns {void}
- */
- remove: function() {
- this.parent.removeChild( this );
- },
- /**
- * Removes the component from the layout and creates a new
- * browser window with the component and its children inside
- *
- * @returns {lm.controls.BrowserPopout}
- */
- popout: function() {
- var browserPopout = this.layoutManager.createPopout( this );
- this.emitBubblingEvent( 'stateChanged' );
- return browserPopout;
- },
- /**
- * Maximises the Item or minimises it if it is already maximised
- *
- * @returns {void}
- */
- toggleMaximise: function( e ) {
- e && e.preventDefault();
- if( this.isMaximised === true ) {
- this.layoutManager._$minimiseItem( this );
- } else {
- this.layoutManager._$maximiseItem( this );
- }
- this.isMaximised = !this.isMaximised;
- this.emitBubblingEvent( 'stateChanged' );
- },
- /**
- * Selects the item if it is not already selected
- *
- * @returns {void}
- */
- select: function() {
- if( this.layoutManager.selectedItem !== this ) {
- this.layoutManager.selectItem( this, true );
- this.element.addClass( 'lm_selected' );
- }
- },
- /**
- * De-selects the item if it is selected
- *
- * @returns {void}
- */
- deselect: function() {
- if( this.layoutManager.selectedItem === this ) {
- this.layoutManager.selectedItem = null;
- this.element.removeClass( 'lm_selected' );
- }
- },
- /**
- * Set this component's title
- *
- * @public
- * @param {String} title
- *
- * @returns {void}
- */
- setTitle: function( title ) {
- this.config.title = title;
- this.emit( 'titleChanged', title );
- this.emit( 'stateChanged' );
- },
- /**
- * Checks whether a provided id is present
- *
- * @public
- * @param {String} id
- *
- * @returns {Boolean} isPresent
- */
- hasId: function( id ) {
- if( !this.config.id ) {
- return false;
- } else if( typeof this.config.id === 'string' ) {
- return this.config.id === id;
- } else if( this.config.id instanceof Array ) {
- return lm.utils.indexOf( id, this.config.id ) !== -1;
- }
- },
- /**
- * Adds an id. Adds it as a string if the component doesn't
- * have an id yet or creates/uses an array
- *
- * @public
- * @param {String} id
- *
- * @returns {void}
- */
- addId: function( id ) {
- if( this.hasId( id ) ) {
- return;
- }
- if( !this.config.id ) {
- this.config.id = id;
- } else if( typeof this.config.id === 'string' ) {
- this.config.id = [ this.config.id, id ];
- } else if( this.config.id instanceof Array ) {
- this.config.id.push( id );
- }
- },
- /**
- * Removes an existing id. Throws an error
- * if the id is not present
- *
- * @public
- * @param {String} id
- *
- * @returns {void}
- */
- removeId: function( id ) {
- if( !this.hasId( id ) ) {
- throw new Error( 'Id not found' );
- }
- if( typeof this.config.id === 'string' ) {
- delete this.config.id;
- } else if( this.config.id instanceof Array ) {
- var index = lm.utils.indexOf( id, this.config.id );
- this.config.id.splice( index, 1 );
- }
- },
- /****************************************
- * SELECTOR
- ****************************************/
- getItemsByFilter: function( filter ) {
- var result = [],
- next = function( contentItem ) {
- for( var i = 0; i < contentItem.contentItems.length; i++ ) {
- if( filter( contentItem.contentItems[ i ] ) === true ) {
- result.push( contentItem.contentItems[ i ] );
- }
- next( contentItem.contentItems[ i ] );
- }
- };
- next( this );
- return result;
- },
- getItemsById: function( id ) {
- return this.getItemsByFilter( function( item ) {
- if( item.config.id instanceof Array ) {
- return lm.utils.indexOf( id, item.config.id ) !== -1;
- } else {
- return item.config.id === id;
- }
- } );
- },
- getItemsByType: function( type ) {
- return this._$getItemsByProperty( 'type', type );
- },
- getComponentsByName: function( componentName ) {
- var components = this._$getItemsByProperty( 'componentName', componentName ),
- instances = [],
- i;
- for( i = 0; i < components.length; i++ ) {
- instances.push( components[ i ].instance );
- }
- return instances;
- },
- /****************************************
- * PACKAGE PRIVATE
- ****************************************/
- _$getItemsByProperty: function( key, value ) {
- return this.getItemsByFilter( function( item ) {
- return item[ key ] === value;
- } );
- },
- _$setParent: function( parent ) {
- this.parent = parent;
- },
- _$highlightDropZone: function( x, y, area ) {
- this.layoutManager.dropTargetIndicator.highlightArea( area );
- },
- _$onDrop: function( contentItem ) {
- this.addChild( contentItem );
- },
- _$hide: function() {
- this._callOnActiveComponents( 'hide' );
- this.element.hide();
- this.layoutManager.updateSize();
- },
- _$show: function() {
- this._callOnActiveComponents( 'show' );
- this.element.show();
- this.layoutManager.updateSize();
- },
- _callOnActiveComponents: function( methodName ) {
- var stacks = this.getItemsByType( 'stack' ),
- activeContentItem,
- i;
- for( i = 0; i < stacks.length; i++ ) {
- activeContentItem = stacks[ i ].getActiveContentItem();
- if( activeContentItem && activeContentItem.isComponent ) {
- activeContentItem.container[ methodName ]();
- }
- }
- },
- /**
- * Destroys this item ands its children
- *
- * @returns {void}
- */
- _$destroy: function() {
- this.emitBubblingEvent( 'beforeItemDestroyed' );
- this.callDownwards( '_$destroy', [], true, true );
- this.element.remove();
- this.emitBubblingEvent( 'itemDestroyed' );
- },
- /**
- * Returns the area the component currently occupies in the format
- *
- * {
- * x1: int
- * xy: int
- * y1: int
- * y2: int
- * contentItem: contentItem
- * }
- */
- _$getArea: function( element ) {
- element = element || this.element;
- var offset = element.offset(),
- width = element.width(),
- height = element.height();
- return {
- x1: offset.left,
- y1: offset.top,
- x2: offset.left + width,
- y2: offset.top + height,
- surface: width * height,
- contentItem: this
- };
- },
- /**
- * The tree of content items is created in two steps: First all content items are instantiated,
- * then init is called recursively from top to bottem. This is the basic init function,
- * it can be used, extended or overwritten by the content items
- *
- * Its behaviour depends on the content item
- *
- * @package private
- *
- * @returns {void}
- */
- _$init: function() {
- var i;
- this.setSize();
- for( i = 0; i < this.contentItems.length; i++ ) {
- this.childElementContainer.append( this.contentItems[ i ].element );
- }
- this.isInitialised = true;
- this.emitBubblingEvent( 'itemCreated' );
- this.emitBubblingEvent( this.type + 'Created' );
- },
- /**
- * Emit an event that bubbles up the item tree.
- *
- * @param {String} name The name of the event
- *
- * @returns {void}
- */
- emitBubblingEvent: function( name ) {
- var event = new lm.utils.BubblingEvent( name, this );
- this.emit( name, event );
- },
- /**
- * Private method, creates all content items for this node at initialisation time
- * PLEASE NOTE, please see addChild for adding contentItems add runtime
- * @private
- * @param {configuration item node} config
- *
- * @returns {void}
- */
- _createContentItems: function( config ) {
- var oContentItem, i;
- if( !( config.content instanceof Array ) ) {
- throw new lm.errors.ConfigurationError( 'content must be an Array', config );
- }
- for( i = 0; i < config.content.length; i++ ) {
- oContentItem = this.layoutManager.createContentItem( config.content[ i ], this );
- this.contentItems.push( oContentItem );
- }
- },
- /**
- * Extends an item configuration node with default settings
- * @private
- * @param {configuration item node} config
- *
- * @returns {configuration item node} extended config
- */
- _extendItemNode: function( config ) {
- for( var key in lm.config.itemDefaultConfig ) {
- if( config[ key ] === undefined ) {
- config[ key ] = lm.config.itemDefaultConfig[ key ];
- }
- }
- return config;
- },
- /**
- * Called for every event on the item tree. Decides whether the event is a bubbling
- * event and propagates it to its parent
- *
- * @param {String} name the name of the event
- * @param {lm.utils.BubblingEvent} event
- *
- * @returns {void}
- */
- _propagateEvent: function( name, event ) {
- if( event instanceof lm.utils.BubblingEvent &&
- event.isPropagationStopped === false &&
- this.isInitialised === true ) {
- /**
- * In some cases (e.g. if an element is created from a DragSource) it
- * doesn't have a parent and is not below root. If that's the case
- * propagate the bubbling event from the top level of the substree directly
- * to the layoutManager
- */
- if( this.isRoot === false && this.parent ) {
- this.parent.emit.apply( this.parent, Array.prototype.slice.call( arguments, 0 ) );
- } else {
- this._scheduleEventPropagationToLayoutManager( name, event );
- }
- }
- },
- /**
- * All raw events bubble up to the root element. Some events that
- * are propagated to - and emitted by - the layoutManager however are
- * only string-based, batched and sanitized to make them more usable
- *
- * @param {String} name the name of the event
- *
- * @private
- * @returns {void}
- */
- _scheduleEventPropagationToLayoutManager: function( name, event ) {
- if( lm.utils.indexOf( name, this._throttledEvents ) === -1 ) {
- this.layoutManager.emit( name, event.origin );
- } else {
- if( this._pendingEventPropagations[ name ] !== true ) {
- this._pendingEventPropagations[ name ] = true;
- lm.utils.animFrame( lm.utils.fnBind( this._propagateEventToLayoutManager, this, [ name, event ] ) );
- }
- }
- },
- /**
- * Callback for events scheduled by _scheduleEventPropagationToLayoutManager
- *
- * @param {String} name the name of the event
- *
- * @private
- * @returns {void}
- */
- _propagateEventToLayoutManager: function( name, event ) {
- this._pendingEventPropagations[ name ] = false;
- this.layoutManager.emit( name, event );
- }
- } );
- /**
- * @param {[type]} layoutManager [description]
- * @param {[type]} config [description]
- * @param {[type]} parent [description]
- */
- lm.items.Component = function( layoutManager, config, parent ) {
- lm.items.AbstractContentItem.call( this, layoutManager, config, parent );
- var ComponentConstructor = layoutManager.getComponent( this.config.componentName ),
- componentConfig = $.extend( true, {}, this.config.componentState || {} );
- componentConfig.componentName = this.config.componentName;
- this.componentName = this.config.componentName;
- if( this.config.title === '' ) {
- this.config.title = this.config.componentName;
- }
- this.isComponent = true;
- this.container = new lm.container.ItemContainer( this.config, this, layoutManager );
- this.instance = new ComponentConstructor( this.container, componentConfig );
- this.element = this.container._element;
- };
- lm.utils.extend( lm.items.Component, lm.items.AbstractContentItem );
- lm.utils.copy( lm.items.Component.prototype, {
- close: function() {
- this.parent.removeChild( this );
- },
- setSize: function() {
- if( this.element.is( ':visible' ) ) {
- // Do not update size of hidden components to prevent unwanted reflows
- this.container._$setSize( this.element.width(), this.element.height() );
- }
- },
- _$init: function() {
- lm.items.AbstractContentItem.prototype._$init.call( this );
- this.container.emit( 'open' );
- },
- _$hide: function() {
- this.container.hide();
- lm.items.AbstractContentItem.prototype._$hide.call( this );
- },
- _$show: function() {
- this.container.show();
- lm.items.AbstractContentItem.prototype._$show.call( this );
- },
- _$shown: function() {
- this.container.shown();
- lm.items.AbstractContentItem.prototype._$shown.call( this );
- },
- _$destroy: function() {
- this.container.emit( 'destroy', this );
- lm.items.AbstractContentItem.prototype._$destroy.call( this );
- },
- /**
- * Dragging onto a component directly is not an option
- *
- * @returns null
- */
- _$getArea: function() {
- return null;
- }
- } );
- lm.items.Root = function( layoutManager, config, containerElement ) {
- lm.items.AbstractContentItem.call( this, layoutManager, config, null );
- this.isRoot = true;
- this.type = 'root';
- this.element = $( '<div class="lm_goldenlayout lm_item lm_root"></div>' );
- this.childElementContainer = this.element;
- this._containerElement = containerElement;
- this._containerElement.append( this.element );
- };
- lm.utils.extend( lm.items.Root, lm.items.AbstractContentItem );
- lm.utils.copy( lm.items.Root.prototype, {
- addChild: function( contentItem ) {
- if( this.contentItems.length > 0 ) {
- throw new Error( 'Root node can only have a single child' );
- }
- contentItem = this.layoutManager._$normalizeContentItem( contentItem, this );
- this.childElementContainer.append( contentItem.element );
- lm.items.AbstractContentItem.prototype.addChild.call( this, contentItem );
- this.callDownwards( 'setSize' );
- this.emitBubblingEvent( 'stateChanged' );
- },
- setSize: function( width, height ) {
- width = (typeof width === 'undefined') ? this._containerElement.width() : width;
- height = (typeof height === 'undefined') ? this._containerElement.height() : height;
- this.element.width( width );
- this.element.height( height );
- /*
- * Root can be empty
- */
- if( this.contentItems[ 0 ] ) {
- this.contentItems[ 0 ].element.width( width );
- this.contentItems[ 0 ].element.height( height );
- }
- },
- _$highlightDropZone: function( x, y, area ) {
- this.layoutManager.tabDropPlaceholder.remove();
- lm.items.AbstractContentItem.prototype._$highlightDropZone.apply( this, arguments );
- },
- _$onDrop: function( contentItem, area ) {
- var stack;
- if( contentItem.isComponent ) {
- stack = this.layoutManager.createContentItem( {
- type: 'stack',
- header: contentItem.config.header || {}
- }, this );
- stack._$init();
- stack.addChild( contentItem );
- contentItem = stack;
- }
- if( !this.contentItems.length ) {
- this.addChild( contentItem );
- } else {
- var type = area.side[ 0 ] == 'x' ? 'row' : 'column';
- var dimension = area.side[ 0 ] == 'x' ? 'width' : 'height';
- var insertBefore = area.side[ 1 ] == '2';
- var column = this.contentItems[ 0 ];
- if( !column instanceof lm.items.RowOrColumn || column.type != type ) {
- var rowOrColumn = this.layoutManager.createContentItem( { type: type }, this );
- this.replaceChild( column, rowOrColumn );
- rowOrColumn.addChild( contentItem, insertBefore ? 0 : undefined, true );
- rowOrColumn.addChild( column, insertBefore ? undefined : 0, true );
- column.config[ dimension ] = 50;
- contentItem.config[ dimension ] = 50;
- rowOrColumn.callDownwards( 'setSize' );
- } else {
- var sibbling = column.contentItems[ insertBefore ? 0 : column.contentItems.length - 1 ]
- column.addChild( contentItem, insertBefore ? 0 : undefined, true );
- sibbling.config[ dimension ] *= 0.5;
- contentItem.config[ dimension ] = sibbling.config[ dimension ];
- column.callDownwards( 'setSize' );
- }
- }
- }
- } );
- lm.items.RowOrColumn = function( isColumn, layoutManager, config, parent ) {
- lm.items.AbstractContentItem.call( this, layoutManager, config, parent );
- this.isRow = !isColumn;
- this.isColumn = isColumn;
- this.element = $( '<div class="lm_item lm_' + ( isColumn ? 'column' : 'row' ) + '"></div>' );
- this.childElementContainer = this.element;
- this._splitterSize = layoutManager.config.dimensions.borderWidth;
- this._isColumn = isColumn;
- this._dimension = isColumn ? 'height' : 'width';
- this._splitter = [];
- this._splitterPosition = null;
- this._splitterMinPosition = null;
- this._splitterMaxPosition = null;
- };
- lm.utils.extend( lm.items.RowOrColumn, lm.items.AbstractContentItem );
- lm.utils.copy( lm.items.RowOrColumn.prototype, {
- /**
- * Add a new contentItem to the Row or Column
- *
- * @param {lm.item.AbstractContentItem} contentItem
- * @param {[int]} index The position of the new item within the Row or Column.
- * If no index is provided the item will be added to the end
- * @param {[bool]} _$suspendResize If true the items won't be resized. This will leave the item in
- * an inconsistent state and is only intended to be used if multiple
- * children need to be added in one go and resize is called afterwards
- *
- * @returns {void}
- */
- addChild: function( contentItem, index, _$suspendResize ) {
- var newItemSize, itemSize, i, splitterElement;
- contentItem = this.layoutManager._$normalizeContentItem( contentItem, this );
- if( index === undefined ) {
- index = this.contentItems.length;
- }
- if( this.contentItems.length > 0 ) {
- splitterElement = this._createSplitter( Math.max( 0, index - 1 ) ).element;
- if( index > 0 ) {
- this.contentItems[ index - 1 ].element.after( splitterElement );
- splitterElement.after( contentItem.element );
- } else {
- this.contentItems[ 0 ].element.before( splitterElement );
- splitterElement.before( contentItem.element );
- }
- } else {
- this.childElementContainer.append( contentItem.element );
- }
- lm.items.AbstractContentItem.prototype.addChild.call( this, contentItem, index );
- newItemSize = ( 1 / this.contentItems.length ) * 100;
- if( _$suspendResize === true ) {
- this.emitBubblingEvent( 'stateChanged' );
- return;
- }
- for( i = 0; i < this.contentItems.length; i++ ) {
- if( this.contentItems[ i ] === contentItem ) {
- contentItem.config[ this._dimension ] = newItemSize;
- } else {
- itemSize = this.contentItems[ i ].config[ this._dimension ] *= ( 100 - newItemSize ) / 100;
- this.contentItems[ i ].config[ this._dimension ] = itemSize;
- }
- }
- this.callDownwards( 'setSize' );
- this.emitBubblingEvent( 'stateChanged' );
- },
- /**
- * Removes a child of this element
- *
- * @param {lm.items.AbstractContentItem} contentItem
- * @param {boolean} keepChild If true the child will be removed, but not destroyed
- *
- * @returns {void}
- */
- removeChild: function( contentItem, keepChild ) {
- var removedItemSize = contentItem.config[ this._dimension ],
- index = lm.utils.indexOf( contentItem, this.contentItems ),
- splitterIndex = Math.max( index - 1, 0 ),
- i,
- childItem;
- if( index === -1 ) {
- throw new Error( 'Can\'t remove child. ContentItem is not child of this Row or Column' );
- }
- /**
- * Remove the splitter before the item or after if the item happens
- * to be the first in the row/column
- */
- if( this._splitter[ splitterIndex ] ) {
- this._splitter[ splitterIndex ]._$destroy();
- this._splitter.splice( splitterIndex, 1 );
- }
- /**
- * Allocate the space that the removed item occupied to the remaining items
- */
- for( i = 0; i < this.contentItems.length; i++ ) {
- if( this.contentItems[ i ] !== contentItem ) {
- this.contentItems[ i ].config[ this._dimension ] += removedItemSize / ( this.contentItems.length - 1 );
- }
- }
- lm.items.AbstractContentItem.prototype.removeChild.call( this, contentItem, keepChild );
- if( this.contentItems.length === 1 && this.config.isClosable === true ) {
- childItem = this.contentItems[ 0 ];
- this.contentItems = [];
- this.parent.replaceChild( this, childItem, true );
- } else {
- this.callDownwards( 'setSize' );
- this.emitBubblingEvent( 'stateChanged' );
- }
- },
- /**
- * Replaces a child of this Row or Column with another contentItem
- *
- * @param {lm.items.AbstractContentItem} oldChild
- * @param {lm.items.AbstractContentItem} newChild
- *
- * @returns {void}
- */
- replaceChild: function( oldChild, newChild ) {
- var size = oldChild.config[ this._dimension ];
- lm.items.AbstractContentItem.prototype.replaceChild.call( this, oldChild, newChild );
- newChild.config[ this._dimension ] = size;
- this.callDownwards( 'setSize' );
- this.emitBubblingEvent( 'stateChanged' );
- },
- /**
- * Called whenever the dimensions of this item or one of its parents change
- *
- * @returns {void}
- */
- setSize: function() {
- if( this.contentItems.length > 0 ) {
- this._calculateRelativeSizes();
- this._setAbsoluteSizes();
- }
- this.emitBubblingEvent( 'stateChanged' );
- this.emit( 'resize' );
- },
- /**
- * Invoked recursively by the layout manager. AbstractContentItem.init appends
- * the contentItem's DOM elements to the container, RowOrColumn init adds splitters
- * in between them
- *
- * @package private
- * @override AbstractContentItem._$init
- * @returns {void}
- */
- _$init: function() {
- if( this.isInitialised === true ) return;
- var i;
- lm.items.AbstractContentItem.prototype._$init.call( this );
- for( i = 0; i < this.contentItems.length - 1; i++ ) {
- this.contentItems[ i ].element.after( this._createSplitter( i ).element );
- }
- },
- /**
- * Turns the relative sizes calculated by _calculateRelativeSizes into
- * absolute pixel values and applies them to the children's DOM elements
- *
- * Assigns additional pixels to counteract Math.floor
- *
- * @private
- * @returns {void}
- */
- _setAbsoluteSizes: function() {
- var i,
- sizeData = this._calculateAbsoluteSizes();
- for( i = 0; i < this.contentItems.length; i++ ) {
- if( sizeData.additionalPixel - i > 0 ) {
- sizeData.itemSizes[ i ]++;
- }
- if( this._isColumn ) {
- this.contentItems[ i ].element.width( sizeData.totalWidth );
- this.contentItems[ i ].element.height( sizeData.itemSizes[ i ] );
- } else {
- this.contentItems[ i ].element.width( sizeData.itemSizes[ i ] );
- this.contentItems[ i ].element.height( sizeData.totalHeight );
- }
- }
- },
- /**
- * Calculates the absolute sizes of all of the children of this Item.
- * @returns {object} - Set with absolute sizes and additional pixels.
- */
- _calculateAbsoluteSizes: function() {
- var i,
- totalSplitterSize = (this.contentItems.length - 1) * this._splitterSize,
- totalWidth = this.element.width(),
- totalHeight = this.element.height(),
- totalAssigned = 0,
- additionalPixel,
- itemSize,
- itemSizes = [];
- if( this._isColumn ) {
- totalHeight -= totalSplitterSize;
- } else {
- totalWidth -= totalSplitterSize;
- }
- for( i = 0; i < this.contentItems.length; i++ ) {
- if( this._isColumn ) {
- itemSize = Math.floor( totalHeight * ( this.contentItems[ i ].config.height / 100 ) );
- } else {
- itemSize = Math.floor( totalWidth * (this.contentItems[ i ].config.width / 100) );
- }
- totalAssigned += itemSize;
- itemSizes.push( itemSize );
- }
- additionalPixel = Math.floor( (this._isColumn ? totalHeight : totalWidth) - totalAssigned );
- return {
- itemSizes: itemSizes,
- additionalPixel: additionalPixel,
- totalWidth: totalWidth,
- totalHeight: totalHeight
- };
- },
- /**
- * Calculates the relative sizes of all children of this Item. The logic
- * is as follows:
- *
- * - Add up the total size of all items that have a configured size
- *
- * - If the total == 100 (check for floating point errors)
- * Excellent, job done
- *
- * - If the total is > 100,
- * set the size of items without set dimensions to 1/3 and add this to the total
- * set the size off all items so that the total is hundred relative to their original size
- *
- * - If the total is < 100
- * If there are items without set dimensions, distribute the remainder to 100 evenly between them
- * If there are no items without set dimensions, increase all items sizes relative to
- * their original size so that they add up to 100
- *
- * @private
- * @returns {void}
- */
- _calculateRelativeSizes: function() {
- var i,
- total = 0,
- itemsWithoutSetDimension = [],
- dimension = this._isColumn ? 'height' : 'width';
- for( i = 0; i < this.contentItems.length; i++ ) {
- if( this.contentItems[ i ].config[ dimension ] !== undefined ) {
- total += this.contentItems[ i ].config[ dimension ];
- } else {
- itemsWithoutSetDimension.push( this.contentItems[ i ] );
- }
- }
- /**
- * Everything adds up to hundred, all good :-)
- */
- if( Math.round( total ) === 100 ) {
- this._respectMinItemWidth();
- return;
- }
- /**
- * Allocate the remaining size to the items without a set dimension
- */
- if( Math.round( total ) < 100 && itemsWithoutSetDimension.length > 0 ) {
- for( i = 0; i < itemsWithoutSetDimension.length; i++ ) {
- itemsWithoutSetDimension[ i ].config[ dimension ] = ( 100 - total ) / itemsWithoutSetDimension.length;
- }
- this._respectMinItemWidth();
- return;
- }
- /**
- * If the total is > 100, but there are also items without a set dimension left, assing 50
- * as their dimension and add it to the total
- *
- * This will be reset in the next step
- */
- if( Math.round( total ) > 100 ) {
- for( i = 0; i < itemsWithoutSetDimension.length; i++ ) {
- itemsWithoutSetDimension[ i ].config[ dimension ] = 50;
- total += 50;
- }
- }
- /**
- * Set every items size relative to 100 relative to its size to total
- */
- for( i = 0; i < this.contentItems.length; i++ ) {
- this.contentItems[ i ].config[ dimension ] = ( this.contentItems[ i ].config[ dimension ] / total ) * 100;
- }
- this._respectMinItemWidth();
- },
- /**
- * Adjusts the column widths to respect the dimensions minItemWidth if set.
- * @returns {}
- */
- _respectMinItemWidth: function() {
- var minItemWidth = this.layoutManager.config.dimensions ? (this.layoutManager.config.dimensions.minItemWidth || 0) : 0,
- sizeData = null,
- entriesOverMin = [],
- totalOverMin = 0,
- totalUnderMin = 0,
- remainingWidth = 0,
- itemSize = 0,
- contentItem = null,
- reducePercent,
- reducedWidth,
- allEntries = [],
- entry;
- if( this._isColumn || !minItemWidth || this.contentItems.length <= 1 ) {
- return;
- }
- sizeData = this._calculateAbsoluteSizes();
- /**
- * Figure out how much we are under the min item size total and how much room we have to use.
- */
- for( i = 0; i < this.contentItems.length; i++ ) {
- contentItem = this.contentItems[ i ];
- itemSize = sizeData.itemSizes[ i ];
- if( itemSize < minItemWidth ) {
- totalUnderMin += minItemWidth - itemSize;
- entry = { width: minItemWidth };
- }
- else {
- totalOverMin += itemSize - minItemWidth;
- entry = { width: itemSize };
- entriesOverMin.push( entry );
- }
- allEntries.push( entry );
- }
- /**
- * If there is nothing under min, or there is not enough over to make up the difference, do nothing.
- */
- if( totalUnderMin === 0 || totalUnderMin > totalOverMin ) {
- return;
- }
- /**
- * Evenly reduce all columns that are over the min item width to make up the difference.
- */
- reducePercent = totalUnderMin / totalOverMin;
- remainingWidth = totalUnderMin;
- for( i = 0; i < entriesOverMin.length; i++ ) {
- entry = entriesOverMin[ i ];
- reducedWidth = Math.round( ( entry.width - minItemWidth ) * reducePercent );
- remainingWidth -= reducedWidth;
- entry.width -= reducedWidth;
- }
- /**
- * Take anything remaining from the last item.
- */
- if( remainingWidth !== 0 ) {
- allEntries[ allEntries.length - 1 ].width -= remainingWidth;
- }
- /**
- * Set every items size relative to 100 relative to its size to total
- */
- for( i = 0; i < this.contentItems.length; i++ ) {
- this.contentItems[ i ].config.width = (allEntries[ i ].width / sizeData.totalWidth) * 100;
- }
- },
- /**
- * Instantiates a new lm.controls.Splitter, binds events to it and adds
- * it to the array of splitters at the position specified as the index argument
- *
- * What it doesn't do though is append the splitter to the DOM
- *
- * @param {Int} index The position of the splitter
- *
- * @returns {lm.controls.Splitter}
- */
- _createSplitter: function( index ) {
- var splitter;
- splitter = new lm.controls.Splitter( this._isColumn, this._splitterSize );
- splitter.on( 'drag', lm.utils.fnBind( this._onSplitterDrag, this, [ splitter ] ), this );
- splitter.on( 'dragStop', lm.utils.fnBind( this._onSplitterDragStop, this, [ splitter ] ), this );
- splitter.on( 'dragStart', lm.utils.fnBind( this._onSplitterDragStart, this, [ splitter ] ), this );
- this._splitter.splice( index, 0, splitter );
- return splitter;
- },
- /**
- * Locates the instance of lm.controls.Splitter in the array of
- * registered splitters and returns a map containing the contentItem
- * before and after the splitters, both of which are affected if the
- * splitter is moved
- *
- * @param {lm.controls.Splitter} splitter
- *
- * @returns {Object} A map of contentItems that the splitter affects
- */
- _getItemsForSplitter: function( splitter ) {
- var index = lm.utils.indexOf( splitter, this._splitter );
- return {
- before: this.contentItems[ index ],
- after: this.contentItems[ index + 1 ]
- };
- },
- /**
- * Gets the minimum dimensions for the given item configuration array
- * @param item
- * @private
- */
- _getMinimumDimensions: function( arr ) {
- var minWidth = 0, minHeight = 0;
- for( var i = 0; i < arr.length; ++i ) {
- minWidth = Math.max( arr[ i ].minWidth || 0, minWidth );
- minHeight = Math.max( arr[ i ].minHeight || 0, minHeight );
- }
- return { horizontal: minWidth, vertical: minHeight };
- },
- /**
- * Invoked when a splitter's dragListener fires dragStart. Calculates the splitters
- * movement area once (so that it doesn't need calculating on every mousemove event)
- *
- * @param {lm.controls.Splitter} splitter
- *
- * @returns {void}
- */
- _onSplitterDragStart: function( splitter ) {
- var items = this._getItemsForSplitter( splitter ),
- minSize = this.layoutManager.config.dimensions[ this._isColumn ? 'minItemHeight' : 'minItemWidth' ];
- var beforeMinDim = this._getMinimumDimensions( items.before.config.content );
- var beforeMinSize = this._isColumn ? beforeMinDim.vertical : beforeMinDim.horizontal;
- var afterMinDim = this._getMinimumDimensions( items.after.config.content );
- var afterMinSize = this._isColumn ? afterMinDim.vertical : afterMinDim.horizontal;
- this._splitterPosition = 0;
- this._splitterMinPosition = -1 * ( items.before.element[ this._dimension ]() - (beforeMinSize || minSize) );
- this._splitterMaxPosition = items.after.element[ this._dimension ]() - (afterMinSize || minSize);
- },
- /**
- * Invoked when a splitter's DragListener fires drag. Updates the splitters DOM position,
- * but not the sizes of the elements the splitter controls in order to minimize resize events
- *
- * @param {lm.controls.Splitter} splitter
- * @param {Int} offsetX Relative pixel values to the splitters original position. Can be negative
- * @param {Int} offsetY Relative pixel values to the splitters original position. Can be negative
- *
- * @returns {void}
- */
- _onSplitterDrag: function( splitter, offsetX, offsetY ) {
- var offset = this._isColumn ? offsetY : offsetX;
- if( offset > this._splitterMinPosition && offset < this._splitterMaxPosition ) {
- this._splitterPosition = offset;
- splitter.element.css( this._isColumn ? 'top' : 'left', offset );
- }
- },
- /**
- * Invoked when a splitter's DragListener fires dragStop. Resets the splitters DOM position,
- * and applies the new sizes to the elements before and after the splitter and their children
- * on the next animation frame
- *
- * @param {lm.controls.Splitter} splitter
- *
- * @returns {void}
- */
- _onSplitterDragStop: function( splitter ) {
- var items = this._getItemsForSplitter( splitter ),
- sizeBefore = items.before.element[ this._dimension ](),
- sizeAfter = items.after.element[ this._dimension ](),
- splitterPositionInRange = ( this._splitterPosition + sizeBefore ) / ( sizeBefore + sizeAfter ),
- totalRelativeSize = items.before.config[ this._dimension ] + items.after.config[ this._dimension ];
- items.before.config[ this._dimension ] = splitterPositionInRange * totalRelativeSize;
- items.after.config[ this._dimension ] = ( 1 - splitterPositionInRange ) * totalRelativeSize;
- splitter.element.css( {
- 'top': 0,
- 'left': 0
- } );
- lm.utils.animFrame( lm.utils.fnBind( this.callDownwards, this, [ 'setSize' ] ) );
- }
- } );
- lm.items.Stack = function( layoutManager, config, parent ) {
- lm.items.AbstractContentItem.call( this, layoutManager, config, parent );
- this.element = $( '<div class="lm_item lm_stack"></div>' );
- this._activeContentItem = null;
- var cfg = layoutManager.config;
- this._header = { // defaults' reconstruction from old configuration style
- show: cfg.settings.hasHeaders === true && config.hasHeaders !== false,
- popout: cfg.settings.showPopoutIcon && cfg.labels.popout,
- maximise: cfg.settings.showMaximiseIcon && cfg.labels.maximise,
- close: cfg.settings.showCloseIcon && cfg.labels.close,
- minimise: cfg.labels.minimise,
- };
- if( cfg.header ) // load simplified version of header configuration (https://github.com/deepstreamIO/golden-layout/pull/245)
- lm.utils.copy( this._header, cfg.header );
- if( config.header ) // load from stack
- lm.utils.copy( this._header, config.header );
- if( config.content && config.content[ 0 ] && config.content[ 0 ].header ) // load from component if stack omitted
- lm.utils.copy( this._header, config.content[ 0 ].header );
- this._dropZones = {};
- this._dropSegment = null;
- this._contentAreaDimensions = null;
- this._dropIndex = null;
- this.isStack = true;
- this.childElementContainer = $( '<div class="lm_items"></div>' );
- this.header = new lm.controls.Header( layoutManager, this );
- this.element.append( this.header.element );
- this.element.append( this.childElementContainer );
- this._setupHeaderPosition();
- this._$validateClosability();
- };
- lm.utils.extend( lm.items.Stack, lm.items.AbstractContentItem );
- lm.utils.copy( lm.items.Stack.prototype, {
- setSize: function() {
- var i,
- headerSize = this._header.show ? this.layoutManager.config.dimensions.headerHeight : 0,
- contentWidth = this.element.width() - (this._sided ? headerSize : 0),
- contentHeight = this.element.height() - (!this._sided ? headerSize : 0);
- this.childElementContainer.width( contentWidth );
- this.childElementContainer.height( contentHeight );
- for( i = 0; i < this.contentItems.length; i++ ) {
- this.contentItems[ i ].element.width( contentWidth ).height( contentHeight );
- }
- this.emit( 'resize' );
- this.emitBubblingEvent( 'stateChanged' );
- },
- _$init: function() {
- var i, initialItem;
- if( this.isInitialised === true ) return;
- lm.items.AbstractContentItem.prototype._$init.call( this );
- for( i = 0; i < this.contentItems.length; i++ ) {
- this.header.createTab( this.contentItems[ i ] );
- this.contentItems[ i ]._$hide();
- }
- if( this.contentItems.length > 0 ) {
- initialItem = this.contentItems[ this.config.activeItemIndex || 0 ];
- if( !initialItem ) {
- throw new Error( 'Configured activeItemIndex out of bounds' );
- }
- this.setActiveContentItem( initialItem );
- }
- },
- setActiveContentItem: function( contentItem ) {
- if( lm.utils.indexOf( contentItem, this.contentItems ) === -1 ) {
- throw new Error( 'contentItem is not a child of this stack' );
- }
- if( this._activeContentItem !== null ) {
- this._activeContentItem._$hide();
- }
- this._activeContentItem = contentItem;
- this.header.setActiveContentItem( contentItem );
- contentItem._$show();
- this.emit( 'activeContentItemChanged', contentItem );
- this.emitBubblingEvent( 'stateChanged' );
- },
- getActiveContentItem: function() {
- return this.header.activeContentItem;
- },
- addChild: function( contentItem, index ) {
- contentItem = this.layoutManager._$normalizeContentItem( contentItem, this );
- lm.items.AbstractContentItem.prototype.addChild.call( this, contentItem, index );
- this.childElementContainer.append( contentItem.element );
- this.header.createTab( contentItem, index );
- this.setActiveContentItem( contentItem );
- this.callDownwards( 'setSize' );
- this._$validateClosability();
- this.emitBubblingEvent( 'stateChanged' );
- },
- removeChild: function( contentItem, keepChild ) {
- var index = lm.utils.indexOf( contentItem, this.contentItems );
- var curIndex = lm.utils.indexOf( this._activeContentItem, this.contentItems );
- lm.items.AbstractContentItem.prototype.removeChild.call( this, contentItem, keepChild );
- this.header.removeTab( contentItem );
- if( this.contentItems.length > 0 ) {
- var indexToFocus = index > curIndex ? curIndex : curIndex - 1;
- if (curIndex == index)
- indexToFocus = Math.max( index - 1, 0 );
- this.setActiveContentItem( this.contentItems[ indexToFocus ] );
- } else {
- this._activeContentItem = null;
- }
- this._$validateClosability();
- this.emitBubblingEvent( 'stateChanged' );
- },
- /**
- * Validates that the stack is still closable or not. If a stack is able
- * to close, but has a non closable component added to it, the stack is no
- * longer closable until all components are closable.
- *
- * @returns {void}
- */
- _$validateClosability: function() {
- var contentItem,
- isClosable,
- len,
- i;
- isClosable = this.header._isClosable();
- for( i = 0, len = this.contentItems.length; i < len; i++ ) {
- if( !isClosable ) {
- break;
- }
- isClosable = this.contentItems[ i ].config.isClosable;
- }
- this.header._$setClosable( isClosable );
- },
- _$destroy: function() {
- lm.items.AbstractContentItem.prototype._$destroy.call( this );
- this.header._$destroy();
- },
- /**
- * Ok, this one is going to be the tricky one: The user has dropped {contentItem} onto this stack.
- *
- * It was dropped on either the stacks header or the top, right, bottom or left bit of the content area
- * (which one of those is stored in this._dropSegment). Now, if the user has dropped on the header the case
- * is relatively clear: We add the item to the existing stack... job done (might be good to have
- * tab reordering at some point, but lets not sweat it right now)
- *
- * If the item was dropped on the content part things are a bit more complicated. If it was dropped on either the
- * top or bottom region we need to create a new column and place the items accordingly.
- * Unless, of course if the stack is already within a column... in which case we want
- * to add the newly created item to the existing column...
- * either prepend or append it, depending on wether its top or bottom.
- *
- * Same thing for rows and left / right drop segments... so in total there are 9 things that can potentially happen
- * (left, top, right, bottom) * is child of the right parent (row, column) + header drop
- *
- * @param {lm.item} contentItem
- *
- * @returns {void}
- */
- _$onDrop: function( contentItem ) {
- /*
- * The item was dropped on the header area. Just add it as a child of this stack and
- * get the hell out of this logic
- */
- if( this._dropSegment === 'header' ) {
- this._resetHeaderDropZone();
- this.addChild( contentItem, this._dropIndex );
- return;
- }
- /*
- * The stack is empty. Let's just add the element.
- */
- if( this._dropSegment === 'body' ) {
- this.addChild( contentItem );
- return;
- }
- /*
- * The item was dropped on the top-, left-, bottom- or right- part of the content. Let's
- * aggregate some conditions to make the if statements later on more readable
- */
- var isVertical = this._dropSegment === 'top' || this._dropSegment === 'bottom',
- isHorizontal = this._dropSegment === 'left' || this._dropSegment === 'right',
- insertBefore = this._dropSegment === 'top' || this._dropSegment === 'left',
- hasCorrectParent = ( isVertical && this.parent.isColumn ) || ( isHorizontal && this.parent.isRow ),
- type = isVertical ? 'column' : 'row',
- dimension = isVertical ? 'height' : 'width',
- index,
- stack,
- rowOrColumn;
- /*
- * The content item can be either a component or a stack. If it is a component, wrap it into a stack
- */
- if( contentItem.isComponent ) {
- stack = this.layoutManager.createContentItem( {
- type: 'stack',
- header: contentItem.config.header || {}
- }, this );
- stack._$init();
- stack.addChild( contentItem );
- contentItem = stack;
- }
- /*
- * If the item is dropped on top or bottom of a column or left and right of a row, it's already
- * layd out in the correct way. Just add it as a child
- */
- if( hasCorrectParent ) {
- index = lm.utils.indexOf( this, this.parent.contentItems );
- this.parent.addChild( contentItem, insertBefore ? index : index + 1, true );
- this.config[ dimension ] *= 0.5;
- contentItem.config[ dimension ] = this.config[ dimension ];
- this.parent.callDownwards( 'setSize' );
- /*
- * This handles items that are dropped on top or bottom of a row or left / right of a column. We need
- * to create the appropriate contentItem for them to live in
- */
- } else {
- type = isVertical ? 'column' : 'row';
- rowOrColumn = this.layoutManager.createContentItem( { type: type }, this );
- this.parent.replaceChild( this, rowOrColumn );
- rowOrColumn.addChild( contentItem, insertBefore ? 0 : undefined, true );
- rowOrColumn.addChild( this, insertBefore ? undefined : 0, true );
- this.config[ dimension ] = 50;
- contentItem.config[ dimension ] = 50;
- rowOrColumn.callDownwards( 'setSize' );
- }
- },
- /**
- * If the user hovers above the header part of the stack, indicate drop positions for tabs.
- * otherwise indicate which segment of the body the dragged item would be dropped on
- *
- * @param {Int} x Absolute Screen X
- * @param {Int} y Absolute Screen Y
- *
- * @returns {void}
- */
- _$highlightDropZone: function( x, y ) {
- var segment, area;
- for( segment in this._contentAreaDimensions ) {
- area = this._contentAreaDimensions[ segment ].hoverArea;
- if( area.x1 < x && area.x2 > x && area.y1 < y && area.y2 > y ) {
- if( segment === 'header' ) {
- this._dropSegment = 'header';
- this._highlightHeaderDropZone( this._sided ? y : x );
- } else {
- this._resetHeaderDropZone();
- this._highlightBodyDropZone( segment );
- }
- return;
- }
- }
- },
- _$getArea: function() {
- if( this.element.is( ':visible' ) === false ) {
- return null;
- }
- var getArea = lm.items.AbstractContentItem.prototype._$getArea,
- headerArea = getArea.call( this, this.header.element ),
- contentArea = getArea.call( this, this.childElementContainer ),
- contentWidth = contentArea.x2 - contentArea.x1,
- contentHeight = contentArea.y2 - contentArea.y1;
- this._contentAreaDimensions = {
- header: {
- hoverArea: {
- x1: headerArea.x1,
- y1: headerArea.y1,
- x2: headerArea.x2,
- y2: headerArea.y2
- },
- highlightArea: {
- x1: headerArea.x1,
- y1: headerArea.y1,
- x2: headerArea.x2,
- y2: headerArea.y2
- }
- }
- };
- /**
- * If this Stack is a parent to rows, columns or other stacks only its
- * header is a valid dropzone.
- */
- if( this._activeContentItem && this._activeContentItem.isComponent === false ) {
- return headerArea;
- }
- /**
- * Highlight the entire body if the stack is empty
- */
- if( this.contentItems.length === 0 ) {
- this._contentAreaDimensions.body = {
- hoverArea: {
- x1: contentArea.x1,
- y1: contentArea.y1,
- x2: contentArea.x2,
- y2: contentArea.y2
- },
- highlightArea: {
- x1: contentArea.x1,
- y1: contentArea.y1,
- x2: contentArea.x2,
- y2: contentArea.y2
- }
- };
- return getArea.call( this, this.element );
- }
- this._contentAreaDimensions.left = {
- hoverArea: {
- x1: contentArea.x1,
- y1: contentArea.y1,
- x2: contentArea.x1 + contentWidth * 0.25,
- y2: contentArea.y2
- },
- highlightArea: {
- x1: contentArea.x1,
- y1: contentArea.y1,
- x2: contentArea.x1 + contentWidth * 0.5,
- y2: contentArea.y2
- }
- };
- this._contentAreaDimensions.top = {
- hoverArea: {
- x1: contentArea.x1 + contentWidth * 0.25,
- y1: contentArea.y1,
- x2: contentArea.x1 + contentWidth * 0.75,
- y2: contentArea.y1 + contentHeight * 0.5
- },
- highlightArea: {
- x1: contentArea.x1,
- y1: contentArea.y1,
- x2: contentArea.x2,
- y2: contentArea.y1 + contentHeight * 0.5
- }
- };
- this._contentAreaDimensions.right = {
- hoverArea: {
- x1: contentArea.x1 + contentWidth * 0.75,
- y1: contentArea.y1,
- x2: contentArea.x2,
- y2: contentArea.y2
- },
- highlightArea: {
- x1: contentArea.x1 + contentWidth * 0.5,
- y1: contentArea.y1,
- x2: contentArea.x2,
- y2: contentArea.y2
- }
- };
- this._contentAreaDimensions.bottom = {
- hoverArea: {
- x1: contentArea.x1 + contentWidth * 0.25,
- y1: contentArea.y1 + contentHeight * 0.5,
- x2: contentArea.x1 + contentWidth * 0.75,
- y2: contentArea.y2
- },
- highlightArea: {
- x1: contentArea.x1,
- y1: contentArea.y1 + contentHeight * 0.5,
- x2: contentArea.x2,
- y2: contentArea.y2
- }
- };
- return getArea.call( this, this.element );
- },
- _highlightHeaderDropZone: function( x ) {
- var i,
- tabElement,
- tabsLength = this.header.tabs.length,
- isAboveTab = false,
- tabTop,
- tabLeft,
- offset,
- placeHolderLeft,
- headerOffset,
- tabWidth,
- halfX;
- // Empty stack
- if( tabsLength === 0 ) {
- headerOffset = this.header.element.offset();
- this.layoutManager.dropTargetIndicator.highlightArea( {
- x1: headerOffset.left,
- x2: headerOffset.left + 100,
- y1: headerOffset.top + this.header.element.height() - 20,
- y2: headerOffset.top + this.header.element.height()
- } );
- return;
- }
- for( i = 0; i < tabsLength; i++ ) {
- tabElement = this.header.tabs[ i ].element;
- offset = tabElement.offset();
- if( this._sided ) {
- tabLeft = offset.top;
- tabTop = offset.left;
- tabWidth = tabElement.height();
- } else {
- tabLeft = offset.left;
- tabTop = offset.top;
- tabWidth = tabElement.width();
- }
- if( x > tabLeft && x < tabLeft + tabWidth ) {
- isAboveTab = true;
- break;
- }
- }
- if( isAboveTab === false && x < tabLeft ) {
- return;
- }
- halfX = tabLeft + tabWidth / 2;
- if( x < halfX ) {
- this._dropIndex = i;
- tabElement.before( this.layoutManager.tabDropPlaceholder );
- } else {
- this._dropIndex = Math.min( i + 1, tabsLength );
- tabElement.after( this.layoutManager.tabDropPlaceholder );
- }
- if( this._sided ) {
- placeHolderTop = this.layoutManager.tabDropPlaceholder.offset().top;
- this.layoutManager.dropTargetIndicator.highlightArea( {
- x1: tabTop,
- x2: tabTop + tabElement.innerHeight(),
- y1: placeHolderTop,
- y2: placeHolderTop + this.layoutManager.tabDropPlaceholder.width()
- } );
- return;
- }
- placeHolderLeft = this.layoutManager.tabDropPlaceholder.offset().left;
- this.layoutManager.dropTargetIndicator.highlightArea( {
- x1: placeHolderLeft,
- x2: placeHolderLeft + this.layoutManager.tabDropPlaceholder.width(),
- y1: tabTop,
- y2: tabTop + tabElement.innerHeight()
- } );
- },
- _resetHeaderDropZone: function() {
- this.layoutManager.tabDropPlaceholder.remove();
- },
- _setupHeaderPosition: function() {
- var side = [ 'right', 'left', 'bottom' ].indexOf( this._header.show ) >= 0 && this._header.show;
- this.header.element.toggle( !!this._header.show );
- this._side = side;
- this._sided = [ 'right', 'left' ].indexOf( this._side ) >= 0;
- this.element.removeClass( 'lm_left lm_right lm_bottom' );
- if( this._side )
- this.element.addClass( 'lm_' + this._side );
- if( this.element.find( '.lm_header' ).length && this.childElementContainer ) {
- var headerPosition = [ 'right', 'bottom' ].indexOf( this._side ) >= 0 ? 'before' : 'after';
- this.header.element[ headerPosition ]( this.childElementContainer );
- this.callDownwards( 'setSize' );
- }
- },
- _highlightBodyDropZone: function( segment ) {
- var highlightArea = this._contentAreaDimensions[ segment ].highlightArea;
- this.layoutManager.dropTargetIndicator.highlightArea( highlightArea );
- this._dropSegment = segment;
- }
- } );
- lm.utils.BubblingEvent = function( name, origin ) {
- this.name = name;
- this.origin = origin;
- this.isPropagationStopped = false;
- };
- lm.utils.BubblingEvent.prototype.stopPropagation = function() {
- this.isPropagationStopped = true;
- };
- /**
- * Minifies and unminifies configs by replacing frequent keys
- * and values with one letter substitutes
- *
- * @constructor
- */
- lm.utils.ConfigMinifier = function() {
- this._keys = [
- 'settings',
- 'hasHeaders',
- 'constrainDragToContainer',
- 'constrainDragToHeader',
- 'selectionEnabled',
- 'dimensions',
- 'borderWidth',
- 'minItemHeight',
- 'minItemWidth',
- 'headerHeight',
- 'dragProxyWidth',
- 'dragProxyHeight',
- 'labels',
- 'close',
- 'maximise',
- 'minimise',
- 'popout',
- 'content',
- 'componentName',
- 'componentState',
- 'id',
- 'width',
- 'type',
- 'height',
- 'isClosable',
- 'title',
- 'popoutWholeStack',
- 'openPopouts',
- 'parentId',
- 'activeItemIndex',
- 'reorderEnabled'
- //Maximum 36 entries, do not cross this line!
- ];
- this._values = [
- true,
- false,
- 'row',
- 'column',
- 'stack',
- 'component',
- 'close',
- 'maximise',
- 'minimise',
- 'open in new window'
- ];
- };
- lm.utils.copy( lm.utils.ConfigMinifier.prototype, {
- /**
- * Takes a GoldenLayout configuration object and
- * replaces its keys and values recursively with
- * one letter counterparts
- *
- * @param {Object} config A GoldenLayout config object
- *
- * @returns {Object} minified config
- */
- minifyConfig: function( config ) {
- var min = {};
- this._nextLevel( config, min, '_min' );
- return min;
- },
- /**
- * Takes a configuration Object that was previously minified
- * using minifyConfig and returns its original version
- *
- * @param {Object} minifiedConfig
- *
- * @returns {Object} the original configuration
- */
- unminifyConfig: function( minifiedConfig ) {
- var orig = {};
- this._nextLevel( minifiedConfig, orig, '_max' );
- return orig;
- },
- /**
- * Recursive function, called for every level of the config structure
- *
- * @param {Array|Object} orig
- * @param {Array|Object} min
- * @param {String} translationFn
- *
- * @returns {void}
- */
- _nextLevel: function( from, to, translationFn ) {
- var key, minKey;
- for( key in from ) {
- /**
- * For in returns array indices as keys, so let's cast them to numbers
- */
- if( from instanceof Array ) key = parseInt( key, 10 );
- /**
- * In case something has extended Object prototypes
- */
- if( !from.hasOwnProperty( key ) ) continue;
- /**
- * Translate the key to a one letter substitute
- */
- minKey = this[ translationFn ]( key, this._keys );
- /**
- * For Arrays and Objects, create a new Array/Object
- * on the minified object and recurse into it
- */
- if( typeof from[ key ] === 'object' ) {
- to[ minKey ] = from[ key ] instanceof Array ? [] : {};
- this._nextLevel( from[ key ], to[ minKey ], translationFn );
- /**
- * For primitive values (Strings, Numbers, Boolean etc.)
- * minify the value
- */
- } else {
- to[ minKey ] = this[ translationFn ]( from[ key ], this._values );
- }
- }
- },
- /**
- * Minifies value based on a dictionary
- *
- * @param {String|Boolean} value
- * @param {Array<String|Boolean>} dictionary
- *
- * @returns {String} The minified version
- */
- _min: function( value, dictionary ) {
- /**
- * If a value actually is a single character, prefix it
- * with ___ to avoid mistaking it for a minification code
- */
- if( typeof value === 'string' && value.length === 1 ) {
- return '___' + value;
- }
- var index = lm.utils.indexOf( value, dictionary );
- /**
- * value not found in the dictionary, return it unmodified
- */
- if( index === -1 ) {
- return value;
- /**
- * value found in dictionary, return its base36 counterpart
- */
- } else {
- return index.toString( 36 );
- }
- },
- _max: function( value, dictionary ) {
- /**
- * value is a single character. Assume that it's a translation
- * and return the original value from the dictionary
- */
- if( typeof value === 'string' && value.length === 1 ) {
- return dictionary[ parseInt( value, 36 ) ];
- }
- /**
- * value originally was a single character and was prefixed with ___
- * to avoid mistaking it for a translation. Remove the prefix
- * and return the original character
- */
- if( typeof value === 'string' && value.substr( 0, 3 ) === '___' ) {
- return value[ 3 ];
- }
- /**
- * value was not minified
- */
- return value;
- }
- } );
- /**
- * An EventEmitter singleton that propagates events
- * across multiple windows. This is a little bit trickier since
- * windows are allowed to open childWindows in their own right
- *
- * This means that we deal with a tree of windows. Hence the rules for event propagation are:
- *
- * - Propagate events from this layout to both parents and children
- * - Propagate events from parent to this and children
- * - Propagate events from children to the other children (but not the emitting one) and the parent
- *
- * @constructor
- *
- * @param {lm.LayoutManager} layoutManager
- */
- lm.utils.EventHub = function( layoutManager ) {
- lm.utils.EventEmitter.call( this );
- this._layoutManager = layoutManager;
- this._dontPropagateToParent = null;
- this._childEventSource = null;
- this.on( lm.utils.EventEmitter.ALL_EVENT, lm.utils.fnBind( this._onEventFromThis, this ) );
- this._boundOnEventFromChild = lm.utils.fnBind( this._onEventFromChild, this );
- $( window ).on( 'gl_child_event', this._boundOnEventFromChild );
- };
- /**
- * Called on every event emitted on this eventHub, regardles of origin.
- *
- * @private
- *
- * @param {Mixed}
- *
- * @returns {void}
- */
- lm.utils.EventHub.prototype._onEventFromThis = function() {
- var args = Array.prototype.slice.call( arguments );
- if( this._layoutManager.isSubWindow && args[ 0 ] !== this._dontPropagateToParent ) {
- this._propagateToParent( args );
- }
- this._propagateToChildren( args );
- //Reset
- this._dontPropagateToParent = null;
- this._childEventSource = null;
- };
- /**
- * Called by the parent layout.
- *
- * @param {Array} args Event name + arguments
- *
- * @returns {void}
- */
- lm.utils.EventHub.prototype._$onEventFromParent = function( args ) {
- this._dontPropagateToParent = args[ 0 ];
- this.emit.apply( this, args );
- };
- /**
- * Callback for child events raised on the window
- *
- * @param {DOMEvent} event
- * @private
- *
- * @returns {void}
- */
- lm.utils.EventHub.prototype._onEventFromChild = function( event ) {
- this._childEventSource = event.originalEvent.__gl;
- this.emit.apply( this, event.originalEvent.__glArgs );
- };
- /**
- * Propagates the event to the parent by emitting
- * it on the parent's DOM window
- *
- * @param {Array} args Event name + arguments
- * @private
- *
- * @returns {void}
- */
- lm.utils.EventHub.prototype._propagateToParent = function( args ) {
- var event,
- eventName = 'gl_child_event';
- if( document.createEvent ) {
- event = window.opener.document.createEvent( 'HTMLEvents' );
- event.initEvent( eventName, true, true );
- } else {
- event = window.opener.document.createEventObject();
- event.eventType = eventName;
- }
- event.eventName = eventName;
- event.__glArgs = args;
- event.__gl = this._layoutManager;
- if( document.createEvent ) {
- window.opener.dispatchEvent( event );
- } else {
- window.opener.fireEvent( 'on' + event.eventType, event );
- }
- };
- /**
- * Propagate events to children
- *
- * @param {Array} args Event name + arguments
- * @private
- *
- * @returns {void}
- */
- lm.utils.EventHub.prototype._propagateToChildren = function( args ) {
- var childGl, i;
- for( i = 0; i < this._layoutManager.openPopouts.length; i++ ) {
- childGl = this._layoutManager.openPopouts[ i ].getGlInstance();
- if( childGl && childGl !== this._childEventSource ) {
- childGl.eventHub._$onEventFromParent( args );
- }
- }
- };
- /**
- * Destroys the EventHub
- *
- * @public
- * @returns {void}
- */
- lm.utils.EventHub.prototype.destroy = function() {
- $( window ).off( 'gl_child_event', this._boundOnEventFromChild );
- };
- /**
- * A specialised GoldenLayout component that binds GoldenLayout container
- * lifecycle events to react components
- *
- * @constructor
- *
- * @param {lm.container.ItemContainer} container
- * @param {Object} state state is not required for react components
- */
- lm.utils.ReactComponentHandler = function( container, state ) {
- this._reactComponent = null;
- this._originalComponentWillUpdate = null;
- this._container = container;
- this._initialState = state;
- this._reactClass = this._getReactClass();
- this._container.on( 'open', this._render, this );
- this._container.on( 'destroy', this._destroy, this );
- };
- lm.utils.copy( lm.utils.ReactComponentHandler.prototype, {
- /**
- * Creates the react class and component and hydrates it with
- * the initial state - if one is present
- *
- * By default, react's getInitialState will be used
- *
- * @private
- * @returns {void}
- */
- _render: function() {
- this._reactComponent = ReactDOM.render( this._getReactComponent(), this._container.getElement()[ 0 ] );
- this._originalComponentWillUpdate = this._reactComponent.componentWillUpdate || function() {
- };
- this._reactComponent.componentWillUpdate = this._onUpdate.bind( this );
- if( this._container.getState() ) {
- this._reactComponent.setState( this._container.getState() );
- }
- },
- /**
- * Removes the component from the DOM and thus invokes React's unmount lifecycle
- *
- * @private
- * @returns {void}
- */
- _destroy: function() {
- ReactDOM.unmountComponentAtNode( this._container.getElement()[ 0 ] );
- this._container.off( 'open', this._render, this );
- this._container.off( 'destroy', this._destroy, this );
- },
- /**
- * Hooks into React's state management and applies the componentstate
- * to GoldenLayout
- *
- * @private
- * @returns {void}
- */
- _onUpdate: function( nextProps, nextState ) {
- this._container.setState( nextState );
- this._originalComponentWillUpdate.call( this._reactComponent, nextProps, nextState );
- },
- /**
- * Retrieves the react class from GoldenLayout's registry
- *
- * @private
- * @returns {React.Class}
- */
- _getReactClass: function() {
- var componentName = this._container._config.component;
- var reactClass;
- if( !componentName ) {
- throw new Error( 'No react component name. type: react-component needs a field `component`' );
- }
- reactClass = this._container.layoutManager.getComponent( componentName );
- if( !reactClass ) {
- throw new Error( 'React component "' + componentName + '" not found. ' +
- 'Please register all components with GoldenLayout using `registerComponent(name, component)`' );
- }
- return reactClass;
- },
- /**
- * Copies and extends the properties array and returns the React element
- *
- * @private
- * @returns {React.Element}
- */
- _getReactComponent: function() {
- var defaultProps = {
- glEventHub: this._container.layoutManager.eventHub,
- glContainer: this._container,
- };
- var props = $.extend( defaultProps, this._container._config.props );
- return React.createElement( this._reactClass, props );
- }
- } );})(window.$);
|