togetherjsPackage.js 575 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722272327242725272627272728272927302731273227332734273527362737273827392740274127422743274427452746274727482749275027512752275327542755275627572758275927602761276227632764276527662767276827692770277127722773277427752776277727782779278027812782278327842785278627872788278927902791279227932794279527962797279827992800280128022803280428052806280728082809281028112812281328142815281628172818281928202821282228232824282528262827282828292830283128322833283428352836283728382839284028412842284328442845284628472848284928502851285228532854285528562857285828592860286128622863286428652866286728682869287028712872287328742875287628772878287928802881288228832884288528862887288828892890289128922893289428952896289728982899290029012902290329042905290629072908290929102911291229132914291529162917291829192920292129222923292429252926292729282929293029312932293329342935293629372938293929402941294229432944294529462947294829492950295129522953295429552956295729582959296029612962296329642965296629672968296929702971297229732974297529762977297829792980298129822983298429852986298729882989299029912992299329942995299629972998299930003001300230033004300530063007300830093010301130123013301430153016301730183019302030213022302330243025302630273028302930303031303230333034303530363037303830393040304130423043304430453046304730483049305030513052305330543055305630573058305930603061306230633064306530663067306830693070307130723073307430753076307730783079308030813082308330843085308630873088308930903091309230933094309530963097309830993100310131023103310431053106310731083109311031113112311331143115311631173118311931203121312231233124312531263127312831293130313131323133313431353136313731383139314031413142314331443145314631473148314931503151315231533154315531563157315831593160316131623163316431653166316731683169317031713172317331743175317631773178317931803181318231833184318531863187318831893190319131923193319431953196319731983199320032013202320332043205320632073208320932103211321232133214321532163217321832193220322132223223322432253226322732283229323032313232323332343235323632373238323932403241324232433244324532463247324832493250325132523253325432553256325732583259326032613262326332643265326632673268326932703271327232733274327532763277327832793280328132823283328432853286328732883289329032913292329332943295329632973298329933003301330233033304330533063307330833093310331133123313331433153316331733183319332033213322332333243325332633273328332933303331333233333334333533363337333833393340334133423343334433453346334733483349335033513352335333543355335633573358335933603361336233633364336533663367336833693370337133723373337433753376337733783379338033813382338333843385338633873388338933903391339233933394339533963397339833993400340134023403340434053406340734083409341034113412341334143415341634173418341934203421342234233424342534263427342834293430343134323433343434353436343734383439344034413442344334443445344634473448344934503451345234533454345534563457345834593460346134623463346434653466346734683469347034713472347334743475347634773478347934803481348234833484348534863487348834893490349134923493349434953496349734983499350035013502350335043505350635073508350935103511351235133514351535163517351835193520352135223523352435253526352735283529353035313532353335343535353635373538353935403541354235433544354535463547354835493550355135523553355435553556355735583559356035613562356335643565356635673568356935703571357235733574357535763577357835793580358135823583358435853586358735883589359035913592359335943595359635973598359936003601360236033604360536063607360836093610361136123613361436153616361736183619362036213622362336243625362636273628362936303631363236333634363536363637363836393640364136423643364436453646364736483649365036513652365336543655365636573658365936603661366236633664366536663667366836693670367136723673367436753676367736783679368036813682368336843685368636873688368936903691369236933694369536963697369836993700370137023703370437053706370737083709371037113712371337143715371637173718371937203721372237233724372537263727372837293730373137323733373437353736373737383739374037413742374337443745374637473748374937503751375237533754375537563757375837593760376137623763376437653766376737683769377037713772377337743775377637773778377937803781378237833784378537863787378837893790379137923793379437953796379737983799380038013802380338043805380638073808380938103811381238133814381538163817381838193820382138223823382438253826382738283829383038313832383338343835383638373838383938403841384238433844384538463847384838493850385138523853385438553856385738583859386038613862386338643865386638673868386938703871387238733874387538763877387838793880388138823883388438853886388738883889389038913892389338943895389638973898389939003901390239033904390539063907390839093910391139123913391439153916391739183919392039213922392339243925392639273928392939303931393239333934393539363937393839393940394139423943394439453946394739483949395039513952395339543955395639573958395939603961396239633964396539663967396839693970397139723973397439753976397739783979398039813982398339843985398639873988398939903991399239933994399539963997399839994000400140024003400440054006400740084009401040114012401340144015401640174018401940204021402240234024402540264027402840294030403140324033403440354036403740384039404040414042404340444045404640474048404940504051405240534054405540564057405840594060406140624063406440654066406740684069407040714072407340744075407640774078407940804081408240834084408540864087408840894090409140924093409440954096409740984099410041014102410341044105410641074108410941104111411241134114411541164117411841194120412141224123412441254126412741284129413041314132413341344135413641374138413941404141414241434144414541464147414841494150415141524153415441554156415741584159416041614162416341644165416641674168416941704171417241734174417541764177417841794180418141824183418441854186418741884189419041914192419341944195419641974198419942004201420242034204420542064207420842094210421142124213421442154216421742184219422042214222422342244225422642274228422942304231423242334234423542364237423842394240424142424243424442454246424742484249425042514252425342544255425642574258425942604261426242634264426542664267426842694270427142724273427442754276427742784279428042814282428342844285428642874288428942904291429242934294429542964297429842994300430143024303430443054306430743084309431043114312431343144315431643174318431943204321432243234324432543264327432843294330433143324333433443354336433743384339434043414342434343444345434643474348434943504351435243534354435543564357435843594360436143624363436443654366436743684369437043714372437343744375437643774378437943804381438243834384438543864387438843894390439143924393439443954396439743984399440044014402440344044405440644074408440944104411441244134414441544164417441844194420442144224423442444254426442744284429443044314432443344344435443644374438443944404441444244434444444544464447444844494450445144524453445444554456445744584459446044614462446344644465446644674468446944704471447244734474447544764477447844794480448144824483448444854486448744884489449044914492449344944495449644974498449945004501450245034504450545064507450845094510451145124513451445154516451745184519452045214522452345244525452645274528452945304531453245334534453545364537453845394540454145424543454445454546454745484549455045514552455345544555455645574558455945604561456245634564456545664567456845694570457145724573457445754576457745784579458045814582458345844585458645874588458945904591459245934594459545964597459845994600460146024603460446054606460746084609461046114612461346144615461646174618461946204621462246234624462546264627462846294630463146324633463446354636463746384639464046414642464346444645464646474648464946504651465246534654465546564657465846594660466146624663466446654666466746684669467046714672467346744675467646774678467946804681468246834684468546864687468846894690469146924693469446954696469746984699470047014702470347044705470647074708470947104711471247134714471547164717471847194720472147224723472447254726472747284729473047314732473347344735473647374738473947404741474247434744474547464747474847494750475147524753475447554756475747584759476047614762476347644765476647674768476947704771477247734774477547764777477847794780478147824783478447854786478747884789479047914792479347944795479647974798479948004801480248034804480548064807480848094810481148124813481448154816481748184819482048214822482348244825482648274828482948304831483248334834483548364837483848394840484148424843484448454846484748484849485048514852485348544855485648574858485948604861486248634864486548664867486848694870487148724873487448754876487748784879488048814882488348844885488648874888488948904891489248934894489548964897489848994900490149024903490449054906490749084909491049114912491349144915491649174918491949204921492249234924492549264927492849294930493149324933493449354936493749384939494049414942494349444945494649474948494949504951495249534954495549564957495849594960496149624963496449654966496749684969497049714972497349744975497649774978497949804981498249834984498549864987498849894990499149924993499449954996499749984999500050015002500350045005500650075008500950105011501250135014501550165017501850195020502150225023502450255026502750285029503050315032503350345035503650375038503950405041504250435044504550465047504850495050505150525053505450555056505750585059506050615062506350645065506650675068506950705071507250735074507550765077507850795080508150825083508450855086508750885089509050915092509350945095509650975098509951005101510251035104510551065107510851095110511151125113511451155116511751185119512051215122512351245125512651275128512951305131513251335134513551365137513851395140514151425143514451455146514751485149515051515152515351545155515651575158515951605161516251635164516551665167516851695170517151725173517451755176517751785179518051815182518351845185518651875188518951905191519251935194519551965197519851995200520152025203520452055206520752085209521052115212521352145215521652175218521952205221522252235224522552265227522852295230523152325233523452355236523752385239524052415242524352445245524652475248524952505251525252535254525552565257525852595260526152625263526452655266526752685269527052715272527352745275527652775278527952805281528252835284528552865287528852895290529152925293529452955296529752985299530053015302530353045305530653075308530953105311531253135314531553165317531853195320532153225323532453255326532753285329533053315332533353345335533653375338533953405341534253435344534553465347534853495350535153525353535453555356535753585359536053615362536353645365536653675368536953705371537253735374537553765377537853795380538153825383538453855386538753885389539053915392539353945395539653975398539954005401540254035404540554065407540854095410541154125413541454155416541754185419542054215422542354245425542654275428542954305431543254335434543554365437543854395440544154425443544454455446544754485449545054515452545354545455545654575458545954605461546254635464546554665467546854695470547154725473547454755476547754785479548054815482548354845485548654875488548954905491549254935494549554965497549854995500550155025503550455055506550755085509551055115512551355145515551655175518551955205521552255235524552555265527552855295530553155325533553455355536553755385539554055415542554355445545554655475548554955505551555255535554555555565557555855595560556155625563556455655566556755685569557055715572557355745575557655775578557955805581558255835584558555865587558855895590559155925593559455955596559755985599560056015602560356045605560656075608560956105611561256135614561556165617561856195620562156225623562456255626562756285629563056315632563356345635563656375638563956405641564256435644564556465647564856495650565156525653565456555656565756585659566056615662566356645665566656675668566956705671567256735674567556765677567856795680568156825683568456855686568756885689569056915692569356945695569656975698569957005701570257035704570557065707570857095710571157125713571457155716571757185719572057215722572357245725572657275728572957305731573257335734573557365737573857395740574157425743574457455746574757485749575057515752575357545755575657575758575957605761576257635764576557665767576857695770577157725773577457755776577757785779578057815782578357845785578657875788578957905791579257935794579557965797579857995800580158025803580458055806580758085809581058115812581358145815581658175818581958205821582258235824582558265827582858295830583158325833583458355836583758385839584058415842584358445845584658475848584958505851585258535854585558565857585858595860586158625863586458655866586758685869587058715872587358745875587658775878587958805881588258835884588558865887588858895890589158925893589458955896589758985899590059015902590359045905590659075908590959105911591259135914591559165917591859195920592159225923592459255926592759285929593059315932593359345935593659375938593959405941594259435944594559465947594859495950595159525953595459555956595759585959596059615962596359645965596659675968596959705971597259735974597559765977597859795980598159825983598459855986598759885989599059915992599359945995599659975998599960006001600260036004600560066007600860096010601160126013601460156016601760186019602060216022602360246025602660276028602960306031603260336034603560366037603860396040604160426043604460456046604760486049605060516052605360546055605660576058605960606061606260636064606560666067606860696070607160726073607460756076607760786079608060816082608360846085608660876088608960906091609260936094609560966097609860996100610161026103610461056106610761086109611061116112611361146115611661176118611961206121612261236124612561266127612861296130613161326133613461356136613761386139614061416142614361446145614661476148614961506151615261536154615561566157615861596160616161626163616461656166616761686169617061716172617361746175617661776178617961806181618261836184618561866187618861896190619161926193619461956196619761986199620062016202620362046205620662076208620962106211621262136214621562166217621862196220622162226223622462256226622762286229623062316232623362346235623662376238623962406241624262436244624562466247624862496250625162526253625462556256625762586259626062616262626362646265626662676268626962706271627262736274627562766277627862796280628162826283628462856286628762886289629062916292629362946295629662976298629963006301630263036304630563066307630863096310631163126313631463156316631763186319632063216322632363246325632663276328632963306331633263336334633563366337633863396340634163426343634463456346634763486349635063516352635363546355635663576358635963606361636263636364636563666367636863696370637163726373637463756376637763786379638063816382638363846385638663876388638963906391639263936394639563966397639863996400640164026403640464056406640764086409641064116412641364146415641664176418641964206421642264236424642564266427642864296430643164326433643464356436643764386439644064416442644364446445644664476448644964506451645264536454645564566457645864596460646164626463646464656466646764686469647064716472647364746475647664776478647964806481648264836484648564866487648864896490649164926493649464956496649764986499650065016502650365046505650665076508650965106511651265136514651565166517651865196520652165226523652465256526652765286529653065316532653365346535653665376538653965406541654265436544654565466547654865496550655165526553655465556556655765586559656065616562656365646565656665676568656965706571657265736574657565766577657865796580658165826583658465856586658765886589659065916592659365946595659665976598659966006601660266036604660566066607660866096610661166126613661466156616661766186619662066216622662366246625662666276628662966306631663266336634663566366637663866396640664166426643664466456646664766486649665066516652665366546655665666576658665966606661666266636664666566666667666866696670667166726673667466756676667766786679668066816682668366846685668666876688668966906691669266936694669566966697669866996700670167026703670467056706670767086709671067116712671367146715671667176718671967206721672267236724672567266727672867296730673167326733673467356736673767386739674067416742674367446745674667476748674967506751675267536754675567566757675867596760676167626763676467656766676767686769677067716772677367746775677667776778677967806781678267836784678567866787678867896790679167926793679467956796679767986799680068016802680368046805680668076808680968106811681268136814681568166817681868196820682168226823682468256826682768286829683068316832683368346835683668376838683968406841684268436844684568466847684868496850685168526853685468556856685768586859686068616862686368646865686668676868686968706871687268736874687568766877687868796880688168826883688468856886688768886889689068916892689368946895689668976898689969006901690269036904690569066907690869096910691169126913691469156916691769186919692069216922692369246925692669276928692969306931693269336934693569366937693869396940694169426943694469456946694769486949695069516952695369546955695669576958695969606961696269636964696569666967696869696970697169726973697469756976697769786979698069816982698369846985698669876988698969906991699269936994699569966997699869997000700170027003700470057006700770087009701070117012701370147015701670177018701970207021702270237024702570267027702870297030703170327033703470357036703770387039704070417042704370447045704670477048704970507051705270537054705570567057705870597060706170627063706470657066706770687069707070717072707370747075707670777078707970807081708270837084708570867087708870897090709170927093709470957096709770987099710071017102710371047105710671077108710971107111711271137114711571167117711871197120712171227123712471257126712771287129713071317132713371347135713671377138713971407141714271437144714571467147714871497150715171527153715471557156715771587159716071617162716371647165716671677168716971707171717271737174717571767177717871797180718171827183718471857186718771887189719071917192719371947195719671977198719972007201720272037204720572067207720872097210721172127213721472157216721772187219722072217222722372247225722672277228722972307231723272337234723572367237723872397240724172427243724472457246724772487249725072517252725372547255725672577258725972607261726272637264726572667267726872697270727172727273727472757276727772787279728072817282728372847285728672877288728972907291729272937294729572967297729872997300730173027303730473057306730773087309731073117312731373147315731673177318731973207321732273237324732573267327732873297330733173327333733473357336733773387339734073417342734373447345734673477348734973507351735273537354735573567357735873597360736173627363736473657366736773687369737073717372737373747375737673777378737973807381738273837384738573867387738873897390739173927393739473957396739773987399740074017402740374047405740674077408740974107411741274137414741574167417741874197420742174227423742474257426742774287429743074317432743374347435743674377438743974407441744274437444744574467447744874497450745174527453745474557456745774587459746074617462746374647465746674677468746974707471747274737474747574767477747874797480748174827483748474857486748774887489749074917492749374947495749674977498749975007501750275037504750575067507750875097510751175127513751475157516751775187519752075217522752375247525752675277528752975307531753275337534753575367537753875397540754175427543754475457546754775487549755075517552755375547555755675577558755975607561756275637564756575667567756875697570757175727573757475757576757775787579758075817582758375847585758675877588758975907591759275937594759575967597759875997600760176027603760476057606760776087609761076117612761376147615761676177618761976207621762276237624762576267627762876297630763176327633763476357636763776387639764076417642764376447645764676477648764976507651765276537654765576567657765876597660766176627663766476657666766776687669767076717672767376747675767676777678767976807681768276837684768576867687768876897690769176927693769476957696769776987699770077017702770377047705770677077708770977107711771277137714771577167717771877197720772177227723772477257726772777287729773077317732773377347735773677377738773977407741774277437744774577467747774877497750775177527753775477557756775777587759776077617762776377647765776677677768776977707771777277737774777577767777777877797780778177827783778477857786778777887789779077917792779377947795779677977798779978007801780278037804780578067807780878097810781178127813781478157816781778187819782078217822782378247825782678277828782978307831783278337834783578367837783878397840784178427843784478457846784778487849785078517852785378547855785678577858785978607861786278637864786578667867786878697870787178727873787478757876787778787879788078817882788378847885788678877888788978907891789278937894789578967897789878997900790179027903790479057906790779087909791079117912791379147915791679177918791979207921792279237924792579267927792879297930793179327933793479357936793779387939794079417942794379447945794679477948794979507951795279537954795579567957795879597960796179627963796479657966796779687969797079717972797379747975797679777978797979807981798279837984798579867987798879897990799179927993799479957996799779987999800080018002800380048005800680078008800980108011801280138014801580168017801880198020802180228023802480258026802780288029803080318032803380348035803680378038803980408041804280438044804580468047804880498050805180528053805480558056805780588059806080618062806380648065806680678068806980708071807280738074807580768077807880798080808180828083808480858086808780888089809080918092809380948095809680978098809981008101810281038104810581068107810881098110811181128113811481158116811781188119812081218122812381248125812681278128812981308131813281338134813581368137813881398140814181428143814481458146814781488149815081518152815381548155815681578158815981608161816281638164816581668167816881698170817181728173817481758176817781788179818081818182818381848185818681878188818981908191819281938194819581968197819881998200820182028203820482058206820782088209821082118212821382148215821682178218821982208221822282238224822582268227822882298230823182328233823482358236823782388239824082418242824382448245824682478248824982508251825282538254825582568257825882598260826182628263826482658266826782688269827082718272827382748275827682778278827982808281828282838284828582868287828882898290829182928293829482958296829782988299830083018302830383048305830683078308830983108311831283138314831583168317831883198320832183228323832483258326832783288329833083318332833383348335833683378338833983408341834283438344834583468347834883498350835183528353835483558356835783588359836083618362836383648365836683678368836983708371837283738374837583768377837883798380838183828383838483858386838783888389839083918392839383948395839683978398839984008401840284038404840584068407840884098410841184128413841484158416841784188419842084218422842384248425842684278428842984308431843284338434843584368437843884398440844184428443844484458446844784488449845084518452845384548455845684578458845984608461846284638464846584668467846884698470847184728473847484758476847784788479848084818482848384848485848684878488848984908491849284938494849584968497849884998500850185028503850485058506850785088509851085118512851385148515851685178518851985208521852285238524852585268527852885298530853185328533853485358536853785388539854085418542854385448545854685478548854985508551855285538554855585568557855885598560856185628563856485658566856785688569857085718572857385748575857685778578857985808581858285838584858585868587858885898590859185928593859485958596859785988599860086018602860386048605860686078608860986108611861286138614861586168617861886198620862186228623862486258626862786288629863086318632863386348635863686378638863986408641864286438644864586468647864886498650865186528653865486558656865786588659866086618662866386648665866686678668866986708671867286738674867586768677867886798680868186828683868486858686868786888689869086918692869386948695869686978698869987008701870287038704870587068707870887098710871187128713871487158716871787188719872087218722872387248725872687278728872987308731873287338734873587368737873887398740874187428743874487458746874787488749875087518752875387548755875687578758875987608761876287638764876587668767876887698770877187728773877487758776877787788779878087818782878387848785878687878788878987908791879287938794879587968797879887998800880188028803880488058806880788088809881088118812881388148815881688178818881988208821882288238824882588268827882888298830883188328833883488358836883788388839884088418842884388448845884688478848884988508851885288538854885588568857885888598860886188628863886488658866886788688869887088718872887388748875887688778878887988808881888288838884888588868887888888898890889188928893889488958896889788988899890089018902890389048905890689078908890989108911891289138914891589168917891889198920892189228923892489258926892789288929893089318932893389348935893689378938893989408941894289438944894589468947894889498950895189528953895489558956895789588959896089618962896389648965896689678968896989708971897289738974897589768977897889798980898189828983898489858986898789888989899089918992899389948995899689978998899990009001900290039004900590069007900890099010901190129013901490159016901790189019902090219022902390249025902690279028902990309031903290339034903590369037903890399040904190429043904490459046904790489049905090519052905390549055905690579058905990609061906290639064906590669067906890699070907190729073907490759076907790789079908090819082908390849085908690879088908990909091909290939094909590969097909890999100910191029103910491059106910791089109911091119112911391149115911691179118911991209121912291239124912591269127912891299130913191329133913491359136913791389139914091419142914391449145914691479148914991509151915291539154915591569157915891599160916191629163916491659166916791689169917091719172917391749175917691779178917991809181918291839184918591869187918891899190919191929193919491959196919791989199920092019202920392049205920692079208920992109211921292139214921592169217921892199220922192229223922492259226922792289229923092319232923392349235923692379238923992409241924292439244924592469247924892499250925192529253925492559256925792589259926092619262926392649265926692679268926992709271927292739274927592769277927892799280928192829283928492859286928792889289929092919292929392949295929692979298929993009301930293039304930593069307930893099310931193129313931493159316931793189319932093219322932393249325932693279328932993309331933293339334933593369337933893399340934193429343934493459346934793489349935093519352935393549355935693579358935993609361936293639364936593669367936893699370937193729373937493759376937793789379938093819382938393849385938693879388938993909391939293939394939593969397939893999400940194029403940494059406940794089409941094119412941394149415941694179418941994209421942294239424942594269427942894299430943194329433943494359436943794389439944094419442944394449445944694479448944994509451945294539454945594569457945894599460946194629463946494659466946794689469947094719472947394749475947694779478947994809481948294839484948594869487948894899490949194929493949494959496949794989499950095019502950395049505950695079508950995109511951295139514951595169517951895199520952195229523952495259526952795289529953095319532953395349535953695379538953995409541954295439544954595469547954895499550955195529553955495559556955795589559956095619562956395649565956695679568956995709571957295739574957595769577957895799580958195829583958495859586958795889589959095919592959395949595959695979598959996009601960296039604960596069607960896099610961196129613961496159616961796189619962096219622962396249625962696279628962996309631963296339634963596369637963896399640964196429643964496459646964796489649965096519652965396549655965696579658965996609661966296639664966596669667966896699670967196729673967496759676967796789679968096819682968396849685968696879688968996909691969296939694969596969697969896999700970197029703970497059706970797089709971097119712971397149715971697179718971997209721972297239724972597269727972897299730973197329733973497359736973797389739974097419742974397449745974697479748974997509751975297539754975597569757975897599760976197629763976497659766976797689769977097719772977397749775977697779778977997809781978297839784978597869787978897899790979197929793979497959796979797989799980098019802980398049805980698079808980998109811981298139814981598169817981898199820982198229823982498259826982798289829983098319832983398349835983698379838983998409841984298439844984598469847984898499850985198529853985498559856985798589859986098619862986398649865986698679868986998709871987298739874987598769877987898799880988198829883988498859886988798889889989098919892989398949895989698979898989999009901990299039904990599069907990899099910991199129913991499159916991799189919992099219922992399249925992699279928992999309931993299339934993599369937993899399940994199429943994499459946994799489949995099519952995399549955995699579958995999609961996299639964996599669967996899699970997199729973997499759976997799789979998099819982998399849985998699879988998999909991999299939994999599969997999899991000010001100021000310004100051000610007100081000910010100111001210013100141001510016100171001810019100201002110022100231002410025100261002710028100291003010031100321003310034100351003610037100381003910040100411004210043100441004510046100471004810049100501005110052100531005410055100561005710058100591006010061100621006310064100651006610067100681006910070100711007210073100741007510076100771007810079100801008110082100831008410085100861008710088100891009010091100921009310094100951009610097100981009910100101011010210103101041010510106101071010810109101101011110112101131011410115101161011710118101191012010121101221012310124101251012610127101281012910130101311013210133101341013510136101371013810139101401014110142101431014410145101461014710148101491015010151101521015310154101551015610157101581015910160101611016210163101641016510166101671016810169101701017110172101731017410175101761017710178101791018010181101821018310184101851018610187101881018910190101911019210193101941019510196101971019810199102001020110202102031020410205102061020710208102091021010211102121021310214102151021610217102181021910220102211022210223102241022510226102271022810229102301023110232102331023410235102361023710238102391024010241102421024310244102451024610247102481024910250102511025210253102541025510256102571025810259102601026110262102631026410265102661026710268102691027010271102721027310274102751027610277102781027910280102811028210283102841028510286102871028810289102901029110292102931029410295102961029710298102991030010301103021030310304103051030610307103081030910310103111031210313103141031510316103171031810319103201032110322103231032410325103261032710328103291033010331103321033310334103351033610337103381033910340103411034210343103441034510346103471034810349103501035110352103531035410355103561035710358103591036010361103621036310364103651036610367103681036910370103711037210373103741037510376103771037810379103801038110382103831038410385103861038710388103891039010391103921039310394103951039610397103981039910400104011040210403104041040510406104071040810409104101041110412104131041410415104161041710418104191042010421104221042310424104251042610427104281042910430104311043210433104341043510436104371043810439104401044110442104431044410445104461044710448104491045010451104521045310454104551045610457104581045910460104611046210463104641046510466
  1. (function() {/**
  2. * almond 0.2.5 Copyright (c) 2011-2012, The Dojo Foundation All Rights Reserved.
  3. * Available via the MIT or new BSD license.
  4. * see: http://github.com/jrburke/almond for details
  5. */
  6. //Going sloppy to avoid 'use strict' string cost, but strict practices should
  7. //be followed.
  8. /*jslint sloppy: true */
  9. /*global setTimeout: false */
  10. var requirejs, require, define;
  11. (function (undef) {
  12. var main, req, makeMap, handlers,
  13. defined = {},
  14. waiting = {},
  15. config = {},
  16. defining = {},
  17. hasOwn = Object.prototype.hasOwnProperty,
  18. aps = [].slice;
  19. function hasProp(obj, prop) {
  20. return hasOwn.call(obj, prop);
  21. }
  22. /**
  23. * Given a relative module name, like ./something, normalize it to
  24. * a real name that can be mapped to a path.
  25. * @param {String} name the relative name
  26. * @param {String} baseName a real name that the name arg is relative
  27. * to.
  28. * @returns {String} normalized name
  29. */
  30. function normalize(name, baseName) {
  31. var nameParts, nameSegment, mapValue, foundMap,
  32. foundI, foundStarMap, starI, i, j, part,
  33. baseParts = baseName && baseName.split("/"),
  34. map = config.map,
  35. starMap = (map && map['*']) || {};
  36. //Adjust any relative paths.
  37. if (name && name.charAt(0) === ".") {
  38. //If have a base name, try to normalize against it,
  39. //otherwise, assume it is a top-level require that will
  40. //be relative to baseUrl in the end.
  41. if (baseName) {
  42. //Convert baseName to array, and lop off the last part,
  43. //so that . matches that "directory" and not name of the baseName's
  44. //module. For instance, baseName of "one/two/three", maps to
  45. //"one/two/three.js", but we want the directory, "one/two" for
  46. //this normalization.
  47. baseParts = baseParts.slice(0, baseParts.length - 1);
  48. name = baseParts.concat(name.split("/"));
  49. //start trimDots
  50. for (i = 0; i < name.length; i += 1) {
  51. part = name[i];
  52. if (part === ".") {
  53. name.splice(i, 1);
  54. i -= 1;
  55. } else if (part === "..") {
  56. if (i === 1 && (name[2] === '..' || name[0] === '..')) {
  57. //End of the line. Keep at least one non-dot
  58. //path segment at the front so it can be mapped
  59. //correctly to disk. Otherwise, there is likely
  60. //no path mapping for a path starting with '..'.
  61. //This can still fail, but catches the most reasonable
  62. //uses of ..
  63. break;
  64. } else if (i > 0) {
  65. name.splice(i - 1, 2);
  66. i -= 2;
  67. }
  68. }
  69. }
  70. //end trimDots
  71. name = name.join("/");
  72. } else if (name.indexOf('./') === 0) {
  73. // No baseName, so this is ID is resolved relative
  74. // to baseUrl, pull off the leading dot.
  75. name = name.substring(2);
  76. }
  77. }
  78. //Apply map config if available.
  79. if ((baseParts || starMap) && map) {
  80. nameParts = name.split('/');
  81. for (i = nameParts.length; i > 0; i -= 1) {
  82. nameSegment = nameParts.slice(0, i).join("/");
  83. if (baseParts) {
  84. //Find the longest baseName segment match in the config.
  85. //So, do joins on the biggest to smallest lengths of baseParts.
  86. for (j = baseParts.length; j > 0; j -= 1) {
  87. mapValue = map[baseParts.slice(0, j).join('/')];
  88. //baseName segment has config, find if it has one for
  89. //this name.
  90. if (mapValue) {
  91. mapValue = mapValue[nameSegment];
  92. if (mapValue) {
  93. //Match, update name to the new value.
  94. foundMap = mapValue;
  95. foundI = i;
  96. break;
  97. }
  98. }
  99. }
  100. }
  101. if (foundMap) {
  102. break;
  103. }
  104. //Check for a star map match, but just hold on to it,
  105. //if there is a shorter segment match later in a matching
  106. //config, then favor over this star map.
  107. if (!foundStarMap && starMap && starMap[nameSegment]) {
  108. foundStarMap = starMap[nameSegment];
  109. starI = i;
  110. }
  111. }
  112. if (!foundMap && foundStarMap) {
  113. foundMap = foundStarMap;
  114. foundI = starI;
  115. }
  116. if (foundMap) {
  117. nameParts.splice(0, foundI, foundMap);
  118. name = nameParts.join('/');
  119. }
  120. }
  121. return name;
  122. }
  123. function makeRequire(relName, forceSync) {
  124. return function () {
  125. //A version of a require function that passes a moduleName
  126. //value for items that may need to
  127. //look up paths relative to the moduleName
  128. return req.apply(undef, aps.call(arguments, 0).concat([relName, forceSync]));
  129. };
  130. }
  131. function makeNormalize(relName) {
  132. return function (name) {
  133. return normalize(name, relName);
  134. };
  135. }
  136. function makeLoad(depName) {
  137. return function (value) {
  138. defined[depName] = value;
  139. };
  140. }
  141. function callDep(name) {
  142. if (hasProp(waiting, name)) {
  143. var args = waiting[name];
  144. delete waiting[name];
  145. defining[name] = true;
  146. main.apply(undef, args);
  147. }
  148. if (!hasProp(defined, name) && !hasProp(defining, name)) {
  149. throw new Error('No ' + name);
  150. }
  151. return defined[name];
  152. }
  153. //Turns a plugin!resource to [plugin, resource]
  154. //with the plugin being undefined if the name
  155. //did not have a plugin prefix.
  156. function splitPrefix(name) {
  157. var prefix,
  158. index = name ? name.indexOf('!') : -1;
  159. if (index > -1) {
  160. prefix = name.substring(0, index);
  161. name = name.substring(index + 1, name.length);
  162. }
  163. return [prefix, name];
  164. }
  165. /**
  166. * Makes a name map, normalizing the name, and using a plugin
  167. * for normalization if necessary. Grabs a ref to plugin
  168. * too, as an optimization.
  169. */
  170. makeMap = function (name, relName) {
  171. var plugin,
  172. parts = splitPrefix(name),
  173. prefix = parts[0];
  174. name = parts[1];
  175. if (prefix) {
  176. prefix = normalize(prefix, relName);
  177. plugin = callDep(prefix);
  178. }
  179. //Normalize according
  180. if (prefix) {
  181. if (plugin && plugin.normalize) {
  182. name = plugin.normalize(name, makeNormalize(relName));
  183. } else {
  184. name = normalize(name, relName);
  185. }
  186. } else {
  187. name = normalize(name, relName);
  188. parts = splitPrefix(name);
  189. prefix = parts[0];
  190. name = parts[1];
  191. if (prefix) {
  192. plugin = callDep(prefix);
  193. }
  194. }
  195. //Using ridiculous property names for space reasons
  196. return {
  197. f: prefix ? prefix + '!' + name : name, //fullName
  198. n: name,
  199. pr: prefix,
  200. p: plugin
  201. };
  202. };
  203. function makeConfig(name) {
  204. return function () {
  205. return (config && config.config && config.config[name]) || {};
  206. };
  207. }
  208. handlers = {
  209. require: function (name) {
  210. return makeRequire(name);
  211. },
  212. exports: function (name) {
  213. var e = defined[name];
  214. if (typeof e !== 'undefined') {
  215. return e;
  216. } else {
  217. return (defined[name] = {});
  218. }
  219. },
  220. module: function (name) {
  221. return {
  222. id: name,
  223. uri: '',
  224. exports: defined[name],
  225. config: makeConfig(name)
  226. };
  227. }
  228. };
  229. main = function (name, deps, callback, relName) {
  230. var cjsModule, depName, ret, map, i,
  231. args = [],
  232. usingExports;
  233. //Use name if no relName
  234. relName = relName || name;
  235. //Call the callback to define the module, if necessary.
  236. if (typeof callback === 'function') {
  237. //Pull out the defined dependencies and pass the ordered
  238. //values to the callback.
  239. //Default to [require, exports, module] if no deps
  240. deps = !deps.length && callback.length ? ['require', 'exports', 'module'] : deps;
  241. for (i = 0; i < deps.length; i += 1) {
  242. map = makeMap(deps[i], relName);
  243. depName = map.f;
  244. //Fast path CommonJS standard dependencies.
  245. if (depName === "require") {
  246. args[i] = handlers.require(name);
  247. } else if (depName === "exports") {
  248. //CommonJS module spec 1.1
  249. args[i] = handlers.exports(name);
  250. usingExports = true;
  251. } else if (depName === "module") {
  252. //CommonJS module spec 1.1
  253. cjsModule = args[i] = handlers.module(name);
  254. } else if (hasProp(defined, depName) ||
  255. hasProp(waiting, depName) ||
  256. hasProp(defining, depName)) {
  257. args[i] = callDep(depName);
  258. } else if (map.p) {
  259. map.p.load(map.n, makeRequire(relName, true), makeLoad(depName), {});
  260. args[i] = defined[depName];
  261. } else {
  262. throw new Error(name + ' missing ' + depName);
  263. }
  264. }
  265. ret = callback.apply(defined[name], args);
  266. if (name) {
  267. //If setting exports via "module" is in play,
  268. //favor that over return value and exports. After that,
  269. //favor a non-undefined return value over exports use.
  270. if (cjsModule && cjsModule.exports !== undef &&
  271. cjsModule.exports !== defined[name]) {
  272. defined[name] = cjsModule.exports;
  273. } else if (ret !== undef || !usingExports) {
  274. //Use the return value from the function.
  275. defined[name] = ret;
  276. }
  277. }
  278. } else if (name) {
  279. //May just be an object definition for the module. Only
  280. //worry about defining if have a module name.
  281. defined[name] = callback;
  282. }
  283. };
  284. requirejs = require = req = function (deps, callback, relName, forceSync, alt) {
  285. if (typeof deps === "string") {
  286. if (handlers[deps]) {
  287. //callback in this case is really relName
  288. return handlers[deps](callback);
  289. }
  290. //Just return the module wanted. In this scenario, the
  291. //deps arg is the module name, and second arg (if passed)
  292. //is just the relName.
  293. //Normalize module name, if it contains . or ..
  294. return callDep(makeMap(deps, callback).f);
  295. } else if (!deps.splice) {
  296. //deps is a config object, not an array.
  297. config = deps;
  298. if (callback.splice) {
  299. //callback is an array, which means it is a dependency list.
  300. //Adjust args if there are dependencies
  301. deps = callback;
  302. callback = relName;
  303. relName = null;
  304. } else {
  305. deps = undef;
  306. }
  307. }
  308. //Support require(['a'])
  309. callback = callback || function () {};
  310. //If relName is a function, it is an errback handler,
  311. //so remove it.
  312. if (typeof relName === 'function') {
  313. relName = forceSync;
  314. forceSync = alt;
  315. }
  316. //Simulate async callback;
  317. if (forceSync) {
  318. main(undef, deps, callback, relName);
  319. } else {
  320. //Using a non-zero value because of concern for what old browsers
  321. //do, and latest browsers "upgrade" to 4 if lower value is used:
  322. //http://www.whatwg.org/specs/web-apps/current-work/multipage/timers.html#dom-windowtimers-settimeout:
  323. //If want a value immediately, use require('id') instead -- something
  324. //that works in almond on the global level, but not guaranteed and
  325. //unlikely to work in other AMD implementations.
  326. setTimeout(function () {
  327. main(undef, deps, callback, relName);
  328. }, 4);
  329. }
  330. return req;
  331. };
  332. /**
  333. * Just drops the config on the floor, but returns req in case
  334. * the config return value is used.
  335. */
  336. req.config = function (cfg) {
  337. config = cfg;
  338. if (config.deps) {
  339. req(config.deps, config.callback);
  340. }
  341. return req;
  342. };
  343. define = function (name, deps, callback) {
  344. //This module may not have dependencies
  345. if (!deps.splice) {
  346. //deps is not an array, so probably means
  347. //an object literal or factory function for
  348. //the value. Adjust args.
  349. callback = deps;
  350. deps = [];
  351. }
  352. if (!hasProp(defined, name) && !hasProp(waiting, name)) {
  353. waiting[name] = [name, deps, callback];
  354. }
  355. };
  356. define.amd = {
  357. jQuery: true
  358. };
  359. }());
  360. define("libs/almond", function(){});
  361. /*! jQuery v1.11.1 | (c) 2005, 2014 jQuery Foundation, Inc. | jquery.org/license */
  362. !function(a,b){"object"==typeof module&&"object"==typeof module.exports?module.exports=a.document?b(a,!0):function(a){if(!a.document)throw new Error("jQuery requires a window with a document");return b(a)}:b(a)}("undefined"!=typeof window?window:this,function(a,b){var c=[],d=c.slice,e=c.concat,f=c.push,g=c.indexOf,h={},i=h.toString,j=h.hasOwnProperty,k={},l="1.11.1",m=function(a,b){return new m.fn.init(a,b)},n=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,o=/^-ms-/,p=/-([\da-z])/gi,q=function(a,b){return b.toUpperCase()};m.fn=m.prototype={jquery:l,constructor:m,selector:"",length:0,toArray:function(){return d.call(this)},get:function(a){return null!=a?0>a?this[a+this.length]:this[a]:d.call(this)},pushStack:function(a){var b=m.merge(this.constructor(),a);return b.prevObject=this,b.context=this.context,b},each:function(a,b){return m.each(this,a,b)},map:function(a){return this.pushStack(m.map(this,function(b,c){return a.call(b,c,b)}))},slice:function(){return this.pushStack(d.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(a){var b=this.length,c=+a+(0>a?b:0);return this.pushStack(c>=0&&b>c?[this[c]]:[])},end:function(){return this.prevObject||this.constructor(null)},push:f,sort:c.sort,splice:c.splice},m.extend=m.fn.extend=function(){var a,b,c,d,e,f,g=arguments[0]||{},h=1,i=arguments.length,j=!1;for("boolean"==typeof g&&(j=g,g=arguments[h]||{},h++),"object"==typeof g||m.isFunction(g)||(g={}),h===i&&(g=this,h--);i>h;h++)if(null!=(e=arguments[h]))for(d in e)a=g[d],c=e[d],g!==c&&(j&&c&&(m.isPlainObject(c)||(b=m.isArray(c)))?(b?(b=!1,f=a&&m.isArray(a)?a:[]):f=a&&m.isPlainObject(a)?a:{},g[d]=m.extend(j,f,c)):void 0!==c&&(g[d]=c));return g},m.extend({expando:"jQuery"+(l+Math.random()).replace(/\D/g,""),isReady:!0,error:function(a){throw new Error(a)},noop:function(){},isFunction:function(a){return"function"===m.type(a)},isArray:Array.isArray||function(a){return"array"===m.type(a)},isWindow:function(a){return null!=a&&a==a.window},isNumeric:function(a){return!m.isArray(a)&&a-parseFloat(a)>=0},isEmptyObject:function(a){var b;for(b in a)return!1;return!0},isPlainObject:function(a){var b;if(!a||"object"!==m.type(a)||a.nodeType||m.isWindow(a))return!1;try{if(a.constructor&&!j.call(a,"constructor")&&!j.call(a.constructor.prototype,"isPrototypeOf"))return!1}catch(c){return!1}if(k.ownLast)for(b in a)return j.call(a,b);for(b in a);return void 0===b||j.call(a,b)},type:function(a){return null==a?a+"":"object"==typeof a||"function"==typeof a?h[i.call(a)]||"object":typeof a},globalEval:function(b){b&&m.trim(b)&&(a.execScript||function(b){a.eval.call(a,b)})(b)},camelCase:function(a){return a.replace(o,"ms-").replace(p,q)},nodeName:function(a,b){return a.nodeName&&a.nodeName.toLowerCase()===b.toLowerCase()},each:function(a,b,c){var d,e=0,f=a.length,g=r(a);if(c){if(g){for(;f>e;e++)if(d=b.apply(a[e],c),d===!1)break}else for(e in a)if(d=b.apply(a[e],c),d===!1)break}else if(g){for(;f>e;e++)if(d=b.call(a[e],e,a[e]),d===!1)break}else for(e in a)if(d=b.call(a[e],e,a[e]),d===!1)break;return a},trim:function(a){return null==a?"":(a+"").replace(n,"")},makeArray:function(a,b){var c=b||[];return null!=a&&(r(Object(a))?m.merge(c,"string"==typeof a?[a]:a):f.call(c,a)),c},inArray:function(a,b,c){var d;if(b){if(g)return g.call(b,a,c);for(d=b.length,c=c?0>c?Math.max(0,d+c):c:0;d>c;c++)if(c in b&&b[c]===a)return c}return-1},merge:function(a,b){var c=+b.length,d=0,e=a.length;while(c>d)a[e++]=b[d++];if(c!==c)while(void 0!==b[d])a[e++]=b[d++];return a.length=e,a},grep:function(a,b,c){for(var d,e=[],f=0,g=a.length,h=!c;g>f;f++)d=!b(a[f],f),d!==h&&e.push(a[f]);return e},map:function(a,b,c){var d,f=0,g=a.length,h=r(a),i=[];if(h)for(;g>f;f++)d=b(a[f],f,c),null!=d&&i.push(d);else for(f in a)d=b(a[f],f,c),null!=d&&i.push(d);return e.apply([],i)},guid:1,proxy:function(a,b){var c,e,f;return"string"==typeof b&&(f=a[b],b=a,a=f),m.isFunction(a)?(c=d.call(arguments,2),e=function(){return a.apply(b||this,c.concat(d.call(arguments)))},e.guid=a.guid=a.guid||m.guid++,e):void 0},now:function(){return+new Date},support:k}),m.each("Boolean Number String Function Array Date RegExp Object Error".split(" "),function(a,b){h["[object "+b+"]"]=b.toLowerCase()});function r(a){var b=a.length,c=m.type(a);return"function"===c||m.isWindow(a)?!1:1===a.nodeType&&b?!0:"array"===c||0===b||"number"==typeof b&&b>0&&b-1 in a}var s=function(a){var b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u="sizzle"+-new Date,v=a.document,w=0,x=0,y=gb(),z=gb(),A=gb(),B=function(a,b){return a===b&&(l=!0),0},C="undefined",D=1<<31,E={}.hasOwnProperty,F=[],G=F.pop,H=F.push,I=F.push,J=F.slice,K=F.indexOf||function(a){for(var b=0,c=this.length;c>b;b++)if(this[b]===a)return b;return-1},L="checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped",M="[\\x20\\t\\r\\n\\f]",N="(?:\\\\.|[\\w-]|[^\\x00-\\xa0])+",O=N.replace("w","w#"),P="\\["+M+"*("+N+")(?:"+M+"*([*^$|!~]?=)"+M+"*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|("+O+"))|)"+M+"*\\]",Q=":("+N+")(?:\\((('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|((?:\\\\.|[^\\\\()[\\]]|"+P+")*)|.*)\\)|)",R=new RegExp("^"+M+"+|((?:^|[^\\\\])(?:\\\\.)*)"+M+"+$","g"),S=new RegExp("^"+M+"*,"+M+"*"),T=new RegExp("^"+M+"*([>+~]|"+M+")"+M+"*"),U=new RegExp("="+M+"*([^\\]'\"]*?)"+M+"*\\]","g"),V=new RegExp(Q),W=new RegExp("^"+O+"$"),X={ID:new RegExp("^#("+N+")"),CLASS:new RegExp("^\\.("+N+")"),TAG:new RegExp("^("+N.replace("w","w*")+")"),ATTR:new RegExp("^"+P),PSEUDO:new RegExp("^"+Q),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+M+"*(even|odd|(([+-]|)(\\d*)n|)"+M+"*(?:([+-]|)"+M+"*(\\d+)|))"+M+"*\\)|)","i"),bool:new RegExp("^(?:"+L+")$","i"),needsContext:new RegExp("^"+M+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+M+"*((?:-\\d)?\\d*)"+M+"*\\)|)(?=[^-]|$)","i")},Y=/^(?:input|select|textarea|button)$/i,Z=/^h\d$/i,$=/^[^{]+\{\s*\[native \w/,_=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,ab=/[+~]/,bb=/'|\\/g,cb=new RegExp("\\\\([\\da-f]{1,6}"+M+"?|("+M+")|.)","ig"),db=function(a,b,c){var d="0x"+b-65536;return d!==d||c?b:0>d?String.fromCharCode(d+65536):String.fromCharCode(d>>10|55296,1023&d|56320)};try{I.apply(F=J.call(v.childNodes),v.childNodes),F[v.childNodes.length].nodeType}catch(eb){I={apply:F.length?function(a,b){H.apply(a,J.call(b))}:function(a,b){var c=a.length,d=0;while(a[c++]=b[d++]);a.length=c-1}}}function fb(a,b,d,e){var f,h,j,k,l,o,r,s,w,x;if((b?b.ownerDocument||b:v)!==n&&m(b),b=b||n,d=d||[],!a||"string"!=typeof a)return d;if(1!==(k=b.nodeType)&&9!==k)return[];if(p&&!e){if(f=_.exec(a))if(j=f[1]){if(9===k){if(h=b.getElementById(j),!h||!h.parentNode)return d;if(h.id===j)return d.push(h),d}else if(b.ownerDocument&&(h=b.ownerDocument.getElementById(j))&&t(b,h)&&h.id===j)return d.push(h),d}else{if(f[2])return I.apply(d,b.getElementsByTagName(a)),d;if((j=f[3])&&c.getElementsByClassName&&b.getElementsByClassName)return I.apply(d,b.getElementsByClassName(j)),d}if(c.qsa&&(!q||!q.test(a))){if(s=r=u,w=b,x=9===k&&a,1===k&&"object"!==b.nodeName.toLowerCase()){o=g(a),(r=b.getAttribute("id"))?s=r.replace(bb,"\\$&"):b.setAttribute("id",s),s="[id='"+s+"'] ",l=o.length;while(l--)o[l]=s+qb(o[l]);w=ab.test(a)&&ob(b.parentNode)||b,x=o.join(",")}if(x)try{return I.apply(d,w.querySelectorAll(x)),d}catch(y){}finally{r||b.removeAttribute("id")}}}return i(a.replace(R,"$1"),b,d,e)}function gb(){var a=[];function b(c,e){return a.push(c+" ")>d.cacheLength&&delete b[a.shift()],b[c+" "]=e}return b}function hb(a){return a[u]=!0,a}function ib(a){var b=n.createElement("div");try{return!!a(b)}catch(c){return!1}finally{b.parentNode&&b.parentNode.removeChild(b),b=null}}function jb(a,b){var c=a.split("|"),e=a.length;while(e--)d.attrHandle[c[e]]=b}function kb(a,b){var c=b&&a,d=c&&1===a.nodeType&&1===b.nodeType&&(~b.sourceIndex||D)-(~a.sourceIndex||D);if(d)return d;if(c)while(c=c.nextSibling)if(c===b)return-1;return a?1:-1}function lb(a){return function(b){var c=b.nodeName.toLowerCase();return"input"===c&&b.type===a}}function mb(a){return function(b){var c=b.nodeName.toLowerCase();return("input"===c||"button"===c)&&b.type===a}}function nb(a){return hb(function(b){return b=+b,hb(function(c,d){var e,f=a([],c.length,b),g=f.length;while(g--)c[e=f[g]]&&(c[e]=!(d[e]=c[e]))})})}function ob(a){return a&&typeof a.getElementsByTagName!==C&&a}c=fb.support={},f=fb.isXML=function(a){var b=a&&(a.ownerDocument||a).documentElement;return b?"HTML"!==b.nodeName:!1},m=fb.setDocument=function(a){var b,e=a?a.ownerDocument||a:v,g=e.defaultView;return e!==n&&9===e.nodeType&&e.documentElement?(n=e,o=e.documentElement,p=!f(e),g&&g!==g.top&&(g.addEventListener?g.addEventListener("unload",function(){m()},!1):g.attachEvent&&g.attachEvent("onunload",function(){m()})),c.attributes=ib(function(a){return a.className="i",!a.getAttribute("className")}),c.getElementsByTagName=ib(function(a){return a.appendChild(e.createComment("")),!a.getElementsByTagName("*").length}),c.getElementsByClassName=$.test(e.getElementsByClassName)&&ib(function(a){return a.innerHTML="<div class='a'></div><div class='a i'></div>",a.firstChild.className="i",2===a.getElementsByClassName("i").length}),c.getById=ib(function(a){return o.appendChild(a).id=u,!e.getElementsByName||!e.getElementsByName(u).length}),c.getById?(d.find.ID=function(a,b){if(typeof b.getElementById!==C&&p){var c=b.getElementById(a);return c&&c.parentNode?[c]:[]}},d.filter.ID=function(a){var b=a.replace(cb,db);return function(a){return a.getAttribute("id")===b}}):(delete d.find.ID,d.filter.ID=function(a){var b=a.replace(cb,db);return function(a){var c=typeof a.getAttributeNode!==C&&a.getAttributeNode("id");return c&&c.value===b}}),d.find.TAG=c.getElementsByTagName?function(a,b){return typeof b.getElementsByTagName!==C?b.getElementsByTagName(a):void 0}:function(a,b){var c,d=[],e=0,f=b.getElementsByTagName(a);if("*"===a){while(c=f[e++])1===c.nodeType&&d.push(c);return d}return f},d.find.CLASS=c.getElementsByClassName&&function(a,b){return typeof b.getElementsByClassName!==C&&p?b.getElementsByClassName(a):void 0},r=[],q=[],(c.qsa=$.test(e.querySelectorAll))&&(ib(function(a){a.innerHTML="<select msallowclip=''><option selected=''></option></select>",a.querySelectorAll("[msallowclip^='']").length&&q.push("[*^$]="+M+"*(?:''|\"\")"),a.querySelectorAll("[selected]").length||q.push("\\["+M+"*(?:value|"+L+")"),a.querySelectorAll(":checked").length||q.push(":checked")}),ib(function(a){var b=e.createElement("input");b.setAttribute("type","hidden"),a.appendChild(b).setAttribute("name","D"),a.querySelectorAll("[name=d]").length&&q.push("name"+M+"*[*^$|!~]?="),a.querySelectorAll(":enabled").length||q.push(":enabled",":disabled"),a.querySelectorAll("*,:x"),q.push(",.*:")})),(c.matchesSelector=$.test(s=o.matches||o.webkitMatchesSelector||o.mozMatchesSelector||o.oMatchesSelector||o.msMatchesSelector))&&ib(function(a){c.disconnectedMatch=s.call(a,"div"),s.call(a,"[s!='']:x"),r.push("!=",Q)}),q=q.length&&new RegExp(q.join("|")),r=r.length&&new RegExp(r.join("|")),b=$.test(o.compareDocumentPosition),t=b||$.test(o.contains)?function(a,b){var c=9===a.nodeType?a.documentElement:a,d=b&&b.parentNode;return a===d||!(!d||1!==d.nodeType||!(c.contains?c.contains(d):a.compareDocumentPosition&&16&a.compareDocumentPosition(d)))}:function(a,b){if(b)while(b=b.parentNode)if(b===a)return!0;return!1},B=b?function(a,b){if(a===b)return l=!0,0;var d=!a.compareDocumentPosition-!b.compareDocumentPosition;return d?d:(d=(a.ownerDocument||a)===(b.ownerDocument||b)?a.compareDocumentPosition(b):1,1&d||!c.sortDetached&&b.compareDocumentPosition(a)===d?a===e||a.ownerDocument===v&&t(v,a)?-1:b===e||b.ownerDocument===v&&t(v,b)?1:k?K.call(k,a)-K.call(k,b):0:4&d?-1:1)}:function(a,b){if(a===b)return l=!0,0;var c,d=0,f=a.parentNode,g=b.parentNode,h=[a],i=[b];if(!f||!g)return a===e?-1:b===e?1:f?-1:g?1:k?K.call(k,a)-K.call(k,b):0;if(f===g)return kb(a,b);c=a;while(c=c.parentNode)h.unshift(c);c=b;while(c=c.parentNode)i.unshift(c);while(h[d]===i[d])d++;return d?kb(h[d],i[d]):h[d]===v?-1:i[d]===v?1:0},e):n},fb.matches=function(a,b){return fb(a,null,null,b)},fb.matchesSelector=function(a,b){if((a.ownerDocument||a)!==n&&m(a),b=b.replace(U,"='$1']"),!(!c.matchesSelector||!p||r&&r.test(b)||q&&q.test(b)))try{var d=s.call(a,b);if(d||c.disconnectedMatch||a.document&&11!==a.document.nodeType)return d}catch(e){}return fb(b,n,null,[a]).length>0},fb.contains=function(a,b){return(a.ownerDocument||a)!==n&&m(a),t(a,b)},fb.attr=function(a,b){(a.ownerDocument||a)!==n&&m(a);var e=d.attrHandle[b.toLowerCase()],f=e&&E.call(d.attrHandle,b.toLowerCase())?e(a,b,!p):void 0;return void 0!==f?f:c.attributes||!p?a.getAttribute(b):(f=a.getAttributeNode(b))&&f.specified?f.value:null},fb.error=function(a){throw new Error("Syntax error, unrecognized expression: "+a)},fb.uniqueSort=function(a){var b,d=[],e=0,f=0;if(l=!c.detectDuplicates,k=!c.sortStable&&a.slice(0),a.sort(B),l){while(b=a[f++])b===a[f]&&(e=d.push(f));while(e--)a.splice(d[e],1)}return k=null,a},e=fb.getText=function(a){var b,c="",d=0,f=a.nodeType;if(f){if(1===f||9===f||11===f){if("string"==typeof a.textContent)return a.textContent;for(a=a.firstChild;a;a=a.nextSibling)c+=e(a)}else if(3===f||4===f)return a.nodeValue}else while(b=a[d++])c+=e(b);return c},d=fb.selectors={cacheLength:50,createPseudo:hb,match:X,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(a){return a[1]=a[1].replace(cb,db),a[3]=(a[3]||a[4]||a[5]||"").replace(cb,db),"~="===a[2]&&(a[3]=" "+a[3]+" "),a.slice(0,4)},CHILD:function(a){return a[1]=a[1].toLowerCase(),"nth"===a[1].slice(0,3)?(a[3]||fb.error(a[0]),a[4]=+(a[4]?a[5]+(a[6]||1):2*("even"===a[3]||"odd"===a[3])),a[5]=+(a[7]+a[8]||"odd"===a[3])):a[3]&&fb.error(a[0]),a},PSEUDO:function(a){var b,c=!a[6]&&a[2];return X.CHILD.test(a[0])?null:(a[3]?a[2]=a[4]||a[5]||"":c&&V.test(c)&&(b=g(c,!0))&&(b=c.indexOf(")",c.length-b)-c.length)&&(a[0]=a[0].slice(0,b),a[2]=c.slice(0,b)),a.slice(0,3))}},filter:{TAG:function(a){var b=a.replace(cb,db).toLowerCase();return"*"===a?function(){return!0}:function(a){return a.nodeName&&a.nodeName.toLowerCase()===b}},CLASS:function(a){var b=y[a+" "];return b||(b=new RegExp("(^|"+M+")"+a+"("+M+"|$)"))&&y(a,function(a){return b.test("string"==typeof a.className&&a.className||typeof a.getAttribute!==C&&a.getAttribute("class")||"")})},ATTR:function(a,b,c){return function(d){var e=fb.attr(d,a);return null==e?"!="===b:b?(e+="","="===b?e===c:"!="===b?e!==c:"^="===b?c&&0===e.indexOf(c):"*="===b?c&&e.indexOf(c)>-1:"$="===b?c&&e.slice(-c.length)===c:"~="===b?(" "+e+" ").indexOf(c)>-1:"|="===b?e===c||e.slice(0,c.length+1)===c+"-":!1):!0}},CHILD:function(a,b,c,d,e){var f="nth"!==a.slice(0,3),g="last"!==a.slice(-4),h="of-type"===b;return 1===d&&0===e?function(a){return!!a.parentNode}:function(b,c,i){var j,k,l,m,n,o,p=f!==g?"nextSibling":"previousSibling",q=b.parentNode,r=h&&b.nodeName.toLowerCase(),s=!i&&!h;if(q){if(f){while(p){l=b;while(l=l[p])if(h?l.nodeName.toLowerCase()===r:1===l.nodeType)return!1;o=p="only"===a&&!o&&"nextSibling"}return!0}if(o=[g?q.firstChild:q.lastChild],g&&s){k=q[u]||(q[u]={}),j=k[a]||[],n=j[0]===w&&j[1],m=j[0]===w&&j[2],l=n&&q.childNodes[n];while(l=++n&&l&&l[p]||(m=n=0)||o.pop())if(1===l.nodeType&&++m&&l===b){k[a]=[w,n,m];break}}else if(s&&(j=(b[u]||(b[u]={}))[a])&&j[0]===w)m=j[1];else while(l=++n&&l&&l[p]||(m=n=0)||o.pop())if((h?l.nodeName.toLowerCase()===r:1===l.nodeType)&&++m&&(s&&((l[u]||(l[u]={}))[a]=[w,m]),l===b))break;return m-=e,m===d||m%d===0&&m/d>=0}}},PSEUDO:function(a,b){var c,e=d.pseudos[a]||d.setFilters[a.toLowerCase()]||fb.error("unsupported pseudo: "+a);return e[u]?e(b):e.length>1?(c=[a,a,"",b],d.setFilters.hasOwnProperty(a.toLowerCase())?hb(function(a,c){var d,f=e(a,b),g=f.length;while(g--)d=K.call(a,f[g]),a[d]=!(c[d]=f[g])}):function(a){return e(a,0,c)}):e}},pseudos:{not:hb(function(a){var b=[],c=[],d=h(a.replace(R,"$1"));return d[u]?hb(function(a,b,c,e){var f,g=d(a,null,e,[]),h=a.length;while(h--)(f=g[h])&&(a[h]=!(b[h]=f))}):function(a,e,f){return b[0]=a,d(b,null,f,c),!c.pop()}}),has:hb(function(a){return function(b){return fb(a,b).length>0}}),contains:hb(function(a){return function(b){return(b.textContent||b.innerText||e(b)).indexOf(a)>-1}}),lang:hb(function(a){return W.test(a||"")||fb.error("unsupported lang: "+a),a=a.replace(cb,db).toLowerCase(),function(b){var c;do if(c=p?b.lang:b.getAttribute("xml:lang")||b.getAttribute("lang"))return c=c.toLowerCase(),c===a||0===c.indexOf(a+"-");while((b=b.parentNode)&&1===b.nodeType);return!1}}),target:function(b){var c=a.location&&a.location.hash;return c&&c.slice(1)===b.id},root:function(a){return a===o},focus:function(a){return a===n.activeElement&&(!n.hasFocus||n.hasFocus())&&!!(a.type||a.href||~a.tabIndex)},enabled:function(a){return a.disabled===!1},disabled:function(a){return a.disabled===!0},checked:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&!!a.checked||"option"===b&&!!a.selected},selected:function(a){return a.parentNode&&a.parentNode.selectedIndex,a.selected===!0},empty:function(a){for(a=a.firstChild;a;a=a.nextSibling)if(a.nodeType<6)return!1;return!0},parent:function(a){return!d.pseudos.empty(a)},header:function(a){return Z.test(a.nodeName)},input:function(a){return Y.test(a.nodeName)},button:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&"button"===a.type||"button"===b},text:function(a){var b;return"input"===a.nodeName.toLowerCase()&&"text"===a.type&&(null==(b=a.getAttribute("type"))||"text"===b.toLowerCase())},first:nb(function(){return[0]}),last:nb(function(a,b){return[b-1]}),eq:nb(function(a,b,c){return[0>c?c+b:c]}),even:nb(function(a,b){for(var c=0;b>c;c+=2)a.push(c);return a}),odd:nb(function(a,b){for(var c=1;b>c;c+=2)a.push(c);return a}),lt:nb(function(a,b,c){for(var d=0>c?c+b:c;--d>=0;)a.push(d);return a}),gt:nb(function(a,b,c){for(var d=0>c?c+b:c;++d<b;)a.push(d);return a})}},d.pseudos.nth=d.pseudos.eq;for(b in{radio:!0,checkbox:!0,file:!0,password:!0,image:!0})d.pseudos[b]=lb(b);for(b in{submit:!0,reset:!0})d.pseudos[b]=mb(b);function pb(){}pb.prototype=d.filters=d.pseudos,d.setFilters=new pb,g=fb.tokenize=function(a,b){var c,e,f,g,h,i,j,k=z[a+" "];if(k)return b?0:k.slice(0);h=a,i=[],j=d.preFilter;while(h){(!c||(e=S.exec(h)))&&(e&&(h=h.slice(e[0].length)||h),i.push(f=[])),c=!1,(e=T.exec(h))&&(c=e.shift(),f.push({value:c,type:e[0].replace(R," ")}),h=h.slice(c.length));for(g in d.filter)!(e=X[g].exec(h))||j[g]&&!(e=j[g](e))||(c=e.shift(),f.push({value:c,type:g,matches:e}),h=h.slice(c.length));if(!c)break}return b?h.length:h?fb.error(a):z(a,i).slice(0)};function qb(a){for(var b=0,c=a.length,d="";c>b;b++)d+=a[b].value;return d}function rb(a,b,c){var d=b.dir,e=c&&"parentNode"===d,f=x++;return b.first?function(b,c,f){while(b=b[d])if(1===b.nodeType||e)return a(b,c,f)}:function(b,c,g){var h,i,j=[w,f];if(g){while(b=b[d])if((1===b.nodeType||e)&&a(b,c,g))return!0}else while(b=b[d])if(1===b.nodeType||e){if(i=b[u]||(b[u]={}),(h=i[d])&&h[0]===w&&h[1]===f)return j[2]=h[2];if(i[d]=j,j[2]=a(b,c,g))return!0}}}function sb(a){return a.length>1?function(b,c,d){var e=a.length;while(e--)if(!a[e](b,c,d))return!1;return!0}:a[0]}function tb(a,b,c){for(var d=0,e=b.length;e>d;d++)fb(a,b[d],c);return c}function ub(a,b,c,d,e){for(var f,g=[],h=0,i=a.length,j=null!=b;i>h;h++)(f=a[h])&&(!c||c(f,d,e))&&(g.push(f),j&&b.push(h));return g}function vb(a,b,c,d,e,f){return d&&!d[u]&&(d=vb(d)),e&&!e[u]&&(e=vb(e,f)),hb(function(f,g,h,i){var j,k,l,m=[],n=[],o=g.length,p=f||tb(b||"*",h.nodeType?[h]:h,[]),q=!a||!f&&b?p:ub(p,m,a,h,i),r=c?e||(f?a:o||d)?[]:g:q;if(c&&c(q,r,h,i),d){j=ub(r,n),d(j,[],h,i),k=j.length;while(k--)(l=j[k])&&(r[n[k]]=!(q[n[k]]=l))}if(f){if(e||a){if(e){j=[],k=r.length;while(k--)(l=r[k])&&j.push(q[k]=l);e(null,r=[],j,i)}k=r.length;while(k--)(l=r[k])&&(j=e?K.call(f,l):m[k])>-1&&(f[j]=!(g[j]=l))}}else r=ub(r===g?r.splice(o,r.length):r),e?e(null,g,r,i):I.apply(g,r)})}function wb(a){for(var b,c,e,f=a.length,g=d.relative[a[0].type],h=g||d.relative[" "],i=g?1:0,k=rb(function(a){return a===b},h,!0),l=rb(function(a){return K.call(b,a)>-1},h,!0),m=[function(a,c,d){return!g&&(d||c!==j)||((b=c).nodeType?k(a,c,d):l(a,c,d))}];f>i;i++)if(c=d.relative[a[i].type])m=[rb(sb(m),c)];else{if(c=d.filter[a[i].type].apply(null,a[i].matches),c[u]){for(e=++i;f>e;e++)if(d.relative[a[e].type])break;return vb(i>1&&sb(m),i>1&&qb(a.slice(0,i-1).concat({value:" "===a[i-2].type?"*":""})).replace(R,"$1"),c,e>i&&wb(a.slice(i,e)),f>e&&wb(a=a.slice(e)),f>e&&qb(a))}m.push(c)}return sb(m)}function xb(a,b){var c=b.length>0,e=a.length>0,f=function(f,g,h,i,k){var l,m,o,p=0,q="0",r=f&&[],s=[],t=j,u=f||e&&d.find.TAG("*",k),v=w+=null==t?1:Math.random()||.1,x=u.length;for(k&&(j=g!==n&&g);q!==x&&null!=(l=u[q]);q++){if(e&&l){m=0;while(o=a[m++])if(o(l,g,h)){i.push(l);break}k&&(w=v)}c&&((l=!o&&l)&&p--,f&&r.push(l))}if(p+=q,c&&q!==p){m=0;while(o=b[m++])o(r,s,g,h);if(f){if(p>0)while(q--)r[q]||s[q]||(s[q]=G.call(i));s=ub(s)}I.apply(i,s),k&&!f&&s.length>0&&p+b.length>1&&fb.uniqueSort(i)}return k&&(w=v,j=t),r};return c?hb(f):f}return h=fb.compile=function(a,b){var c,d=[],e=[],f=A[a+" "];if(!f){b||(b=g(a)),c=b.length;while(c--)f=wb(b[c]),f[u]?d.push(f):e.push(f);f=A(a,xb(e,d)),f.selector=a}return f},i=fb.select=function(a,b,e,f){var i,j,k,l,m,n="function"==typeof a&&a,o=!f&&g(a=n.selector||a);if(e=e||[],1===o.length){if(j=o[0]=o[0].slice(0),j.length>2&&"ID"===(k=j[0]).type&&c.getById&&9===b.nodeType&&p&&d.relative[j[1].type]){if(b=(d.find.ID(k.matches[0].replace(cb,db),b)||[])[0],!b)return e;n&&(b=b.parentNode),a=a.slice(j.shift().value.length)}i=X.needsContext.test(a)?0:j.length;while(i--){if(k=j[i],d.relative[l=k.type])break;if((m=d.find[l])&&(f=m(k.matches[0].replace(cb,db),ab.test(j[0].type)&&ob(b.parentNode)||b))){if(j.splice(i,1),a=f.length&&qb(j),!a)return I.apply(e,f),e;break}}}return(n||h(a,o))(f,b,!p,e,ab.test(a)&&ob(b.parentNode)||b),e},c.sortStable=u.split("").sort(B).join("")===u,c.detectDuplicates=!!l,m(),c.sortDetached=ib(function(a){return 1&a.compareDocumentPosition(n.createElement("div"))}),ib(function(a){return a.innerHTML="<a href='#'></a>","#"===a.firstChild.getAttribute("href")})||jb("type|href|height|width",function(a,b,c){return c?void 0:a.getAttribute(b,"type"===b.toLowerCase()?1:2)}),c.attributes&&ib(function(a){return a.innerHTML="<input/>",a.firstChild.setAttribute("value",""),""===a.firstChild.getAttribute("value")})||jb("value",function(a,b,c){return c||"input"!==a.nodeName.toLowerCase()?void 0:a.defaultValue}),ib(function(a){return null==a.getAttribute("disabled")})||jb(L,function(a,b,c){var d;return c?void 0:a[b]===!0?b.toLowerCase():(d=a.getAttributeNode(b))&&d.specified?d.value:null}),fb}(a);m.find=s,m.expr=s.selectors,m.expr[":"]=m.expr.pseudos,m.unique=s.uniqueSort,m.text=s.getText,m.isXMLDoc=s.isXML,m.contains=s.contains;var t=m.expr.match.needsContext,u=/^<(\w+)\s*\/?>(?:<\/\1>|)$/,v=/^.[^:#\[\.,]*$/;function w(a,b,c){if(m.isFunction(b))return m.grep(a,function(a,d){return!!b.call(a,d,a)!==c});if(b.nodeType)return m.grep(a,function(a){return a===b!==c});if("string"==typeof b){if(v.test(b))return m.filter(b,a,c);b=m.filter(b,a)}return m.grep(a,function(a){return m.inArray(a,b)>=0!==c})}m.filter=function(a,b,c){var d=b[0];return c&&(a=":not("+a+")"),1===b.length&&1===d.nodeType?m.find.matchesSelector(d,a)?[d]:[]:m.find.matches(a,m.grep(b,function(a){return 1===a.nodeType}))},m.fn.extend({find:function(a){var b,c=[],d=this,e=d.length;if("string"!=typeof a)return this.pushStack(m(a).filter(function(){for(b=0;e>b;b++)if(m.contains(d[b],this))return!0}));for(b=0;e>b;b++)m.find(a,d[b],c);return c=this.pushStack(e>1?m.unique(c):c),c.selector=this.selector?this.selector+" "+a:a,c},filter:function(a){return this.pushStack(w(this,a||[],!1))},not:function(a){return this.pushStack(w(this,a||[],!0))},is:function(a){return!!w(this,"string"==typeof a&&t.test(a)?m(a):a||[],!1).length}});var x,y=a.document,z=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]*))$/,A=m.fn.init=function(a,b){var c,d;if(!a)return this;if("string"==typeof a){if(c="<"===a.charAt(0)&&">"===a.charAt(a.length-1)&&a.length>=3?[null,a,null]:z.exec(a),!c||!c[1]&&b)return!b||b.jquery?(b||x).find(a):this.constructor(b).find(a);if(c[1]){if(b=b instanceof m?b[0]:b,m.merge(this,m.parseHTML(c[1],b&&b.nodeType?b.ownerDocument||b:y,!0)),u.test(c[1])&&m.isPlainObject(b))for(c in b)m.isFunction(this[c])?this[c](b[c]):this.attr(c,b[c]);return this}if(d=y.getElementById(c[2]),d&&d.parentNode){if(d.id!==c[2])return x.find(a);this.length=1,this[0]=d}return this.context=y,this.selector=a,this}return a.nodeType?(this.context=this[0]=a,this.length=1,this):m.isFunction(a)?"undefined"!=typeof x.ready?x.ready(a):a(m):(void 0!==a.selector&&(this.selector=a.selector,this.context=a.context),m.makeArray(a,this))};A.prototype=m.fn,x=m(y);var B=/^(?:parents|prev(?:Until|All))/,C={children:!0,contents:!0,next:!0,prev:!0};m.extend({dir:function(a,b,c){var d=[],e=a[b];while(e&&9!==e.nodeType&&(void 0===c||1!==e.nodeType||!m(e).is(c)))1===e.nodeType&&d.push(e),e=e[b];return d},sibling:function(a,b){for(var c=[];a;a=a.nextSibling)1===a.nodeType&&a!==b&&c.push(a);return c}}),m.fn.extend({has:function(a){var b,c=m(a,this),d=c.length;return this.filter(function(){for(b=0;d>b;b++)if(m.contains(this,c[b]))return!0})},closest:function(a,b){for(var c,d=0,e=this.length,f=[],g=t.test(a)||"string"!=typeof a?m(a,b||this.context):0;e>d;d++)for(c=this[d];c&&c!==b;c=c.parentNode)if(c.nodeType<11&&(g?g.index(c)>-1:1===c.nodeType&&m.find.matchesSelector(c,a))){f.push(c);break}return this.pushStack(f.length>1?m.unique(f):f)},index:function(a){return a?"string"==typeof a?m.inArray(this[0],m(a)):m.inArray(a.jquery?a[0]:a,this):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(a,b){return this.pushStack(m.unique(m.merge(this.get(),m(a,b))))},addBack:function(a){return this.add(null==a?this.prevObject:this.prevObject.filter(a))}});function D(a,b){do a=a[b];while(a&&1!==a.nodeType);return a}m.each({parent:function(a){var b=a.parentNode;return b&&11!==b.nodeType?b:null},parents:function(a){return m.dir(a,"parentNode")},parentsUntil:function(a,b,c){return m.dir(a,"parentNode",c)},next:function(a){return D(a,"nextSibling")},prev:function(a){return D(a,"previousSibling")},nextAll:function(a){return m.dir(a,"nextSibling")},prevAll:function(a){return m.dir(a,"previousSibling")},nextUntil:function(a,b,c){return m.dir(a,"nextSibling",c)},prevUntil:function(a,b,c){return m.dir(a,"previousSibling",c)},siblings:function(a){return m.sibling((a.parentNode||{}).firstChild,a)},children:function(a){return m.sibling(a.firstChild)},contents:function(a){return m.nodeName(a,"iframe")?a.contentDocument||a.contentWindow.document:m.merge([],a.childNodes)}},function(a,b){m.fn[a]=function(c,d){var e=m.map(this,b,c);return"Until"!==a.slice(-5)&&(d=c),d&&"string"==typeof d&&(e=m.filter(d,e)),this.length>1&&(C[a]||(e=m.unique(e)),B.test(a)&&(e=e.reverse())),this.pushStack(e)}});var E=/\S+/g,F={};function G(a){var b=F[a]={};return m.each(a.match(E)||[],function(a,c){b[c]=!0}),b}m.Callbacks=function(a){a="string"==typeof a?F[a]||G(a):m.extend({},a);var b,c,d,e,f,g,h=[],i=!a.once&&[],j=function(l){for(c=a.memory&&l,d=!0,f=g||0,g=0,e=h.length,b=!0;h&&e>f;f++)if(h[f].apply(l[0],l[1])===!1&&a.stopOnFalse){c=!1;break}b=!1,h&&(i?i.length&&j(i.shift()):c?h=[]:k.disable())},k={add:function(){if(h){var d=h.length;!function f(b){m.each(b,function(b,c){var d=m.type(c);"function"===d?a.unique&&k.has(c)||h.push(c):c&&c.length&&"string"!==d&&f(c)})}(arguments),b?e=h.length:c&&(g=d,j(c))}return this},remove:function(){return h&&m.each(arguments,function(a,c){var d;while((d=m.inArray(c,h,d))>-1)h.splice(d,1),b&&(e>=d&&e--,f>=d&&f--)}),this},has:function(a){return a?m.inArray(a,h)>-1:!(!h||!h.length)},empty:function(){return h=[],e=0,this},disable:function(){return h=i=c=void 0,this},disabled:function(){return!h},lock:function(){return i=void 0,c||k.disable(),this},locked:function(){return!i},fireWith:function(a,c){return!h||d&&!i||(c=c||[],c=[a,c.slice?c.slice():c],b?i.push(c):j(c)),this},fire:function(){return k.fireWith(this,arguments),this},fired:function(){return!!d}};return k},m.extend({Deferred:function(a){var b=[["resolve","done",m.Callbacks("once memory"),"resolved"],["reject","fail",m.Callbacks("once memory"),"rejected"],["notify","progress",m.Callbacks("memory")]],c="pending",d={state:function(){return c},always:function(){return e.done(arguments).fail(arguments),this},then:function(){var a=arguments;return m.Deferred(function(c){m.each(b,function(b,f){var g=m.isFunction(a[b])&&a[b];e[f[1]](function(){var a=g&&g.apply(this,arguments);a&&m.isFunction(a.promise)?a.promise().done(c.resolve).fail(c.reject).progress(c.notify):c[f[0]+"With"](this===d?c.promise():this,g?[a]:arguments)})}),a=null}).promise()},promise:function(a){return null!=a?m.extend(a,d):d}},e={};return d.pipe=d.then,m.each(b,function(a,f){var g=f[2],h=f[3];d[f[1]]=g.add,h&&g.add(function(){c=h},b[1^a][2].disable,b[2][2].lock),e[f[0]]=function(){return e[f[0]+"With"](this===e?d:this,arguments),this},e[f[0]+"With"]=g.fireWith}),d.promise(e),a&&a.call(e,e),e},when:function(a){var b=0,c=d.call(arguments),e=c.length,f=1!==e||a&&m.isFunction(a.promise)?e:0,g=1===f?a:m.Deferred(),h=function(a,b,c){return function(e){b[a]=this,c[a]=arguments.length>1?d.call(arguments):e,c===i?g.notifyWith(b,c):--f||g.resolveWith(b,c)}},i,j,k;if(e>1)for(i=new Array(e),j=new Array(e),k=new Array(e);e>b;b++)c[b]&&m.isFunction(c[b].promise)?c[b].promise().done(h(b,k,c)).fail(g.reject).progress(h(b,j,i)):--f;return f||g.resolveWith(k,c),g.promise()}});var H;m.fn.ready=function(a){return m.ready.promise().done(a),this},m.extend({isReady:!1,readyWait:1,holdReady:function(a){a?m.readyWait++:m.ready(!0)},ready:function(a){if(a===!0?!--m.readyWait:!m.isReady){if(!y.body)return setTimeout(m.ready);m.isReady=!0,a!==!0&&--m.readyWait>0||(H.resolveWith(y,[m]),m.fn.triggerHandler&&(m(y).triggerHandler("ready"),m(y).off("ready")))}}});function I(){y.addEventListener?(y.removeEventListener("DOMContentLoaded",J,!1),a.removeEventListener("load",J,!1)):(y.detachEvent("onreadystatechange",J),a.detachEvent("onload",J))}function J(){(y.addEventListener||"load"===event.type||"complete"===y.readyState)&&(I(),m.ready())}m.ready.promise=function(b){if(!H)if(H=m.Deferred(),"complete"===y.readyState)setTimeout(m.ready);else if(y.addEventListener)y.addEventListener("DOMContentLoaded",J,!1),a.addEventListener("load",J,!1);else{y.attachEvent("onreadystatechange",J),a.attachEvent("onload",J);var c=!1;try{c=null==a.frameElement&&y.documentElement}catch(d){}c&&c.doScroll&&!function e(){if(!m.isReady){try{c.doScroll("left")}catch(a){return setTimeout(e,50)}I(),m.ready()}}()}return H.promise(b)};var K="undefined",L;for(L in m(k))break;k.ownLast="0"!==L,k.inlineBlockNeedsLayout=!1,m(function(){var a,b,c,d;c=y.getElementsByTagName("body")[0],c&&c.style&&(b=y.createElement("div"),d=y.createElement("div"),d.style.cssText="position:absolute;border:0;width:0;height:0;top:0;left:-9999px",c.appendChild(d).appendChild(b),typeof b.style.zoom!==K&&(b.style.cssText="display:inline;margin:0;border:0;padding:1px;width:1px;zoom:1",k.inlineBlockNeedsLayout=a=3===b.offsetWidth,a&&(c.style.zoom=1)),c.removeChild(d))}),function(){var a=y.createElement("div");if(null==k.deleteExpando){k.deleteExpando=!0;try{delete a.test}catch(b){k.deleteExpando=!1}}a=null}(),m.acceptData=function(a){var b=m.noData[(a.nodeName+" ").toLowerCase()],c=+a.nodeType||1;return 1!==c&&9!==c?!1:!b||b!==!0&&a.getAttribute("classid")===b};var M=/^(?:\{[\w\W]*\}|\[[\w\W]*\])$/,N=/([A-Z])/g;function O(a,b,c){if(void 0===c&&1===a.nodeType){var d="data-"+b.replace(N,"-$1").toLowerCase();if(c=a.getAttribute(d),"string"==typeof c){try{c="true"===c?!0:"false"===c?!1:"null"===c?null:+c+""===c?+c:M.test(c)?m.parseJSON(c):c}catch(e){}m.data(a,b,c)}else c=void 0}return c}function P(a){var b;for(b in a)if(("data"!==b||!m.isEmptyObject(a[b]))&&"toJSON"!==b)return!1;return!0}function Q(a,b,d,e){if(m.acceptData(a)){var f,g,h=m.expando,i=a.nodeType,j=i?m.cache:a,k=i?a[h]:a[h]&&h;
  363. if(k&&j[k]&&(e||j[k].data)||void 0!==d||"string"!=typeof b)return k||(k=i?a[h]=c.pop()||m.guid++:h),j[k]||(j[k]=i?{}:{toJSON:m.noop}),("object"==typeof b||"function"==typeof b)&&(e?j[k]=m.extend(j[k],b):j[k].data=m.extend(j[k].data,b)),g=j[k],e||(g.data||(g.data={}),g=g.data),void 0!==d&&(g[m.camelCase(b)]=d),"string"==typeof b?(f=g[b],null==f&&(f=g[m.camelCase(b)])):f=g,f}}function R(a,b,c){if(m.acceptData(a)){var d,e,f=a.nodeType,g=f?m.cache:a,h=f?a[m.expando]:m.expando;if(g[h]){if(b&&(d=c?g[h]:g[h].data)){m.isArray(b)?b=b.concat(m.map(b,m.camelCase)):b in d?b=[b]:(b=m.camelCase(b),b=b in d?[b]:b.split(" ")),e=b.length;while(e--)delete d[b[e]];if(c?!P(d):!m.isEmptyObject(d))return}(c||(delete g[h].data,P(g[h])))&&(f?m.cleanData([a],!0):k.deleteExpando||g!=g.window?delete g[h]:g[h]=null)}}}m.extend({cache:{},noData:{"applet ":!0,"embed ":!0,"object ":"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000"},hasData:function(a){return a=a.nodeType?m.cache[a[m.expando]]:a[m.expando],!!a&&!P(a)},data:function(a,b,c){return Q(a,b,c)},removeData:function(a,b){return R(a,b)},_data:function(a,b,c){return Q(a,b,c,!0)},_removeData:function(a,b){return R(a,b,!0)}}),m.fn.extend({data:function(a,b){var c,d,e,f=this[0],g=f&&f.attributes;if(void 0===a){if(this.length&&(e=m.data(f),1===f.nodeType&&!m._data(f,"parsedAttrs"))){c=g.length;while(c--)g[c]&&(d=g[c].name,0===d.indexOf("data-")&&(d=m.camelCase(d.slice(5)),O(f,d,e[d])));m._data(f,"parsedAttrs",!0)}return e}return"object"==typeof a?this.each(function(){m.data(this,a)}):arguments.length>1?this.each(function(){m.data(this,a,b)}):f?O(f,a,m.data(f,a)):void 0},removeData:function(a){return this.each(function(){m.removeData(this,a)})}}),m.extend({queue:function(a,b,c){var d;return a?(b=(b||"fx")+"queue",d=m._data(a,b),c&&(!d||m.isArray(c)?d=m._data(a,b,m.makeArray(c)):d.push(c)),d||[]):void 0},dequeue:function(a,b){b=b||"fx";var c=m.queue(a,b),d=c.length,e=c.shift(),f=m._queueHooks(a,b),g=function(){m.dequeue(a,b)};"inprogress"===e&&(e=c.shift(),d--),e&&("fx"===b&&c.unshift("inprogress"),delete f.stop,e.call(a,g,f)),!d&&f&&f.empty.fire()},_queueHooks:function(a,b){var c=b+"queueHooks";return m._data(a,c)||m._data(a,c,{empty:m.Callbacks("once memory").add(function(){m._removeData(a,b+"queue"),m._removeData(a,c)})})}}),m.fn.extend({queue:function(a,b){var c=2;return"string"!=typeof a&&(b=a,a="fx",c--),arguments.length<c?m.queue(this[0],a):void 0===b?this:this.each(function(){var c=m.queue(this,a,b);m._queueHooks(this,a),"fx"===a&&"inprogress"!==c[0]&&m.dequeue(this,a)})},dequeue:function(a){return this.each(function(){m.dequeue(this,a)})},clearQueue:function(a){return this.queue(a||"fx",[])},promise:function(a,b){var c,d=1,e=m.Deferred(),f=this,g=this.length,h=function(){--d||e.resolveWith(f,[f])};"string"!=typeof a&&(b=a,a=void 0),a=a||"fx";while(g--)c=m._data(f[g],a+"queueHooks"),c&&c.empty&&(d++,c.empty.add(h));return h(),e.promise(b)}});var S=/[+-]?(?:\d*\.|)\d+(?:[eE][+-]?\d+|)/.source,T=["Top","Right","Bottom","Left"],U=function(a,b){return a=b||a,"none"===m.css(a,"display")||!m.contains(a.ownerDocument,a)},V=m.access=function(a,b,c,d,e,f,g){var h=0,i=a.length,j=null==c;if("object"===m.type(c)){e=!0;for(h in c)m.access(a,b,h,c[h],!0,f,g)}else if(void 0!==d&&(e=!0,m.isFunction(d)||(g=!0),j&&(g?(b.call(a,d),b=null):(j=b,b=function(a,b,c){return j.call(m(a),c)})),b))for(;i>h;h++)b(a[h],c,g?d:d.call(a[h],h,b(a[h],c)));return e?a:j?b.call(a):i?b(a[0],c):f},W=/^(?:checkbox|radio)$/i;!function(){var a=y.createElement("input"),b=y.createElement("div"),c=y.createDocumentFragment();if(b.innerHTML=" <link/><table></table><a href='/a'>a</a><input type='checkbox'/>",k.leadingWhitespace=3===b.firstChild.nodeType,k.tbody=!b.getElementsByTagName("tbody").length,k.htmlSerialize=!!b.getElementsByTagName("link").length,k.html5Clone="<:nav></:nav>"!==y.createElement("nav").cloneNode(!0).outerHTML,a.type="checkbox",a.checked=!0,c.appendChild(a),k.appendChecked=a.checked,b.innerHTML="<textarea>x</textarea>",k.noCloneChecked=!!b.cloneNode(!0).lastChild.defaultValue,c.appendChild(b),b.innerHTML="<input type='radio' checked='checked' name='t'/>",k.checkClone=b.cloneNode(!0).cloneNode(!0).lastChild.checked,k.noCloneEvent=!0,b.attachEvent&&(b.attachEvent("onclick",function(){k.noCloneEvent=!1}),b.cloneNode(!0).click()),null==k.deleteExpando){k.deleteExpando=!0;try{delete b.test}catch(d){k.deleteExpando=!1}}}(),function(){var b,c,d=y.createElement("div");for(b in{submit:!0,change:!0,focusin:!0})c="on"+b,(k[b+"Bubbles"]=c in a)||(d.setAttribute(c,"t"),k[b+"Bubbles"]=d.attributes[c].expando===!1);d=null}();var X=/^(?:input|select|textarea)$/i,Y=/^key/,Z=/^(?:mouse|pointer|contextmenu)|click/,$=/^(?:focusinfocus|focusoutblur)$/,_=/^([^.]*)(?:\.(.+)|)$/;function ab(){return!0}function bb(){return!1}function cb(){try{return y.activeElement}catch(a){}}m.event={global:{},add:function(a,b,c,d,e){var f,g,h,i,j,k,l,n,o,p,q,r=m._data(a);if(r){c.handler&&(i=c,c=i.handler,e=i.selector),c.guid||(c.guid=m.guid++),(g=r.events)||(g=r.events={}),(k=r.handle)||(k=r.handle=function(a){return typeof m===K||a&&m.event.triggered===a.type?void 0:m.event.dispatch.apply(k.elem,arguments)},k.elem=a),b=(b||"").match(E)||[""],h=b.length;while(h--)f=_.exec(b[h])||[],o=q=f[1],p=(f[2]||"").split(".").sort(),o&&(j=m.event.special[o]||{},o=(e?j.delegateType:j.bindType)||o,j=m.event.special[o]||{},l=m.extend({type:o,origType:q,data:d,handler:c,guid:c.guid,selector:e,needsContext:e&&m.expr.match.needsContext.test(e),namespace:p.join(".")},i),(n=g[o])||(n=g[o]=[],n.delegateCount=0,j.setup&&j.setup.call(a,d,p,k)!==!1||(a.addEventListener?a.addEventListener(o,k,!1):a.attachEvent&&a.attachEvent("on"+o,k))),j.add&&(j.add.call(a,l),l.handler.guid||(l.handler.guid=c.guid)),e?n.splice(n.delegateCount++,0,l):n.push(l),m.event.global[o]=!0);a=null}},remove:function(a,b,c,d,e){var f,g,h,i,j,k,l,n,o,p,q,r=m.hasData(a)&&m._data(a);if(r&&(k=r.events)){b=(b||"").match(E)||[""],j=b.length;while(j--)if(h=_.exec(b[j])||[],o=q=h[1],p=(h[2]||"").split(".").sort(),o){l=m.event.special[o]||{},o=(d?l.delegateType:l.bindType)||o,n=k[o]||[],h=h[2]&&new RegExp("(^|\\.)"+p.join("\\.(?:.*\\.|)")+"(\\.|$)"),i=f=n.length;while(f--)g=n[f],!e&&q!==g.origType||c&&c.guid!==g.guid||h&&!h.test(g.namespace)||d&&d!==g.selector&&("**"!==d||!g.selector)||(n.splice(f,1),g.selector&&n.delegateCount--,l.remove&&l.remove.call(a,g));i&&!n.length&&(l.teardown&&l.teardown.call(a,p,r.handle)!==!1||m.removeEvent(a,o,r.handle),delete k[o])}else for(o in k)m.event.remove(a,o+b[j],c,d,!0);m.isEmptyObject(k)&&(delete r.handle,m._removeData(a,"events"))}},trigger:function(b,c,d,e){var f,g,h,i,k,l,n,o=[d||y],p=j.call(b,"type")?b.type:b,q=j.call(b,"namespace")?b.namespace.split("."):[];if(h=l=d=d||y,3!==d.nodeType&&8!==d.nodeType&&!$.test(p+m.event.triggered)&&(p.indexOf(".")>=0&&(q=p.split("."),p=q.shift(),q.sort()),g=p.indexOf(":")<0&&"on"+p,b=b[m.expando]?b:new m.Event(p,"object"==typeof b&&b),b.isTrigger=e?2:3,b.namespace=q.join("."),b.namespace_re=b.namespace?new RegExp("(^|\\.)"+q.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,b.result=void 0,b.target||(b.target=d),c=null==c?[b]:m.makeArray(c,[b]),k=m.event.special[p]||{},e||!k.trigger||k.trigger.apply(d,c)!==!1)){if(!e&&!k.noBubble&&!m.isWindow(d)){for(i=k.delegateType||p,$.test(i+p)||(h=h.parentNode);h;h=h.parentNode)o.push(h),l=h;l===(d.ownerDocument||y)&&o.push(l.defaultView||l.parentWindow||a)}n=0;while((h=o[n++])&&!b.isPropagationStopped())b.type=n>1?i:k.bindType||p,f=(m._data(h,"events")||{})[b.type]&&m._data(h,"handle"),f&&f.apply(h,c),f=g&&h[g],f&&f.apply&&m.acceptData(h)&&(b.result=f.apply(h,c),b.result===!1&&b.preventDefault());if(b.type=p,!e&&!b.isDefaultPrevented()&&(!k._default||k._default.apply(o.pop(),c)===!1)&&m.acceptData(d)&&g&&d[p]&&!m.isWindow(d)){l=d[g],l&&(d[g]=null),m.event.triggered=p;try{d[p]()}catch(r){}m.event.triggered=void 0,l&&(d[g]=l)}return b.result}},dispatch:function(a){a=m.event.fix(a);var b,c,e,f,g,h=[],i=d.call(arguments),j=(m._data(this,"events")||{})[a.type]||[],k=m.event.special[a.type]||{};if(i[0]=a,a.delegateTarget=this,!k.preDispatch||k.preDispatch.call(this,a)!==!1){h=m.event.handlers.call(this,a,j),b=0;while((f=h[b++])&&!a.isPropagationStopped()){a.currentTarget=f.elem,g=0;while((e=f.handlers[g++])&&!a.isImmediatePropagationStopped())(!a.namespace_re||a.namespace_re.test(e.namespace))&&(a.handleObj=e,a.data=e.data,c=((m.event.special[e.origType]||{}).handle||e.handler).apply(f.elem,i),void 0!==c&&(a.result=c)===!1&&(a.preventDefault(),a.stopPropagation()))}return k.postDispatch&&k.postDispatch.call(this,a),a.result}},handlers:function(a,b){var c,d,e,f,g=[],h=b.delegateCount,i=a.target;if(h&&i.nodeType&&(!a.button||"click"!==a.type))for(;i!=this;i=i.parentNode||this)if(1===i.nodeType&&(i.disabled!==!0||"click"!==a.type)){for(e=[],f=0;h>f;f++)d=b[f],c=d.selector+" ",void 0===e[c]&&(e[c]=d.needsContext?m(c,this).index(i)>=0:m.find(c,this,null,[i]).length),e[c]&&e.push(d);e.length&&g.push({elem:i,handlers:e})}return h<b.length&&g.push({elem:this,handlers:b.slice(h)}),g},fix:function(a){if(a[m.expando])return a;var b,c,d,e=a.type,f=a,g=this.fixHooks[e];g||(this.fixHooks[e]=g=Z.test(e)?this.mouseHooks:Y.test(e)?this.keyHooks:{}),d=g.props?this.props.concat(g.props):this.props,a=new m.Event(f),b=d.length;while(b--)c=d[b],a[c]=f[c];return a.target||(a.target=f.srcElement||y),3===a.target.nodeType&&(a.target=a.target.parentNode),a.metaKey=!!a.metaKey,g.filter?g.filter(a,f):a},props:"altKey bubbles cancelable ctrlKey currentTarget eventPhase metaKey relatedTarget shiftKey target timeStamp view which".split(" "),fixHooks:{},keyHooks:{props:"char charCode key keyCode".split(" "),filter:function(a,b){return null==a.which&&(a.which=null!=b.charCode?b.charCode:b.keyCode),a}},mouseHooks:{props:"button buttons clientX clientY fromElement offsetX offsetY pageX pageY screenX screenY toElement".split(" "),filter:function(a,b){var c,d,e,f=b.button,g=b.fromElement;return null==a.pageX&&null!=b.clientX&&(d=a.target.ownerDocument||y,e=d.documentElement,c=d.body,a.pageX=b.clientX+(e&&e.scrollLeft||c&&c.scrollLeft||0)-(e&&e.clientLeft||c&&c.clientLeft||0),a.pageY=b.clientY+(e&&e.scrollTop||c&&c.scrollTop||0)-(e&&e.clientTop||c&&c.clientTop||0)),!a.relatedTarget&&g&&(a.relatedTarget=g===a.target?b.toElement:g),a.which||void 0===f||(a.which=1&f?1:2&f?3:4&f?2:0),a}},special:{load:{noBubble:!0},focus:{trigger:function(){if(this!==cb()&&this.focus)try{return this.focus(),!1}catch(a){}},delegateType:"focusin"},blur:{trigger:function(){return this===cb()&&this.blur?(this.blur(),!1):void 0},delegateType:"focusout"},click:{trigger:function(){return m.nodeName(this,"input")&&"checkbox"===this.type&&this.click?(this.click(),!1):void 0},_default:function(a){return m.nodeName(a.target,"a")}},beforeunload:{postDispatch:function(a){void 0!==a.result&&a.originalEvent&&(a.originalEvent.returnValue=a.result)}}},simulate:function(a,b,c,d){var e=m.extend(new m.Event,c,{type:a,isSimulated:!0,originalEvent:{}});d?m.event.trigger(e,null,b):m.event.dispatch.call(b,e),e.isDefaultPrevented()&&c.preventDefault()}},m.removeEvent=y.removeEventListener?function(a,b,c){a.removeEventListener&&a.removeEventListener(b,c,!1)}:function(a,b,c){var d="on"+b;a.detachEvent&&(typeof a[d]===K&&(a[d]=null),a.detachEvent(d,c))},m.Event=function(a,b){return this instanceof m.Event?(a&&a.type?(this.originalEvent=a,this.type=a.type,this.isDefaultPrevented=a.defaultPrevented||void 0===a.defaultPrevented&&a.returnValue===!1?ab:bb):this.type=a,b&&m.extend(this,b),this.timeStamp=a&&a.timeStamp||m.now(),void(this[m.expando]=!0)):new m.Event(a,b)},m.Event.prototype={isDefaultPrevented:bb,isPropagationStopped:bb,isImmediatePropagationStopped:bb,preventDefault:function(){var a=this.originalEvent;this.isDefaultPrevented=ab,a&&(a.preventDefault?a.preventDefault():a.returnValue=!1)},stopPropagation:function(){var a=this.originalEvent;this.isPropagationStopped=ab,a&&(a.stopPropagation&&a.stopPropagation(),a.cancelBubble=!0)},stopImmediatePropagation:function(){var a=this.originalEvent;this.isImmediatePropagationStopped=ab,a&&a.stopImmediatePropagation&&a.stopImmediatePropagation(),this.stopPropagation()}},m.each({mouseenter:"mouseover",mouseleave:"mouseout",pointerenter:"pointerover",pointerleave:"pointerout"},function(a,b){m.event.special[a]={delegateType:b,bindType:b,handle:function(a){var c,d=this,e=a.relatedTarget,f=a.handleObj;return(!e||e!==d&&!m.contains(d,e))&&(a.type=f.origType,c=f.handler.apply(this,arguments),a.type=b),c}}}),k.submitBubbles||(m.event.special.submit={setup:function(){return m.nodeName(this,"form")?!1:void m.event.add(this,"click._submit keypress._submit",function(a){var b=a.target,c=m.nodeName(b,"input")||m.nodeName(b,"button")?b.form:void 0;c&&!m._data(c,"submitBubbles")&&(m.event.add(c,"submit._submit",function(a){a._submit_bubble=!0}),m._data(c,"submitBubbles",!0))})},postDispatch:function(a){a._submit_bubble&&(delete a._submit_bubble,this.parentNode&&!a.isTrigger&&m.event.simulate("submit",this.parentNode,a,!0))},teardown:function(){return m.nodeName(this,"form")?!1:void m.event.remove(this,"._submit")}}),k.changeBubbles||(m.event.special.change={setup:function(){return X.test(this.nodeName)?(("checkbox"===this.type||"radio"===this.type)&&(m.event.add(this,"propertychange._change",function(a){"checked"===a.originalEvent.propertyName&&(this._just_changed=!0)}),m.event.add(this,"click._change",function(a){this._just_changed&&!a.isTrigger&&(this._just_changed=!1),m.event.simulate("change",this,a,!0)})),!1):void m.event.add(this,"beforeactivate._change",function(a){var b=a.target;X.test(b.nodeName)&&!m._data(b,"changeBubbles")&&(m.event.add(b,"change._change",function(a){!this.parentNode||a.isSimulated||a.isTrigger||m.event.simulate("change",this.parentNode,a,!0)}),m._data(b,"changeBubbles",!0))})},handle:function(a){var b=a.target;return this!==b||a.isSimulated||a.isTrigger||"radio"!==b.type&&"checkbox"!==b.type?a.handleObj.handler.apply(this,arguments):void 0},teardown:function(){return m.event.remove(this,"._change"),!X.test(this.nodeName)}}),k.focusinBubbles||m.each({focus:"focusin",blur:"focusout"},function(a,b){var c=function(a){m.event.simulate(b,a.target,m.event.fix(a),!0)};m.event.special[b]={setup:function(){var d=this.ownerDocument||this,e=m._data(d,b);e||d.addEventListener(a,c,!0),m._data(d,b,(e||0)+1)},teardown:function(){var d=this.ownerDocument||this,e=m._data(d,b)-1;e?m._data(d,b,e):(d.removeEventListener(a,c,!0),m._removeData(d,b))}}}),m.fn.extend({on:function(a,b,c,d,e){var f,g;if("object"==typeof a){"string"!=typeof b&&(c=c||b,b=void 0);for(f in a)this.on(f,b,c,a[f],e);return this}if(null==c&&null==d?(d=b,c=b=void 0):null==d&&("string"==typeof b?(d=c,c=void 0):(d=c,c=b,b=void 0)),d===!1)d=bb;else if(!d)return this;return 1===e&&(g=d,d=function(a){return m().off(a),g.apply(this,arguments)},d.guid=g.guid||(g.guid=m.guid++)),this.each(function(){m.event.add(this,a,d,c,b)})},one:function(a,b,c,d){return this.on(a,b,c,d,1)},off:function(a,b,c){var d,e;if(a&&a.preventDefault&&a.handleObj)return d=a.handleObj,m(a.delegateTarget).off(d.namespace?d.origType+"."+d.namespace:d.origType,d.selector,d.handler),this;if("object"==typeof a){for(e in a)this.off(e,b,a[e]);return this}return(b===!1||"function"==typeof b)&&(c=b,b=void 0),c===!1&&(c=bb),this.each(function(){m.event.remove(this,a,c,b)})},trigger:function(a,b){return this.each(function(){m.event.trigger(a,b,this)})},triggerHandler:function(a,b){var c=this[0];return c?m.event.trigger(a,b,c,!0):void 0}});function db(a){var b=eb.split("|"),c=a.createDocumentFragment();if(c.createElement)while(b.length)c.createElement(b.pop());return c}var eb="abbr|article|aside|audio|bdi|canvas|data|datalist|details|figcaption|figure|footer|header|hgroup|mark|meter|nav|output|progress|section|summary|time|video",fb=/ jQuery\d+="(?:null|\d+)"/g,gb=new RegExp("<(?:"+eb+")[\\s/>]","i"),hb=/^\s+/,ib=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi,jb=/<([\w:]+)/,kb=/<tbody/i,lb=/<|&#?\w+;/,mb=/<(?:script|style|link)/i,nb=/checked\s*(?:[^=]|=\s*.checked.)/i,ob=/^$|\/(?:java|ecma)script/i,pb=/^true\/(.*)/,qb=/^\s*<!(?:\[CDATA\[|--)|(?:\]\]|--)>\s*$/g,rb={option:[1,"<select multiple='multiple'>","</select>"],legend:[1,"<fieldset>","</fieldset>"],area:[1,"<map>","</map>"],param:[1,"<object>","</object>"],thead:[1,"<table>","</table>"],tr:[2,"<table><tbody>","</tbody></table>"],col:[2,"<table><tbody></tbody><colgroup>","</colgroup></table>"],td:[3,"<table><tbody><tr>","</tr></tbody></table>"],_default:k.htmlSerialize?[0,"",""]:[1,"X<div>","</div>"]},sb=db(y),tb=sb.appendChild(y.createElement("div"));rb.optgroup=rb.option,rb.tbody=rb.tfoot=rb.colgroup=rb.caption=rb.thead,rb.th=rb.td;function ub(a,b){var c,d,e=0,f=typeof a.getElementsByTagName!==K?a.getElementsByTagName(b||"*"):typeof a.querySelectorAll!==K?a.querySelectorAll(b||"*"):void 0;if(!f)for(f=[],c=a.childNodes||a;null!=(d=c[e]);e++)!b||m.nodeName(d,b)?f.push(d):m.merge(f,ub(d,b));return void 0===b||b&&m.nodeName(a,b)?m.merge([a],f):f}function vb(a){W.test(a.type)&&(a.defaultChecked=a.checked)}function wb(a,b){return m.nodeName(a,"table")&&m.nodeName(11!==b.nodeType?b:b.firstChild,"tr")?a.getElementsByTagName("tbody")[0]||a.appendChild(a.ownerDocument.createElement("tbody")):a}function xb(a){return a.type=(null!==m.find.attr(a,"type"))+"/"+a.type,a}function yb(a){var b=pb.exec(a.type);return b?a.type=b[1]:a.removeAttribute("type"),a}function zb(a,b){for(var c,d=0;null!=(c=a[d]);d++)m._data(c,"globalEval",!b||m._data(b[d],"globalEval"))}function Ab(a,b){if(1===b.nodeType&&m.hasData(a)){var c,d,e,f=m._data(a),g=m._data(b,f),h=f.events;if(h){delete g.handle,g.events={};for(c in h)for(d=0,e=h[c].length;e>d;d++)m.event.add(b,c,h[c][d])}g.data&&(g.data=m.extend({},g.data))}}function Bb(a,b){var c,d,e;if(1===b.nodeType){if(c=b.nodeName.toLowerCase(),!k.noCloneEvent&&b[m.expando]){e=m._data(b);for(d in e.events)m.removeEvent(b,d,e.handle);b.removeAttribute(m.expando)}"script"===c&&b.text!==a.text?(xb(b).text=a.text,yb(b)):"object"===c?(b.parentNode&&(b.outerHTML=a.outerHTML),k.html5Clone&&a.innerHTML&&!m.trim(b.innerHTML)&&(b.innerHTML=a.innerHTML)):"input"===c&&W.test(a.type)?(b.defaultChecked=b.checked=a.checked,b.value!==a.value&&(b.value=a.value)):"option"===c?b.defaultSelected=b.selected=a.defaultSelected:("input"===c||"textarea"===c)&&(b.defaultValue=a.defaultValue)}}m.extend({clone:function(a,b,c){var d,e,f,g,h,i=m.contains(a.ownerDocument,a);if(k.html5Clone||m.isXMLDoc(a)||!gb.test("<"+a.nodeName+">")?f=a.cloneNode(!0):(tb.innerHTML=a.outerHTML,tb.removeChild(f=tb.firstChild)),!(k.noCloneEvent&&k.noCloneChecked||1!==a.nodeType&&11!==a.nodeType||m.isXMLDoc(a)))for(d=ub(f),h=ub(a),g=0;null!=(e=h[g]);++g)d[g]&&Bb(e,d[g]);if(b)if(c)for(h=h||ub(a),d=d||ub(f),g=0;null!=(e=h[g]);g++)Ab(e,d[g]);else Ab(a,f);return d=ub(f,"script"),d.length>0&&zb(d,!i&&ub(a,"script")),d=h=e=null,f},buildFragment:function(a,b,c,d){for(var e,f,g,h,i,j,l,n=a.length,o=db(b),p=[],q=0;n>q;q++)if(f=a[q],f||0===f)if("object"===m.type(f))m.merge(p,f.nodeType?[f]:f);else if(lb.test(f)){h=h||o.appendChild(b.createElement("div")),i=(jb.exec(f)||["",""])[1].toLowerCase(),l=rb[i]||rb._default,h.innerHTML=l[1]+f.replace(ib,"<$1></$2>")+l[2],e=l[0];while(e--)h=h.lastChild;if(!k.leadingWhitespace&&hb.test(f)&&p.push(b.createTextNode(hb.exec(f)[0])),!k.tbody){f="table"!==i||kb.test(f)?"<table>"!==l[1]||kb.test(f)?0:h:h.firstChild,e=f&&f.childNodes.length;while(e--)m.nodeName(j=f.childNodes[e],"tbody")&&!j.childNodes.length&&f.removeChild(j)}m.merge(p,h.childNodes),h.textContent="";while(h.firstChild)h.removeChild(h.firstChild);h=o.lastChild}else p.push(b.createTextNode(f));h&&o.removeChild(h),k.appendChecked||m.grep(ub(p,"input"),vb),q=0;while(f=p[q++])if((!d||-1===m.inArray(f,d))&&(g=m.contains(f.ownerDocument,f),h=ub(o.appendChild(f),"script"),g&&zb(h),c)){e=0;while(f=h[e++])ob.test(f.type||"")&&c.push(f)}return h=null,o},cleanData:function(a,b){for(var d,e,f,g,h=0,i=m.expando,j=m.cache,l=k.deleteExpando,n=m.event.special;null!=(d=a[h]);h++)if((b||m.acceptData(d))&&(f=d[i],g=f&&j[f])){if(g.events)for(e in g.events)n[e]?m.event.remove(d,e):m.removeEvent(d,e,g.handle);j[f]&&(delete j[f],l?delete d[i]:typeof d.removeAttribute!==K?d.removeAttribute(i):d[i]=null,c.push(f))}}}),m.fn.extend({text:function(a){return V(this,function(a){return void 0===a?m.text(this):this.empty().append((this[0]&&this[0].ownerDocument||y).createTextNode(a))},null,a,arguments.length)},append:function(){return this.domManip(arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=wb(this,a);b.appendChild(a)}})},prepend:function(){return this.domManip(arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=wb(this,a);b.insertBefore(a,b.firstChild)}})},before:function(){return this.domManip(arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this)})},after:function(){return this.domManip(arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this.nextSibling)})},remove:function(a,b){for(var c,d=a?m.filter(a,this):this,e=0;null!=(c=d[e]);e++)b||1!==c.nodeType||m.cleanData(ub(c)),c.parentNode&&(b&&m.contains(c.ownerDocument,c)&&zb(ub(c,"script")),c.parentNode.removeChild(c));return this},empty:function(){for(var a,b=0;null!=(a=this[b]);b++){1===a.nodeType&&m.cleanData(ub(a,!1));while(a.firstChild)a.removeChild(a.firstChild);a.options&&m.nodeName(a,"select")&&(a.options.length=0)}return this},clone:function(a,b){return a=null==a?!1:a,b=null==b?a:b,this.map(function(){return m.clone(this,a,b)})},html:function(a){return V(this,function(a){var b=this[0]||{},c=0,d=this.length;if(void 0===a)return 1===b.nodeType?b.innerHTML.replace(fb,""):void 0;if(!("string"!=typeof a||mb.test(a)||!k.htmlSerialize&&gb.test(a)||!k.leadingWhitespace&&hb.test(a)||rb[(jb.exec(a)||["",""])[1].toLowerCase()])){a=a.replace(ib,"<$1></$2>");try{for(;d>c;c++)b=this[c]||{},1===b.nodeType&&(m.cleanData(ub(b,!1)),b.innerHTML=a);b=0}catch(e){}}b&&this.empty().append(a)},null,a,arguments.length)},replaceWith:function(){var a=arguments[0];return this.domManip(arguments,function(b){a=this.parentNode,m.cleanData(ub(this)),a&&a.replaceChild(b,this)}),a&&(a.length||a.nodeType)?this:this.remove()},detach:function(a){return this.remove(a,!0)},domManip:function(a,b){a=e.apply([],a);var c,d,f,g,h,i,j=0,l=this.length,n=this,o=l-1,p=a[0],q=m.isFunction(p);if(q||l>1&&"string"==typeof p&&!k.checkClone&&nb.test(p))return this.each(function(c){var d=n.eq(c);q&&(a[0]=p.call(this,c,d.html())),d.domManip(a,b)});if(l&&(i=m.buildFragment(a,this[0].ownerDocument,!1,this),c=i.firstChild,1===i.childNodes.length&&(i=c),c)){for(g=m.map(ub(i,"script"),xb),f=g.length;l>j;j++)d=i,j!==o&&(d=m.clone(d,!0,!0),f&&m.merge(g,ub(d,"script"))),b.call(this[j],d,j);if(f)for(h=g[g.length-1].ownerDocument,m.map(g,yb),j=0;f>j;j++)d=g[j],ob.test(d.type||"")&&!m._data(d,"globalEval")&&m.contains(h,d)&&(d.src?m._evalUrl&&m._evalUrl(d.src):m.globalEval((d.text||d.textContent||d.innerHTML||"").replace(qb,"")));i=c=null}return this}}),m.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(a,b){m.fn[a]=function(a){for(var c,d=0,e=[],g=m(a),h=g.length-1;h>=d;d++)c=d===h?this:this.clone(!0),m(g[d])[b](c),f.apply(e,c.get());return this.pushStack(e)}});var Cb,Db={};function Eb(b,c){var d,e=m(c.createElement(b)).appendTo(c.body),f=a.getDefaultComputedStyle&&(d=a.getDefaultComputedStyle(e[0]))?d.display:m.css(e[0],"display");return e.detach(),f}function Fb(a){var b=y,c=Db[a];return c||(c=Eb(a,b),"none"!==c&&c||(Cb=(Cb||m("<iframe frameborder='0' width='0' height='0'/>")).appendTo(b.documentElement),b=(Cb[0].contentWindow||Cb[0].contentDocument).document,b.write(),b.close(),c=Eb(a,b),Cb.detach()),Db[a]=c),c}!function(){var a;k.shrinkWrapBlocks=function(){if(null!=a)return a;a=!1;var b,c,d;return c=y.getElementsByTagName("body")[0],c&&c.style?(b=y.createElement("div"),d=y.createElement("div"),d.style.cssText="position:absolute;border:0;width:0;height:0;top:0;left:-9999px",c.appendChild(d).appendChild(b),typeof b.style.zoom!==K&&(b.style.cssText="-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;display:block;margin:0;border:0;padding:1px;width:1px;zoom:1",b.appendChild(y.createElement("div")).style.width="5px",a=3!==b.offsetWidth),c.removeChild(d),a):void 0}}();var Gb=/^margin/,Hb=new RegExp("^("+S+")(?!px)[a-z%]+$","i"),Ib,Jb,Kb=/^(top|right|bottom|left)$/;a.getComputedStyle?(Ib=function(a){return a.ownerDocument.defaultView.getComputedStyle(a,null)},Jb=function(a,b,c){var d,e,f,g,h=a.style;return c=c||Ib(a),g=c?c.getPropertyValue(b)||c[b]:void 0,c&&(""!==g||m.contains(a.ownerDocument,a)||(g=m.style(a,b)),Hb.test(g)&&Gb.test(b)&&(d=h.width,e=h.minWidth,f=h.maxWidth,h.minWidth=h.maxWidth=h.width=g,g=c.width,h.width=d,h.minWidth=e,h.maxWidth=f)),void 0===g?g:g+""}):y.documentElement.currentStyle&&(Ib=function(a){return a.currentStyle},Jb=function(a,b,c){var d,e,f,g,h=a.style;return c=c||Ib(a),g=c?c[b]:void 0,null==g&&h&&h[b]&&(g=h[b]),Hb.test(g)&&!Kb.test(b)&&(d=h.left,e=a.runtimeStyle,f=e&&e.left,f&&(e.left=a.currentStyle.left),h.left="fontSize"===b?"1em":g,g=h.pixelLeft+"px",h.left=d,f&&(e.left=f)),void 0===g?g:g+""||"auto"});function Lb(a,b){return{get:function(){var c=a();if(null!=c)return c?void delete this.get:(this.get=b).apply(this,arguments)}}}!function(){var b,c,d,e,f,g,h;if(b=y.createElement("div"),b.innerHTML=" <link/><table></table><a href='/a'>a</a><input type='checkbox'/>",d=b.getElementsByTagName("a")[0],c=d&&d.style){c.cssText="float:left;opacity:.5",k.opacity="0.5"===c.opacity,k.cssFloat=!!c.cssFloat,b.style.backgroundClip="content-box",b.cloneNode(!0).style.backgroundClip="",k.clearCloneStyle="content-box"===b.style.backgroundClip,k.boxSizing=""===c.boxSizing||""===c.MozBoxSizing||""===c.WebkitBoxSizing,m.extend(k,{reliableHiddenOffsets:function(){return null==g&&i(),g},boxSizingReliable:function(){return null==f&&i(),f},pixelPosition:function(){return null==e&&i(),e},reliableMarginRight:function(){return null==h&&i(),h}});function i(){var b,c,d,i;c=y.getElementsByTagName("body")[0],c&&c.style&&(b=y.createElement("div"),d=y.createElement("div"),d.style.cssText="position:absolute;border:0;width:0;height:0;top:0;left:-9999px",c.appendChild(d).appendChild(b),b.style.cssText="-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;display:block;margin-top:1%;top:1%;border:1px;padding:1px;width:4px;position:absolute",e=f=!1,h=!0,a.getComputedStyle&&(e="1%"!==(a.getComputedStyle(b,null)||{}).top,f="4px"===(a.getComputedStyle(b,null)||{width:"4px"}).width,i=b.appendChild(y.createElement("div")),i.style.cssText=b.style.cssText="-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;display:block;margin:0;border:0;padding:0",i.style.marginRight=i.style.width="0",b.style.width="1px",h=!parseFloat((a.getComputedStyle(i,null)||{}).marginRight)),b.innerHTML="<table><tr><td></td><td>t</td></tr></table>",i=b.getElementsByTagName("td"),i[0].style.cssText="margin:0;border:0;padding:0;display:none",g=0===i[0].offsetHeight,g&&(i[0].style.display="",i[1].style.display="none",g=0===i[0].offsetHeight),c.removeChild(d))}}}(),m.swap=function(a,b,c,d){var e,f,g={};for(f in b)g[f]=a.style[f],a.style[f]=b[f];e=c.apply(a,d||[]);for(f in b)a.style[f]=g[f];return e};var Mb=/alpha\([^)]*\)/i,Nb=/opacity\s*=\s*([^)]*)/,Ob=/^(none|table(?!-c[ea]).+)/,Pb=new RegExp("^("+S+")(.*)$","i"),Qb=new RegExp("^([+-])=("+S+")","i"),Rb={position:"absolute",visibility:"hidden",display:"block"},Sb={letterSpacing:"0",fontWeight:"400"},Tb=["Webkit","O","Moz","ms"];function Ub(a,b){if(b in a)return b;var c=b.charAt(0).toUpperCase()+b.slice(1),d=b,e=Tb.length;while(e--)if(b=Tb[e]+c,b in a)return b;return d}function Vb(a,b){for(var c,d,e,f=[],g=0,h=a.length;h>g;g++)d=a[g],d.style&&(f[g]=m._data(d,"olddisplay"),c=d.style.display,b?(f[g]||"none"!==c||(d.style.display=""),""===d.style.display&&U(d)&&(f[g]=m._data(d,"olddisplay",Fb(d.nodeName)))):(e=U(d),(c&&"none"!==c||!e)&&m._data(d,"olddisplay",e?c:m.css(d,"display"))));for(g=0;h>g;g++)d=a[g],d.style&&(b&&"none"!==d.style.display&&""!==d.style.display||(d.style.display=b?f[g]||"":"none"));return a}function Wb(a,b,c){var d=Pb.exec(b);return d?Math.max(0,d[1]-(c||0))+(d[2]||"px"):b}function Xb(a,b,c,d,e){for(var f=c===(d?"border":"content")?4:"width"===b?1:0,g=0;4>f;f+=2)"margin"===c&&(g+=m.css(a,c+T[f],!0,e)),d?("content"===c&&(g-=m.css(a,"padding"+T[f],!0,e)),"margin"!==c&&(g-=m.css(a,"border"+T[f]+"Width",!0,e))):(g+=m.css(a,"padding"+T[f],!0,e),"padding"!==c&&(g+=m.css(a,"border"+T[f]+"Width",!0,e)));return g}function Yb(a,b,c){var d=!0,e="width"===b?a.offsetWidth:a.offsetHeight,f=Ib(a),g=k.boxSizing&&"border-box"===m.css(a,"boxSizing",!1,f);if(0>=e||null==e){if(e=Jb(a,b,f),(0>e||null==e)&&(e=a.style[b]),Hb.test(e))return e;d=g&&(k.boxSizingReliable()||e===a.style[b]),e=parseFloat(e)||0}return e+Xb(a,b,c||(g?"border":"content"),d,f)+"px"}m.extend({cssHooks:{opacity:{get:function(a,b){if(b){var c=Jb(a,"opacity");return""===c?"1":c}}}},cssNumber:{columnCount:!0,fillOpacity:!0,flexGrow:!0,flexShrink:!0,fontWeight:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,widows:!0,zIndex:!0,zoom:!0},cssProps:{"float":k.cssFloat?"cssFloat":"styleFloat"},style:function(a,b,c,d){if(a&&3!==a.nodeType&&8!==a.nodeType&&a.style){var e,f,g,h=m.camelCase(b),i=a.style;if(b=m.cssProps[h]||(m.cssProps[h]=Ub(i,h)),g=m.cssHooks[b]||m.cssHooks[h],void 0===c)return g&&"get"in g&&void 0!==(e=g.get(a,!1,d))?e:i[b];if(f=typeof c,"string"===f&&(e=Qb.exec(c))&&(c=(e[1]+1)*e[2]+parseFloat(m.css(a,b)),f="number"),null!=c&&c===c&&("number"!==f||m.cssNumber[h]||(c+="px"),k.clearCloneStyle||""!==c||0!==b.indexOf("background")||(i[b]="inherit"),!(g&&"set"in g&&void 0===(c=g.set(a,c,d)))))try{i[b]=c}catch(j){}}},css:function(a,b,c,d){var e,f,g,h=m.camelCase(b);return b=m.cssProps[h]||(m.cssProps[h]=Ub(a.style,h)),g=m.cssHooks[b]||m.cssHooks[h],g&&"get"in g&&(f=g.get(a,!0,c)),void 0===f&&(f=Jb(a,b,d)),"normal"===f&&b in Sb&&(f=Sb[b]),""===c||c?(e=parseFloat(f),c===!0||m.isNumeric(e)?e||0:f):f}}),m.each(["height","width"],function(a,b){m.cssHooks[b]={get:function(a,c,d){return c?Ob.test(m.css(a,"display"))&&0===a.offsetWidth?m.swap(a,Rb,function(){return Yb(a,b,d)}):Yb(a,b,d):void 0},set:function(a,c,d){var e=d&&Ib(a);return Wb(a,c,d?Xb(a,b,d,k.boxSizing&&"border-box"===m.css(a,"boxSizing",!1,e),e):0)}}}),k.opacity||(m.cssHooks.opacity={get:function(a,b){return Nb.test((b&&a.currentStyle?a.currentStyle.filter:a.style.filter)||"")?.01*parseFloat(RegExp.$1)+"":b?"1":""},set:function(a,b){var c=a.style,d=a.currentStyle,e=m.isNumeric(b)?"alpha(opacity="+100*b+")":"",f=d&&d.filter||c.filter||"";c.zoom=1,(b>=1||""===b)&&""===m.trim(f.replace(Mb,""))&&c.removeAttribute&&(c.removeAttribute("filter"),""===b||d&&!d.filter)||(c.filter=Mb.test(f)?f.replace(Mb,e):f+" "+e)}}),m.cssHooks.marginRight=Lb(k.reliableMarginRight,function(a,b){return b?m.swap(a,{display:"inline-block"},Jb,[a,"marginRight"]):void 0}),m.each({margin:"",padding:"",border:"Width"},function(a,b){m.cssHooks[a+b]={expand:function(c){for(var d=0,e={},f="string"==typeof c?c.split(" "):[c];4>d;d++)e[a+T[d]+b]=f[d]||f[d-2]||f[0];return e}},Gb.test(a)||(m.cssHooks[a+b].set=Wb)}),m.fn.extend({css:function(a,b){return V(this,function(a,b,c){var d,e,f={},g=0;if(m.isArray(b)){for(d=Ib(a),e=b.length;e>g;g++)f[b[g]]=m.css(a,b[g],!1,d);return f}return void 0!==c?m.style(a,b,c):m.css(a,b)},a,b,arguments.length>1)},show:function(){return Vb(this,!0)},hide:function(){return Vb(this)},toggle:function(a){return"boolean"==typeof a?a?this.show():this.hide():this.each(function(){U(this)?m(this).show():m(this).hide()})}});function Zb(a,b,c,d,e){return new Zb.prototype.init(a,b,c,d,e)}m.Tween=Zb,Zb.prototype={constructor:Zb,init:function(a,b,c,d,e,f){this.elem=a,this.prop=c,this.easing=e||"swing",this.options=b,this.start=this.now=this.cur(),this.end=d,this.unit=f||(m.cssNumber[c]?"":"px")
  364. },cur:function(){var a=Zb.propHooks[this.prop];return a&&a.get?a.get(this):Zb.propHooks._default.get(this)},run:function(a){var b,c=Zb.propHooks[this.prop];return this.pos=b=this.options.duration?m.easing[this.easing](a,this.options.duration*a,0,1,this.options.duration):a,this.now=(this.end-this.start)*b+this.start,this.options.step&&this.options.step.call(this.elem,this.now,this),c&&c.set?c.set(this):Zb.propHooks._default.set(this),this}},Zb.prototype.init.prototype=Zb.prototype,Zb.propHooks={_default:{get:function(a){var b;return null==a.elem[a.prop]||a.elem.style&&null!=a.elem.style[a.prop]?(b=m.css(a.elem,a.prop,""),b&&"auto"!==b?b:0):a.elem[a.prop]},set:function(a){m.fx.step[a.prop]?m.fx.step[a.prop](a):a.elem.style&&(null!=a.elem.style[m.cssProps[a.prop]]||m.cssHooks[a.prop])?m.style(a.elem,a.prop,a.now+a.unit):a.elem[a.prop]=a.now}}},Zb.propHooks.scrollTop=Zb.propHooks.scrollLeft={set:function(a){a.elem.nodeType&&a.elem.parentNode&&(a.elem[a.prop]=a.now)}},m.easing={linear:function(a){return a},swing:function(a){return.5-Math.cos(a*Math.PI)/2}},m.fx=Zb.prototype.init,m.fx.step={};var $b,_b,ac=/^(?:toggle|show|hide)$/,bc=new RegExp("^(?:([+-])=|)("+S+")([a-z%]*)$","i"),cc=/queueHooks$/,dc=[ic],ec={"*":[function(a,b){var c=this.createTween(a,b),d=c.cur(),e=bc.exec(b),f=e&&e[3]||(m.cssNumber[a]?"":"px"),g=(m.cssNumber[a]||"px"!==f&&+d)&&bc.exec(m.css(c.elem,a)),h=1,i=20;if(g&&g[3]!==f){f=f||g[3],e=e||[],g=+d||1;do h=h||".5",g/=h,m.style(c.elem,a,g+f);while(h!==(h=c.cur()/d)&&1!==h&&--i)}return e&&(g=c.start=+g||+d||0,c.unit=f,c.end=e[1]?g+(e[1]+1)*e[2]:+e[2]),c}]};function fc(){return setTimeout(function(){$b=void 0}),$b=m.now()}function gc(a,b){var c,d={height:a},e=0;for(b=b?1:0;4>e;e+=2-b)c=T[e],d["margin"+c]=d["padding"+c]=a;return b&&(d.opacity=d.width=a),d}function hc(a,b,c){for(var d,e=(ec[b]||[]).concat(ec["*"]),f=0,g=e.length;g>f;f++)if(d=e[f].call(c,b,a))return d}function ic(a,b,c){var d,e,f,g,h,i,j,l,n=this,o={},p=a.style,q=a.nodeType&&U(a),r=m._data(a,"fxshow");c.queue||(h=m._queueHooks(a,"fx"),null==h.unqueued&&(h.unqueued=0,i=h.empty.fire,h.empty.fire=function(){h.unqueued||i()}),h.unqueued++,n.always(function(){n.always(function(){h.unqueued--,m.queue(a,"fx").length||h.empty.fire()})})),1===a.nodeType&&("height"in b||"width"in b)&&(c.overflow=[p.overflow,p.overflowX,p.overflowY],j=m.css(a,"display"),l="none"===j?m._data(a,"olddisplay")||Fb(a.nodeName):j,"inline"===l&&"none"===m.css(a,"float")&&(k.inlineBlockNeedsLayout&&"inline"!==Fb(a.nodeName)?p.zoom=1:p.display="inline-block")),c.overflow&&(p.overflow="hidden",k.shrinkWrapBlocks()||n.always(function(){p.overflow=c.overflow[0],p.overflowX=c.overflow[1],p.overflowY=c.overflow[2]}));for(d in b)if(e=b[d],ac.exec(e)){if(delete b[d],f=f||"toggle"===e,e===(q?"hide":"show")){if("show"!==e||!r||void 0===r[d])continue;q=!0}o[d]=r&&r[d]||m.style(a,d)}else j=void 0;if(m.isEmptyObject(o))"inline"===("none"===j?Fb(a.nodeName):j)&&(p.display=j);else{r?"hidden"in r&&(q=r.hidden):r=m._data(a,"fxshow",{}),f&&(r.hidden=!q),q?m(a).show():n.done(function(){m(a).hide()}),n.done(function(){var b;m._removeData(a,"fxshow");for(b in o)m.style(a,b,o[b])});for(d in o)g=hc(q?r[d]:0,d,n),d in r||(r[d]=g.start,q&&(g.end=g.start,g.start="width"===d||"height"===d?1:0))}}function jc(a,b){var c,d,e,f,g;for(c in a)if(d=m.camelCase(c),e=b[d],f=a[c],m.isArray(f)&&(e=f[1],f=a[c]=f[0]),c!==d&&(a[d]=f,delete a[c]),g=m.cssHooks[d],g&&"expand"in g){f=g.expand(f),delete a[d];for(c in f)c in a||(a[c]=f[c],b[c]=e)}else b[d]=e}function kc(a,b,c){var d,e,f=0,g=dc.length,h=m.Deferred().always(function(){delete i.elem}),i=function(){if(e)return!1;for(var b=$b||fc(),c=Math.max(0,j.startTime+j.duration-b),d=c/j.duration||0,f=1-d,g=0,i=j.tweens.length;i>g;g++)j.tweens[g].run(f);return h.notifyWith(a,[j,f,c]),1>f&&i?c:(h.resolveWith(a,[j]),!1)},j=h.promise({elem:a,props:m.extend({},b),opts:m.extend(!0,{specialEasing:{}},c),originalProperties:b,originalOptions:c,startTime:$b||fc(),duration:c.duration,tweens:[],createTween:function(b,c){var d=m.Tween(a,j.opts,b,c,j.opts.specialEasing[b]||j.opts.easing);return j.tweens.push(d),d},stop:function(b){var c=0,d=b?j.tweens.length:0;if(e)return this;for(e=!0;d>c;c++)j.tweens[c].run(1);return b?h.resolveWith(a,[j,b]):h.rejectWith(a,[j,b]),this}}),k=j.props;for(jc(k,j.opts.specialEasing);g>f;f++)if(d=dc[f].call(j,a,k,j.opts))return d;return m.map(k,hc,j),m.isFunction(j.opts.start)&&j.opts.start.call(a,j),m.fx.timer(m.extend(i,{elem:a,anim:j,queue:j.opts.queue})),j.progress(j.opts.progress).done(j.opts.done,j.opts.complete).fail(j.opts.fail).always(j.opts.always)}m.Animation=m.extend(kc,{tweener:function(a,b){m.isFunction(a)?(b=a,a=["*"]):a=a.split(" ");for(var c,d=0,e=a.length;e>d;d++)c=a[d],ec[c]=ec[c]||[],ec[c].unshift(b)},prefilter:function(a,b){b?dc.unshift(a):dc.push(a)}}),m.speed=function(a,b,c){var d=a&&"object"==typeof a?m.extend({},a):{complete:c||!c&&b||m.isFunction(a)&&a,duration:a,easing:c&&b||b&&!m.isFunction(b)&&b};return d.duration=m.fx.off?0:"number"==typeof d.duration?d.duration:d.duration in m.fx.speeds?m.fx.speeds[d.duration]:m.fx.speeds._default,(null==d.queue||d.queue===!0)&&(d.queue="fx"),d.old=d.complete,d.complete=function(){m.isFunction(d.old)&&d.old.call(this),d.queue&&m.dequeue(this,d.queue)},d},m.fn.extend({fadeTo:function(a,b,c,d){return this.filter(U).css("opacity",0).show().end().animate({opacity:b},a,c,d)},animate:function(a,b,c,d){var e=m.isEmptyObject(a),f=m.speed(b,c,d),g=function(){var b=kc(this,m.extend({},a),f);(e||m._data(this,"finish"))&&b.stop(!0)};return g.finish=g,e||f.queue===!1?this.each(g):this.queue(f.queue,g)},stop:function(a,b,c){var d=function(a){var b=a.stop;delete a.stop,b(c)};return"string"!=typeof a&&(c=b,b=a,a=void 0),b&&a!==!1&&this.queue(a||"fx",[]),this.each(function(){var b=!0,e=null!=a&&a+"queueHooks",f=m.timers,g=m._data(this);if(e)g[e]&&g[e].stop&&d(g[e]);else for(e in g)g[e]&&g[e].stop&&cc.test(e)&&d(g[e]);for(e=f.length;e--;)f[e].elem!==this||null!=a&&f[e].queue!==a||(f[e].anim.stop(c),b=!1,f.splice(e,1));(b||!c)&&m.dequeue(this,a)})},finish:function(a){return a!==!1&&(a=a||"fx"),this.each(function(){var b,c=m._data(this),d=c[a+"queue"],e=c[a+"queueHooks"],f=m.timers,g=d?d.length:0;for(c.finish=!0,m.queue(this,a,[]),e&&e.stop&&e.stop.call(this,!0),b=f.length;b--;)f[b].elem===this&&f[b].queue===a&&(f[b].anim.stop(!0),f.splice(b,1));for(b=0;g>b;b++)d[b]&&d[b].finish&&d[b].finish.call(this);delete c.finish})}}),m.each(["toggle","show","hide"],function(a,b){var c=m.fn[b];m.fn[b]=function(a,d,e){return null==a||"boolean"==typeof a?c.apply(this,arguments):this.animate(gc(b,!0),a,d,e)}}),m.each({slideDown:gc("show"),slideUp:gc("hide"),slideToggle:gc("toggle"),fadeIn:{opacity:"show"},fadeOut:{opacity:"hide"},fadeToggle:{opacity:"toggle"}},function(a,b){m.fn[a]=function(a,c,d){return this.animate(b,a,c,d)}}),m.timers=[],m.fx.tick=function(){var a,b=m.timers,c=0;for($b=m.now();c<b.length;c++)a=b[c],a()||b[c]!==a||b.splice(c--,1);b.length||m.fx.stop(),$b=void 0},m.fx.timer=function(a){m.timers.push(a),a()?m.fx.start():m.timers.pop()},m.fx.interval=13,m.fx.start=function(){_b||(_b=setInterval(m.fx.tick,m.fx.interval))},m.fx.stop=function(){clearInterval(_b),_b=null},m.fx.speeds={slow:600,fast:200,_default:400},m.fn.delay=function(a,b){return a=m.fx?m.fx.speeds[a]||a:a,b=b||"fx",this.queue(b,function(b,c){var d=setTimeout(b,a);c.stop=function(){clearTimeout(d)}})},function(){var a,b,c,d,e;b=y.createElement("div"),b.setAttribute("className","t"),b.innerHTML=" <link/><table></table><a href='/a'>a</a><input type='checkbox'/>",d=b.getElementsByTagName("a")[0],c=y.createElement("select"),e=c.appendChild(y.createElement("option")),a=b.getElementsByTagName("input")[0],d.style.cssText="top:1px",k.getSetAttribute="t"!==b.className,k.style=/top/.test(d.getAttribute("style")),k.hrefNormalized="/a"===d.getAttribute("href"),k.checkOn=!!a.value,k.optSelected=e.selected,k.enctype=!!y.createElement("form").enctype,c.disabled=!0,k.optDisabled=!e.disabled,a=y.createElement("input"),a.setAttribute("value",""),k.input=""===a.getAttribute("value"),a.value="t",a.setAttribute("type","radio"),k.radioValue="t"===a.value}();var lc=/\r/g;m.fn.extend({val:function(a){var b,c,d,e=this[0];{if(arguments.length)return d=m.isFunction(a),this.each(function(c){var e;1===this.nodeType&&(e=d?a.call(this,c,m(this).val()):a,null==e?e="":"number"==typeof e?e+="":m.isArray(e)&&(e=m.map(e,function(a){return null==a?"":a+""})),b=m.valHooks[this.type]||m.valHooks[this.nodeName.toLowerCase()],b&&"set"in b&&void 0!==b.set(this,e,"value")||(this.value=e))});if(e)return b=m.valHooks[e.type]||m.valHooks[e.nodeName.toLowerCase()],b&&"get"in b&&void 0!==(c=b.get(e,"value"))?c:(c=e.value,"string"==typeof c?c.replace(lc,""):null==c?"":c)}}}),m.extend({valHooks:{option:{get:function(a){var b=m.find.attr(a,"value");return null!=b?b:m.trim(m.text(a))}},select:{get:function(a){for(var b,c,d=a.options,e=a.selectedIndex,f="select-one"===a.type||0>e,g=f?null:[],h=f?e+1:d.length,i=0>e?h:f?e:0;h>i;i++)if(c=d[i],!(!c.selected&&i!==e||(k.optDisabled?c.disabled:null!==c.getAttribute("disabled"))||c.parentNode.disabled&&m.nodeName(c.parentNode,"optgroup"))){if(b=m(c).val(),f)return b;g.push(b)}return g},set:function(a,b){var c,d,e=a.options,f=m.makeArray(b),g=e.length;while(g--)if(d=e[g],m.inArray(m.valHooks.option.get(d),f)>=0)try{d.selected=c=!0}catch(h){d.scrollHeight}else d.selected=!1;return c||(a.selectedIndex=-1),e}}}}),m.each(["radio","checkbox"],function(){m.valHooks[this]={set:function(a,b){return m.isArray(b)?a.checked=m.inArray(m(a).val(),b)>=0:void 0}},k.checkOn||(m.valHooks[this].get=function(a){return null===a.getAttribute("value")?"on":a.value})});var mc,nc,oc=m.expr.attrHandle,pc=/^(?:checked|selected)$/i,qc=k.getSetAttribute,rc=k.input;m.fn.extend({attr:function(a,b){return V(this,m.attr,a,b,arguments.length>1)},removeAttr:function(a){return this.each(function(){m.removeAttr(this,a)})}}),m.extend({attr:function(a,b,c){var d,e,f=a.nodeType;if(a&&3!==f&&8!==f&&2!==f)return typeof a.getAttribute===K?m.prop(a,b,c):(1===f&&m.isXMLDoc(a)||(b=b.toLowerCase(),d=m.attrHooks[b]||(m.expr.match.bool.test(b)?nc:mc)),void 0===c?d&&"get"in d&&null!==(e=d.get(a,b))?e:(e=m.find.attr(a,b),null==e?void 0:e):null!==c?d&&"set"in d&&void 0!==(e=d.set(a,c,b))?e:(a.setAttribute(b,c+""),c):void m.removeAttr(a,b))},removeAttr:function(a,b){var c,d,e=0,f=b&&b.match(E);if(f&&1===a.nodeType)while(c=f[e++])d=m.propFix[c]||c,m.expr.match.bool.test(c)?rc&&qc||!pc.test(c)?a[d]=!1:a[m.camelCase("default-"+c)]=a[d]=!1:m.attr(a,c,""),a.removeAttribute(qc?c:d)},attrHooks:{type:{set:function(a,b){if(!k.radioValue&&"radio"===b&&m.nodeName(a,"input")){var c=a.value;return a.setAttribute("type",b),c&&(a.value=c),b}}}}}),nc={set:function(a,b,c){return b===!1?m.removeAttr(a,c):rc&&qc||!pc.test(c)?a.setAttribute(!qc&&m.propFix[c]||c,c):a[m.camelCase("default-"+c)]=a[c]=!0,c}},m.each(m.expr.match.bool.source.match(/\w+/g),function(a,b){var c=oc[b]||m.find.attr;oc[b]=rc&&qc||!pc.test(b)?function(a,b,d){var e,f;return d||(f=oc[b],oc[b]=e,e=null!=c(a,b,d)?b.toLowerCase():null,oc[b]=f),e}:function(a,b,c){return c?void 0:a[m.camelCase("default-"+b)]?b.toLowerCase():null}}),rc&&qc||(m.attrHooks.value={set:function(a,b,c){return m.nodeName(a,"input")?void(a.defaultValue=b):mc&&mc.set(a,b,c)}}),qc||(mc={set:function(a,b,c){var d=a.getAttributeNode(c);return d||a.setAttributeNode(d=a.ownerDocument.createAttribute(c)),d.value=b+="","value"===c||b===a.getAttribute(c)?b:void 0}},oc.id=oc.name=oc.coords=function(a,b,c){var d;return c?void 0:(d=a.getAttributeNode(b))&&""!==d.value?d.value:null},m.valHooks.button={get:function(a,b){var c=a.getAttributeNode(b);return c&&c.specified?c.value:void 0},set:mc.set},m.attrHooks.contenteditable={set:function(a,b,c){mc.set(a,""===b?!1:b,c)}},m.each(["width","height"],function(a,b){m.attrHooks[b]={set:function(a,c){return""===c?(a.setAttribute(b,"auto"),c):void 0}}})),k.style||(m.attrHooks.style={get:function(a){return a.style.cssText||void 0},set:function(a,b){return a.style.cssText=b+""}});var sc=/^(?:input|select|textarea|button|object)$/i,tc=/^(?:a|area)$/i;m.fn.extend({prop:function(a,b){return V(this,m.prop,a,b,arguments.length>1)},removeProp:function(a){return a=m.propFix[a]||a,this.each(function(){try{this[a]=void 0,delete this[a]}catch(b){}})}}),m.extend({propFix:{"for":"htmlFor","class":"className"},prop:function(a,b,c){var d,e,f,g=a.nodeType;if(a&&3!==g&&8!==g&&2!==g)return f=1!==g||!m.isXMLDoc(a),f&&(b=m.propFix[b]||b,e=m.propHooks[b]),void 0!==c?e&&"set"in e&&void 0!==(d=e.set(a,c,b))?d:a[b]=c:e&&"get"in e&&null!==(d=e.get(a,b))?d:a[b]},propHooks:{tabIndex:{get:function(a){var b=m.find.attr(a,"tabindex");return b?parseInt(b,10):sc.test(a.nodeName)||tc.test(a.nodeName)&&a.href?0:-1}}}}),k.hrefNormalized||m.each(["href","src"],function(a,b){m.propHooks[b]={get:function(a){return a.getAttribute(b,4)}}}),k.optSelected||(m.propHooks.selected={get:function(a){var b=a.parentNode;return b&&(b.selectedIndex,b.parentNode&&b.parentNode.selectedIndex),null}}),m.each(["tabIndex","readOnly","maxLength","cellSpacing","cellPadding","rowSpan","colSpan","useMap","frameBorder","contentEditable"],function(){m.propFix[this.toLowerCase()]=this}),k.enctype||(m.propFix.enctype="encoding");var uc=/[\t\r\n\f]/g;m.fn.extend({addClass:function(a){var b,c,d,e,f,g,h=0,i=this.length,j="string"==typeof a&&a;if(m.isFunction(a))return this.each(function(b){m(this).addClass(a.call(this,b,this.className))});if(j)for(b=(a||"").match(E)||[];i>h;h++)if(c=this[h],d=1===c.nodeType&&(c.className?(" "+c.className+" ").replace(uc," "):" ")){f=0;while(e=b[f++])d.indexOf(" "+e+" ")<0&&(d+=e+" ");g=m.trim(d),c.className!==g&&(c.className=g)}return this},removeClass:function(a){var b,c,d,e,f,g,h=0,i=this.length,j=0===arguments.length||"string"==typeof a&&a;if(m.isFunction(a))return this.each(function(b){m(this).removeClass(a.call(this,b,this.className))});if(j)for(b=(a||"").match(E)||[];i>h;h++)if(c=this[h],d=1===c.nodeType&&(c.className?(" "+c.className+" ").replace(uc," "):"")){f=0;while(e=b[f++])while(d.indexOf(" "+e+" ")>=0)d=d.replace(" "+e+" "," ");g=a?m.trim(d):"",c.className!==g&&(c.className=g)}return this},toggleClass:function(a,b){var c=typeof a;return"boolean"==typeof b&&"string"===c?b?this.addClass(a):this.removeClass(a):this.each(m.isFunction(a)?function(c){m(this).toggleClass(a.call(this,c,this.className,b),b)}:function(){if("string"===c){var b,d=0,e=m(this),f=a.match(E)||[];while(b=f[d++])e.hasClass(b)?e.removeClass(b):e.addClass(b)}else(c===K||"boolean"===c)&&(this.className&&m._data(this,"__className__",this.className),this.className=this.className||a===!1?"":m._data(this,"__className__")||"")})},hasClass:function(a){for(var b=" "+a+" ",c=0,d=this.length;d>c;c++)if(1===this[c].nodeType&&(" "+this[c].className+" ").replace(uc," ").indexOf(b)>=0)return!0;return!1}}),m.each("blur focus focusin focusout load resize scroll unload click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup error contextmenu".split(" "),function(a,b){m.fn[b]=function(a,c){return arguments.length>0?this.on(b,null,a,c):this.trigger(b)}}),m.fn.extend({hover:function(a,b){return this.mouseenter(a).mouseleave(b||a)},bind:function(a,b,c){return this.on(a,null,b,c)},unbind:function(a,b){return this.off(a,null,b)},delegate:function(a,b,c,d){return this.on(b,a,c,d)},undelegate:function(a,b,c){return 1===arguments.length?this.off(a,"**"):this.off(b,a||"**",c)}});var vc=m.now(),wc=/\?/,xc=/(,)|(\[|{)|(}|])|"(?:[^"\\\r\n]|\\["\\\/bfnrt]|\\u[\da-fA-F]{4})*"\s*:?|true|false|null|-?(?!0\d)\d+(?:\.\d+|)(?:[eE][+-]?\d+|)/g;m.parseJSON=function(b){if(a.JSON&&a.JSON.parse)return a.JSON.parse(b+"");var c,d=null,e=m.trim(b+"");return e&&!m.trim(e.replace(xc,function(a,b,e,f){return c&&b&&(d=0),0===d?a:(c=e||b,d+=!f-!e,"")}))?Function("return "+e)():m.error("Invalid JSON: "+b)},m.parseXML=function(b){var c,d;if(!b||"string"!=typeof b)return null;try{a.DOMParser?(d=new DOMParser,c=d.parseFromString(b,"text/xml")):(c=new ActiveXObject("Microsoft.XMLDOM"),c.async="false",c.loadXML(b))}catch(e){c=void 0}return c&&c.documentElement&&!c.getElementsByTagName("parsererror").length||m.error("Invalid XML: "+b),c};var yc,zc,Ac=/#.*$/,Bc=/([?&])_=[^&]*/,Cc=/^(.*?):[ \t]*([^\r\n]*)\r?$/gm,Dc=/^(?:about|app|app-storage|.+-extension|file|res|widget):$/,Ec=/^(?:GET|HEAD)$/,Fc=/^\/\//,Gc=/^([\w.+-]+:)(?:\/\/(?:[^\/?#]*@|)([^\/?#:]*)(?::(\d+)|)|)/,Hc={},Ic={},Jc="*/".concat("*");try{zc=location.href}catch(Kc){zc=y.createElement("a"),zc.href="",zc=zc.href}yc=Gc.exec(zc.toLowerCase())||[];function Lc(a){return function(b,c){"string"!=typeof b&&(c=b,b="*");var d,e=0,f=b.toLowerCase().match(E)||[];if(m.isFunction(c))while(d=f[e++])"+"===d.charAt(0)?(d=d.slice(1)||"*",(a[d]=a[d]||[]).unshift(c)):(a[d]=a[d]||[]).push(c)}}function Mc(a,b,c,d){var e={},f=a===Ic;function g(h){var i;return e[h]=!0,m.each(a[h]||[],function(a,h){var j=h(b,c,d);return"string"!=typeof j||f||e[j]?f?!(i=j):void 0:(b.dataTypes.unshift(j),g(j),!1)}),i}return g(b.dataTypes[0])||!e["*"]&&g("*")}function Nc(a,b){var c,d,e=m.ajaxSettings.flatOptions||{};for(d in b)void 0!==b[d]&&((e[d]?a:c||(c={}))[d]=b[d]);return c&&m.extend(!0,a,c),a}function Oc(a,b,c){var d,e,f,g,h=a.contents,i=a.dataTypes;while("*"===i[0])i.shift(),void 0===e&&(e=a.mimeType||b.getResponseHeader("Content-Type"));if(e)for(g in h)if(h[g]&&h[g].test(e)){i.unshift(g);break}if(i[0]in c)f=i[0];else{for(g in c){if(!i[0]||a.converters[g+" "+i[0]]){f=g;break}d||(d=g)}f=f||d}return f?(f!==i[0]&&i.unshift(f),c[f]):void 0}function Pc(a,b,c,d){var e,f,g,h,i,j={},k=a.dataTypes.slice();if(k[1])for(g in a.converters)j[g.toLowerCase()]=a.converters[g];f=k.shift();while(f)if(a.responseFields[f]&&(c[a.responseFields[f]]=b),!i&&d&&a.dataFilter&&(b=a.dataFilter(b,a.dataType)),i=f,f=k.shift())if("*"===f)f=i;else if("*"!==i&&i!==f){if(g=j[i+" "+f]||j["* "+f],!g)for(e in j)if(h=e.split(" "),h[1]===f&&(g=j[i+" "+h[0]]||j["* "+h[0]])){g===!0?g=j[e]:j[e]!==!0&&(f=h[0],k.unshift(h[1]));break}if(g!==!0)if(g&&a["throws"])b=g(b);else try{b=g(b)}catch(l){return{state:"parsererror",error:g?l:"No conversion from "+i+" to "+f}}}return{state:"success",data:b}}m.extend({active:0,lastModified:{},etag:{},ajaxSettings:{url:zc,type:"GET",isLocal:Dc.test(yc[1]),global:!0,processData:!0,async:!0,contentType:"application/x-www-form-urlencoded; charset=UTF-8",accepts:{"*":Jc,text:"text/plain",html:"text/html",xml:"application/xml, text/xml",json:"application/json, text/javascript"},contents:{xml:/xml/,html:/html/,json:/json/},responseFields:{xml:"responseXML",text:"responseText",json:"responseJSON"},converters:{"* text":String,"text html":!0,"text json":m.parseJSON,"text xml":m.parseXML},flatOptions:{url:!0,context:!0}},ajaxSetup:function(a,b){return b?Nc(Nc(a,m.ajaxSettings),b):Nc(m.ajaxSettings,a)},ajaxPrefilter:Lc(Hc),ajaxTransport:Lc(Ic),ajax:function(a,b){"object"==typeof a&&(b=a,a=void 0),b=b||{};var c,d,e,f,g,h,i,j,k=m.ajaxSetup({},b),l=k.context||k,n=k.context&&(l.nodeType||l.jquery)?m(l):m.event,o=m.Deferred(),p=m.Callbacks("once memory"),q=k.statusCode||{},r={},s={},t=0,u="canceled",v={readyState:0,getResponseHeader:function(a){var b;if(2===t){if(!j){j={};while(b=Cc.exec(f))j[b[1].toLowerCase()]=b[2]}b=j[a.toLowerCase()]}return null==b?null:b},getAllResponseHeaders:function(){return 2===t?f:null},setRequestHeader:function(a,b){var c=a.toLowerCase();return t||(a=s[c]=s[c]||a,r[a]=b),this},overrideMimeType:function(a){return t||(k.mimeType=a),this},statusCode:function(a){var b;if(a)if(2>t)for(b in a)q[b]=[q[b],a[b]];else v.always(a[v.status]);return this},abort:function(a){var b=a||u;return i&&i.abort(b),x(0,b),this}};if(o.promise(v).complete=p.add,v.success=v.done,v.error=v.fail,k.url=((a||k.url||zc)+"").replace(Ac,"").replace(Fc,yc[1]+"//"),k.type=b.method||b.type||k.method||k.type,k.dataTypes=m.trim(k.dataType||"*").toLowerCase().match(E)||[""],null==k.crossDomain&&(c=Gc.exec(k.url.toLowerCase()),k.crossDomain=!(!c||c[1]===yc[1]&&c[2]===yc[2]&&(c[3]||("http:"===c[1]?"80":"443"))===(yc[3]||("http:"===yc[1]?"80":"443")))),k.data&&k.processData&&"string"!=typeof k.data&&(k.data=m.param(k.data,k.traditional)),Mc(Hc,k,b,v),2===t)return v;h=k.global,h&&0===m.active++&&m.event.trigger("ajaxStart"),k.type=k.type.toUpperCase(),k.hasContent=!Ec.test(k.type),e=k.url,k.hasContent||(k.data&&(e=k.url+=(wc.test(e)?"&":"?")+k.data,delete k.data),k.cache===!1&&(k.url=Bc.test(e)?e.replace(Bc,"$1_="+vc++):e+(wc.test(e)?"&":"?")+"_="+vc++)),k.ifModified&&(m.lastModified[e]&&v.setRequestHeader("If-Modified-Since",m.lastModified[e]),m.etag[e]&&v.setRequestHeader("If-None-Match",m.etag[e])),(k.data&&k.hasContent&&k.contentType!==!1||b.contentType)&&v.setRequestHeader("Content-Type",k.contentType),v.setRequestHeader("Accept",k.dataTypes[0]&&k.accepts[k.dataTypes[0]]?k.accepts[k.dataTypes[0]]+("*"!==k.dataTypes[0]?", "+Jc+"; q=0.01":""):k.accepts["*"]);for(d in k.headers)v.setRequestHeader(d,k.headers[d]);if(k.beforeSend&&(k.beforeSend.call(l,v,k)===!1||2===t))return v.abort();u="abort";for(d in{success:1,error:1,complete:1})v[d](k[d]);if(i=Mc(Ic,k,b,v)){v.readyState=1,h&&n.trigger("ajaxSend",[v,k]),k.async&&k.timeout>0&&(g=setTimeout(function(){v.abort("timeout")},k.timeout));try{t=1,i.send(r,x)}catch(w){if(!(2>t))throw w;x(-1,w)}}else x(-1,"No Transport");function x(a,b,c,d){var j,r,s,u,w,x=b;2!==t&&(t=2,g&&clearTimeout(g),i=void 0,f=d||"",v.readyState=a>0?4:0,j=a>=200&&300>a||304===a,c&&(u=Oc(k,v,c)),u=Pc(k,u,v,j),j?(k.ifModified&&(w=v.getResponseHeader("Last-Modified"),w&&(m.lastModified[e]=w),w=v.getResponseHeader("etag"),w&&(m.etag[e]=w)),204===a||"HEAD"===k.type?x="nocontent":304===a?x="notmodified":(x=u.state,r=u.data,s=u.error,j=!s)):(s=x,(a||!x)&&(x="error",0>a&&(a=0))),v.status=a,v.statusText=(b||x)+"",j?o.resolveWith(l,[r,x,v]):o.rejectWith(l,[v,x,s]),v.statusCode(q),q=void 0,h&&n.trigger(j?"ajaxSuccess":"ajaxError",[v,k,j?r:s]),p.fireWith(l,[v,x]),h&&(n.trigger("ajaxComplete",[v,k]),--m.active||m.event.trigger("ajaxStop")))}return v},getJSON:function(a,b,c){return m.get(a,b,c,"json")},getScript:function(a,b){return m.get(a,void 0,b,"script")}}),m.each(["get","post"],function(a,b){m[b]=function(a,c,d,e){return m.isFunction(c)&&(e=e||d,d=c,c=void 0),m.ajax({url:a,type:b,dataType:e,data:c,success:d})}}),m.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(a,b){m.fn[b]=function(a){return this.on(b,a)}}),m._evalUrl=function(a){return m.ajax({url:a,type:"GET",dataType:"script",async:!1,global:!1,"throws":!0})},m.fn.extend({wrapAll:function(a){if(m.isFunction(a))return this.each(function(b){m(this).wrapAll(a.call(this,b))});if(this[0]){var b=m(a,this[0].ownerDocument).eq(0).clone(!0);this[0].parentNode&&b.insertBefore(this[0]),b.map(function(){var a=this;while(a.firstChild&&1===a.firstChild.nodeType)a=a.firstChild;return a}).append(this)}return this},wrapInner:function(a){return this.each(m.isFunction(a)?function(b){m(this).wrapInner(a.call(this,b))}:function(){var b=m(this),c=b.contents();c.length?c.wrapAll(a):b.append(a)})},wrap:function(a){var b=m.isFunction(a);return this.each(function(c){m(this).wrapAll(b?a.call(this,c):a)})},unwrap:function(){return this.parent().each(function(){m.nodeName(this,"body")||m(this).replaceWith(this.childNodes)}).end()}}),m.expr.filters.hidden=function(a){return a.offsetWidth<=0&&a.offsetHeight<=0||!k.reliableHiddenOffsets()&&"none"===(a.style&&a.style.display||m.css(a,"display"))},m.expr.filters.visible=function(a){return!m.expr.filters.hidden(a)};var Qc=/%20/g,Rc=/\[\]$/,Sc=/\r?\n/g,Tc=/^(?:submit|button|image|reset|file)$/i,Uc=/^(?:input|select|textarea|keygen)/i;function Vc(a,b,c,d){var e;if(m.isArray(b))m.each(b,function(b,e){c||Rc.test(a)?d(a,e):Vc(a+"["+("object"==typeof e?b:"")+"]",e,c,d)});else if(c||"object"!==m.type(b))d(a,b);else for(e in b)Vc(a+"["+e+"]",b[e],c,d)}m.param=function(a,b){var c,d=[],e=function(a,b){b=m.isFunction(b)?b():null==b?"":b,d[d.length]=encodeURIComponent(a)+"="+encodeURIComponent(b)};if(void 0===b&&(b=m.ajaxSettings&&m.ajaxSettings.traditional),m.isArray(a)||a.jquery&&!m.isPlainObject(a))m.each(a,function(){e(this.name,this.value)});else for(c in a)Vc(c,a[c],b,e);return d.join("&").replace(Qc,"+")},m.fn.extend({serialize:function(){return m.param(this.serializeArray())},serializeArray:function(){return this.map(function(){var a=m.prop(this,"elements");return a?m.makeArray(a):this}).filter(function(){var a=this.type;return this.name&&!m(this).is(":disabled")&&Uc.test(this.nodeName)&&!Tc.test(a)&&(this.checked||!W.test(a))}).map(function(a,b){var c=m(this).val();return null==c?null:m.isArray(c)?m.map(c,function(a){return{name:b.name,value:a.replace(Sc,"\r\n")}}):{name:b.name,value:c.replace(Sc,"\r\n")}}).get()}}),m.ajaxSettings.xhr=void 0!==a.ActiveXObject?function(){return!this.isLocal&&/^(get|post|head|put|delete|options)$/i.test(this.type)&&Zc()||$c()}:Zc;var Wc=0,Xc={},Yc=m.ajaxSettings.xhr();a.ActiveXObject&&m(a).on("unload",function(){for(var a in Xc)Xc[a](void 0,!0)}),k.cors=!!Yc&&"withCredentials"in Yc,Yc=k.ajax=!!Yc,Yc&&m.ajaxTransport(function(a){if(!a.crossDomain||k.cors){var b;return{send:function(c,d){var e,f=a.xhr(),g=++Wc;if(f.open(a.type,a.url,a.async,a.username,a.password),a.xhrFields)for(e in a.xhrFields)f[e]=a.xhrFields[e];a.mimeType&&f.overrideMimeType&&f.overrideMimeType(a.mimeType),a.crossDomain||c["X-Requested-With"]||(c["X-Requested-With"]="XMLHttpRequest");for(e in c)void 0!==c[e]&&f.setRequestHeader(e,c[e]+"");f.send(a.hasContent&&a.data||null),b=function(c,e){var h,i,j;if(b&&(e||4===f.readyState))if(delete Xc[g],b=void 0,f.onreadystatechange=m.noop,e)4!==f.readyState&&f.abort();else{j={},h=f.status,"string"==typeof f.responseText&&(j.text=f.responseText);try{i=f.statusText}catch(k){i=""}h||!a.isLocal||a.crossDomain?1223===h&&(h=204):h=j.text?200:404}j&&d(h,i,j,f.getAllResponseHeaders())},a.async?4===f.readyState?setTimeout(b):f.onreadystatechange=Xc[g]=b:b()},abort:function(){b&&b(void 0,!0)}}}});function Zc(){try{return new a.XMLHttpRequest}catch(b){}}function $c(){try{return new a.ActiveXObject("Microsoft.XMLHTTP")}catch(b){}}m.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/(?:java|ecma)script/},converters:{"text script":function(a){return m.globalEval(a),a}}}),m.ajaxPrefilter("script",function(a){void 0===a.cache&&(a.cache=!1),a.crossDomain&&(a.type="GET",a.global=!1)}),m.ajaxTransport("script",function(a){if(a.crossDomain){var b,c=y.head||m("head")[0]||y.documentElement;return{send:function(d,e){b=y.createElement("script"),b.async=!0,a.scriptCharset&&(b.charset=a.scriptCharset),b.src=a.url,b.onload=b.onreadystatechange=function(a,c){(c||!b.readyState||/loaded|complete/.test(b.readyState))&&(b.onload=b.onreadystatechange=null,b.parentNode&&b.parentNode.removeChild(b),b=null,c||e(200,"success"))},c.insertBefore(b,c.firstChild)},abort:function(){b&&b.onload(void 0,!0)}}}});var _c=[],ad=/(=)\?(?=&|$)|\?\?/;m.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var a=_c.pop()||m.expando+"_"+vc++;return this[a]=!0,a}}),m.ajaxPrefilter("json jsonp",function(b,c,d){var e,f,g,h=b.jsonp!==!1&&(ad.test(b.url)?"url":"string"==typeof b.data&&!(b.contentType||"").indexOf("application/x-www-form-urlencoded")&&ad.test(b.data)&&"data");return h||"jsonp"===b.dataTypes[0]?(e=b.jsonpCallback=m.isFunction(b.jsonpCallback)?b.jsonpCallback():b.jsonpCallback,h?b[h]=b[h].replace(ad,"$1"+e):b.jsonp!==!1&&(b.url+=(wc.test(b.url)?"&":"?")+b.jsonp+"="+e),b.converters["script json"]=function(){return g||m.error(e+" was not called"),g[0]},b.dataTypes[0]="json",f=a[e],a[e]=function(){g=arguments},d.always(function(){a[e]=f,b[e]&&(b.jsonpCallback=c.jsonpCallback,_c.push(e)),g&&m.isFunction(f)&&f(g[0]),g=f=void 0}),"script"):void 0}),m.parseHTML=function(a,b,c){if(!a||"string"!=typeof a)return null;"boolean"==typeof b&&(c=b,b=!1),b=b||y;var d=u.exec(a),e=!c&&[];return d?[b.createElement(d[1])]:(d=m.buildFragment([a],b,e),e&&e.length&&m(e).remove(),m.merge([],d.childNodes))};var bd=m.fn.load;m.fn.load=function(a,b,c){if("string"!=typeof a&&bd)return bd.apply(this,arguments);var d,e,f,g=this,h=a.indexOf(" ");return h>=0&&(d=m.trim(a.slice(h,a.length)),a=a.slice(0,h)),m.isFunction(b)?(c=b,b=void 0):b&&"object"==typeof b&&(f="POST"),g.length>0&&m.ajax({url:a,type:f,dataType:"html",data:b}).done(function(a){e=arguments,g.html(d?m("<div>").append(m.parseHTML(a)).find(d):a)}).complete(c&&function(a,b){g.each(c,e||[a.responseText,b,a])}),this},m.expr.filters.animated=function(a){return m.grep(m.timers,function(b){return a===b.elem}).length};var cd=a.document.documentElement;function dd(a){return m.isWindow(a)?a:9===a.nodeType?a.defaultView||a.parentWindow:!1}m.offset={setOffset:function(a,b,c){var d,e,f,g,h,i,j,k=m.css(a,"position"),l=m(a),n={};"static"===k&&(a.style.position="relative"),h=l.offset(),f=m.css(a,"top"),i=m.css(a,"left"),j=("absolute"===k||"fixed"===k)&&m.inArray("auto",[f,i])>-1,j?(d=l.position(),g=d.top,e=d.left):(g=parseFloat(f)||0,e=parseFloat(i)||0),m.isFunction(b)&&(b=b.call(a,c,h)),null!=b.top&&(n.top=b.top-h.top+g),null!=b.left&&(n.left=b.left-h.left+e),"using"in b?b.using.call(a,n):l.css(n)}},m.fn.extend({offset:function(a){if(arguments.length)return void 0===a?this:this.each(function(b){m.offset.setOffset(this,a,b)});var b,c,d={top:0,left:0},e=this[0],f=e&&e.ownerDocument;if(f)return b=f.documentElement,m.contains(b,e)?(typeof e.getBoundingClientRect!==K&&(d=e.getBoundingClientRect()),c=dd(f),{top:d.top+(c.pageYOffset||b.scrollTop)-(b.clientTop||0),left:d.left+(c.pageXOffset||b.scrollLeft)-(b.clientLeft||0)}):d},position:function(){if(this[0]){var a,b,c={top:0,left:0},d=this[0];return"fixed"===m.css(d,"position")?b=d.getBoundingClientRect():(a=this.offsetParent(),b=this.offset(),m.nodeName(a[0],"html")||(c=a.offset()),c.top+=m.css(a[0],"borderTopWidth",!0),c.left+=m.css(a[0],"borderLeftWidth",!0)),{top:b.top-c.top-m.css(d,"marginTop",!0),left:b.left-c.left-m.css(d,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var a=this.offsetParent||cd;while(a&&!m.nodeName(a,"html")&&"static"===m.css(a,"position"))a=a.offsetParent;return a||cd})}}),m.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(a,b){var c=/Y/.test(b);m.fn[a]=function(d){return V(this,function(a,d,e){var f=dd(a);return void 0===e?f?b in f?f[b]:f.document.documentElement[d]:a[d]:void(f?f.scrollTo(c?m(f).scrollLeft():e,c?e:m(f).scrollTop()):a[d]=e)},a,d,arguments.length,null)}}),m.each(["top","left"],function(a,b){m.cssHooks[b]=Lb(k.pixelPosition,function(a,c){return c?(c=Jb(a,b),Hb.test(c)?m(a).position()[b]+"px":c):void 0})}),m.each({Height:"height",Width:"width"},function(a,b){m.each({padding:"inner"+a,content:b,"":"outer"+a},function(c,d){m.fn[d]=function(d,e){var f=arguments.length&&(c||"boolean"!=typeof d),g=c||(d===!0||e===!0?"margin":"border");return V(this,function(b,c,d){var e;return m.isWindow(b)?b.document.documentElement["client"+a]:9===b.nodeType?(e=b.documentElement,Math.max(b.body["scroll"+a],e["scroll"+a],b.body["offset"+a],e["offset"+a],e["client"+a])):void 0===d?m.css(b,c,g):m.style(b,c,d,g)},b,f?d:void 0,f,null)}})}),m.fn.size=function(){return this.length},m.fn.andSelf=m.fn.addBack,"function"==typeof define&&define.amd&&define("jquery",[],function(){return m});var ed=a.jQuery,fd=a.$;return m.noConflict=function(b){return a.$===m&&(a.$=fd),b&&a.jQuery===m&&(a.jQuery=ed),m},typeof b===K&&(a.jQuery=a.$=m),m});
  365. /* This Source Code Form is subject to the terms of the Mozilla Public
  366. * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  367. * You can obtain one at http://mozilla.org/MPL/2.0/. */
  368. define('jqueryPlugins',["jquery"], function ($) {
  369. // This isn't really a "module" since it just patches jQuery itself
  370. // FIX ME Animations TO DO
  371. // walkthrough animations go here
  372. // animate participant cursor and box popping in when they enter the session
  373. // animate participant cursor and box popping out when they leave the session
  374. // animate the participant cursor -> rotate down when they're down the page
  375. $.fn.rotateCursorDown = function () {
  376. $('svg').animate({borderSpacing: -150, opacity: 1}, {
  377. step: function(now, fx) {
  378. if (fx.prop == "borderSpacing") {
  379. $(this).css('-webkit-transform', 'rotate('+now+'deg)')
  380. .css('-moz-transform', 'rotate('+now+'deg)')
  381. .css('-ms-transform', 'rotate('+now+'deg)')
  382. .css('-o-transform', 'rotate('+now+'deg)')
  383. .css('transform', 'rotate('+now+'deg)');
  384. } else {
  385. $(this).css(fx.prop, now);
  386. }
  387. },
  388. duration: 500
  389. }, 'linear').promise().then(function () {
  390. this.css('-webkit-transform', '');
  391. this.css('-moz-transform', '');
  392. this.css('-ms-transform', '');
  393. this.css('-o-transform', '');
  394. this.css('transform', '');
  395. this.css("opacity", "");
  396. });
  397. };
  398. // animate the participant cursor -> rotate up when they're on the same frame as the user
  399. $.fn.rotateCursorDown = function () {
  400. $('.togetherjs-cursor svg').animate({borderSpacing: 0, opacity: 1}, {
  401. step: function(now, fx) {
  402. if (fx.prop == "borderSpacing") {
  403. $(this).css('-webkit-transform', 'rotate('+now+'deg)')
  404. .css('-moz-transform', 'rotate('+now+'deg)')
  405. .css('-ms-transform', 'rotate('+now+'deg)')
  406. .css('-o-transform', 'rotate('+now+'deg)')
  407. .css('transform', 'rotate('+now+'deg)');
  408. } else {
  409. $(this).css(fx.prop, now);
  410. }
  411. },
  412. duration: 500
  413. }, 'linear').promise().then(function () {
  414. this.css('-webkit-transform', '');
  415. this.css('-moz-transform', '');
  416. this.css('-ms-transform', '');
  417. this.css('-o-transform', '');
  418. this.css('transform', '');
  419. this.css("opacity", "");
  420. });
  421. };
  422. // Move notification when another notification slides in //
  423. /* Pop in window from dock button: */
  424. $.fn.popinWindow = function () {
  425. //mobile popout window with no animation
  426. if($.browser.mobile) {
  427. //starting position
  428. this.css({
  429. left: "0px",
  430. opacity: 1,
  431. "zIndex": 8888
  432. });
  433. //starting position for arrow
  434. $('#togetherjs-window-pointer-right').css({
  435. left: "+=74px",
  436. opacity: 1,
  437. "zIndex": 8888
  438. });
  439. //animate arrow out
  440. $('#togetherjs-window-pointer-right').animate({
  441. opacity: 1,
  442. left: "-=78px"
  443. }, {
  444. duration:60, easing:"linear"
  445. });
  446. $('#togetherjs-window-pointer-right').queue();
  447. //bounce arrow back
  448. $('#togetherjs-window-pointer-right').animate({
  449. left:'+=4px'
  450. }, {
  451. duration:60, easing:"linear"
  452. });
  453. //animate window out
  454. this.animate({
  455. opacity: 1,
  456. left: "0px"
  457. }, {
  458. duration:60, easing:"linear"
  459. });
  460. this.queue();
  461. //bounce window back
  462. this.animate({
  463. left:'0px'
  464. }, {
  465. duration:60, easing:"linear"
  466. });
  467. }
  468. else {
  469. //starting position
  470. this.css({
  471. left: "+=74px",
  472. opacity: 1,
  473. "zIndex": 8888
  474. });
  475. //starting position for arrow
  476. $('#togetherjs-window-pointer-right').css({
  477. left: "+=74px",
  478. opacity: 1,
  479. "zIndex": 8888
  480. });
  481. //animate arrow out
  482. $('#togetherjs-window-pointer-right').animate({
  483. opacity: 1,
  484. left: "-=78px"
  485. }, {
  486. duration:60, easing:"linear"
  487. });
  488. $('#togetherjs-window-pointer-right').queue();
  489. //bounce arrow back
  490. $('#togetherjs-window-pointer-right').animate({
  491. left:'+=4px'
  492. }, {
  493. duration:60, easing:"linear"
  494. });
  495. //animate window out
  496. this.animate({
  497. opacity: 1,
  498. left: "-=78px"
  499. }, {
  500. duration:60, easing:"linear"
  501. });
  502. this.queue();
  503. //bounce window back
  504. this.animate({
  505. left:'+=4px'
  506. }, {
  507. duration:60, easing:"linear"
  508. });
  509. }
  510. };
  511. /* Slide in notification window: */
  512. $.fn.slideIn = function () {
  513. this.css({
  514. //top: "240px",
  515. left: "+=74px",
  516. opacity: 0,
  517. "zIndex": 8888
  518. });
  519. return this.animate({
  520. "left": "-=74px",
  521. opacity: 1,
  522. "zIndex": 9999
  523. }, "fast");
  524. };
  525. /* Used to fade away notification windows + flip the bottom of them out: */
  526. $.fn.fadeOut = function () {
  527. this.animate({borderSpacing: -90, opacity: 0.5}, {
  528. step: function(now, fx) {
  529. if (fx.prop == "borderSpacing") {
  530. $(this).css('-webkit-transform', 'perspective( 600px ) rotateX('+now+'deg)')
  531. .css('-moz-transform', 'perspective( 600px ) rotateX('+now+'deg)')
  532. .css('-ms-transform', 'perspective( 600px ) rotateX('+now+'deg)')
  533. .css('-o-transform', 'perspective( 600px ) rotateX('+now+'deg)')
  534. .css('transform', 'perspective( 600px ) rotateX('+now+'deg)');
  535. } else {
  536. $(this).css(fx.prop, now);
  537. }
  538. },
  539. duration: 500
  540. }, 'linear').promise().then(function () {
  541. this.css('-webkit-transform', '');
  542. this.css('-moz-transform', '');
  543. this.css('-ms-transform', '');
  544. this.css('-o-transform', '');
  545. this.css('transform', '');
  546. this.css("opacity", "");
  547. });
  548. return this;
  549. };
  550. /* used when user goes down to participant cursor location on screen */
  551. $.fn.easeTo = function (y) {
  552. return this.animate({
  553. scrollTop: y
  554. }, {
  555. duration: 400,
  556. easing: "swing"
  557. });
  558. };
  559. // avatar animate in
  560. $.fn.animateDockEntry = function () {
  561. var height = this.height();
  562. var width = this.width();
  563. var backgroundSize = height + 4;
  564. var margin = parseInt(this.css("marginLeft"), 10);
  565. // set starting position CSS for avatar
  566. this.css({
  567. marginLeft: margin + width/2,
  568. height: 0,
  569. width: 0,
  570. backgroundSize: "0 0"
  571. });
  572. var self = this;
  573. //then animate avatar to the actual dimensions, and reset the values
  574. this.animate({
  575. marginLeft: margin,
  576. height: height,
  577. width: width,
  578. backgroundSize: backgroundSize
  579. }, {
  580. duration: 600
  581. }).promise().then(function () {
  582. self.css({
  583. marginLeft: "",
  584. height: "",
  585. width: "",
  586. backgroundSize: ""
  587. });
  588. });
  589. return this;
  590. };
  591. // avatar animate out, reverse of above
  592. $.fn.animateDockExit = function () {
  593. // get the current avatar dimenensions
  594. var height = this.height();
  595. var width = this.width();
  596. var backgroundSize = height + 4;
  597. var margin = parseInt(this.css("marginLeft"), 10);
  598. //then animate avatar to shrink to nothing, and reset the values again
  599. // FIXME this needs to animate from the CENTER
  600. this.animate({
  601. marginLeft: margin + width/2,
  602. height: 0,
  603. width: 0,
  604. backgroundSize: "0 0",
  605. opacity: 0
  606. }, 600 );
  607. return this;
  608. };
  609. $.fn.animateCursorEntry = function () {
  610. // Make the cursor bubble pop in
  611. };
  612. // keyboard typing animation
  613. $.fn.animateKeyboard = function () {
  614. var one = this.find(".togetherjs-typing-ellipse-one");
  615. var two = this.find(".togetherjs-typing-ellipse-two");
  616. var three = this.find(".togetherjs-typing-ellipse-three");
  617. var count = -1;
  618. var run = (function () {
  619. count = (count+1) % 4;
  620. if (count === 0) {
  621. one.css("opacity", 0.5);
  622. two.css("opacity", 0.5);
  623. three.css("opacity", 0.5);
  624. } else if (count == 1) {
  625. one.css("opacity", 1);
  626. } else if (count == 2) {
  627. two.css("opacity", 1);
  628. } else { // count==3
  629. three.css("opacity", 1);
  630. }
  631. }).bind(this);
  632. run();
  633. var interval = setInterval(run, 300);
  634. this.data("animateKeyboard", interval);
  635. };
  636. $.fn.stopKeyboardAnimation = function () {
  637. clearTimeout(this.data("animateKeyboard"));
  638. this.data("animateKeyboard", null);
  639. };
  640. // FIXME: not sure if this is legit, but at least the modern mobile devices we
  641. // care about should have this defined:
  642. if (! $.browser) {
  643. $.browser = {};
  644. }
  645. $.browser.mobile = window.orientation !== undefined;
  646. if (navigator.userAgent.search(/mobile/i) != -1) {
  647. // FIXME: At least on the Firefox OS simulator I need this
  648. $.browser.mobile = true;
  649. }
  650. if ($.browser.mobile && window.matchMedia && ! window.matchMedia("screen and (max-screen-width: 480px)").matches) {
  651. // FIXME: for Firefox OS simulator really:
  652. document.body.className += " togetherjs-mobile-browser";
  653. }
  654. });
  655. /* This Source Code Form is subject to the terms of the Mozilla Public
  656. * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  657. * You can obtain one at http://mozilla.org/MPL/2.0/. */
  658. define('util',["jquery", "jqueryPlugins"], function ($) {
  659. var util = {};
  660. util.Deferred = $.Deferred;
  661. TogetherJS.$ = $;
  662. /* A simple class pattern, use like:
  663. var Foo = util.Class({
  664. constructor: function (a, b) {
  665. init the class
  666. },
  667. otherMethod: ...
  668. });
  669. You can also give a superclass as the optional first argument.
  670. Instantiation does not require "new"
  671. */
  672. util.Class = function (superClass, prototype) {
  673. var a;
  674. if (prototype === undefined) {
  675. prototype = superClass;
  676. } else {
  677. if (superClass.prototype) {
  678. superClass = superClass.prototype;
  679. }
  680. var newPrototype = Object.create(superClass);
  681. for (a in prototype) {
  682. if (prototype.hasOwnProperty(a)) {
  683. newPrototype[a] = prototype[a];
  684. }
  685. }
  686. prototype = newPrototype;
  687. }
  688. var ClassObject = function () {
  689. var obj = Object.create(prototype);
  690. obj.constructor.apply(obj, arguments);
  691. obj.constructor = ClassObject;
  692. return obj;
  693. };
  694. ClassObject.prototype = prototype;
  695. if (prototype.constructor.name) {
  696. ClassObject.className = prototype.constructor.name;
  697. ClassObject.toString = function () {
  698. return '[Class ' + this.className + ']';
  699. };
  700. }
  701. if (prototype.classMethods) {
  702. for (a in prototype.classMethods) {
  703. if (prototype.classMethods.hasOwnProperty(a)) {
  704. ClassObject[a] = prototype.classMethods[a];
  705. }
  706. }
  707. }
  708. return ClassObject;
  709. };
  710. /* Extends obj with other, or copies obj if no other is given. */
  711. util.extend = TogetherJS._extend;
  712. util.forEachAttr = function (obj, callback, context) {
  713. context = context || obj;
  714. for (var a in obj) {
  715. if (obj.hasOwnProperty(a)) {
  716. callback.call(context, obj[a], a);
  717. }
  718. }
  719. };
  720. /* Trim whitespace from a string */
  721. util.trim = function trim(s) {
  722. return s.replace(/^\s+/, "").replace(/\s+$/, "");
  723. };
  724. /* Convert a string into something safe to use as an HTML class name */
  725. util.safeClassName = function safeClassName(name) {
  726. return name.replace(/[^a-zA-Z0-9_\-]/g, "_") || "class";
  727. };
  728. util.AssertionError = function (message) {
  729. if (! this instanceof util.AssertionError) {
  730. return new util.AssertionError(message);
  731. }
  732. this.message = message;
  733. this.name = "AssertionError";
  734. };
  735. util.AssertionError.prototype = Error.prototype;
  736. util.assert = function (cond) {
  737. if (! cond) {
  738. var args = ["Assertion error:"].concat(Array.prototype.slice.call(arguments, 1));
  739. console.error.apply(console, args);
  740. if (console.trace) {
  741. console.trace();
  742. }
  743. throw new util.AssertionError(args.join(" "));
  744. }
  745. };
  746. /* Generates a random ID */
  747. util.generateId = function (length) {
  748. length = length || 10;
  749. var letters = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUV0123456789';
  750. var s = '';
  751. for (var i=0; i<length; i++) {
  752. s += letters.charAt(Math.floor(Math.random() * letters.length));
  753. }
  754. return s;
  755. };
  756. util.pickRandom = function (array) {
  757. return array[Math.floor(Math.random() * array.length)];
  758. };
  759. util.mixinEvents = TogetherJS._mixinEvents;
  760. util.Module = util.Class({
  761. constructor: function (name) {
  762. this._name = name;
  763. },
  764. toString: function () {
  765. return '[Module ' + this._name + ']';
  766. }
  767. });
  768. util.blobToBase64 = function (blob) {
  769. // Oh this is just terrible
  770. var binary = '';
  771. var bytes = new Uint8Array(blob);
  772. var len = bytes.byteLength;
  773. for (var i=0; i<len; i++) {
  774. binary += String.fromCharCode(bytes[i]);
  775. }
  776. return btoa(binary);
  777. };
  778. util.truncateCommonDomain = function (url, base) {
  779. /* Remove the scheme and domain from url, if it matches the scheme and domain
  780. of base */
  781. if (! base) {
  782. return url;
  783. }
  784. var regex = /^https?:\/\/[^\/]*/i;
  785. var match = regex.exec(url);
  786. var matchBase = regex.exec(base);
  787. if (match && matchBase && match[0] == matchBase[0]) {
  788. // There is a common scheme and domain
  789. return url.substr(match[0].length);
  790. }
  791. return url;
  792. };
  793. util.makeUrlAbsolute = function (url, base) {
  794. if (url.search(/^(http|https|ws|wss):/i) === 0) {
  795. // Absolute URL
  796. return url;
  797. }
  798. if (url.search(/^\/\/[^\/]/) === 0) {
  799. var scheme = (/^(http|https|ws|wss):/i).exec(base);
  800. util.assert(scheme, "No scheme on base URL", base);
  801. return scheme[1] + ":" + url;
  802. }
  803. if (url.search(/^\//) === 0) {
  804. var domain = (/^(http|https|ws|wss):\/\/[^\/]+/i).exec(base);
  805. util.assert(domain, "No scheme/domain on base URL", base);
  806. return domain[0] + url;
  807. }
  808. var last = (/[^\/]+$/).exec(base);
  809. util.assert(last, "Does not appear to be a URL?", base);
  810. var lastBase = base.substr(0, last.index);
  811. return lastBase + url;
  812. };
  813. util.assertValidUrl = function (url) {
  814. /* This does some simple assertions that the url is valid:
  815. - it must be a string
  816. - it must be http(s)://... or data:...
  817. - it must not contain a space, quotation, or close paren
  818. */
  819. util.assert(typeof url == "string", "URLs must be a string:", url);
  820. util.assert(url.search(/^(http:\/\/|https:\/\/|\/\/|data:)/i) === 0,
  821. "URL must have an http, https, data, or // scheme:", url);
  822. util.assert(url.search(/[\)\'\"\ ]/) === -1,
  823. "URLs cannot contain ), ', \", or spaces:", JSON.stringify(url));
  824. };
  825. util.resolver = function (deferred, func) {
  826. util.assert(deferred.then, "Bad deferred:", deferred);
  827. util.assert(typeof func == "function", "Not a function:", func);
  828. return function () {
  829. var result;
  830. try {
  831. result = func.apply(this, arguments);
  832. } catch (e) {
  833. deferred.reject(e);
  834. throw e;
  835. }
  836. if (result && result.then) {
  837. result.then(function () {
  838. deferred.resolveWith(this, arguments);
  839. }, function () {
  840. deferred.rejectWith(this, arguments);
  841. });
  842. // FIXME: doesn't pass progress through
  843. } else if (result === undefined) {
  844. deferred.resolve();
  845. } else {
  846. deferred.resolve(result);
  847. }
  848. return result;
  849. };
  850. };
  851. /* Detects if a value is a promise. Right now the presence of a
  852. `.then()` method is the best we can do.
  853. */
  854. util.isPromise = function (obj) {
  855. return typeof obj == "object" && obj.then;
  856. };
  857. /* Makes a value into a promise, by returning an already-resolved
  858. promise if a non-promise objectx is given.
  859. */
  860. util.makePromise = function (obj) {
  861. if (util.isPromise(obj)) {
  862. return obj;
  863. } else {
  864. return $.Deferred(function (def) {
  865. def.resolve(obj);
  866. });
  867. }
  868. };
  869. /* Resolves several promises (the promises are the arguments to the function)
  870. or the first argument may be an array of promises.
  871. Returns a promise that will resolve with the results of all the
  872. promises. If any promise fails then the returned promise fails.
  873. FIXME: if a promise has more than one return value (like with
  874. promise.resolve(a, b)) then the latter arguments will be lost.
  875. */
  876. util.resolveMany = function () {
  877. var args;
  878. var oneArg = false;
  879. if (arguments.length == 1 && Array.isArray(arguments[0])) {
  880. oneArg = true;
  881. args = arguments[0];
  882. } else {
  883. args = Array.prototype.slice.call(arguments);
  884. }
  885. return util.Deferred(function (def) {
  886. var count = args.length;
  887. if (! count) {
  888. def.resolve();
  889. return;
  890. }
  891. var allResults = [];
  892. var anyError = false;
  893. args.forEach(function (arg, index) {
  894. arg.then(function (result) {
  895. allResults[index] = result;
  896. count--;
  897. check();
  898. }, function (error) {
  899. allResults[index] = error;
  900. anyError = true;
  901. count--;
  902. check();
  903. });
  904. });
  905. function check() {
  906. if (! count) {
  907. if (anyError) {
  908. if (oneArg) {
  909. def.reject(allResults);
  910. } else {
  911. def.reject.apply(def, allResults);
  912. }
  913. } else {
  914. if (oneArg) {
  915. def.resolve(allResults);
  916. } else {
  917. def.resolve.apply(def, allResults);
  918. }
  919. }
  920. }
  921. }
  922. });
  923. };
  924. util.readFileImage = function (el) {
  925. return util.Deferred(function (def) {
  926. var reader = new FileReader();
  927. reader.onload = function () {
  928. def.resolve("data:image/jpeg;base64," + util.blobToBase64(this.result));
  929. };
  930. reader.onerror = function () {
  931. def.reject(this.error);
  932. };
  933. reader.readAsArrayBuffer(el.files[0]);
  934. });
  935. };
  936. util.matchElement = function(el, selector) {
  937. var res = selector;
  938. if (selector === true || ! selector) {
  939. return !!selector;
  940. }
  941. try {
  942. return $(el).is(selector);
  943. } catch (e) {
  944. console.warn("Bad selector:", selector, "error:", e);
  945. return false;
  946. }
  947. };
  948. util.testExpose = function (objs) {
  949. if (typeof TogetherJSTestSpy == "undefined") {
  950. return;
  951. }
  952. util.forEachAttr(objs, function (value, attr) {
  953. TogetherJSTestSpy[attr] = value;
  954. });
  955. };
  956. return util;
  957. });
  958. define('analytics',["util"], function (util) {
  959. var analytics = util.Module("analytics");
  960. analytics.activate = function () {
  961. var enable = TogetherJS.config.get("enableAnalytics");
  962. var code = TogetherJS.config.get("analyticsCode");
  963. TogetherJS.config.close("enableAnalytics");
  964. TogetherJS.config.close("analyticsCode");
  965. if (! (enable && code)) {
  966. return;
  967. }
  968. // This is intended to be global:
  969. var gaq = window._gaq || [];
  970. gaq.push(["_setAccount", code]);
  971. gaq.push(['_setDomainName', location.hostname]);
  972. gaq.push(["_trackPageview"]);
  973. window._gaq = gaq;
  974. (function() {
  975. var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;
  976. ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
  977. var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);
  978. })();
  979. };
  980. return analytics;
  981. });
  982. /* This Source Code Form is subject to the terms of the Mozilla Public
  983. * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  984. * You can obtain one at http://mozilla.org/MPL/2.0/. */
  985. /* Channel abstraction. Supported channels:
  986. - WebSocket to an address
  987. - postMessage between windows
  988. In the future:
  989. - XMLHttpRequest to a server (with some form of queuing)
  990. The interface:
  991. channel = new ChannelName(parameters)
  992. The instantiation is specific to the kind of channel
  993. Methods:
  994. onmessage: set to function (jsonData)
  995. rawdata: set to true if you want onmessage to receive raw string data
  996. onclose: set to function ()
  997. send: function (string or jsonData)
  998. close: function ()
  999. .send() will encode the data if it is not a string.
  1000. (should I include readyState as an attribute?)
  1001. Channels must accept messages immediately, caching if the connection
  1002. is not fully established yet.
  1003. */
  1004. define('channels',["util"], function (util) {
  1005. var channels = util.Module("channels");
  1006. /* Subclasses must define:
  1007. - ._send(string)
  1008. - ._setupConnection()
  1009. - ._ready()
  1010. - .close() (and must set this.closed to true)
  1011. And must call:
  1012. - ._flush() on open
  1013. - ._incoming(string) on incoming message
  1014. - onclose() (not onmessage - instead _incoming)
  1015. - emit("close")
  1016. */
  1017. var AbstractChannel = util.mixinEvents({
  1018. onmessage: null,
  1019. rawdata: false,
  1020. onclose: null,
  1021. closed: false,
  1022. baseConstructor: function () {
  1023. this._buffer = [];
  1024. this._setupConnection();
  1025. },
  1026. send: function (data) {
  1027. if (this.closed) {
  1028. throw 'Cannot send to a closed connection';
  1029. }
  1030. if (typeof data != "string") {
  1031. data = JSON.stringify(data);
  1032. }
  1033. if (! this._ready()) {
  1034. this._buffer.push(data);
  1035. return;
  1036. }
  1037. this._send(data);
  1038. },
  1039. _flush: function () {
  1040. for (var i=0; i<this._buffer.length; i++) {
  1041. this._send(this._buffer[i]);
  1042. }
  1043. this._buffer = [];
  1044. },
  1045. _incoming: function (data) {
  1046. if (! this.rawdata) {
  1047. try {
  1048. data = JSON.parse(data);
  1049. } catch (e) {
  1050. console.error("Got invalid JSON data:", data.substr(0, 40));
  1051. throw e;
  1052. }
  1053. }
  1054. if (this.onmessage) {
  1055. this.onmessage(data);
  1056. }
  1057. this.emit("message", data);
  1058. }
  1059. });
  1060. channels.WebSocketChannel = util.Class(AbstractChannel, {
  1061. constructor: function (address) {
  1062. if (address.search(/^https?:/i) === 0) {
  1063. address = address.replace(/^http/i, 'ws');
  1064. }
  1065. this.address = address;
  1066. this.socket = null;
  1067. this._reopening = false;
  1068. this._lastConnectTime = 0;
  1069. this._backoff = 0;
  1070. this.baseConstructor();
  1071. },
  1072. backoffTime: 50, // Milliseconds to add to each reconnect time
  1073. maxBackoffTime: 1500,
  1074. backoffDetection: 2000, // Amount of time since last connection attempt that shows we need to back off
  1075. toString: function () {
  1076. var s = '[WebSocketChannel to ' + this.address;
  1077. if (! this.socket) {
  1078. s += ' (socket unopened)';
  1079. } else {
  1080. s += ' readyState: ' + this.socket.readyState;
  1081. }
  1082. if (this.closed) {
  1083. s += ' CLOSED';
  1084. }
  1085. return s + ']';
  1086. },
  1087. close: function () {
  1088. this.closed = true;
  1089. if (this.socket) {
  1090. // socket.onclose will call this.onclose:
  1091. this.socket.close();
  1092. } else {
  1093. if (this.onclose) {
  1094. this.onclose();
  1095. }
  1096. this.emit("close");
  1097. }
  1098. },
  1099. _send: function (data) {
  1100. this.socket.send(data);
  1101. },
  1102. _ready: function () {
  1103. return this.socket && this.socket.readyState == this.socket.OPEN;
  1104. },
  1105. _setupConnection: function () {
  1106. if (this.closed) {
  1107. return;
  1108. }
  1109. this._lastConnectTime = Date.now();
  1110. this.socket = new WebSocket(this.address);
  1111. this.socket.onopen = (function () {
  1112. this._flush();
  1113. this._reopening = false;
  1114. }).bind(this);
  1115. this.socket.onclose = (function (event) {
  1116. this.socket = null;
  1117. var method = "error";
  1118. if (event.wasClean) {
  1119. // FIXME: should I even log clean closes?
  1120. method = "log";
  1121. }
  1122. console[method]('WebSocket close', event.wasClean ? 'clean' : 'unclean',
  1123. 'code:', event.code, 'reason:', event.reason || 'none');
  1124. if (! this.closed) {
  1125. this._reopening = true;
  1126. if (Date.now() - this._lastConnectTime > this.backoffDetection) {
  1127. this._backoff = 0;
  1128. } else {
  1129. this._backoff++;
  1130. }
  1131. var time = Math.min(this._backoff * this.backoffTime, this.maxBackoffTime);
  1132. setTimeout((function () {
  1133. this._setupConnection();
  1134. }).bind(this), time);
  1135. }
  1136. }).bind(this);
  1137. this.socket.onmessage = (function (event) {
  1138. this._incoming(event.data);
  1139. }).bind(this);
  1140. this.socket.onerror = (function (event) {
  1141. console.error('WebSocket error:', event.data);
  1142. }).bind(this);
  1143. }
  1144. });
  1145. /* Sends TO a window or iframe */
  1146. channels.PostMessageChannel = util.Class(AbstractChannel, {
  1147. _pingPollPeriod: 100, // milliseconds
  1148. _pingPollIncrease: 100, // +100 milliseconds for each failure
  1149. _pingMax: 2000, // up to a max of 2000 milliseconds
  1150. constructor: function (win, expectedOrigin) {
  1151. this.expectedOrigin = expectedOrigin;
  1152. this._pingReceived = false;
  1153. this._receiveMessage = this._receiveMessage.bind(this);
  1154. if (win) {
  1155. this.bindWindow(win, true);
  1156. }
  1157. this._pingFailures = 0;
  1158. this.baseConstructor();
  1159. },
  1160. toString: function () {
  1161. var s = '[PostMessageChannel';
  1162. if (this.window) {
  1163. s += ' to window ' + this.window;
  1164. } else {
  1165. s += ' not bound to a window';
  1166. }
  1167. if (this.window && ! this._pingReceived) {
  1168. s += ' still establishing';
  1169. }
  1170. return s + ']';
  1171. },
  1172. bindWindow: function (win, noSetup) {
  1173. if (this.window) {
  1174. this.close();
  1175. // Though we deinitialized everything, we aren't exactly closed:
  1176. this.closed = false;
  1177. }
  1178. if (win && win.contentWindow) {
  1179. win = win.contentWindow;
  1180. }
  1181. this.window = win;
  1182. // FIXME: The distinction between this.window and window seems unimportant
  1183. // in the case of postMessage
  1184. var w = this.window;
  1185. // In a Content context we add the listener to the local window
  1186. // object, but in the addon context we add the listener to some
  1187. // other window, like the one we were given:
  1188. if (typeof window != "undefined") {
  1189. w = window;
  1190. }
  1191. w.addEventListener("message", this._receiveMessage, false);
  1192. if (! noSetup) {
  1193. this._setupConnection();
  1194. }
  1195. },
  1196. _send: function (data) {
  1197. this.window.postMessage(data, this.expectedOrigin || "*");
  1198. },
  1199. _ready: function () {
  1200. return this.window && this._pingReceived;
  1201. },
  1202. _setupConnection: function () {
  1203. if (this.closed || this._pingReceived || (! this.window)) {
  1204. return;
  1205. }
  1206. this._pingFailures++;
  1207. this._send("hello");
  1208. // We'll keep sending ping messages until we get a reply
  1209. var time = this._pingPollPeriod + (this._pingPollIncrease * this._pingFailures);
  1210. time = time > this._pingPollMax ? this._pingPollMax : time;
  1211. this._pingTimeout = setTimeout(this._setupConnection.bind(this), time);
  1212. },
  1213. _receiveMessage: function (event) {
  1214. if (event.source !== this.window) {
  1215. return;
  1216. }
  1217. if (this.expectedOrigin && event.origin != this.expectedOrigin) {
  1218. console.info("Expected message from", this.expectedOrigin,
  1219. "but got message from", event.origin);
  1220. return;
  1221. }
  1222. if (! this.expectedOrigin) {
  1223. this.expectedOrigin = event.origin;
  1224. }
  1225. if (event.data == "hello") {
  1226. this._pingReceived = true;
  1227. if (this._pingTimeout) {
  1228. clearTimeout(this._pingTimeout);
  1229. this._pingTimeout = null;
  1230. }
  1231. this._flush();
  1232. return;
  1233. }
  1234. this._incoming(event.data);
  1235. },
  1236. close: function () {
  1237. this.closed = true;
  1238. this._pingReceived = false;
  1239. if (this._pingTimeout) {
  1240. clearTimeout(this._pingTimeout);
  1241. }
  1242. window.removeEventListener("message", this._receiveMessage, false);
  1243. if (this.onclose) {
  1244. this.onclose();
  1245. }
  1246. this.emit("close");
  1247. }
  1248. });
  1249. /* Handles message FROM an exterior window/parent */
  1250. channels.PostMessageIncomingChannel = util.Class(AbstractChannel, {
  1251. constructor: function (expectedOrigin) {
  1252. this.source = null;
  1253. this.expectedOrigin = expectedOrigin;
  1254. this._receiveMessage = this._receiveMessage.bind(this);
  1255. window.addEventListener("message", this._receiveMessage, false);
  1256. this.baseConstructor();
  1257. },
  1258. toString: function () {
  1259. var s = '[PostMessageIncomingChannel';
  1260. if (this.source) {
  1261. s += ' bound to source ' + s;
  1262. } else {
  1263. s += ' awaiting source';
  1264. }
  1265. return s + ']';
  1266. },
  1267. _send: function (data) {
  1268. this.source.postMessage(data, this.expectedOrigin);
  1269. },
  1270. _ready: function () {
  1271. return !!this.source;
  1272. },
  1273. _setupConnection: function () {
  1274. },
  1275. _receiveMessage: function (event) {
  1276. if (this.expectedOrigin && this.expectedOrigin != "*" &&
  1277. event.origin != this.expectedOrigin) {
  1278. // FIXME: Maybe not worth mentioning?
  1279. console.info("Expected message from", this.expectedOrigin,
  1280. "but got message from", event.origin);
  1281. return;
  1282. }
  1283. if (! this.expectedOrigin) {
  1284. this.expectedOrigin = event.origin;
  1285. }
  1286. if (! this.source) {
  1287. this.source = event.source;
  1288. }
  1289. if (event.data == "hello") {
  1290. // Just a ping
  1291. this.source.postMessage("hello", this.expectedOrigin);
  1292. return;
  1293. }
  1294. this._incoming(event.data);
  1295. },
  1296. close: function () {
  1297. this.closed = true;
  1298. window.removeEventListener("message", this._receiveMessage, false);
  1299. if (this._pingTimeout) {
  1300. clearTimeout(this._pingTimeout);
  1301. }
  1302. if (this.onclose) {
  1303. this.onclose();
  1304. }
  1305. this.emit("close");
  1306. }
  1307. });
  1308. channels.Router = util.Class(util.mixinEvents({
  1309. constructor: function (channel) {
  1310. this._channelMessage = this._channelMessage.bind(this);
  1311. this._channelClosed = this._channelClosed.bind(this);
  1312. this._routes = Object.create(null);
  1313. if (channel) {
  1314. this.bindChannel(channel);
  1315. }
  1316. },
  1317. bindChannel: function (channel) {
  1318. if (this.channel) {
  1319. this.channel.removeListener("message", this._channelMessage);
  1320. this.channel.removeListener("close", this._channelClosed);
  1321. }
  1322. this.channel = channel;
  1323. this.channel.on("message", this._channelMessage.bind(this));
  1324. this.channel.on("close", this._channelClosed.bind(this));
  1325. },
  1326. _channelMessage: function (msg) {
  1327. if (msg.type == "route") {
  1328. var id = msg.routeId;
  1329. var route = this._routes[id];
  1330. if (! route) {
  1331. console.warn("No route with the id", id);
  1332. return;
  1333. }
  1334. if (msg.close) {
  1335. this._closeRoute(route.id);
  1336. } else {
  1337. if (route.onmessage) {
  1338. route.onmessage(msg.message);
  1339. }
  1340. route.emit("message", msg.message);
  1341. }
  1342. }
  1343. },
  1344. _channelClosed: function () {
  1345. for (var id in this._routes) {
  1346. this._closeRoute(id);
  1347. }
  1348. },
  1349. _closeRoute: function (id) {
  1350. var route = this._routes[id];
  1351. if (route.onclose) {
  1352. route.onclose();
  1353. }
  1354. route.emit("close");
  1355. delete this._routes[id];
  1356. },
  1357. makeRoute: function (id) {
  1358. id = id || util.generateId();
  1359. var route = Route(this, id);
  1360. this._routes[id] = route;
  1361. return route;
  1362. }
  1363. }));
  1364. var Route = util.Class(util.mixinEvents({
  1365. constructor: function (router, id) {
  1366. this.router = router;
  1367. this.id = id;
  1368. },
  1369. send: function (msg) {
  1370. this.router.channel.send({
  1371. type: "route",
  1372. routeId: this.id,
  1373. message: msg
  1374. });
  1375. },
  1376. close: function () {
  1377. if (this.router._routes[this.id] !== this) {
  1378. // This route instance has been overwritten, so ignore
  1379. return;
  1380. }
  1381. delete this.router._routes[this.id];
  1382. }
  1383. }));
  1384. return channels;
  1385. });
  1386. /* This Source Code Form is subject to the terms of the Mozilla Public
  1387. * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  1388. * You can obtain one at http://mozilla.org/MPL/2.0/. */
  1389. define('storage',["util"], function (util) {
  1390. var assert = util.assert;
  1391. var Deferred = util.Deferred;
  1392. var DEFAULT_SETTINGS = {
  1393. name: "",
  1394. defaultName: "",
  1395. avatar: null,
  1396. stickyShare: null,
  1397. color: null,
  1398. seenIntroDialog: false,
  1399. seenWalkthrough: false,
  1400. dontShowRtcInfo: false
  1401. };
  1402. var DEBUG_STORAGE = false;
  1403. var Storage = util.Class({
  1404. constructor: function (name, storage, prefix) {
  1405. this.name = name;
  1406. this.storage = storage;
  1407. this.prefix = prefix;
  1408. },
  1409. get: function (key, defaultValue) {
  1410. var self = this;
  1411. return Deferred(function (def) {
  1412. // Strictly this isn't necessary, but eventually I want to move to something more
  1413. // async for the storage, and this simulates that much better.
  1414. setTimeout(util.resolver(def, function () {
  1415. key = self.prefix + key;
  1416. var value = self.storage.getItem(key);
  1417. if (! value) {
  1418. value = defaultValue;
  1419. if (DEBUG_STORAGE) {
  1420. console.debug("Get storage", key, "defaults to", value);
  1421. }
  1422. } else {
  1423. value = JSON.parse(value);
  1424. if (DEBUG_STORAGE) {
  1425. console.debug("Get storage", key, "=", value);
  1426. }
  1427. }
  1428. return value;
  1429. }));
  1430. });
  1431. },
  1432. set: function (key, value) {
  1433. var self = this;
  1434. if (value !== undefined) {
  1435. value = JSON.stringify(value);
  1436. }
  1437. return Deferred(function (def) {
  1438. key = self.prefix + key;
  1439. if (value === undefined) {
  1440. self.storage.removeItem(key);
  1441. if (DEBUG_STORAGE) {
  1442. console.debug("Delete storage", key);
  1443. }
  1444. } else {
  1445. self.storage.setItem(key, value);
  1446. if (DEBUG_STORAGE) {
  1447. console.debug("Set storage", key, value);
  1448. }
  1449. }
  1450. setTimeout(def.resolve);
  1451. });
  1452. },
  1453. clear: function () {
  1454. var self = this;
  1455. var promises = [];
  1456. return Deferred((function (def) {
  1457. this.keys().then(function (keys) {
  1458. keys.forEach(function (key) {
  1459. // FIXME: technically we're ignoring the promise returned by all
  1460. // these sets:
  1461. promises.push(self.set(key, undefined));
  1462. });
  1463. util.resolveMany(promises).then(function () {
  1464. def.resolve();
  1465. });
  1466. });
  1467. }).bind(this));
  1468. },
  1469. keys: function (prefix, excludePrefix) {
  1470. // Returns a list of keys, potentially with the given prefix
  1471. var self = this;
  1472. return Deferred(function (def) {
  1473. setTimeout(util.resolver(def, function () {
  1474. prefix = prefix || "";
  1475. var result = [];
  1476. for (var i = 0; i < self.storage.length; i++) {
  1477. var key = self.storage.key(i);
  1478. if (key.indexOf(self.prefix + prefix) === 0) {
  1479. var shortKey = key.substr(self.prefix.length);
  1480. if (excludePrefix) {
  1481. shortKey = shortKey.substr(prefix.length);
  1482. }
  1483. result.push(shortKey);
  1484. }
  1485. }
  1486. return result;
  1487. }));
  1488. });
  1489. },
  1490. toString: function () {
  1491. return '[storage for ' + this.name + ']';
  1492. }
  1493. });
  1494. var namePrefix = TogetherJS.config.get("storagePrefix");
  1495. TogetherJS.config.close("storagePrefix");
  1496. var storage = Storage('localStorage', localStorage, namePrefix + ".");
  1497. storage.settings = util.mixinEvents({
  1498. defaults: DEFAULT_SETTINGS,
  1499. get: function (name) {
  1500. assert(storage.settings.defaults.hasOwnProperty(name), "Unknown setting:", name);
  1501. return storage.get("settings." + name, storage.settings.defaults[name]);
  1502. },
  1503. set: function (name, value) {
  1504. assert(storage.settings.defaults.hasOwnProperty(name), "Unknown setting:", name);
  1505. return storage.set("settings." + name, value);
  1506. }
  1507. });
  1508. storage.tab = Storage('sessionStorage', sessionStorage, namePrefix + "-session.");
  1509. return storage;
  1510. });
  1511. /* This Source Code Form is subject to the terms of the Mozilla Public
  1512. * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  1513. * You can obtain one at http://mozilla.org/MPL/2.0/. */
  1514. define('session',["require", "util", "channels", "jquery", "storage"], function (require, util, channels, $, storage) {
  1515. var DEBUG = true;
  1516. // This is the amount of time in which a hello-back must be received after a hello
  1517. // for us to respect a URL change:
  1518. var HELLO_BACK_CUTOFF = 1500;
  1519. var session = util.mixinEvents(util.Module("session"));
  1520. var assert = util.assert;
  1521. // We will load this module later (there's a circular import):
  1522. var peers;
  1523. // This is the hub we connect to:
  1524. session.shareId = null;
  1525. // This is the ID that identifies this client:
  1526. session.clientId = null;
  1527. session.router = channels.Router();
  1528. // Indicates if TogetherJS has just started (not continuing from a saved session):
  1529. session.firstRun = false;
  1530. // This is the key we use for localStorage:
  1531. var localStoragePrefix = "togetherjs.";
  1532. // This is the channel to the hub:
  1533. var channel = null;
  1534. // Setting, essentially global:
  1535. session.AVATAR_SIZE = 90;
  1536. var MAX_SESSION_AGE = 30*24*60*60*1000; // 30 days
  1537. /****************************************
  1538. * URLs
  1539. */
  1540. var includeHashInUrl = TogetherJS.config.get("includeHashInUrl");
  1541. TogetherJS.config.close("includeHashInUrl");
  1542. var currentUrl = (location.href + "").replace(/\#.*$/, "");
  1543. if (includeHashInUrl) {
  1544. currentUrl = location.href;
  1545. }
  1546. session.hubUrl = function (id) {
  1547. id = id || session.shareId;
  1548. assert(id, "URL cannot be resolved before TogetherJS.shareId has been initialized");
  1549. TogetherJS.config.close("hubBase");
  1550. var hubBase = TogetherJS.config.get("hubBase");
  1551. return hubBase.replace(/\/*$/, "") + "/hub/" + id;
  1552. };
  1553. session.shareUrl = function () {
  1554. assert(session.shareId, "Attempted to access shareUrl() before shareId is set");
  1555. var hash = location.hash;
  1556. var m = /\?[^#]*/.exec(location.href);
  1557. var query = "";
  1558. if (m) {
  1559. query = m[0];
  1560. }
  1561. hash = hash.replace(/&?togetherjs-[a-zA-Z0-9]+/, "");
  1562. hash = hash || "#";
  1563. return location.protocol + "//" + location.host + location.pathname + query +
  1564. hash + "&togetherjs=" + session.shareId;
  1565. };
  1566. session.recordUrl = function () {
  1567. assert(session.shareId);
  1568. var url = TogetherJS.baseUrl.replace(/\/*$/, "") + "/togetherjs/recorder.html";
  1569. url += "#&togetherjs=" + session.shareId + "&hubBase=" + TogetherJS.config.get("hubBase");
  1570. return url;
  1571. };
  1572. /* location.href without the hash */
  1573. session.currentUrl = function () {
  1574. if (includeHashInUrl) {
  1575. return location.href;
  1576. } else {
  1577. return location.href.replace(/#.*/, "");
  1578. }
  1579. };
  1580. /****************************************
  1581. * Message handling/dispatching
  1582. */
  1583. session.hub = util.mixinEvents({});
  1584. var IGNORE_MESSAGES = TogetherJS.config.get("ignoreMessages");
  1585. if (IGNORE_MESSAGES === true) {
  1586. DEBUG = false;
  1587. IGNORE_MESSAGES = [];
  1588. }
  1589. // These are messages sent by clients who aren't "part" of the TogetherJS session:
  1590. var MESSAGES_WITHOUT_CLIENTID = ["who", "invite", "init-connection"];
  1591. // We ignore incoming messages from the channel until this is true:
  1592. var readyForMessages = false;
  1593. function openChannel() {
  1594. assert(! channel, "Attempt to re-open channel");
  1595. console.info("Connecting to", session.hubUrl(), location.href);
  1596. var c = channels.WebSocketChannel(session.hubUrl());
  1597. c.onmessage = function (msg) {
  1598. if (! readyForMessages) {
  1599. if (DEBUG) {
  1600. console.info("In (but ignored for being early):", msg);
  1601. }
  1602. return;
  1603. }
  1604. if (DEBUG && IGNORE_MESSAGES.indexOf(msg.type) == -1) {
  1605. console.info("In:", msg);
  1606. }
  1607. if (! peers) {
  1608. // We're getting messages before everything is fully initialized
  1609. console.warn("Message received before all modules loaded (ignoring):", msg);
  1610. return;
  1611. }
  1612. if ((! msg.clientId) && MESSAGES_WITHOUT_CLIENTID.indexOf(msg.type) == -1) {
  1613. console.warn("Got message without clientId, where clientId is required", msg);
  1614. return;
  1615. }
  1616. if (msg.clientId) {
  1617. msg.peer = peers.getPeer(msg.clientId, msg);
  1618. }
  1619. if (msg.type == "hello" || msg.type == "hello-back" || msg.type == "peer-update") {
  1620. // We do this here to make sure this is run before any other
  1621. // hello handlers:
  1622. msg.peer.updateFromHello(msg);
  1623. }
  1624. if (msg.peer) {
  1625. msg.sameUrl = msg.peer.url == currentUrl;
  1626. if (!msg.peer.isSelf) {
  1627. msg.peer.updateMessageDate(msg);
  1628. }
  1629. }
  1630. session.hub.emit(msg.type, msg);
  1631. TogetherJS._onmessage(msg);
  1632. };
  1633. channel = c;
  1634. session.router.bindChannel(channel);
  1635. }
  1636. session.send = function (msg) {
  1637. if (DEBUG && IGNORE_MESSAGES.indexOf(msg.type) == -1) {
  1638. console.info("Send:", msg);
  1639. }
  1640. msg.clientId = session.clientId;
  1641. channel.send(msg);
  1642. };
  1643. session.appSend = function (msg) {
  1644. var type = msg.type;
  1645. if (type.search(/^togetherjs\./) === 0) {
  1646. type = type.substr("togetherjs.".length);
  1647. } else if (type.search(/^app\./) === -1) {
  1648. type = "app." + type;
  1649. }
  1650. msg.type = type;
  1651. session.send(msg);
  1652. };
  1653. /****************************************
  1654. * Standard message responses
  1655. */
  1656. /* Always say hello back, and keep track of peers: */
  1657. session.hub.on("hello hello-back", function (msg) {
  1658. if (msg.type == "hello") {
  1659. sendHello(true);
  1660. }
  1661. if (session.isClient && (! msg.isClient) &&
  1662. session.firstRun && session.timeHelloSent &&
  1663. Date.now() - session.timeHelloSent < HELLO_BACK_CUTOFF) {
  1664. processFirstHello(msg);
  1665. }
  1666. });
  1667. session.hub.on("who", function (msg) {
  1668. sendHello(true);
  1669. });
  1670. function processFirstHello(msg) {
  1671. if (! msg.sameUrl) {
  1672. var url = msg.url;
  1673. if (msg.urlHash) {
  1674. url += msg.urlHash;
  1675. }
  1676. require("ui").showUrlChangeMessage(msg.peer, url);
  1677. location.href = url;
  1678. }
  1679. }
  1680. session.timeHelloSent = null;
  1681. function sendHello(helloBack) {
  1682. var msg = session.makeHelloMessage(helloBack);
  1683. if (! helloBack) {
  1684. session.timeHelloSent = Date.now();
  1685. peers.Self.url = msg.url;
  1686. }
  1687. session.send(msg);
  1688. }
  1689. session.makeHelloMessage = function (helloBack) {
  1690. var msg = {
  1691. name: peers.Self.name || peers.Self.defaultName,
  1692. avatar: peers.Self.avatar,
  1693. color: peers.Self.color,
  1694. url: session.currentUrl(),
  1695. urlHash: location.hash,
  1696. // FIXME: titles update, we should track those changes:
  1697. title: document.title,
  1698. rtcSupported: session.RTCSupported,
  1699. isClient: session.isClient
  1700. };
  1701. if (helloBack) {
  1702. msg.type = "hello-back";
  1703. } else {
  1704. msg.type = "hello";
  1705. msg.clientVersion = TogetherJS.version;
  1706. }
  1707. if (! TogetherJS.startup.continued) {
  1708. msg.starting = true;
  1709. }
  1710. // This is a chance for other modules to effect the hello message:
  1711. session.emit("prepare-hello", msg);
  1712. return msg;
  1713. };
  1714. /****************************************
  1715. * Lifecycle (start and end)
  1716. */
  1717. // These are Javascript files that implement features, and so must
  1718. // be injected at runtime because they aren't pulled in naturally
  1719. // via define().
  1720. // ui must be the first item:
  1721. var features = ["peers", "ui", "chat", "webrtc", "cursor", "startup", "videos", "forms", "visibilityApi", "youtubeVideos"];
  1722. function getRoomName(prefix, maxSize) {
  1723. var findRoom = TogetherJS.config.get("hubBase").replace(/\/*$/, "") + "/findroom";
  1724. return $.ajax({
  1725. url: findRoom,
  1726. dataType: "json",
  1727. data: {prefix: prefix, max: maxSize}
  1728. }).then(function (resp) {
  1729. return resp.name;
  1730. });
  1731. }
  1732. function initIdentityId() {
  1733. return util.Deferred(function (def) {
  1734. if (session.identityId) {
  1735. def.resolve();
  1736. return;
  1737. }
  1738. storage.get("identityId").then(function (identityId) {
  1739. if (! identityId) {
  1740. identityId = util.generateId();
  1741. storage.set("identityId", identityId);
  1742. }
  1743. session.identityId = identityId;
  1744. // We don't actually have to wait for the set to succede, so
  1745. // long as session.identityId is set
  1746. def.resolve();
  1747. });
  1748. });
  1749. }
  1750. initIdentityId.done = initIdentityId();
  1751. function initShareId() {
  1752. return util.Deferred(function (def) {
  1753. var hash = location.hash;
  1754. var shareId = session.shareId;
  1755. var isClient = true;
  1756. var set = true;
  1757. var sessionId;
  1758. session.firstRun = ! TogetherJS.startup.continued;
  1759. if (! shareId) {
  1760. if (TogetherJS.startup._joinShareId) {
  1761. // Like, below, this *also* means we got the shareId from the hash
  1762. // (in togetherjs.js):
  1763. shareId = TogetherJS.startup._joinShareId;
  1764. }
  1765. }
  1766. if (! shareId) {
  1767. // FIXME: I'm not sure if this will ever happen, because togetherjs.js should
  1768. // handle it
  1769. var m = /&?togetherjs=([^&]*)/.exec(hash);
  1770. if (m) {
  1771. isClient = ! m[1];
  1772. shareId = m[2];
  1773. var newHash = hash.substr(0, m.index) + hash.substr(m.index + m[0].length);
  1774. location.hash = newHash;
  1775. }
  1776. }
  1777. return storage.tab.get("status").then(function (saved) {
  1778. var findRoom = TogetherJS.config.get("findRoom");
  1779. TogetherJS.config.close("findRoom");
  1780. if (findRoom && saved && findRoom != saved.shareId) {
  1781. console.info("Ignoring findRoom in lieu of continued session");
  1782. } else if (findRoom && TogetherJS.startup._joinShareId) {
  1783. console.info("Ignoring findRoom in lieu of explicit invite to session");
  1784. }
  1785. if (findRoom && typeof findRoom == "string" && (! saved) && (! TogetherJS.startup._joinShareId)) {
  1786. isClient = true;
  1787. shareId = findRoom;
  1788. sessionId = util.generateId();
  1789. } else if (findRoom && (! saved) && (! TogetherJS.startup._joinShareId)) {
  1790. assert(findRoom.prefix && typeof findRoom.prefix == "string", "Bad findRoom.prefix", findRoom);
  1791. assert(findRoom.max && typeof findRoom.max == "number" && findRoom.max > 0,
  1792. "Bad findRoom.max", findRoom);
  1793. sessionId = util.generateId();
  1794. if (findRoom.prefix.search(/[^a-zA-Z0-9]/) != -1) {
  1795. console.warn("Bad value for findRoom.prefix:", JSON.stringify(findRoom.prefix));
  1796. }
  1797. getRoomName(findRoom.prefix, findRoom.max).then(function (shareId) {
  1798. // FIXME: duplicates code below:
  1799. session.clientId = session.identityId + "." + sessionId;
  1800. storage.tab.set("status", {reason: "joined", shareId: shareId, running: true, date: Date.now(), sessionId: sessionId});
  1801. session.isClient = true;
  1802. session.shareId = shareId;
  1803. session.emit("shareId");
  1804. def.resolve(session.shareId);
  1805. });
  1806. return;
  1807. } else if (TogetherJS.startup._launch) {
  1808. if (saved) {
  1809. isClient = saved.reason == "joined";
  1810. if (! shareId) {
  1811. shareId = saved.shareId;
  1812. }
  1813. sessionId = saved.sessionId;
  1814. } else {
  1815. isClient = TogetherJS.startup.reason == "joined";
  1816. assert(! sessionId);
  1817. sessionId = util.generateId();
  1818. }
  1819. if (! shareId) {
  1820. shareId = util.generateId();
  1821. }
  1822. } else if (saved) {
  1823. isClient = saved.reason == "joined";
  1824. TogetherJS.startup.reason = saved.reason;
  1825. TogetherJS.startup.continued = true;
  1826. shareId = saved.shareId;
  1827. sessionId = saved.sessionId;
  1828. // The only case when we don't need to set the storage status again is when
  1829. // we're already set to be running
  1830. set = ! saved.running;
  1831. } else {
  1832. throw new util.AssertionError("No saved status, and no startup._launch request; why did TogetherJS start?");
  1833. }
  1834. assert(session.identityId);
  1835. session.clientId = session.identityId + "." + sessionId;
  1836. if (set) {
  1837. storage.tab.set("status", {reason: TogetherJS.startup.reason, shareId: shareId, running: true, date: Date.now(), sessionId: sessionId});
  1838. }
  1839. session.isClient = isClient;
  1840. session.shareId = shareId;
  1841. session.emit("shareId");
  1842. def.resolve(session.shareId);
  1843. });
  1844. });
  1845. }
  1846. function initStartTarget() {
  1847. var id;
  1848. if (TogetherJS.startup.button) {
  1849. id = TogetherJS.startup.button.id;
  1850. if (id) {
  1851. storage.set("startTarget", id);
  1852. }
  1853. return;
  1854. }
  1855. storage.get("startTarget").then(function (id) {
  1856. var el = document.getElementById(id);
  1857. if (el) {
  1858. TogetherJS.startup.button = el;
  1859. }
  1860. });
  1861. }
  1862. session.start = function () {
  1863. initStartTarget();
  1864. initIdentityId().then(function () {
  1865. initShareId().then(function () {
  1866. readyForMessages = false;
  1867. openChannel();
  1868. require(["ui"], function (ui) {
  1869. TogetherJS.running = true;
  1870. ui.prepareUI();
  1871. require(features, function () {
  1872. $(function () {
  1873. peers = require("peers");
  1874. var startup = require("startup");
  1875. session.emit("start");
  1876. session.once("ui-ready", function () {
  1877. readyForMessages = true;
  1878. startup.start();
  1879. });
  1880. ui.activateUI();
  1881. TogetherJS.config.close("enableAnalytics");
  1882. if (TogetherJS.config.get("enableAnalytics")) {
  1883. require(["analytics"], function (analytics) {
  1884. analytics.activate();
  1885. });
  1886. }
  1887. peers._SelfLoaded.then(function () {
  1888. sendHello(false);
  1889. });
  1890. TogetherJS.emit("ready");
  1891. });
  1892. });
  1893. });
  1894. });
  1895. });
  1896. };
  1897. session.close = function (reason) {
  1898. TogetherJS.running = false;
  1899. var msg = {type: "bye"};
  1900. if (reason) {
  1901. msg.reason = reason;
  1902. }
  1903. session.send(msg);
  1904. session.emit("close");
  1905. var name = window.name;
  1906. storage.tab.get("status").then(function (saved) {
  1907. if (! saved) {
  1908. console.warn("No session information saved in", "status." + name);
  1909. } else {
  1910. saved.running = false;
  1911. saved.date = Date.now();
  1912. storage.tab.set("status", saved);
  1913. }
  1914. channel.close();
  1915. channel = null;
  1916. session.shareId = null;
  1917. session.emit("shareId");
  1918. TogetherJS.emit("close");
  1919. TogetherJS._teardown();
  1920. });
  1921. };
  1922. session.on("start", function () {
  1923. $(window).on("resize", resizeEvent);
  1924. if (includeHashInUrl) {
  1925. $(window).on("hashchange", hashchangeEvent);
  1926. }
  1927. });
  1928. session.on("close", function () {
  1929. $(window).off("resize", resizeEvent);
  1930. if (includeHashInUrl) {
  1931. $(window).off("hashchange", hashchangeEvent);
  1932. }
  1933. });
  1934. function hashchangeEvent() {
  1935. // needed because when message arives from peer this variable will be checked to
  1936. // decide weather to show actions or not
  1937. sendHello(false);
  1938. }
  1939. function resizeEvent() {
  1940. session.emit("resize");
  1941. }
  1942. if (TogetherJS.startup._launch) {
  1943. setTimeout(session.start);
  1944. }
  1945. util.testExpose({
  1946. getChannel: function () {
  1947. return channel;
  1948. }
  1949. });
  1950. return session;
  1951. });
  1952. /* This Source Code Form is subject to the terms of the Mozilla Public
  1953. * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  1954. * You can obtain one at http://mozilla.org/MPL/2.0/. */
  1955. // FIXME: maybe it would be better to dynamically assemble the first
  1956. // argument to define() here to include the localized module:
  1957. define('templates',["util", "require"], function (util, require) {
  1958. var assert = util.assert;
  1959. function clean(t) {
  1960. // Removes <% /* ... */ %> comments:
  1961. t = t.replace(/[<][%]\s*\/\*[\S\s\r\n]*\*\/\s*[%][>]/, "");
  1962. t = util.trim(t);
  1963. t = t.replace(/http:\/\/localhost:8080/g, TogetherJS.baseUrl);
  1964. t = t.replace(/TOOL_NAME/g, '<span class="togetherjs-tool-name">TogetherJS</span>');
  1965. t = t.replace(/SITE_NAME/g, '<strong class="togetherjs-site-name">[site name]</strong>');
  1966. t = t.replace(/TOOL_SITE_LINK/g, '<a href="https://togetherjs.com/" target="_blank"><span class="togetherjs-tool-name">TogetherJS</span></a>');
  1967. return t;
  1968. }
  1969. var lang = TogetherJS.getConfig("lang") || "en-US";
  1970. var moduleName = "templates-" + lang;
  1971. var templatesLang;
  1972. require([moduleName], function (mod) {
  1973. templatesLang = mod;
  1974. });
  1975. return function (resourceName) {
  1976. // Sometimes require([moduleName]) doesn't return even after the
  1977. // module has been loaded, but this sync version of require() will
  1978. // pick up the module in that case:
  1979. if (! templatesLang) {
  1980. try {
  1981. templatesLang = require(moduleName);
  1982. } catch (e) {
  1983. console.warn("Error requiring module:", e);
  1984. }
  1985. }
  1986. assert(templatesLang, "Templates not yet loaded");
  1987. return clean(templatesLang[resourceName]);
  1988. };
  1989. });
  1990. /* This Source Code Form is subject to the terms of the Mozilla Public
  1991. * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  1992. * You can obtain one at http://mozilla.org/MPL/2.0/. */
  1993. define('peers',["util", "session", "storage", "require", "templates"], function (util, session, storage, require, templates) {
  1994. var peers = util.Module("peers");
  1995. var assert = util.assert;
  1996. var CHECK_ACTIVITY_INTERVAL = 10*1000; // Every 10 seconds see if someone has gone idle
  1997. var IDLE_TIME = 3*60*1000; // Idle time is 3 minutes
  1998. var TAB_IDLE_TIME = 2*60*1000; // When you tab away, after two minutes you'll say you are idle
  1999. var BYE_TIME = 10*60*1000; // After 10 minutes of inactivity the person is considered to be "gone"
  2000. var ui;
  2001. require(["ui"], function (uiModule) {
  2002. ui = uiModule;
  2003. });
  2004. var DEFAULT_NICKNAMES = templates("names").split(/,\s*/g);
  2005. var Peer = util.Class({
  2006. isSelf: false,
  2007. constructor: function (id, attrs) {
  2008. attrs = attrs || {};
  2009. assert(id);
  2010. assert(! Peer.peers[id]);
  2011. this.id = id;
  2012. this.identityId = attrs.identityId || null;
  2013. this.status = attrs.status || "live";
  2014. this.idle = attrs.status || "active";
  2015. this.name = attrs.name || null;
  2016. this.avatar = attrs.avatar || null;
  2017. this.color = attrs.color || "#00FF00";
  2018. this.view = ui.PeerView(this);
  2019. this.lastMessageDate = 0;
  2020. this.following = attrs.following || false;
  2021. Peer.peers[id] = this;
  2022. var joined = attrs.joined || false;
  2023. if (attrs.fromHelloMessage) {
  2024. this.updateFromHello(attrs.fromHelloMessage);
  2025. if (attrs.fromHelloMessage.type == "hello") {
  2026. joined = true;
  2027. }
  2028. }
  2029. peers.emit("new-peer", this);
  2030. if (joined) {
  2031. this.view.notifyJoined();
  2032. }
  2033. this.view.update();
  2034. },
  2035. repr: function () {
  2036. return "Peer(" + JSON.stringify(this.id) + ")";
  2037. },
  2038. serialize: function () {
  2039. return {
  2040. id: this.id,
  2041. status: this.status,
  2042. idle: this.idle,
  2043. url: this.url,
  2044. hash: this.hash,
  2045. title: this.title,
  2046. identityId: this.identityId,
  2047. rtcSupported: this.rtcSupported,
  2048. name: this.name,
  2049. avatar: this.avatar,
  2050. color: this.color,
  2051. following: this.following
  2052. };
  2053. },
  2054. destroy: function () {
  2055. this.view.destroy();
  2056. delete Peer.peers[this.id];
  2057. },
  2058. updateMessageDate: function (msg) {
  2059. if (this.idle == "inactive") {
  2060. this.update({idle: "active"});
  2061. }
  2062. if (this.status == "bye") {
  2063. this.unbye();
  2064. }
  2065. this.lastMessageDate = Date.now();
  2066. },
  2067. updateFromHello: function (msg) {
  2068. var urlUpdated = false;
  2069. var activeRTC = false;
  2070. var identityUpdated = false;
  2071. if (msg.url && msg.url != this.url) {
  2072. this.url = msg.url;
  2073. this.hash = null;
  2074. this.title = null;
  2075. urlUpdated = true;
  2076. }
  2077. if (msg.hash != this.hash) {
  2078. this.hash = msg.urlHash;
  2079. urlUpdated = true;
  2080. }
  2081. if (msg.title != this.title) {
  2082. this.title = msg.title;
  2083. urlUpdated = true;
  2084. }
  2085. if (msg.rtcSupported !== undefined) {
  2086. this.rtcSupported = msg.rtcSupported;
  2087. }
  2088. if (msg.identityId !== undefined) {
  2089. this.identityId = msg.identityId;
  2090. }
  2091. if (msg.name && msg.name != this.name) {
  2092. this.name = msg.name;
  2093. identityUpdated = true;
  2094. }
  2095. if (msg.avatar && msg.avatar != this.avatar) {
  2096. util.assertValidUrl(msg.avatar);
  2097. this.avatar = msg.avatar;
  2098. identityUpdated = true;
  2099. }
  2100. if (msg.color && msg.color != this.color) {
  2101. this.color = msg.color;
  2102. identityUpdated = true;
  2103. }
  2104. if (msg.isClient !== undefined) {
  2105. this.isCreator = ! msg.isClient;
  2106. }
  2107. if (this.status != "live") {
  2108. this.status = "live";
  2109. peers.emit("status-updated", this);
  2110. }
  2111. if (this.idle != "active") {
  2112. this.idle = "active";
  2113. peers.emit("idle-updated", this);
  2114. }
  2115. if (msg.rtcSupported) {
  2116. peers.emit("rtc-supported", this);
  2117. }
  2118. if (urlUpdated) {
  2119. peers.emit("url-updated", this);
  2120. }
  2121. if (identityUpdated) {
  2122. peers.emit("identity-updated", this);
  2123. }
  2124. // FIXME: I can't decide if this is the only time we need to emit
  2125. // this message (and not .update() or other methods)
  2126. if (this.following) {
  2127. session.emit("follow-peer", this);
  2128. }
  2129. },
  2130. update: function (attrs) {
  2131. // FIXME: should probably test that only a couple attributes are settable
  2132. // particularly status and idle
  2133. if (attrs.idle) {
  2134. this.idle = attrs.idle;
  2135. }
  2136. if (attrs.status) {
  2137. this.status = attrs.status;
  2138. }
  2139. this.view.update();
  2140. },
  2141. className: function (prefix) {
  2142. prefix = prefix || "";
  2143. return prefix + util.safeClassName(this.id);
  2144. },
  2145. bye: function () {
  2146. if (this.status != "bye") {
  2147. this.status = "bye";
  2148. peers.emit("status-updated", this);
  2149. }
  2150. this.view.update();
  2151. },
  2152. unbye: function () {
  2153. if (this.status == "bye") {
  2154. this.status = "live";
  2155. peers.emit("status-updated", this);
  2156. }
  2157. this.view.update();
  2158. },
  2159. nudge: function () {
  2160. session.send({
  2161. type: "url-change-nudge",
  2162. url: location.href,
  2163. to: this.id
  2164. });
  2165. },
  2166. follow: function () {
  2167. if (this.following) {
  2168. return;
  2169. }
  2170. peers.getAllPeers().forEach(function (p) {
  2171. if (p.following) {
  2172. p.unfollow();
  2173. }
  2174. });
  2175. this.following = true;
  2176. // We have to make sure we remember this, even if we change URLs:
  2177. storeSerialization();
  2178. this.view.update();
  2179. session.emit("follow-peer", this);
  2180. },
  2181. unfollow: function () {
  2182. this.following = false;
  2183. storeSerialization();
  2184. this.view.update();
  2185. }
  2186. });
  2187. // FIXME: I can't decide where this should actually go, seems weird
  2188. // that it is emitted and handled in the same module
  2189. session.on("follow-peer", function (peer) {
  2190. if (peer.url != session.currentUrl()) {
  2191. var url = peer.url;
  2192. if (peer.urlHash) {
  2193. url += peer.urlHash;
  2194. }
  2195. location.href = url;
  2196. }
  2197. });
  2198. Peer.peers = {};
  2199. Peer.deserialize = function (obj) {
  2200. obj.fromStorage = true;
  2201. var peer = Peer(obj.id, obj);
  2202. };
  2203. peers.Self = undefined;
  2204. session.on("start", function () {
  2205. if (peers.Self) {
  2206. return;
  2207. }
  2208. /* Same interface as Peer, represents oneself (local user): */
  2209. peers.Self = util.mixinEvents({
  2210. isSelf: true,
  2211. id: session.clientId,
  2212. identityId: session.identityId,
  2213. status: "live",
  2214. idle: "active",
  2215. name: null,
  2216. avatar: null,
  2217. color: null,
  2218. defaultName: null,
  2219. loaded: false,
  2220. isCreator: ! session.isClient,
  2221. update: function (attrs) {
  2222. var updatePeers = false;
  2223. var updateIdle = false;
  2224. var updateMsg = {type: "peer-update"};
  2225. if (typeof attrs.name == "string" && attrs.name != this.name) {
  2226. this.name = attrs.name;
  2227. updateMsg.name = this.name;
  2228. if (! attrs.fromLoad) {
  2229. storage.settings.set("name", this.name);
  2230. updatePeers = true;
  2231. }
  2232. }
  2233. if (attrs.avatar && attrs.avatar != this.avatar) {
  2234. util.assertValidUrl(attrs.avatar);
  2235. this.avatar = attrs.avatar;
  2236. updateMsg.avatar = this.avatar;
  2237. if (! attrs.fromLoad) {
  2238. storage.settings.set("avatar", this.avatar);
  2239. updatePeers = true;
  2240. }
  2241. }
  2242. if (attrs.color && attrs.color != this.color) {
  2243. this.color = attrs.color;
  2244. updateMsg.color = this.color;
  2245. if (! attrs.fromLoad) {
  2246. storage.settings.set("color", this.color);
  2247. updatePeers = true;
  2248. }
  2249. }
  2250. if (attrs.defaultName && attrs.defaultName != this.defaultName) {
  2251. this.defaultName = attrs.defaultName;
  2252. if (! attrs.fromLoad) {
  2253. storage.settings.set("defaultName", this.defaultName);
  2254. updatePeers = true;
  2255. }
  2256. }
  2257. if (attrs.status && attrs.status != this.status) {
  2258. this.status = attrs.status;
  2259. peers.emit("status-updated", this);
  2260. }
  2261. if (attrs.idle && attrs.idle != this.idle) {
  2262. this.idle = attrs.idle;
  2263. updateIdle = true;
  2264. peers.emit("idle-updated", this);
  2265. }
  2266. this.view.update();
  2267. if (updatePeers && ! attrs.fromLoad) {
  2268. session.emit("self-updated");
  2269. session.send(updateMsg);
  2270. }
  2271. if (updateIdle && ! attrs.fromLoad) {
  2272. session.send({
  2273. type: "idle-status",
  2274. idle: this.idle
  2275. });
  2276. }
  2277. },
  2278. className: function (prefix) {
  2279. prefix = prefix || "";
  2280. return prefix + "self";
  2281. },
  2282. _loadFromSettings: function () {
  2283. return util.resolveMany(
  2284. storage.settings.get("name"),
  2285. storage.settings.get("avatar"),
  2286. storage.settings.get("defaultName"),
  2287. storage.settings.get("color")).then((function (name, avatar, defaultName, color) {
  2288. if (! defaultName) {
  2289. defaultName = util.pickRandom(DEFAULT_NICKNAMES);
  2290. storage.settings.set("defaultName", defaultName);
  2291. }
  2292. if (! color) {
  2293. color = Math.floor(Math.random() * 0xffffff).toString(16);
  2294. while (color.length < 6) {
  2295. color = "0" + color;
  2296. }
  2297. color = "#" + color;
  2298. storage.settings.set("color", color);
  2299. }
  2300. if (! avatar) {
  2301. avatar = TogetherJS.baseUrl + "/togetherjs/images/default-avatar.png";
  2302. }
  2303. this.update({
  2304. name: name,
  2305. avatar: avatar,
  2306. defaultName: defaultName,
  2307. color: color,
  2308. fromLoad: true
  2309. });
  2310. peers._SelfLoaded.resolve();
  2311. }).bind(this)); // FIXME: ignoring error
  2312. },
  2313. _loadFromApp: function () {
  2314. // FIXME: I wonder if these should be optionally functions?
  2315. // We could test typeof==function to distinguish between a getter and a concrete value
  2316. var getUserName = TogetherJS.config.get("getUserName");
  2317. var getUserColor = TogetherJS.config.get("getUserColor");
  2318. var getUserAvatar = TogetherJS.config.get("getUserAvatar");
  2319. var name, color, avatar;
  2320. if (getUserName) {
  2321. if (typeof getUserName == "string") {
  2322. name = getUserName;
  2323. } else {
  2324. name = getUserName();
  2325. }
  2326. if (name && typeof name != "string") {
  2327. // FIXME: test for HTML safe? Not that we require it, but
  2328. // <>'s are probably a sign something is wrong.
  2329. console.warn("Error in getUserName(): should return a string (got", name, ")");
  2330. name = null;
  2331. }
  2332. }
  2333. if (getUserColor) {
  2334. if (typeof getUserColor == "string") {
  2335. color = getUserColor;
  2336. } else {
  2337. color = getUserColor();
  2338. }
  2339. if (color && typeof color != "string") {
  2340. // FIXME: would be nice to test for color-ness here.
  2341. console.warn("Error in getUserColor(): should return a string (got", color, ")");
  2342. color = null;
  2343. }
  2344. }
  2345. if (getUserAvatar) {
  2346. if (typeof getUserAvatar == "string") {
  2347. avatar = getUserAvatar;
  2348. } else {
  2349. avatar = getUserAvatar();
  2350. }
  2351. if (avatar && typeof avatar != "string") {
  2352. console.warn("Error in getUserAvatar(): should return a string (got", avatar, ")");
  2353. avatar = null;
  2354. }
  2355. }
  2356. if (name || color || avatar) {
  2357. this.update({
  2358. name: name,
  2359. color: color,
  2360. avatar: avatar
  2361. });
  2362. }
  2363. }
  2364. });
  2365. peers.Self.view = ui.PeerView(peers.Self);
  2366. storage.tab.get("peerCache").then(deserialize);
  2367. peers.Self._loadFromSettings().then(function() {
  2368. peers.Self._loadFromApp();
  2369. peers.Self.view.update();
  2370. session.emit("self-updated");
  2371. });
  2372. });
  2373. session.on("refresh-user-data", function () {
  2374. if (peers.Self) {
  2375. peers.Self._loadFromApp();
  2376. }
  2377. });
  2378. TogetherJS.config.track(
  2379. "getUserName",
  2380. TogetherJS.config.track(
  2381. "getUserColor",
  2382. TogetherJS.config.track(
  2383. "getUserAvatar",
  2384. function () {
  2385. if (peers.Self) {
  2386. peers.Self._loadFromApp();
  2387. }
  2388. }
  2389. )
  2390. )
  2391. );
  2392. peers._SelfLoaded = util.Deferred();
  2393. function serialize() {
  2394. var peers = [];
  2395. util.forEachAttr(Peer.peers, function (peer) {
  2396. peers.push(peer.serialize());
  2397. });
  2398. return {
  2399. peers: peers
  2400. };
  2401. }
  2402. function deserialize(obj) {
  2403. if (! obj) {
  2404. return;
  2405. }
  2406. obj.peers.forEach(function (peer) {
  2407. Peer.deserialize(peer);
  2408. });
  2409. }
  2410. peers.getPeer = function getPeer(id, message, ignoreMissing) {
  2411. assert(id);
  2412. var peer = Peer.peers[id];
  2413. if (id === session.clientId) {
  2414. return peers.Self;
  2415. }
  2416. if (message && ! peer) {
  2417. peer = Peer(id, {fromHelloMessage: message});
  2418. return peer;
  2419. }
  2420. if (ignoreMissing && !peer) {
  2421. return null;
  2422. }
  2423. assert(peer, "No peer with id:", id);
  2424. if (message &&
  2425. (message.type == "hello" || message.type == "hello-back" ||
  2426. message.type == "peer-update")) {
  2427. peer.updateFromHello(message);
  2428. peer.view.update();
  2429. }
  2430. return Peer.peers[id];
  2431. };
  2432. peers.getAllPeers = function (liveOnly) {
  2433. var result = [];
  2434. util.forEachAttr(Peer.peers, function (peer) {
  2435. if (liveOnly && peer.status != "live") {
  2436. return;
  2437. }
  2438. result.push(peer);
  2439. });
  2440. return result;
  2441. };
  2442. function checkActivity() {
  2443. var ps = peers.getAllPeers();
  2444. var now = Date.now();
  2445. ps.forEach(function (p) {
  2446. if (p.idle == "active" && now - p.lastMessageDate > IDLE_TIME) {
  2447. p.update({idle: "inactive"});
  2448. }
  2449. if (p.status != "bye" && now - p.lastMessageDate > BYE_TIME) {
  2450. p.bye();
  2451. }
  2452. });
  2453. }
  2454. session.hub.on("bye", function (msg) {
  2455. var peer = peers.getPeer(msg.clientId);
  2456. peer.bye();
  2457. });
  2458. var checkActivityTask = null;
  2459. session.on("start", function () {
  2460. if (checkActivityTask) {
  2461. console.warn("Old peers checkActivityTask left over?");
  2462. clearTimeout(checkActivityTask);
  2463. }
  2464. checkActivityTask = setInterval(checkActivity, CHECK_ACTIVITY_INTERVAL);
  2465. });
  2466. session.on("close", function () {
  2467. util.forEachAttr(Peer.peers, function (peer) {
  2468. peer.destroy();
  2469. });
  2470. storage.tab.set("peerCache", undefined);
  2471. clearTimeout(checkActivityTask);
  2472. checkActivityTask = null;
  2473. });
  2474. var tabIdleTimeout = null;
  2475. session.on("visibility-change", function (hidden) {
  2476. if (hidden) {
  2477. if (tabIdleTimeout) {
  2478. clearTimeout(tabIdleTimeout);
  2479. }
  2480. tabIdleTimeout = setTimeout(function () {
  2481. peers.Self.update({idle: "inactive"});
  2482. }, TAB_IDLE_TIME);
  2483. } else {
  2484. if (tabIdleTimeout) {
  2485. clearTimeout(tabIdleTimeout);
  2486. }
  2487. if (peers.Self.idle == "inactive") {
  2488. peers.Self.update({idle: "active"});
  2489. }
  2490. }
  2491. });
  2492. session.hub.on("idle-status", function (msg) {
  2493. msg.peer.update({idle: msg.idle});
  2494. });
  2495. // Pings are a straight alive check, and contain no more information:
  2496. session.hub.on("ping", function () {
  2497. session.send({type: "ping-back"});
  2498. });
  2499. window.addEventListener("pagehide", function () {
  2500. // FIXME: not certain if this should be tab local or not:
  2501. storeSerialization();
  2502. }, false);
  2503. function storeSerialization() {
  2504. storage.tab.set("peerCache", serialize());
  2505. }
  2506. util.mixinEvents(peers);
  2507. util.testExpose({
  2508. setIdleTime: function (time) {
  2509. IDLE_TIME = time;
  2510. CHECK_ACTIVITY_INTERVAL = time / 2;
  2511. if (TogetherJS.running) {
  2512. clearTimeout(checkActivityTask);
  2513. checkActivityTask = setInterval(checkActivity, CHECK_ACTIVITY_INTERVAL);
  2514. }
  2515. }
  2516. });
  2517. util.testExpose({
  2518. setByeTime: function (time) {
  2519. BYE_TIME = time;
  2520. CHECK_ACTIVITY_INTERVAL = Math.min(CHECK_ACTIVITY_INTERVAL, time / 2);
  2521. if (TogetherJS.running) {
  2522. clearTimeout(checkActivityTask);
  2523. checkActivityTask = setInterval(checkActivity, CHECK_ACTIVITY_INTERVAL);
  2524. }
  2525. }
  2526. });
  2527. return peers;
  2528. });
  2529. /* This Source Code Form is subject to the terms of the Mozilla Public
  2530. * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  2531. * You can obtain one at http://mozilla.org/MPL/2.0/. */
  2532. define('windowing',["jquery", "util", "peers", "session"], function ($, util, peers, session) {
  2533. var assert = util.assert;
  2534. var windowing = util.Module("windowing");
  2535. var $window = $(window);
  2536. // This is also in togetherjs.less, under .togetherjs-animated
  2537. var ANIMATION_DURATION = 1000;
  2538. /* Displays one window. A window must already exist. This hides other windows, and
  2539. positions the window according to its data-bound-to attributes */
  2540. windowing.show = function (element, options) {
  2541. element = $(element);
  2542. options = options || {};
  2543. options.bind = options.bind || element.attr("data-bind-to");
  2544. var notification = element.hasClass("togetherjs-notification");
  2545. var modal = element.hasClass("togetherjs-modal");
  2546. if (options.bind) {
  2547. options.bind = $(options.bind);
  2548. }
  2549. windowing.hide();
  2550. element.stop();
  2551. element.show();
  2552. // In addition to being hidden, the window can be faded out, which we want to undo:
  2553. element.css({opacity: "1"});
  2554. if (options.bind) {
  2555. assert(! modal, "Binding does not currently work with modals");
  2556. bind(element, options.bind);
  2557. }
  2558. if (notification) {
  2559. element.slideIn();
  2560. } else if (! modal) {
  2561. element.popinWindow();
  2562. }
  2563. if (modal) {
  2564. getModalBackground().show();
  2565. modalEscape.bind();
  2566. }
  2567. onClose = options.onClose || null;
  2568. session.emit("display-window", element.attr("id"), element);
  2569. };
  2570. var onClose = null;
  2571. /* Moves a window to be attached to data-bind-to, e.g., the button
  2572. that opened the window. Or you can provide an element that it should bind to. */
  2573. function bind(win, bound) {
  2574. if ($.browser.mobile) {
  2575. return;
  2576. }
  2577. win = $(win);
  2578. assert(bound.length, "Cannot find binding:", bound.selector, "from:", win.selector);
  2579. // FIXME: hardcoding
  2580. var ifacePos = "right";
  2581. //var ifacePos = panelPosition();
  2582. var boundPos = bound.offset();
  2583. boundPos.height = bound.height();
  2584. boundPos.width = bound.width();
  2585. var windowHeight = $window.height();
  2586. boundPos.top -= $window.scrollTop();
  2587. boundPos.left -= $window.scrollLeft();
  2588. // FIXME: I appear to have to add the padding to the width to get a "true"
  2589. // width. But it's still not entirely consistent.
  2590. var height = win.height() + 5;
  2591. var width = win.width() + 20;
  2592. var left, top;
  2593. if (ifacePos == "right") {
  2594. left = boundPos.left - 11 - width;
  2595. top = boundPos.top + (boundPos.height / 2) - (height / 2);
  2596. } else if (ifacePos == "left") {
  2597. left = boundPos.left + boundPos.width + 15;
  2598. top = boundPos.top + (boundPos.height / 2) - (height / 2);
  2599. } else if (ifacePos == "bottom") {
  2600. left = (boundPos.left + boundPos.width / 2) - (width / 2);
  2601. top = boundPos.top - 10 - height;
  2602. }
  2603. top = Math.min(windowHeight - 10 - height, Math.max(10, top));
  2604. win.css({
  2605. top: top + "px",
  2606. left: left + "px"
  2607. });
  2608. if (win.hasClass("togetherjs-window")) {
  2609. $("#togetherjs-window-pointer-right, #togetherjs-window-pointer-left").hide();
  2610. var pointer = $("#togetherjs-window-pointer-" + ifacePos);
  2611. pointer.show();
  2612. if (ifacePos == "right") {
  2613. pointer.css({
  2614. top: boundPos.top + Math.floor(boundPos.height / 2) + "px",
  2615. left: left + win.width() + 9 + "px"
  2616. });
  2617. } else if (ifacePos == "left") {
  2618. pointer.css({
  2619. top: boundPos.top + Math.floor(boundPos.height / 2) + "px",
  2620. left: (left - 5) + "px"
  2621. });
  2622. } else {
  2623. console.warn("don't know how to deal with position:", ifacePos);
  2624. }
  2625. }
  2626. win.data("boundTo", bound.selector || "#" + bound.attr("id"));
  2627. bound.addClass("togetherjs-active");
  2628. }
  2629. session.on("resize", function () {
  2630. var win = $(".togetherjs-modal:visible, .togetherjs-window:visible");
  2631. if (! win.length) {
  2632. return;
  2633. }
  2634. var boundTo = win.data("boundTo");
  2635. if (! boundTo) {
  2636. return;
  2637. }
  2638. boundTo = $(boundTo);
  2639. bind(win, boundTo);
  2640. });
  2641. windowing.hide = function (els) {
  2642. // FIXME: also hide modals?
  2643. els = els || ".togetherjs-window, .togetherjs-modal, .togetherjs-notification";
  2644. els = $(els);
  2645. els = els.filter(":visible");
  2646. els.filter(":not(.togetherjs-notification)").hide();
  2647. getModalBackground().hide();
  2648. var windows = [];
  2649. els.each(function (index, element) {
  2650. element = $(element);
  2651. windows.push(element);
  2652. var bound = element.data("boundTo");
  2653. if (! bound) {
  2654. return;
  2655. }
  2656. bound = $(bound);
  2657. bound.addClass("togetherjs-animated").addClass("togetherjs-color-pulse");
  2658. setTimeout(function () {
  2659. bound.removeClass("togetherjs-color-pulse").removeClass("togetherjs-animated");
  2660. }, ANIMATION_DURATION+10);
  2661. element.data("boundTo", null);
  2662. bound.removeClass("togetherjs-active");
  2663. if (element.hasClass("togetherjs-notification")) {
  2664. element.fadeOut().promise().then(function () {
  2665. this.hide();
  2666. });
  2667. }
  2668. });
  2669. $("#togetherjs-window-pointer-right, #togetherjs-window-pointer-left").hide();
  2670. if (onClose) {
  2671. onClose();
  2672. onClose = null;
  2673. }
  2674. if (windows.length) {
  2675. session.emit("hide-window", windows);
  2676. }
  2677. };
  2678. windowing.showNotification = function (element, options) {
  2679. element = $(element);
  2680. options = options || {};
  2681. assert(false);
  2682. };
  2683. windowing.toggle = function (el) {
  2684. el = $(el);
  2685. if (el.is(":visible")) {
  2686. windowing.hide(el);
  2687. } else {
  2688. windowing.show(el);
  2689. }
  2690. };
  2691. function bindEvents(el) {
  2692. el.find(".togetherjs-close, .togetherjs-dismiss").click(function (event) {
  2693. var w = $(event.target).closest(".togetherjs-window, .togetherjs-modal, .togetherjs-notification");
  2694. windowing.hide(w);
  2695. event.stopPropagation();
  2696. return false;
  2697. });
  2698. }
  2699. function getModalBackground() {
  2700. if (getModalBackground.element) {
  2701. return getModalBackground.element;
  2702. }
  2703. var background = $("#togetherjs-modal-background");
  2704. assert(background.length);
  2705. getModalBackground.element = background;
  2706. background.click(function () {
  2707. windowing.hide();
  2708. });
  2709. return background;
  2710. }
  2711. var modalEscape = {
  2712. bind: function () {
  2713. $(document).keydown(modalEscape.onKeydown);
  2714. },
  2715. unbind: function () {
  2716. $(document).unbind("keydown", modalEscape.onKeydown);
  2717. },
  2718. onKeydown: function (event) {
  2719. if (event.which == 27) {
  2720. windowing.hide();
  2721. }
  2722. }
  2723. };
  2724. session.on("close", function () {
  2725. modalEscape.unbind();
  2726. });
  2727. session.on("new-element", function (el) {
  2728. bindEvents(el);
  2729. });
  2730. return windowing;
  2731. });
  2732. /* This Source Code Form is subject to the terms of the Mozilla Public
  2733. * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  2734. * You can obtain one at http://mozilla.org/MPL/2.0/. */
  2735. define('templating',["jquery", "util", "peers", "windowing", "session"], function ($, util, peers, windowing, session) {
  2736. var assert = util.assert;
  2737. var templating = util.Module("templating");
  2738. templating.clone = function (templateId) {
  2739. templateId = "#togetherjs-template-" + templateId;
  2740. var template = $(templateId);
  2741. assert(template.length, "No template found with id:", templateId);
  2742. template = template.clone();
  2743. template.attr("id", null);
  2744. // FIXME: if called directly, doesn't emit new-element event:
  2745. return template;
  2746. };
  2747. templating.sub = function (templateId, variables) {
  2748. var template = templating.clone(templateId);
  2749. variables = variables || {};
  2750. util.forEachAttr(variables, function (value, attr) {
  2751. // FIXME: do the substitution... somehow?
  2752. var subs = template.find(".togetherjs-sub-" + attr).removeClass("togetherjs-sub-" + attr);
  2753. if (subs.length) {
  2754. if (typeof value == "string") {
  2755. subs.text(value);
  2756. } else if (value instanceof $) {
  2757. subs.append(value);
  2758. } else {
  2759. assert(false, "Unknown variable value type:", attr, "=", value);
  2760. }
  2761. }
  2762. var ifs = template.find(".togetherjs-if-" + attr).removeClass("togetherjs-sub-" + attr);
  2763. if (! value) {
  2764. ifs.hide();
  2765. }
  2766. ifs = template.find(".togetherjs-ifnot-" + attr).removeClass("togetherjs-ifnot-" + attr);
  2767. if (value) {
  2768. ifs.hide();
  2769. }
  2770. var attrName = "data-togetherjs-subattr-" + attr;
  2771. var attrs = template.find("[" + attrName + "]");
  2772. attrs.each(function (index, element) {
  2773. assert(typeof value == "string");
  2774. element = $(element);
  2775. var subAttribute = element.attr(attrName);
  2776. element.attr(attrName, null);
  2777. element.attr(subAttribute, value);
  2778. });
  2779. });
  2780. if (variables.peer) {
  2781. variables.peer.view.setElement(template);
  2782. }
  2783. if (variables.date) {
  2784. var date = variables.date;
  2785. if (typeof date == "number") {
  2786. date = new Date(date);
  2787. }
  2788. var ampm = "AM";
  2789. var hour = date.getHours();
  2790. if (hour > 12) {
  2791. hour -= 12;
  2792. ampm = "PM";
  2793. }
  2794. var minute = date.getMinutes();
  2795. var t = hour + ":";
  2796. if (minute < 10) {
  2797. t += "0";
  2798. }
  2799. t += minute;
  2800. template.find(".togetherjs-time").text(t);
  2801. template.find(".togetherjs-ampm").text(ampm);
  2802. }
  2803. // FIXME: silly this is on session:
  2804. session.emit("new-element", template);
  2805. return template;
  2806. };
  2807. return templating;
  2808. });
  2809. define('linkify',[], function () {
  2810. // FIXME: this could be moved to a different module, it's pretty stand-alone
  2811. /* Finds any links in the text of an element (or its children) and turns them
  2812. into anchors (with target=_blank) */
  2813. function linkify(el) {
  2814. if (el.jquery) {
  2815. el = el[0];
  2816. }
  2817. el.normalize();
  2818. function linkifyNode(node) {
  2819. var _len = node.childNodes.length;
  2820. for (var i=0; i<_len; i++) {
  2821. if (node.childNodes[i].nodeType == document.ELEMENT_NODE) {
  2822. linkifyNode(node.childNodes[i]);
  2823. }
  2824. }
  2825. var texts = [];
  2826. for (i=0; i<_len; i++) {
  2827. if (node.childNodes[i].nodeType == document.TEXT_NODE) {
  2828. texts.push(node.childNodes[i]);
  2829. }
  2830. }
  2831. texts.forEach(function (item) {
  2832. if (item.nodeType == document.ELEMENT_NODE) {
  2833. linkifyNode(item);
  2834. } else if (item.nodeType == document.TEXT_NODE) {
  2835. while (true) {
  2836. var text = item.nodeValue;
  2837. var regex = /\bhttps?:\/\/[a-z0-9\.\-_](:\d+)?[^ \n\t<>()\[\]]*/i;
  2838. var match = regex.exec(text);
  2839. if (! match) {
  2840. break;
  2841. }
  2842. var leadingNode = document.createTextNode(text.substr(0, match.index));
  2843. node.replaceChild(leadingNode, item);
  2844. var anchor = document.createElement("a");
  2845. anchor.setAttribute("target", "_blank");
  2846. anchor.href = match[0];
  2847. anchor.appendChild(document.createTextNode(match[0]));
  2848. node.insertBefore(anchor, leadingNode.nextSibling);
  2849. var trailing = document.createTextNode(text.substr(match.index + match[0].length));
  2850. node.insertBefore(trailing, anchor.nextSibling);
  2851. item = trailing;
  2852. }
  2853. }
  2854. });
  2855. }
  2856. linkifyNode(el);
  2857. return el;
  2858. }
  2859. return linkify;
  2860. });
  2861. // TinyColor v0.9.13
  2862. // https://github.com/bgrins/TinyColor
  2863. // 2012-11-28, Brian Grinstead, MIT License
  2864. (function(root) {
  2865. var trimLeft = /^[\s,#]+/,
  2866. trimRight = /\s+$/,
  2867. tinyCounter = 0,
  2868. math = Math,
  2869. mathRound = math.round,
  2870. mathMin = math.min,
  2871. mathMax = math.max,
  2872. mathRandom = math.random;
  2873. function tinycolor (color, opts) {
  2874. color = (color) ? color : '';
  2875. // If input is already a tinycolor, return itself
  2876. if (typeof color == "object" && color.hasOwnProperty("_tc_id")) {
  2877. return color;
  2878. }
  2879. var rgb = inputToRGB(color);
  2880. var r = rgb.r,
  2881. g = rgb.g,
  2882. b = rgb.b,
  2883. a = rgb.a,
  2884. roundA = mathRound(100*a) / 100,
  2885. format = rgb.format;
  2886. // Don't let the range of [0,255] come back in [0,1].
  2887. // Potentially lose a little bit of precision here, but will fix issues where
  2888. // .5 gets interpreted as half of the total, instead of half of 1
  2889. // If it was supposed to be 128, this was already taken care of by `inputToRgb`
  2890. if (r < 1) { r = mathRound(r); }
  2891. if (g < 1) { g = mathRound(g); }
  2892. if (b < 1) { b = mathRound(b); }
  2893. return {
  2894. ok: rgb.ok,
  2895. format: format,
  2896. _tc_id: tinyCounter++,
  2897. alpha: a,
  2898. toHsv: function() {
  2899. var hsv = rgbToHsv(r, g, b);
  2900. return { h: hsv.h * 360, s: hsv.s, v: hsv.v, a: a };
  2901. },
  2902. toHsvString: function() {
  2903. var hsv = rgbToHsv(r, g, b);
  2904. var h = mathRound(hsv.h * 360), s = mathRound(hsv.s * 100), v = mathRound(hsv.v * 100);
  2905. return (a == 1) ?
  2906. "hsv(" + h + ", " + s + "%, " + v + "%)" :
  2907. "hsva(" + h + ", " + s + "%, " + v + "%, "+ roundA + ")";
  2908. },
  2909. toHsl: function() {
  2910. var hsl = rgbToHsl(r, g, b);
  2911. return { h: hsl.h * 360, s: hsl.s, l: hsl.l, a: a };
  2912. },
  2913. toHslString: function() {
  2914. var hsl = rgbToHsl(r, g, b);
  2915. var h = mathRound(hsl.h * 360), s = mathRound(hsl.s * 100), l = mathRound(hsl.l * 100);
  2916. return (a == 1) ?
  2917. "hsl(" + h + ", " + s + "%, " + l + "%)" :
  2918. "hsla(" + h + ", " + s + "%, " + l + "%, "+ roundA + ")";
  2919. },
  2920. toHex: function() {
  2921. return rgbToHex(r, g, b);
  2922. },
  2923. toHexString: function() {
  2924. return '#' + rgbToHex(r, g, b);
  2925. },
  2926. toRgb: function() {
  2927. return { r: mathRound(r), g: mathRound(g), b: mathRound(b), a: a };
  2928. },
  2929. toRgbString: function() {
  2930. return (a == 1) ?
  2931. "rgb(" + mathRound(r) + ", " + mathRound(g) + ", " + mathRound(b) + ")" :
  2932. "rgba(" + mathRound(r) + ", " + mathRound(g) + ", " + mathRound(b) + ", " + roundA + ")";
  2933. },
  2934. toPercentageRgb: function() {
  2935. return { r: mathRound(bound01(r, 255) * 100) + "%", g: mathRound(bound01(g, 255) * 100) + "%", b: mathRound(bound01(b, 255) * 100) + "%", a: a };
  2936. },
  2937. toPercentageRgbString: function() {
  2938. return (a == 1) ?
  2939. "rgb(" + mathRound(bound01(r, 255) * 100) + "%, " + mathRound(bound01(g, 255) * 100) + "%, " + mathRound(bound01(b, 255) * 100) + "%)" :
  2940. "rgba(" + mathRound(bound01(r, 255) * 100) + "%, " + mathRound(bound01(g, 255) * 100) + "%, " + mathRound(bound01(b, 255) * 100) + "%, " + roundA + ")";
  2941. },
  2942. toName: function() {
  2943. return hexNames[rgbToHex(r, g, b)] || false;
  2944. },
  2945. toFilter: function() {
  2946. var hex = rgbToHex(r, g, b);
  2947. var secondHex = hex;
  2948. var alphaHex = Math.round(parseFloat(a) * 255).toString(16);
  2949. var secondAlphaHex = alphaHex;
  2950. var gradientType = opts && opts.gradientType ? "GradientType = 1, " : "";
  2951. if (secondColor) {
  2952. var s = tinycolor(secondColor);
  2953. secondHex = s.toHex();
  2954. secondAlphaHex = Math.round(parseFloat(s.alpha) * 255).toString(16);
  2955. }
  2956. return "progid:DXImageTransform.Microsoft.gradient("+gradientType+"startColorstr=#" + pad2(alphaHex) + hex + ",endColorstr=#" + pad2(secondAlphaHex) + secondHex + ")";
  2957. },
  2958. toString: function(format) {
  2959. format = format || this.format;
  2960. var formattedString = false;
  2961. if (format === "rgb") {
  2962. formattedString = this.toRgbString();
  2963. }
  2964. if (format === "prgb") {
  2965. formattedString = this.toPercentageRgbString();
  2966. }
  2967. if (format === "hex") {
  2968. formattedString = this.toHexString();
  2969. }
  2970. if (format === "name") {
  2971. formattedString = this.toName();
  2972. }
  2973. if (format === "hsl") {
  2974. formattedString = this.toHslString();
  2975. }
  2976. if (format === "hsv") {
  2977. formattedString = this.toHsvString();
  2978. }
  2979. return formattedString || this.toHexString();
  2980. }
  2981. };
  2982. }
  2983. // If input is an object, force 1 into "1.0" to handle ratios properly
  2984. // String input requires "1.0" as input, so 1 will be treated as 1
  2985. tinycolor.fromRatio = function(color) {
  2986. if (typeof color == "object") {
  2987. var newColor = {};
  2988. for (var i in color) {
  2989. newColor[i] = convertToPercentage(color[i]);
  2990. }
  2991. color = newColor;
  2992. }
  2993. return tinycolor(color);
  2994. };
  2995. // Given a string or object, convert that input to RGB
  2996. // Possible string inputs:
  2997. //
  2998. // "red"
  2999. // "#f00" or "f00"
  3000. // "#ff0000" or "ff0000"
  3001. // "rgb 255 0 0" or "rgb (255, 0, 0)"
  3002. // "rgb 1.0 0 0" or "rgb (1, 0, 0)"
  3003. // "rgba (255, 0, 0, 1)" or "rgba 255, 0, 0, 1"
  3004. // "rgba (1.0, 0, 0, 1)" or "rgba 1.0, 0, 0, 1"
  3005. // "hsl(0, 100%, 50%)" or "hsl 0 100% 50%"
  3006. // "hsla(0, 100%, 50%, 1)" or "hsla 0 100% 50%, 1"
  3007. // "hsv(0, 100%, 100%)" or "hsv 0 100% 100%"
  3008. //
  3009. function inputToRGB(color) {
  3010. var rgb = { r: 255, g: 255, b: 255 };
  3011. var a = 1;
  3012. var ok = false;
  3013. var format = false;
  3014. if (typeof color == "string") {
  3015. color = stringInputToObject(color);
  3016. }
  3017. if (typeof color == "object") {
  3018. if (color.hasOwnProperty("r") && color.hasOwnProperty("g") && color.hasOwnProperty("b")) {
  3019. rgb = rgbToRgb(color.r, color.g, color.b);
  3020. ok = true;
  3021. format = String(color.r).substr(-1) === "%" ? "prgb" : "rgb";
  3022. }
  3023. else if (color.hasOwnProperty("h") && color.hasOwnProperty("s") && color.hasOwnProperty("v")) {
  3024. color.s = convertToPercentage(color.s);
  3025. color.v = convertToPercentage(color.v);
  3026. rgb = hsvToRgb(color.h, color.s, color.v);
  3027. ok = true;
  3028. format = "hsv";
  3029. }
  3030. else if (color.hasOwnProperty("h") && color.hasOwnProperty("s") && color.hasOwnProperty("l")) {
  3031. color.s = convertToPercentage(color.s);
  3032. color.l = convertToPercentage(color.l);
  3033. rgb = hslToRgb(color.h, color.s, color.l);
  3034. ok = true;
  3035. format = "hsl";
  3036. }
  3037. if (color.hasOwnProperty("a")) {
  3038. a = color.a;
  3039. }
  3040. }
  3041. a = parseFloat(a);
  3042. // Handle invalid alpha characters by setting to 1
  3043. if (isNaN(a) || a < 0 || a > 1) {
  3044. a = 1;
  3045. }
  3046. return {
  3047. ok: ok,
  3048. format: color.format || format,
  3049. r: mathMin(255, mathMax(rgb.r, 0)),
  3050. g: mathMin(255, mathMax(rgb.g, 0)),
  3051. b: mathMin(255, mathMax(rgb.b, 0)),
  3052. a: a
  3053. };
  3054. }
  3055. // Conversion Functions
  3056. // --------------------
  3057. // `rgbToHsl`, `rgbToHsv`, `hslToRgb`, `hsvToRgb` modified from:
  3058. // <http://mjijackson.com/2008/02/rgb-to-hsl-and-rgb-to-hsv-color-model-conversion-algorithms-in-javascript>
  3059. // `rgbToRgb`
  3060. // Handle bounds / percentage checking to conform to CSS color spec
  3061. // <http://www.w3.org/TR/css3-color/>
  3062. // *Assumes:* r, g, b in [0, 255] or [0, 1]
  3063. // *Returns:* { r, g, b } in [0, 255]
  3064. function rgbToRgb(r, g, b){
  3065. return {
  3066. r: bound01(r, 255) * 255,
  3067. g: bound01(g, 255) * 255,
  3068. b: bound01(b, 255) * 255
  3069. };
  3070. }
  3071. // `rgbToHsl`
  3072. // Converts an RGB color value to HSL.
  3073. // *Assumes:* r, g, and b are contained in [0, 255] or [0, 1]
  3074. // *Returns:* { h, s, l } in [0,1]
  3075. function rgbToHsl(r, g, b) {
  3076. r = bound01(r, 255);
  3077. g = bound01(g, 255);
  3078. b = bound01(b, 255);
  3079. var max = mathMax(r, g, b), min = mathMin(r, g, b);
  3080. var h, s, l = (max + min) / 2;
  3081. if(max == min) {
  3082. h = s = 0; // achromatic
  3083. }
  3084. else {
  3085. var d = max - min;
  3086. s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
  3087. switch(max) {
  3088. case r: h = (g - b) / d + (g < b ? 6 : 0); break;
  3089. case g: h = (b - r) / d + 2; break;
  3090. case b: h = (r - g) / d + 4; break;
  3091. }
  3092. h /= 6;
  3093. }
  3094. return { h: h, s: s, l: l };
  3095. }
  3096. // `hslToRgb`
  3097. // Converts an HSL color value to RGB.
  3098. // *Assumes:* h is contained in [0, 1] or [0, 360] and s and l are contained [0, 1] or [0, 100]
  3099. // *Returns:* { r, g, b } in the set [0, 255]
  3100. function hslToRgb(h, s, l) {
  3101. var r, g, b;
  3102. h = bound01(h, 360);
  3103. s = bound01(s, 100);
  3104. l = bound01(l, 100);
  3105. function hue2rgb(p, q, t) {
  3106. if(t < 0) t += 1;
  3107. if(t > 1) t -= 1;
  3108. if(t < 1/6) return p + (q - p) * 6 * t;
  3109. if(t < 1/2) return q;
  3110. if(t < 2/3) return p + (q - p) * (2/3 - t) * 6;
  3111. return p;
  3112. }
  3113. if(s === 0) {
  3114. r = g = b = l; // achromatic
  3115. }
  3116. else {
  3117. var q = l < 0.5 ? l * (1 + s) : l + s - l * s;
  3118. var p = 2 * l - q;
  3119. r = hue2rgb(p, q, h + 1/3);
  3120. g = hue2rgb(p, q, h);
  3121. b = hue2rgb(p, q, h - 1/3);
  3122. }
  3123. return { r: r * 255, g: g * 255, b: b * 255 };
  3124. }
  3125. // `rgbToHsv`
  3126. // Converts an RGB color value to HSV
  3127. // *Assumes:* r, g, and b are contained in the set [0, 255] or [0, 1]
  3128. // *Returns:* { h, s, v } in [0,1]
  3129. function rgbToHsv(r, g, b) {
  3130. r = bound01(r, 255);
  3131. g = bound01(g, 255);
  3132. b = bound01(b, 255);
  3133. var max = mathMax(r, g, b), min = mathMin(r, g, b);
  3134. var h, s, v = max;
  3135. var d = max - min;
  3136. s = max === 0 ? 0 : d / max;
  3137. if(max == min) {
  3138. h = 0; // achromatic
  3139. }
  3140. else {
  3141. switch(max) {
  3142. case r: h = (g - b) / d + (g < b ? 6 : 0); break;
  3143. case g: h = (b - r) / d + 2; break;
  3144. case b: h = (r - g) / d + 4; break;
  3145. }
  3146. h /= 6;
  3147. }
  3148. return { h: h, s: s, v: v };
  3149. }
  3150. // `hsvToRgb`
  3151. // Converts an HSV color value to RGB.
  3152. // *Assumes:* h is contained in [0, 1] or [0, 360] and s and v are contained in [0, 1] or [0, 100]
  3153. // *Returns:* { r, g, b } in the set [0, 255]
  3154. function hsvToRgb(h, s, v) {
  3155. h = bound01(h, 360) * 6;
  3156. s = bound01(s, 100);
  3157. v = bound01(v, 100);
  3158. var i = math.floor(h),
  3159. f = h - i,
  3160. p = v * (1 - s),
  3161. q = v * (1 - f * s),
  3162. t = v * (1 - (1 - f) * s),
  3163. mod = i % 6,
  3164. r = [v, q, p, p, t, v][mod],
  3165. g = [t, v, v, q, p, p][mod],
  3166. b = [p, p, t, v, v, q][mod];
  3167. return { r: r * 255, g: g * 255, b: b * 255 };
  3168. }
  3169. // `rgbToHex`
  3170. // Converts an RGB color to hex
  3171. // Assumes r, g, and b are contained in the set [0, 255]
  3172. // Returns a 3 or 6 character hex
  3173. function rgbToHex(r, g, b) {
  3174. var hex = [
  3175. pad2(mathRound(r).toString(16)),
  3176. pad2(mathRound(g).toString(16)),
  3177. pad2(mathRound(b).toString(16))
  3178. ];
  3179. // Return a 3 character hex if possible
  3180. if (hex[0].charAt(0) == hex[0].charAt(1) && hex[1].charAt(0) == hex[1].charAt(1) && hex[2].charAt(0) == hex[2].charAt(1)) {
  3181. return hex[0].charAt(0) + hex[1].charAt(0) + hex[2].charAt(0);
  3182. }
  3183. return hex.join("");
  3184. }
  3185. // `equals`
  3186. // Can be called with any tinycolor input
  3187. tinycolor.equals = function (color1, color2) {
  3188. if (!color1 || !color2) { return false; }
  3189. return tinycolor(color1).toRgbString() == tinycolor(color2).toRgbString();
  3190. };
  3191. tinycolor.random = function() {
  3192. return tinycolor.fromRatio({
  3193. r: mathRandom(),
  3194. g: mathRandom(),
  3195. b: mathRandom()
  3196. });
  3197. };
  3198. // Modification Functions
  3199. // ----------------------
  3200. // Thanks to less.js for some of the basics here
  3201. // <https://github.com/cloudhead/less.js/blob/master/lib/less/functions.js>
  3202. tinycolor.desaturate = function (color, amount) {
  3203. var hsl = tinycolor(color).toHsl();
  3204. hsl.s -= ((amount || 10) / 100);
  3205. hsl.s = clamp01(hsl.s);
  3206. return tinycolor(hsl);
  3207. };
  3208. tinycolor.saturate = function (color, amount) {
  3209. var hsl = tinycolor(color).toHsl();
  3210. hsl.s += ((amount || 10) / 100);
  3211. hsl.s = clamp01(hsl.s);
  3212. return tinycolor(hsl);
  3213. };
  3214. tinycolor.greyscale = function(color) {
  3215. return tinycolor.desaturate(color, 100);
  3216. };
  3217. tinycolor.lighten = function(color, amount) {
  3218. var hsl = tinycolor(color).toHsl();
  3219. hsl.l += ((amount || 10) / 100);
  3220. hsl.l = clamp01(hsl.l);
  3221. return tinycolor(hsl);
  3222. };
  3223. tinycolor.darken = function (color, amount) {
  3224. var hsl = tinycolor(color).toHsl();
  3225. hsl.l -= ((amount || 10) / 100);
  3226. hsl.l = clamp01(hsl.l);
  3227. return tinycolor(hsl);
  3228. };
  3229. tinycolor.complement = function(color) {
  3230. var hsl = tinycolor(color).toHsl();
  3231. hsl.h = (hsl.h + 180) % 360;
  3232. return tinycolor(hsl);
  3233. };
  3234. // Combination Functions
  3235. // ---------------------
  3236. // Thanks to jQuery xColor for some of the ideas behind these
  3237. // <https://github.com/infusion/jQuery-xcolor/blob/master/jquery.xcolor.js>
  3238. tinycolor.triad = function(color) {
  3239. var hsl = tinycolor(color).toHsl();
  3240. var h = hsl.h;
  3241. return [
  3242. tinycolor(color),
  3243. tinycolor({ h: (h + 120) % 360, s: hsl.s, l: hsl.l }),
  3244. tinycolor({ h: (h + 240) % 360, s: hsl.s, l: hsl.l })
  3245. ];
  3246. };
  3247. tinycolor.tetrad = function(color) {
  3248. var hsl = tinycolor(color).toHsl();
  3249. var h = hsl.h;
  3250. return [
  3251. tinycolor(color),
  3252. tinycolor({ h: (h + 90) % 360, s: hsl.s, l: hsl.l }),
  3253. tinycolor({ h: (h + 180) % 360, s: hsl.s, l: hsl.l }),
  3254. tinycolor({ h: (h + 270) % 360, s: hsl.s, l: hsl.l })
  3255. ];
  3256. };
  3257. tinycolor.splitcomplement = function(color) {
  3258. var hsl = tinycolor(color).toHsl();
  3259. var h = hsl.h;
  3260. return [
  3261. tinycolor(color),
  3262. tinycolor({ h: (h + 72) % 360, s: hsl.s, l: hsl.l}),
  3263. tinycolor({ h: (h + 216) % 360, s: hsl.s, l: hsl.l})
  3264. ];
  3265. };
  3266. tinycolor.analogous = function(color, results, slices) {
  3267. results = results || 6;
  3268. slices = slices || 30;
  3269. var hsl = tinycolor(color).toHsl();
  3270. var part = 360 / slices;
  3271. var ret = [tinycolor(color)];
  3272. for (hsl.h = ((hsl.h - (part * results >> 1)) + 720) % 360; --results; ) {
  3273. hsl.h = (hsl.h + part) % 360;
  3274. ret.push(tinycolor(hsl));
  3275. }
  3276. return ret;
  3277. };
  3278. tinycolor.monochromatic = function(color, results) {
  3279. results = results || 6;
  3280. var hsv = tinycolor(color).toHsv();
  3281. var h = hsv.h, s = hsv.s, v = hsv.v;
  3282. var ret = [];
  3283. var modification = 1 / results;
  3284. while (results--) {
  3285. ret.push(tinycolor({ h: h, s: s, v: v}));
  3286. v = (v + modification) % 1;
  3287. }
  3288. return ret;
  3289. };
  3290. // Readability based on W3C recommendations: http://www.w3.org/TR/AERT#color-contrast
  3291. // Returns object with two properties:
  3292. // .brightness: the difference in brightness between the two colors
  3293. // .color: the difference in color/hue between the two colors
  3294. // An "acceptable" color is considered to have a brightness difference of 125 and a
  3295. // color difference of 500
  3296. tinycolor.readability = function(color1, color2) {
  3297. var a = tinycolor(color1).toRgb(), b = tinycolor(color2).toRgb();
  3298. var brightnessA = (a.r * 299 + a.g * 587 + a.b * 114) / 1000;
  3299. var brightnessB = (b.r * 299 + b.g * 587 + b.b * 114) / 1000;
  3300. var colorDiff = (
  3301. Math.max(a.r, b.r) - Math.min(a.r, b.r) +
  3302. Math.max(a.g, b.g) - Math.min(a.g, b.g) +
  3303. Math.max(a.b, b.b) - Math.min(a.b, b.b));
  3304. return {
  3305. brightness: Math.abs(brightnessA - brightnessB),
  3306. color: colorDiff
  3307. };
  3308. };
  3309. // True if using color1 over color2 (or vice versa) is "readable"
  3310. // Based on: http://www.w3.org/TR/AERT#color-contrast
  3311. // Example:
  3312. // tinycolor.readable("#000", "#111") => false
  3313. tinycolor.readable = function(color1, color2) {
  3314. var readability = tinycolor.readability(color1, color2);
  3315. return readability.brightness > 125 && readability.color > 500;
  3316. };
  3317. // Given a base color and a list of possible foreground or background
  3318. // colors for that base, returns the most readable color.
  3319. // Example:
  3320. // tinycolor.mostReadable("#123", ["#fff", "#000"]) => "#000"
  3321. tinycolor.mostReadable = function(baseColor, colorList) {
  3322. var bestColor;
  3323. var bestScore = 0;
  3324. var bestIsReadable = false;
  3325. for (var i=0; i < colorList.length; i++) {
  3326. var readability = tinycolor.readability(baseColor, colorList[i]);
  3327. var readable = readability.brightness > 125 && readability.color > 500;
  3328. // We normalize both around the "acceptable" breaking point,
  3329. // but rank brightness constrast higher than hue. Why? I'm
  3330. // not sure, seems reasonable.
  3331. var score = 3 * (readability.brightness / 125) + (readability.color / 500);
  3332. if ((readable && ! bestIsReadable) ||
  3333. (readable && bestIsReadable && score > bestScore) ||
  3334. ((! readable) && (! bestIsReadable) && score > bestScore)) {
  3335. bestIsReadable = readable;
  3336. bestScore = score;
  3337. bestColor = colorList[i];
  3338. }
  3339. }
  3340. return bestColor;
  3341. };
  3342. // Big List of Colors
  3343. // ---------
  3344. // <http://www.w3.org/TR/css3-color/#svg-color>
  3345. var names = tinycolor.names = {
  3346. aliceblue: "f0f8ff",
  3347. antiquewhite: "faebd7",
  3348. aqua: "0ff",
  3349. aquamarine: "7fffd4",
  3350. azure: "f0ffff",
  3351. beige: "f5f5dc",
  3352. bisque: "ffe4c4",
  3353. black: "000",
  3354. blanchedalmond: "ffebcd",
  3355. blue: "00f",
  3356. blueviolet: "8a2be2",
  3357. brown: "a52a2a",
  3358. burlywood: "deb887",
  3359. burntsienna: "ea7e5d",
  3360. cadetblue: "5f9ea0",
  3361. chartreuse: "7fff00",
  3362. chocolate: "d2691e",
  3363. coral: "ff7f50",
  3364. cornflowerblue: "6495ed",
  3365. cornsilk: "fff8dc",
  3366. crimson: "dc143c",
  3367. cyan: "0ff",
  3368. darkblue: "00008b",
  3369. darkcyan: "008b8b",
  3370. darkgoldenrod: "b8860b",
  3371. darkgray: "a9a9a9",
  3372. darkgreen: "006400",
  3373. darkgrey: "a9a9a9",
  3374. darkkhaki: "bdb76b",
  3375. darkmagenta: "8b008b",
  3376. darkolivegreen: "556b2f",
  3377. darkorange: "ff8c00",
  3378. darkorchid: "9932cc",
  3379. darkred: "8b0000",
  3380. darksalmon: "e9967a",
  3381. darkseagreen: "8fbc8f",
  3382. darkslateblue: "483d8b",
  3383. darkslategray: "2f4f4f",
  3384. darkslategrey: "2f4f4f",
  3385. darkturquoise: "00ced1",
  3386. darkviolet: "9400d3",
  3387. deeppink: "ff1493",
  3388. deepskyblue: "00bfff",
  3389. dimgray: "696969",
  3390. dimgrey: "696969",
  3391. dodgerblue: "1e90ff",
  3392. firebrick: "b22222",
  3393. floralwhite: "fffaf0",
  3394. forestgreen: "228b22",
  3395. fuchsia: "f0f",
  3396. gainsboro: "dcdcdc",
  3397. ghostwhite: "f8f8ff",
  3398. gold: "ffd700",
  3399. goldenrod: "daa520",
  3400. gray: "808080",
  3401. green: "008000",
  3402. greenyellow: "adff2f",
  3403. grey: "808080",
  3404. honeydew: "f0fff0",
  3405. hotpink: "ff69b4",
  3406. indianred: "cd5c5c",
  3407. indigo: "4b0082",
  3408. ivory: "fffff0",
  3409. khaki: "f0e68c",
  3410. lavender: "e6e6fa",
  3411. lavenderblush: "fff0f5",
  3412. lawngreen: "7cfc00",
  3413. lemonchiffon: "fffacd",
  3414. lightblue: "add8e6",
  3415. lightcoral: "f08080",
  3416. lightcyan: "e0ffff",
  3417. lightgoldenrodyellow: "fafad2",
  3418. lightgray: "d3d3d3",
  3419. lightgreen: "90ee90",
  3420. lightgrey: "d3d3d3",
  3421. lightpink: "ffb6c1",
  3422. lightsalmon: "ffa07a",
  3423. lightseagreen: "20b2aa",
  3424. lightskyblue: "87cefa",
  3425. lightslategray: "789",
  3426. lightslategrey: "789",
  3427. lightsteelblue: "b0c4de",
  3428. lightyellow: "ffffe0",
  3429. lime: "0f0",
  3430. limegreen: "32cd32",
  3431. linen: "faf0e6",
  3432. magenta: "f0f",
  3433. maroon: "800000",
  3434. mediumaquamarine: "66cdaa",
  3435. mediumblue: "0000cd",
  3436. mediumorchid: "ba55d3",
  3437. mediumpurple: "9370db",
  3438. mediumseagreen: "3cb371",
  3439. mediumslateblue: "7b68ee",
  3440. mediumspringgreen: "00fa9a",
  3441. mediumturquoise: "48d1cc",
  3442. mediumvioletred: "c71585",
  3443. midnightblue: "191970",
  3444. mintcream: "f5fffa",
  3445. mistyrose: "ffe4e1",
  3446. moccasin: "ffe4b5",
  3447. navajowhite: "ffdead",
  3448. navy: "000080",
  3449. oldlace: "fdf5e6",
  3450. olive: "808000",
  3451. olivedrab: "6b8e23",
  3452. orange: "ffa500",
  3453. orangered: "ff4500",
  3454. orchid: "da70d6",
  3455. palegoldenrod: "eee8aa",
  3456. palegreen: "98fb98",
  3457. paleturquoise: "afeeee",
  3458. palevioletred: "db7093",
  3459. papayawhip: "ffefd5",
  3460. peachpuff: "ffdab9",
  3461. peru: "cd853f",
  3462. pink: "ffc0cb",
  3463. plum: "dda0dd",
  3464. powderblue: "b0e0e6",
  3465. purple: "800080",
  3466. red: "f00",
  3467. rosybrown: "bc8f8f",
  3468. royalblue: "4169e1",
  3469. saddlebrown: "8b4513",
  3470. salmon: "fa8072",
  3471. sandybrown: "f4a460",
  3472. seagreen: "2e8b57",
  3473. seashell: "fff5ee",
  3474. sienna: "a0522d",
  3475. silver: "c0c0c0",
  3476. skyblue: "87ceeb",
  3477. slateblue: "6a5acd",
  3478. slategray: "708090",
  3479. slategrey: "708090",
  3480. snow: "fffafa",
  3481. springgreen: "00ff7f",
  3482. steelblue: "4682b4",
  3483. tan: "d2b48c",
  3484. teal: "008080",
  3485. thistle: "d8bfd8",
  3486. tomato: "ff6347",
  3487. turquoise: "40e0d0",
  3488. violet: "ee82ee",
  3489. wheat: "f5deb3",
  3490. white: "fff",
  3491. whitesmoke: "f5f5f5",
  3492. yellow: "ff0",
  3493. yellowgreen: "9acd32"
  3494. };
  3495. // Make it easy to access colors via `hexNames[hex]`
  3496. var hexNames = tinycolor.hexNames = flip(names);
  3497. // Utilities
  3498. // ---------
  3499. // `{ 'name1': 'val1' }` becomes `{ 'val1': 'name1' }`
  3500. function flip(o) {
  3501. var flipped = { };
  3502. for (var i in o) {
  3503. if (o.hasOwnProperty(i)) {
  3504. flipped[o[i]] = i;
  3505. }
  3506. }
  3507. return flipped;
  3508. }
  3509. // Take input from [0, n] and return it as [0, 1]
  3510. function bound01(n, max) {
  3511. if (isOnePointZero(n)) { n = "100%"; }
  3512. var processPercent = isPercentage(n);
  3513. n = mathMin(max, mathMax(0, parseFloat(n)));
  3514. // Automatically convert percentage into number
  3515. if (processPercent) {
  3516. n = parseInt(n * max, 10) / 100;
  3517. }
  3518. // Handle floating point rounding errors
  3519. if ((math.abs(n - max) < 0.000001)) {
  3520. return 1;
  3521. }
  3522. // Convert into [0, 1] range if it isn't already
  3523. return (n % max) / parseFloat(max);
  3524. }
  3525. // Force a number between 0 and 1
  3526. function clamp01(val) {
  3527. return mathMin(1, mathMax(0, val));
  3528. }
  3529. // Parse an integer into hex
  3530. function parseHex(val) {
  3531. return parseInt(val, 16);
  3532. }
  3533. // Need to handle 1.0 as 100%, since once it is a number, there is no difference between it and 1
  3534. // <http://stackoverflow.com/questions/7422072/javascript-how-to-detect-number-as-a-decimal-including-1-0>
  3535. function isOnePointZero(n) {
  3536. return typeof n == "string" && n.indexOf('.') != -1 && parseFloat(n) === 1;
  3537. }
  3538. // Check to see if string passed in is a percentage
  3539. function isPercentage(n) {
  3540. return typeof n === "string" && n.indexOf('%') != -1;
  3541. }
  3542. // Force a hex value to have 2 characters
  3543. function pad2(c) {
  3544. return c.length == 1 ? '0' + c : '' + c;
  3545. }
  3546. // Replace a decimal with it's percentage value
  3547. function convertToPercentage(n) {
  3548. if (n <= 1) {
  3549. n = (n * 100) + "%";
  3550. }
  3551. return n;
  3552. }
  3553. var matchers = (function() {
  3554. // <http://www.w3.org/TR/css3-values/#integers>
  3555. var CSS_INTEGER = "[-\\+]?\\d+%?";
  3556. // <http://www.w3.org/TR/css3-values/#number-value>
  3557. var CSS_NUMBER = "[-\\+]?\\d*\\.\\d+%?";
  3558. // Allow positive/negative integer/number. Don't capture the either/or, just the entire outcome.
  3559. var CSS_UNIT = "(?:" + CSS_NUMBER + ")|(?:" + CSS_INTEGER + ")";
  3560. // Actual matching.
  3561. // Parentheses and commas are optional, but not required.
  3562. // Whitespace can take the place of commas or opening paren
  3563. var PERMISSIVE_MATCH3 = "[\\s|\\(]+(" + CSS_UNIT + ")[,|\\s]+(" + CSS_UNIT + ")[,|\\s]+(" + CSS_UNIT + ")\\s*\\)?";
  3564. var PERMISSIVE_MATCH4 = "[\\s|\\(]+(" + CSS_UNIT + ")[,|\\s]+(" + CSS_UNIT + ")[,|\\s]+(" + CSS_UNIT + ")[,|\\s]+(" + CSS_UNIT + ")\\s*\\)?";
  3565. return {
  3566. rgb: new RegExp("rgb" + PERMISSIVE_MATCH3),
  3567. rgba: new RegExp("rgba" + PERMISSIVE_MATCH4),
  3568. hsl: new RegExp("hsl" + PERMISSIVE_MATCH3),
  3569. hsla: new RegExp("hsla" + PERMISSIVE_MATCH4),
  3570. hsv: new RegExp("hsv" + PERMISSIVE_MATCH3),
  3571. hex3: /^([0-9a-fA-F]{1})([0-9a-fA-F]{1})([0-9a-fA-F]{1})$/,
  3572. hex6: /^([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$/
  3573. };
  3574. })();
  3575. // `stringInputToObject`
  3576. // Permissive string parsing. Take in a number of formats, and output an object
  3577. // based on detected format. Returns `{ r, g, b }` or `{ h, s, l }` or `{ h, s, v}`
  3578. function stringInputToObject(color) {
  3579. color = color.replace(trimLeft,'').replace(trimRight, '').toLowerCase();
  3580. var named = false;
  3581. if (names[color]) {
  3582. color = names[color];
  3583. named = true;
  3584. }
  3585. else if (color == 'transparent') {
  3586. return { r: 0, g: 0, b: 0, a: 0 };
  3587. }
  3588. // Try to match string input using regular expressions.
  3589. // Keep most of the number bounding out of this function - don't worry about [0,1] or [0,100] or [0,360]
  3590. // Just return an object and let the conversion functions handle that.
  3591. // This way the result will be the same whether the tinycolor is initialized with string or object.
  3592. var match;
  3593. if ((match = matchers.rgb.exec(color))) {
  3594. return { r: match[1], g: match[2], b: match[3] };
  3595. }
  3596. if ((match = matchers.rgba.exec(color))) {
  3597. return { r: match[1], g: match[2], b: match[3], a: match[4] };
  3598. }
  3599. if ((match = matchers.hsl.exec(color))) {
  3600. return { h: match[1], s: match[2], l: match[3] };
  3601. }
  3602. if ((match = matchers.hsla.exec(color))) {
  3603. return { h: match[1], s: match[2], l: match[3], a: match[4] };
  3604. }
  3605. if ((match = matchers.hsv.exec(color))) {
  3606. return { h: match[1], s: match[2], v: match[3] };
  3607. }
  3608. if ((match = matchers.hex6.exec(color))) {
  3609. return {
  3610. r: parseHex(match[1]),
  3611. g: parseHex(match[2]),
  3612. b: parseHex(match[3]),
  3613. format: named ? "name" : "hex"
  3614. };
  3615. }
  3616. if ((match = matchers.hex3.exec(color))) {
  3617. return {
  3618. r: parseHex(match[1] + '' + match[1]),
  3619. g: parseHex(match[2] + '' + match[2]),
  3620. b: parseHex(match[3] + '' + match[3]),
  3621. format: named ? "name" : "hex"
  3622. };
  3623. }
  3624. return false;
  3625. }
  3626. // Node: Export function
  3627. if (typeof module !== "undefined" && module.exports) {
  3628. module.exports = tinycolor;
  3629. }
  3630. // AMD/requirejs: Define the module
  3631. else if (typeof define !== "undefined") {
  3632. define('tinycolor',[],function () {return tinycolor;});
  3633. }
  3634. // Browser: Expose to window
  3635. else {
  3636. root.tinycolor = tinycolor;
  3637. }
  3638. })(this);
  3639. /* This Source Code Form is subject to the terms of the Mozilla Public
  3640. * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  3641. * You can obtain one at http://mozilla.org/MPL/2.0/. */
  3642. define('elementFinder',["util", "jquery"], function (util, $) {
  3643. var elementFinder = util.Module("elementFinder");
  3644. var assert = util.assert;
  3645. elementFinder.ignoreElement = function ignoreElement(el) {
  3646. if (el instanceof $) {
  3647. el = el[0];
  3648. }
  3649. while (el) {
  3650. if ($(el).hasClass("togetherjs")) {
  3651. return true;
  3652. }
  3653. el = el.parentNode;
  3654. }
  3655. return false;
  3656. };
  3657. elementFinder.elementLocation = function elementLocation(el) {
  3658. assert(el !== null, "Got null element");
  3659. if (el instanceof $) {
  3660. // a jQuery element
  3661. el = el[0];
  3662. }
  3663. if (el[0] && el.attr && el[0].nodeType == 1) {
  3664. // Or a jQuery element not made by us
  3665. el = el[0];
  3666. }
  3667. if (el.id) {
  3668. return "#" + el.id;
  3669. }
  3670. if (el.tagName == "BODY") {
  3671. return "body";
  3672. }
  3673. if (el.tagName == "HEAD") {
  3674. return "head";
  3675. }
  3676. if (el === document) {
  3677. return "document";
  3678. }
  3679. var parent = el.parentNode;
  3680. if ((! parent) || parent == el) {
  3681. console.warn("elementLocation(", el, ") has null parent");
  3682. throw new Error("No locatable parent found");
  3683. }
  3684. var parentLocation = elementLocation(parent);
  3685. var children = parent.childNodes;
  3686. var _len = children.length;
  3687. var index = 0;
  3688. for (var i=0; i<_len; i++) {
  3689. if (children[i] == el) {
  3690. break;
  3691. }
  3692. if (children[i].nodeType == document.ELEMENT_NODE) {
  3693. if (children[i].className.indexOf("togetherjs") != -1) {
  3694. // Don't count our UI
  3695. continue;
  3696. }
  3697. // Don't count text or comments
  3698. index++;
  3699. }
  3700. }
  3701. return parentLocation + ":nth-child(" + (index+1) + ")";
  3702. };
  3703. elementFinder.CannotFind = util.Class({
  3704. constructor: function CannotFind(location, reason, context) {
  3705. this.prefix = "";
  3706. this.location = location;
  3707. this.reason = reason;
  3708. this.context = context;
  3709. },
  3710. toString: function () {
  3711. var loc;
  3712. try {
  3713. loc = elementFinder.elementLocation(this.context);
  3714. } catch (e) {
  3715. loc = this.context;
  3716. }
  3717. return (
  3718. "[CannotFind " + this.prefix +
  3719. "(" + this.location + "): " +
  3720. this.reason + " in " +
  3721. loc + "]");
  3722. }
  3723. });
  3724. elementFinder.findElement = function findElement(loc, container) {
  3725. // FIXME: should this all just be done with document.querySelector()?
  3726. // But no! We can't ignore togetherjs elements with querySelector.
  3727. // But maybe! We *could* make togetherjs elements less obtrusive?
  3728. container = container || document;
  3729. var el, rest;
  3730. if (loc === "body") {
  3731. return document.body;
  3732. } else if (loc === "head") {
  3733. return document.head;
  3734. } else if (loc === "document") {
  3735. return document;
  3736. } else if (loc.indexOf("body") === 0) {
  3737. el = document.body;
  3738. try {
  3739. return findElement(loc.substr(("body").length), el);
  3740. } catch (e) {
  3741. if (e instanceof elementFinder.CannotFind) {
  3742. e.prefix = "body" + e.prefix;
  3743. }
  3744. throw e;
  3745. }
  3746. } else if (loc.indexOf("head") === 0) {
  3747. el = document.head;
  3748. try {
  3749. return findElement(loc.substr(("head").length), el);
  3750. } catch (e) {
  3751. if (e instanceof elementFinder.CannotFind) {
  3752. e.prefix = "head" + e.prefix;
  3753. }
  3754. throw e;
  3755. }
  3756. } else if (loc.indexOf("#") === 0) {
  3757. var id;
  3758. loc = loc.substr(1);
  3759. if (loc.indexOf(":") === -1) {
  3760. id = loc;
  3761. rest = "";
  3762. } else {
  3763. id = loc.substr(0, loc.indexOf(":"));
  3764. rest = loc.substr(loc.indexOf(":"));
  3765. }
  3766. el = document.getElementById(id);
  3767. if (! el) {
  3768. throw elementFinder.CannotFind("#" + id, "No element by that id", container);
  3769. }
  3770. if (rest) {
  3771. try {
  3772. return findElement(rest, el);
  3773. } catch (e) {
  3774. if (e instanceof elementFinder.CannotFind) {
  3775. e.prefix = "#" + id + e.prefix;
  3776. }
  3777. throw e;
  3778. }
  3779. } else {
  3780. return el;
  3781. }
  3782. } else if (loc.indexOf(":nth-child(") === 0) {
  3783. loc = loc.substr((":nth-child(").length);
  3784. if (loc.indexOf(")") == -1) {
  3785. throw "Invalid location, missing ): " + loc;
  3786. }
  3787. var num = loc.substr(0, loc.indexOf(")"));
  3788. num = parseInt(num, 10);
  3789. var count = num;
  3790. loc = loc.substr(loc.indexOf(")") + 1);
  3791. var children = container.childNodes;
  3792. el = null;
  3793. for (var i=0; i<children.length; i++) {
  3794. var child = children[i];
  3795. if (child.nodeType == document.ELEMENT_NODE) {
  3796. if (child.className.indexOf("togetherjs") != -1) {
  3797. continue;
  3798. }
  3799. count--;
  3800. if (count === 0) {
  3801. // this is the element
  3802. el = child;
  3803. break;
  3804. }
  3805. }
  3806. }
  3807. if (! el) {
  3808. throw elementFinder.CannotFind(":nth-child(" + num + ")", "container only has " + (num - count) + " elements", container);
  3809. }
  3810. if (loc) {
  3811. try {
  3812. return elementFinder.findElement(loc, el);
  3813. } catch (e) {
  3814. if (e instanceof elementFinder.CannotFind) {
  3815. e.prefix = ":nth-child(" + num + ")" + e.prefix;
  3816. }
  3817. throw e;
  3818. }
  3819. } else {
  3820. return el;
  3821. }
  3822. } else {
  3823. throw elementFinder.CannotFind(loc, "Malformed location", container);
  3824. }
  3825. };
  3826. elementFinder.elementByPixel = function (height) {
  3827. /* Returns {location: "...", offset: pixels}
  3828. To get the pixel position back, you'd do:
  3829. $(location).offset().top + offset
  3830. */
  3831. function search(start, height) {
  3832. var last = null;
  3833. var children = start.children();
  3834. children.each(function () {
  3835. var el = $(this);
  3836. if (el.hasClass("togetherjs") || el.css("position") == "fixed" || ! el.is(":visible")) {
  3837. return;
  3838. }
  3839. if (el.offset().top > height) {
  3840. return false;
  3841. }
  3842. last = el;
  3843. });
  3844. if ((! children.length) || (! last)) {
  3845. // There are no children, or only inapplicable children
  3846. return {
  3847. location: elementFinder.elementLocation(start[0]),
  3848. offset: height - start.offset().top,
  3849. absoluteTop: height,
  3850. documentHeight: $(document).height()
  3851. };
  3852. }
  3853. return search(last, height);
  3854. }
  3855. return search($(document.body), height);
  3856. };
  3857. elementFinder.pixelForPosition = function (position) {
  3858. /* Inverse of elementFinder.elementByPixel */
  3859. if (position.location == "body") {
  3860. return position.offset;
  3861. }
  3862. var el;
  3863. try {
  3864. el = elementFinder.findElement(position.location);
  3865. } catch (e) {
  3866. if (e instanceof elementFinder.CannotFind && position.absoluteTop) {
  3867. // We don't trust absoluteTop to be quite right locally, so we adjust
  3868. // for the total document height differences:
  3869. var percent = position.absoluteTop / position.documentHeight;
  3870. return $(document).height() * percent;
  3871. }
  3872. throw e;
  3873. }
  3874. var top = $(el).offset().top;
  3875. // FIXME: maybe here we should test for sanity, like if an element is
  3876. // hidden. We can use position.absoluteTop to get a sense of where the
  3877. // element roughly should be. If the sanity check failed we'd use
  3878. // absoluteTop
  3879. return top + position.offset;
  3880. };
  3881. return elementFinder;
  3882. });
  3883. /* This Source Code Form is subject to the terms of the Mozilla Public
  3884. * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  3885. * You can obtain one at http://mozilla.org/MPL/2.0/. */
  3886. /* Loading this module will cause, when TogetherJS is active, the
  3887. session object to emit visibility-change with a `hidden` argument
  3888. whenever the visibility changes, on browsers where we can detect
  3889. it.
  3890. */
  3891. define('visibilityApi',["util", "session"], function (util, session) {
  3892. var visibilityApi = util.Module("visibilityApi");
  3893. var hidden;
  3894. var visibilityChange;
  3895. if (document.hidden !== undefined) { // Opera 12.10 and Firefox 18 and later support
  3896. hidden = "hidden";
  3897. visibilityChange = "visibilitychange";
  3898. } else if (document.mozHidden !== undefined) {
  3899. hidden = "mozHidden";
  3900. visibilityChange = "mozvisibilitychange";
  3901. } else if (document.msHidden !== undefined) {
  3902. hidden = "msHidden";
  3903. visibilityChange = "msvisibilitychange";
  3904. } else if (document.webkitHidden !== undefined) {
  3905. hidden = "webkitHidden";
  3906. visibilityChange = "webkitvisibilitychange";
  3907. }
  3908. session.on("start", function () {
  3909. document.addEventListener(visibilityChange, change, false);
  3910. });
  3911. session.on("close", function () {
  3912. document.removeEventListener(visibilityChange, change, false);
  3913. });
  3914. function change() {
  3915. session.emit("visibility-change", document[hidden]);
  3916. }
  3917. visibilityApi.hidden = function () {
  3918. return document[hidden];
  3919. };
  3920. return visibilityApi;
  3921. });
  3922. /* This Source Code Form is subject to the terms of the Mozilla Public
  3923. * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  3924. * You can obtain one at http://mozilla.org/MPL/2.0/. */
  3925. define('ui',["require", "jquery", "util", "session", "templates", "templating", "linkify", "peers", "windowing", "tinycolor", "elementFinder", "visibilityApi"], function (require, $, util, session, templates, templating, linkify, peers, windowing, tinycolor, elementFinder, visibilityApi) {
  3926. var ui = util.Module('ui');
  3927. var assert = util.assert;
  3928. var AssertionError = util.AssertionError;
  3929. var chat;
  3930. var $window = $(window);
  3931. // This is also in togetherjs.less, as @button-height:
  3932. var BUTTON_HEIGHT = 60 + 1; // 60 is button height, 1 is border
  3933. // chat TextArea
  3934. var TEXTAREA_LINE_HEIGHT = 20; // in pixels
  3935. var TEXTAREA_MAX_LINES = 5;
  3936. // This is also in togetherjs.less, under .togetherjs-animated
  3937. var ANIMATION_DURATION = 1000;
  3938. // Time the new user window sticks around until it fades away:
  3939. var NEW_USER_FADE_TIMEOUT = 5000;
  3940. // This is set when an animation will keep the UI from being ready
  3941. // (until this time):
  3942. var finishedAt = null;
  3943. // Time in milliseconds for the dock to animate out:
  3944. var DOCK_ANIMATION_TIME = 300;
  3945. // If two chat messages come from the same person in this time
  3946. // (milliseconds) then they are collapsed into one message:
  3947. var COLLAPSE_MESSAGE_LIMIT = 5000;
  3948. var COLORS = [
  3949. "#8A2BE2", "#7FFF00", "#DC143C", "#00FFFF", "#8FBC8F", "#FF8C00", "#FF00FF",
  3950. "#FFD700", "#F08080", "#90EE90", "#FF6347"];
  3951. // This would be a circular import, but we just need the chat module sometime
  3952. // after everything is loaded, and this is sure to complete by that time:
  3953. require(["chat"], function (c) {
  3954. chat = c;
  3955. });
  3956. /* Displays some toggleable element; toggleable elements have a
  3957. data-toggles attribute that indicates what other elements should
  3958. be hidden when this element is shown. */
  3959. ui.displayToggle = function (el) {
  3960. el = $(el);
  3961. assert(el.length, "No element", arguments[0]);
  3962. var other = $(el.attr("data-toggles"));
  3963. assert(other.length, "Cannot toggle", el[0], "selector", other.selector);
  3964. other.hide();
  3965. el.show();
  3966. };
  3967. function panelPosition() {
  3968. var iface = $("#togetherjs-dock");
  3969. if (iface.hasClass("togetherjs-dock-right")) {
  3970. return "right";
  3971. } else if (iface.hasClass("togetherjs-dock-left")) {
  3972. return "left";
  3973. } else if (iface.hasClass("togetherjs-dock-bottom")) {
  3974. return "bottom";
  3975. } else {
  3976. throw new AssertionError("#togetherjs-dock doesn't have positioning class");
  3977. }
  3978. }
  3979. ui.container = null;
  3980. // This is used for some signalling when ui.prepareUI and/or
  3981. // ui.activateUI is called before the DOM is fully loaded:
  3982. var deferringPrepareUI = null;
  3983. function deferForContainer(func) {
  3984. /* Defers any calls to func() until after ui.container is set
  3985. Function cannot have a return value (as sometimes the call will
  3986. become async). Use like:
  3987. method: deferForContainer(function (args) {...})
  3988. */
  3989. return function () {
  3990. if (ui.container) {
  3991. func.apply(this, arguments);
  3992. }
  3993. var self = this;
  3994. var args = Array.prototype.slice.call(arguments);
  3995. session.once("ui-ready", function () {
  3996. func.apply(self, args);
  3997. });
  3998. };
  3999. }
  4000. // This is called before activateUI; it doesn't bind anything, but does display
  4001. // the dock
  4002. // FIXME: because this module has lots of requirements we can't do
  4003. // this before those requirements are loaded. Maybe worth splitting
  4004. // this out? OTOH, in production we should have all the files
  4005. // combined so there's not much problem loading those modules.
  4006. ui.prepareUI = function () {
  4007. if (! (document.readyState == "complete" || document.readyState == "interactive")) {
  4008. // Too soon! Wait a sec...
  4009. deferringPrepareUI = "deferring";
  4010. document.addEventListener("DOMContentLoaded", function () {
  4011. var d = deferringPrepareUI;
  4012. deferringPrepareUI = null;
  4013. ui.prepareUI();
  4014. // This happens when ui.activateUI is called before the document has been
  4015. // loaded:
  4016. if (d == "activate") {
  4017. ui.activateUI();
  4018. }
  4019. });
  4020. return;
  4021. }
  4022. var container = ui.container = $(templates("interface"));
  4023. assert(container.length);
  4024. $("body").append(container);
  4025. fixupAvatars(container);
  4026. if (session.firstRun && TogetherJS.startTarget) {
  4027. // Time at which the UI will be fully ready:
  4028. // (We have to do this because the offset won't be quite right
  4029. // until the animation finishes - attempts to calculate the
  4030. // offset without taking into account CSS transforms have so far
  4031. // failed.)
  4032. var timeoutSeconds = DOCK_ANIMATION_TIME / 1000;
  4033. finishedAt = Date.now() + DOCK_ANIMATION_TIME + 50;
  4034. setTimeout(function () {
  4035. finishedAt = Date.now() + DOCK_ANIMATION_TIME + 40;
  4036. var iface = container.find("#togetherjs-dock");
  4037. var start = iface.offset();
  4038. var pos = $(TogetherJS.startTarget).offset();
  4039. pos.top = Math.floor(pos.top - start.top);
  4040. pos.left = Math.floor(pos.left - start.left);
  4041. var translate = "translate(" + pos.left + "px, " + pos.top + "px)";
  4042. iface.css({
  4043. MozTransform: translate,
  4044. WebkitTransform: translate,
  4045. transform: translate,
  4046. opacity: "0.0"
  4047. });
  4048. setTimeout(function () {
  4049. // We keep recalculating because the setTimeout times aren't always so accurate:
  4050. finishedAt = Date.now() + DOCK_ANIMATION_TIME + 20;
  4051. var transition = "transform " + timeoutSeconds + "s ease-out, ";
  4052. transition += "opacity " + timeoutSeconds + "s ease-out";
  4053. iface.css({
  4054. opacity: "1.0",
  4055. MozTransition: "-moz-" + transition,
  4056. MozTransform: "translate(0, 0)",
  4057. WebkitTransition: "-webkit-" + transition,
  4058. WebkitTransform: "translate(0, 0)",
  4059. transition: transition,
  4060. transform: "translate(0, 0)"
  4061. });
  4062. setTimeout(function () {
  4063. finishedAt = null;
  4064. iface.attr("style", "");
  4065. }, 510);
  4066. }, 5);
  4067. }, 5);
  4068. }
  4069. if (TogetherJS.startTarget) {
  4070. var el = $(TogetherJS.startTarget);
  4071. var text = el.text().toLowerCase().replace(/\s+/g, " ");
  4072. text = text.replace(/^\s*/, "").replace(/\s*$/, "");
  4073. if (text == "start togetherjs") {
  4074. el.attr("data-end-togetherjs-html", "End TogetherJS");
  4075. }
  4076. if (el.attr("data-end-togetherjs-html")) {
  4077. el.attr("data-start-togetherjs-html", el.html());
  4078. el.html(el.attr("data-end-togetherjs-html"));
  4079. }
  4080. el.addClass("togetherjs-started");
  4081. }
  4082. ui.container.find(".togetherjs-window > header, .togetherjs-modal > header").each(function () {
  4083. $(this).append($('<button class="togetherjs-close"></button>'));
  4084. });
  4085. TogetherJS.config.track("disableWebRTC", function (hide, previous) {
  4086. if (hide && ! previous) {
  4087. ui.container.find("#togetherjs-audio-button").hide();
  4088. adjustDockSize(-1);
  4089. } else if ((! hide) && previous) {
  4090. ui.container.find("#togetherjs-audio-button").show();
  4091. adjustDockSize(1);
  4092. }
  4093. });
  4094. };
  4095. // After prepareUI, this actually makes the interface live. We have
  4096. // to do this later because we call prepareUI when many components
  4097. // aren't initialized, so we don't even want the user to be able to
  4098. // interact with the interface. But activateUI is called once
  4099. // everything is loaded and ready for interaction.
  4100. ui.activateUI = function () {
  4101. if (deferringPrepareUI) {
  4102. console.warn("ui.activateUI called before document is ready; waiting...");
  4103. deferringPrepareUI = "activate";
  4104. return;
  4105. }
  4106. if (! ui.container) {
  4107. ui.prepareUI();
  4108. }
  4109. var container = ui.container;
  4110. //create the overlay
  4111. if($.browser.mobile) {
  4112. // $("body").append( "\x3cdiv class='overlay' style='position: absolute; top: 0; left: 0; background-color: rgba(0,0,0,0); width: 120%; height: 100%; z-index: 1000; margin: -10px'>\x3c/div>" );
  4113. }
  4114. // The share link:
  4115. ui.prepareShareLink(container);
  4116. container.find("input.togetherjs-share-link").on("keydown", function (event) {
  4117. if (event.which == 27) {
  4118. windowing.hide("#togetherjs-share");
  4119. return false;
  4120. }
  4121. return undefined;
  4122. });
  4123. session.on("shareId", updateShareLink);
  4124. // The chat input element:
  4125. var input = container.find("#togetherjs-chat-input");
  4126. input.bind("keydown", function (event) {
  4127. if (event.which == 13 && !event.shiftKey) { // Enter without Shift pressed
  4128. submitChat();
  4129. return false;
  4130. }
  4131. if (event.which == 27) { // Escape
  4132. windowing.hide("#togetherjs-chat");
  4133. return false;
  4134. }
  4135. });
  4136. function submitChat() {
  4137. var val = input.val();
  4138. if ($.trim(val)) {
  4139. input.val("");
  4140. // triggering the event manually to avoid the addition of newline character to the textarea:
  4141. input.trigger("input").trigger("propertychange");
  4142. chat.submit(val);
  4143. }
  4144. }
  4145. // auto-resize textarea:
  4146. input.on("input propertychange", function () {
  4147. var $this = $(this);
  4148. var actualHeight = $this.height();
  4149. // reset the height of textarea to remove trailing empty space (used for shrinking):
  4150. $this.height(TEXTAREA_LINE_HEIGHT);
  4151. this.scrollTop = 0;
  4152. // scroll to bottom:
  4153. this.scrollTop = 9999;
  4154. var newHeight = this.scrollTop + $this.height();
  4155. var maxHeight = TEXTAREA_MAX_LINES * TEXTAREA_LINE_HEIGHT;
  4156. if (newHeight > maxHeight) {
  4157. newHeight = maxHeight;
  4158. this.style.overflowY = "scroll";
  4159. } else {
  4160. this.style.overflowY = "hidden";
  4161. }
  4162. this.style.height = newHeight + "px";
  4163. var diff = newHeight - actualHeight;
  4164. $("#togetherjs-chat-input-box").height($("#togetherjs-chat-input-box").height() + diff);
  4165. $("#togetherjs-chat-messages").height($("#togetherjs-chat-messages").height() - diff);
  4166. return false;
  4167. });
  4168. util.testExpose({submitChat: submitChat});
  4169. // Moving the window:
  4170. // FIXME: this should probably be stickier, and not just move the window around
  4171. // so abruptly
  4172. var anchor = container.find("#togetherjs-dock-anchor");
  4173. assert(anchor.length);
  4174. // FIXME: This is in place to temporarily disable dock dragging:
  4175. anchor = container.find("#togetherjs-dock-anchor-disabled");
  4176. anchor.mousedown(function (event) {
  4177. var iface = $("#togetherjs-dock");
  4178. // FIXME: switch to .offset() and pageX/Y
  4179. var startPos = panelPosition();
  4180. function selectoff() {
  4181. return false;
  4182. }
  4183. function mousemove(event2) {
  4184. var fromRight = $window.width() + window.pageXOffset - event2.pageX;
  4185. var fromLeft = event2.pageX - window.pageXOffset;
  4186. var fromBottom = $window.height() + window.pageYOffset - event2.pageY;
  4187. // FIXME: this is to temporarily disable the bottom view:
  4188. fromBottom = 10000;
  4189. var pos;
  4190. if (fromLeft < fromRight && fromLeft < fromBottom) {
  4191. pos = "left";
  4192. } else if (fromRight < fromLeft && fromRight < fromBottom) {
  4193. pos = "right";
  4194. } else {
  4195. pos = "bottom";
  4196. }
  4197. iface.removeClass("togetherjs-dock-left");
  4198. iface.removeClass("togetherjs-dock-right");
  4199. iface.removeClass("togetherjs-dock-bottom");
  4200. iface.addClass("togetherjs-dock-" + pos);
  4201. if (startPos && pos != startPos) {
  4202. windowing.hide();
  4203. startPos = null;
  4204. }
  4205. }
  4206. $(document).bind("mousemove", mousemove);
  4207. // If you don't turn selection off it will still select text, and show a
  4208. // text selection cursor:
  4209. $(document).bind("selectstart", selectoff);
  4210. // FIXME: it seems like sometimes we lose the mouseup event, and it's as though
  4211. // the mouse is stuck down:
  4212. $(document).one("mouseup", function () {
  4213. $(document).unbind("mousemove", mousemove);
  4214. $(document).unbind("selectstart", selectoff);
  4215. });
  4216. return false;
  4217. });
  4218. function openDock() {
  4219. $('.togetherjs-window').animate({
  4220. opacity: 1
  4221. });
  4222. $('#togetherjs-dock-participants').animate({
  4223. opacity: 1
  4224. });
  4225. $('#togetherjs-dock #togetherjs-buttons').animate({
  4226. opacity: 1
  4227. });
  4228. //for iphone
  4229. if($(window).width() < 480) {
  4230. $('.togetherjs-dock-right').animate({
  4231. width: "204px"
  4232. }, {
  4233. duration:60, easing:"linear"
  4234. });
  4235. }
  4236. //for ipad
  4237. else {
  4238. $('.togetherjs-dock-right').animate({
  4239. width: "27%"
  4240. }, {
  4241. duration:60, easing:"linear"
  4242. });
  4243. }
  4244. // add bg overlay
  4245. // $("body").append( "\x3cdiv class='overlay' style='position: absolute; top: 0; left: -2px; background-color: rgba(0,0,0,0.5); width: 200%; height: 400%; z-index: 1000; margin: 0px;'>\x3c/div>" );
  4246. //disable vertical scrolling
  4247. // $("body").css({
  4248. // "position": "fixed",
  4249. // top: 0,
  4250. // left: 0
  4251. // });
  4252. //replace the anchor icon
  4253. var src = "/togetherjs/images/togetherjs-logo-close.png";
  4254. $("#togetherjs-dock-anchor #togetherjs-dock-anchor-horizontal img").attr("src", src);
  4255. }
  4256. function closeDock() {
  4257. //enable vertical scrolling
  4258. $("body").css({
  4259. "position": "",
  4260. top: "",
  4261. left: ""
  4262. });
  4263. //replace the anchor icon
  4264. var src = "/togetherjs/images/togetherjs-logo-open.png";
  4265. $("#togetherjs-dock-anchor #togetherjs-dock-anchor-horizontal img").attr("src", src);
  4266. $('.togetherjs-window').animate({
  4267. opacity: 0
  4268. });
  4269. $('#togetherjs-dock-participants').animate({
  4270. opacity: 0
  4271. });
  4272. $('#togetherjs-dock #togetherjs-buttons').animate({
  4273. opacity: 0
  4274. });
  4275. $('.togetherjs-dock-right').animate({
  4276. width: "40px"
  4277. }, {
  4278. duration:60, easing:"linear"
  4279. });
  4280. // remove bg overlay
  4281. //$(".overlay").remove();
  4282. }
  4283. // Setting the anchor button + dock mobile actions
  4284. if($.browser.mobile) {
  4285. // toggle the audio button
  4286. $("#togetherjs-audio-button").click(function () {
  4287. windowing.toggle("#togetherjs-rtc-not-supported");
  4288. });
  4289. // toggle the profile button
  4290. $("#togetherjs-profile-button").click(function () {
  4291. windowing.toggle("#togetherjs-menu-window");
  4292. });
  4293. // $("body").append( "\x3cdiv class='overlay' style='position: absolute; top: 0; left: -2px; background-color: rgba(0,0,0,0.5); width: 200%; height: 400%; z-index: 1000; margin: 0px'>\x3c/div>" );
  4294. //disable vertical scrolling
  4295. // $("body").css({
  4296. // "position": "fixed",
  4297. // top: 0,
  4298. // left: 0
  4299. // });
  4300. //replace the anchor icon
  4301. var src = "/togetherjs/images/togetherjs-logo-close.png";
  4302. $("#togetherjs-dock-anchor #togetherjs-dock-anchor-horizontal img").attr("src", src);
  4303. $("#togetherjs-dock-anchor").toggle(function() {
  4304. closeDock();
  4305. },function(){
  4306. openDock();
  4307. });
  4308. }
  4309. $("#togetherjs-share-button").click(function () {
  4310. windowing.toggle("#togetherjs-share");
  4311. });
  4312. $("#togetherjs-profile-button").click(function (event) {
  4313. if ($.browser.mobile) {
  4314. windowing.show("#togetherjs-menu-window");
  4315. return false;
  4316. }
  4317. toggleMenu();
  4318. event.stopPropagation();
  4319. return false;
  4320. });
  4321. $("#togetherjs-menu-feedback, #togetherjs-menu-feedback-button").click(function(){
  4322. windowing.hide();
  4323. hideMenu();
  4324. windowing.show("#togetherjs-feedback-form");
  4325. });
  4326. $("#togetherjs-menu-help, #togetherjs-menu-help-button").click(function () {
  4327. windowing.hide();
  4328. hideMenu();
  4329. require(["walkthrough"], function (walkthrough) {
  4330. windowing.hide();
  4331. walkthrough.start(false);
  4332. });
  4333. });
  4334. $("#togetherjs-menu-update-name").click(function () {
  4335. var input = $("#togetherjs-menu .togetherjs-self-name");
  4336. input.css({
  4337. width: $("#togetherjs-menu").width() - 32 + "px"
  4338. });
  4339. ui.displayToggle("#togetherjs-menu .togetherjs-self-name");
  4340. $("#togetherjs-menu .togetherjs-self-name").focus();
  4341. });
  4342. $("#togetherjs-menu-update-name-button").click(function () {
  4343. windowing.show("#togetherjs-edit-name-window");
  4344. $("#togetherjs-edit-name-window input").focus();
  4345. });
  4346. $("#togetherjs-menu .togetherjs-self-name").bind("keyup change", function (event) {
  4347. console.log("alrighty", event);
  4348. if (event.which == 13) {
  4349. ui.displayToggle("#togetherjs-self-name-display");
  4350. return;
  4351. }
  4352. var val = $("#togetherjs-menu .togetherjs-self-name").val();
  4353. console.log("values!!", val);
  4354. if (val) {
  4355. peers.Self.update({name: val});
  4356. }
  4357. });
  4358. $("#togetherjs-menu-update-avatar, #togetherjs-menu-update-avatar-button").click(function () {
  4359. hideMenu();
  4360. windowing.show("#togetherjs-avatar-edit");
  4361. });
  4362. $("#togetherjs-menu-end, #togetherjs-menu-end-button").click(function () {
  4363. hideMenu();
  4364. windowing.show("#togetherjs-confirm-end");
  4365. });
  4366. $("#togetherjs-end-session").click(function () {
  4367. session.close();
  4368. //$(".overlay").remove();
  4369. });
  4370. $("#togetherjs-menu-update-color").click(function () {
  4371. var picker = $("#togetherjs-pick-color");
  4372. if (picker.is(":visible")) {
  4373. picker.hide();
  4374. return;
  4375. }
  4376. picker.show();
  4377. bindPicker();
  4378. picker.find(".togetherjs-swatch-active").removeClass("togetherjs-swatch-active");
  4379. picker.find(".togetherjs-swatch[data-color=\"" + peers.Self.color + "\"]").addClass("togetherjs-swatch-active");
  4380. });
  4381. $("#togetherjs-pick-color").click(".togetherjs-swatch", function (event) {
  4382. var swatch = $(event.target);
  4383. var color = swatch.attr("data-color");
  4384. peers.Self.update({
  4385. color: color
  4386. });
  4387. event.stopPropagation();
  4388. return false;
  4389. });
  4390. $("#togetherjs-pick-color").click(function (event) {
  4391. $("#togetherjs-pick-color").hide();
  4392. event.stopPropagation();
  4393. return false;
  4394. });
  4395. COLORS.forEach(function (color) {
  4396. var el = templating.sub("swatch");
  4397. el.attr("data-color", color);
  4398. var darkened = tinycolor.darken(color);
  4399. el.css({
  4400. backgroundColor: color,
  4401. borderColor: darkened
  4402. });
  4403. $("#togetherjs-pick-color").append(el);
  4404. });
  4405. $("#togetherjs-chat-button").click(function () {
  4406. windowing.toggle("#togetherjs-chat");
  4407. });
  4408. session.on("display-window", function (id, element) {
  4409. if (id == "togetherjs-chat") {
  4410. if (! $.browser.mobile) {
  4411. $("#togetherjs-chat-input").focus();
  4412. }
  4413. } else if (id == "togetherjs-share") {
  4414. var link = element.find("input.togetherjs-share-link");
  4415. if (link.is(":visible")) {
  4416. link.focus().select();
  4417. }
  4418. }
  4419. });
  4420. container.find("#togetherjs-chat-notifier").click(function (event) {
  4421. if ($(event.target).is("a") || container.is(".togetherjs-close")) {
  4422. return;
  4423. }
  4424. windowing.show("#togetherjs-chat");
  4425. });
  4426. // FIXME: Don't think this makes sense
  4427. $(".togetherjs header.togetherjs-title").each(function (index, item) {
  4428. var button = $('<button class="togetherjs-minimize"></button>');
  4429. button.click(function (event) {
  4430. var window = button.closest(".togetherjs-window");
  4431. windowing.hide(window);
  4432. });
  4433. $(item).append(button);
  4434. });
  4435. $("#togetherjs-avatar-done").click(function () {
  4436. ui.displayToggle("#togetherjs-no-avatar-edit");
  4437. });
  4438. $("#togetherjs-self-color").css({backgroundColor: peers.Self.color});
  4439. var avatar = peers.Self.avatar;
  4440. if (avatar) {
  4441. $("#togetherjs-self-avatar").attr("src", avatar);
  4442. }
  4443. var starterButton = $("#togetherjs-starter button");
  4444. starterButton.click(function () {
  4445. windowing.show("#togetherjs-about");
  4446. }).addClass("togetherjs-running");
  4447. if (starterButton.text() == "Start TogetherJS") {
  4448. starterButton.attr("data-start-text", starterButton.text());
  4449. starterButton.text("End TogetherJS Session");
  4450. }
  4451. ui.activateAvatarEdit(container, {
  4452. onSave: function () {
  4453. windowing.hide("#togetherjs-avatar-edit");
  4454. }
  4455. });
  4456. TogetherJS.config.track("inviteFromRoom", function (inviter, previous) {
  4457. if (inviter) {
  4458. container.find("#togetherjs-invite").show();
  4459. } else {
  4460. container.find("#togetherjs-invite").hide();
  4461. }
  4462. });
  4463. container.find("#togetherjs-menu-refresh-invite").click(refreshInvite);
  4464. container.find("#togetherjs-menu-invite-anyone").click(function () {
  4465. invite(null);
  4466. });
  4467. // The following lines should be at the end of this function
  4468. // (new code goes above)
  4469. session.emit("new-element", ui.container);
  4470. if (finishedAt && finishedAt > Date.now()) {
  4471. setTimeout(function () {
  4472. finishedAt = null;
  4473. session.emit("ui-ready", ui);
  4474. }, finishedAt - Date.now());
  4475. } else {
  4476. session.emit("ui-ready", ui);
  4477. }
  4478. }; // End ui.activateUI()
  4479. ui.activateAvatarEdit = function (container, options) {
  4480. options = options || {};
  4481. var pendingImage = null;
  4482. container.find(".togetherjs-avatar-save").prop("disabled", true);
  4483. container.find(".togetherjs-avatar-save").click(function () {
  4484. if (pendingImage) {
  4485. peers.Self.update({avatar: pendingImage});
  4486. container.find(".togetherjs-avatar-save").prop("disabled", true);
  4487. if (options.onSave) {
  4488. options.onSave();
  4489. }
  4490. }
  4491. });
  4492. container.find(".togetherjs-upload-avatar").on("change", function () {
  4493. util.readFileImage(this).then(function (url) {
  4494. sizeDownImage(url).then(function (smallUrl) {
  4495. pendingImage = smallUrl;
  4496. container.find(".togetherjs-avatar-preview").css({
  4497. backgroundImage: 'url(' + pendingImage + ')'
  4498. });
  4499. container.find(".togetherjs-avatar-save").prop("disabled", false);
  4500. if (options.onPending) {
  4501. options.onPending();
  4502. }
  4503. });
  4504. });
  4505. });
  4506. };
  4507. function sizeDownImage(imageUrl) {
  4508. return util.Deferred(function (def) {
  4509. var $canvas = $("<canvas>");
  4510. $canvas[0].height = session.AVATAR_SIZE;
  4511. $canvas[0].width = session.AVATAR_SIZE;
  4512. var context = $canvas[0].getContext("2d");
  4513. var img = new Image();
  4514. img.src = imageUrl;
  4515. // Sometimes the DOM updates immediately to call
  4516. // naturalWidth/etc, and sometimes it doesn't; using setTimeout
  4517. // gives it a chance to catch up
  4518. setTimeout(function () {
  4519. var width = img.naturalWidth || img.width;
  4520. var height = img.naturalHeight || img.height;
  4521. width = width * (session.AVATAR_SIZE / height);
  4522. height = session.AVATAR_SIZE;
  4523. context.drawImage(img, 0, 0, width, height);
  4524. def.resolve($canvas[0].toDataURL("image/png"));
  4525. });
  4526. });
  4527. }
  4528. function fixupAvatars(container) {
  4529. /* All <div class="togetherjs-person" /> elements need an element inside,
  4530. so we add that element here */
  4531. container.find(".togetherjs-person").each(function () {
  4532. var $this = $(this);
  4533. var inner = $this.find(".togetherjs-person-avatar-swatch");
  4534. if (! inner.length) {
  4535. $this.append('<div class="togetherjs-person-avatar-swatch"></div>');
  4536. }
  4537. });
  4538. }
  4539. ui.prepareShareLink = function (container) {
  4540. container.find("input.togetherjs-share-link").click(function () {
  4541. $(this).select();
  4542. }).change(function () {
  4543. updateShareLink();
  4544. });
  4545. container.find("a.togetherjs-share-link").click(function () {
  4546. // FIXME: this is currently opening up Bluetooth, not sharing a link
  4547. if (false && window.MozActivity) {
  4548. var activity = new MozActivity({
  4549. name: "share",
  4550. data: {
  4551. type: "url",
  4552. url: $(this).attr("href")
  4553. }
  4554. });
  4555. }
  4556. // FIXME: should show some help if you actually try to follow the link
  4557. // like this, instead of simply suppressing it
  4558. return false;
  4559. });
  4560. updateShareLink();
  4561. };
  4562. // Menu
  4563. function showMenu(event) {
  4564. var el = $("#togetherjs-menu");
  4565. assert(el.length);
  4566. el.show();
  4567. bindMenu();
  4568. $(document).bind("click", maybeHideMenu);
  4569. }
  4570. function bindMenu() {
  4571. var el = $("#togetherjs-menu:visible");
  4572. if (el.length) {
  4573. var bound = $("#togetherjs-profile-button");
  4574. var boundOffset = bound.offset();
  4575. el.css({
  4576. top: boundOffset.top + bound.height() - $window.scrollTop() + "px",
  4577. left: (boundOffset.left + bound.width() - 10 - el.width() - $window.scrollLeft()) + "px"
  4578. });
  4579. }
  4580. }
  4581. function bindPicker() {
  4582. var picker = $("#togetherjs-pick-color:visible");
  4583. if (picker.length) {
  4584. var menu = $("#togetherjs-menu-update-color");
  4585. var menuOffset = menu.offset();
  4586. picker.css({
  4587. top: menuOffset.top + menu.height(),
  4588. left: menuOffset.left
  4589. });
  4590. }
  4591. }
  4592. session.on("resize", function () {
  4593. bindMenu();
  4594. bindPicker();
  4595. });
  4596. function toggleMenu() {
  4597. if ($("#togetherjs-menu").is(":visible")) {
  4598. hideMenu();
  4599. } else {
  4600. showMenu();
  4601. }
  4602. }
  4603. function hideMenu() {
  4604. var el = $("#togetherjs-menu");
  4605. el.hide();
  4606. $(document).unbind("click", maybeHideMenu);
  4607. ui.displayToggle("#togetherjs-self-name-display");
  4608. $("#togetherjs-pick-color").hide();
  4609. }
  4610. function maybeHideMenu(event) {
  4611. var t = event.target;
  4612. while (t) {
  4613. if (t.id == "togetherjs-menu") {
  4614. // Click inside the menu, ignore this
  4615. return;
  4616. }
  4617. t = t.parentNode;
  4618. }
  4619. hideMenu();
  4620. }
  4621. function adjustDockSize(buttons) {
  4622. /* Add or remove spots from the dock; positive number to
  4623. add button(s), negative number to remove button(s)
  4624. */
  4625. assert(typeof buttons == "number");
  4626. assert(buttons && Math.floor(buttons) == buttons);
  4627. var iface = $("#togetherjs-dock");
  4628. var newHeight = iface.height() + (BUTTON_HEIGHT * buttons);
  4629. assert(newHeight >= BUTTON_HEIGHT * 3, "Height went too low (", newHeight,
  4630. "), should never be less than 3 buttons high (", BUTTON_HEIGHT * 3, ")");
  4631. iface.css({
  4632. height: newHeight + "px"
  4633. });
  4634. }
  4635. // Misc
  4636. function updateShareLink() {
  4637. var input = $("input.togetherjs-share-link");
  4638. var link = $("a.togetherjs-share-link");
  4639. var display = $("#togetherjs-session-id");
  4640. if (! session.shareId) {
  4641. input.val("");
  4642. link.attr("href", "#");
  4643. display.text("(none)");
  4644. } else {
  4645. input.val(session.shareUrl());
  4646. link.attr("href", session.shareUrl());
  4647. display.text(session.shareId);
  4648. }
  4649. }
  4650. session.on("close", function () {
  4651. if($.browser.mobile) {
  4652. // remove bg overlay
  4653. //$(".overlay").remove();
  4654. //after hitting End, reset window draggin
  4655. $("body").css({
  4656. "position": "",
  4657. top: "",
  4658. left: ""
  4659. });
  4660. }
  4661. if (ui.container) {
  4662. ui.container.remove();
  4663. ui.container = null;
  4664. }
  4665. // Clear out any other spurious elements:
  4666. $(".togetherjs").remove();
  4667. var starterButton = $("#togetherjs-starter button");
  4668. starterButton.removeClass("togetherjs-running");
  4669. if (starterButton.attr("data-start-text")) {
  4670. starterButton.text(starterButton.attr("data-start-text"));
  4671. starterButton.attr("data-start-text", "");
  4672. }
  4673. if (TogetherJS.startTarget) {
  4674. var el = $(TogetherJS.startTarget);
  4675. if (el.attr("data-start-togetherjs-html")) {
  4676. el.html(el.attr("data-start-togetherjs-html"));
  4677. }
  4678. el.removeClass("togetherjs-started");
  4679. }
  4680. });
  4681. ui.chat = {
  4682. text: function (attrs) {
  4683. assert(typeof attrs.text == "string");
  4684. assert(attrs.peer);
  4685. assert(attrs.messageId);
  4686. var date = attrs.date || Date.now();
  4687. var lastEl = ui.container.find("#togetherjs-chat .togetherjs-chat-message");
  4688. if (lastEl.length) {
  4689. lastEl = $(lastEl[lastEl.length-1]);
  4690. }
  4691. var lastDate = null;
  4692. if (lastEl) {
  4693. lastDate = parseInt(lastEl.attr("data-date"), 10);
  4694. }
  4695. if (lastEl && lastEl.attr("data-person") == attrs.peer.id &&
  4696. lastDate && date < lastDate + COLLAPSE_MESSAGE_LIMIT) {
  4697. lastEl.attr("data-date", date);
  4698. var content = lastEl.find(".togetherjs-chat-content");
  4699. assert(content.length);
  4700. attrs.text = content.text() + "\n" + attrs.text;
  4701. attrs.messageId = lastEl.attr("data-message-id");
  4702. lastEl.remove();
  4703. }
  4704. var el = templating.sub("chat-message", {
  4705. peer: attrs.peer,
  4706. content: attrs.text,
  4707. date: date
  4708. });
  4709. linkify(el.find(".togetherjs-chat-content"));
  4710. el.attr("data-person", attrs.peer.id)
  4711. .attr("data-date", date)
  4712. .attr("data-message-id", attrs.messageId);
  4713. ui.chat.add(el, attrs.messageId, attrs.notify);
  4714. },
  4715. joinedSession: function (attrs) {
  4716. assert(attrs.peer);
  4717. var date = attrs.date || Date.now();
  4718. var el = templating.sub("chat-joined", {
  4719. peer: attrs.peer,
  4720. date: date
  4721. });
  4722. // FIXME: should bind the notification to the dock location
  4723. ui.chat.add(el, attrs.peer.className("join-message-"), 4000);
  4724. },
  4725. leftSession: function (attrs) {
  4726. assert(attrs.peer);
  4727. var date = attrs.date || Date.now();
  4728. var el = templating.sub("chat-left", {
  4729. peer: attrs.peer,
  4730. date: date,
  4731. declinedJoin: attrs.declinedJoin
  4732. });
  4733. // FIXME: should bind the notification to the dock location
  4734. ui.chat.add(el, attrs.peer.className("join-message-"), 4000);
  4735. },
  4736. system: function (attrs) {
  4737. assert(! attrs.peer);
  4738. assert(typeof attrs.text == "string");
  4739. var date = attrs.date || Date.now();
  4740. var el = templating.sub("chat-system", {
  4741. content: attrs.text,
  4742. date: date
  4743. });
  4744. ui.chat.add(el, undefined, true);
  4745. },
  4746. clear: deferForContainer(function () {
  4747. var container = ui.container.find("#togetherjs-chat-messages");
  4748. container.empty();
  4749. }),
  4750. urlChange: function (attrs) {
  4751. assert(attrs.peer);
  4752. assert(typeof attrs.url == "string");
  4753. assert(typeof attrs.sameUrl == "boolean");
  4754. var messageId = attrs.peer.className("url-change-");
  4755. // FIXME: duplicating functionality in .add():
  4756. var realId = "togetherjs-chat-" + messageId;
  4757. var date = attrs.date || Date.now();
  4758. var title;
  4759. // FIXME: strip off common domain from msg.url? E.g., if I'm on
  4760. // http://example.com/foobar, and someone goes to http://example.com/baz then
  4761. // show only /baz
  4762. // FIXME: truncate long titles
  4763. if (attrs.title) {
  4764. title = attrs.title + " (" + attrs.url + ")";
  4765. } else {
  4766. title = attrs.url;
  4767. }
  4768. var el = templating.sub("url-change", {
  4769. peer: attrs.peer,
  4770. date: date,
  4771. href: attrs.url,
  4772. title: title,
  4773. sameUrl: attrs.sameUrl
  4774. });
  4775. el.find(".togetherjs-nudge").click(function () {
  4776. attrs.peer.nudge();
  4777. return false;
  4778. });
  4779. el.find(".togetherjs-follow").click(function () {
  4780. var url = attrs.peer.url;
  4781. if (attrs.peer.urlHash) {
  4782. url += attrs.peer.urlHash;
  4783. }
  4784. location.href = url;
  4785. });
  4786. var notify = ! attrs.sameUrl;
  4787. if (attrs.sameUrl && ! $("#" + realId).length) {
  4788. // Don't bother showing a same-url notification, if no previous notification
  4789. // had been shown
  4790. return;
  4791. }
  4792. ui.chat.add(el, messageId, notify);
  4793. },
  4794. invite: function (attrs) {
  4795. assert(attrs.peer);
  4796. assert(typeof attrs.url == "string");
  4797. var messageId = attrs.peer.className("invite-");
  4798. var date = attrs.date || Date.now();
  4799. var hrefTitle = attrs.url.replace(/\#?&togetherjs=.*/, "").replace(/^\w+:\/\//, "");
  4800. var el = templating.sub("invite", {
  4801. peer: attrs.peer,
  4802. date: date,
  4803. href: attrs.url,
  4804. hrefTitle: hrefTitle,
  4805. forEveryone: attrs.forEveryone
  4806. });
  4807. if (attrs.forEveryone) {
  4808. el.find("a").click(function () {
  4809. // FIXME: hacky way to do this:
  4810. chat.submit("Followed link to " + attrs.url);
  4811. });
  4812. }
  4813. ui.chat.add(el, messageId, true);
  4814. },
  4815. hideTimeout: null,
  4816. add: deferForContainer(function (el, id, notify) {
  4817. if (id) {
  4818. el.attr("id", "togetherjs-chat-" + util.safeClassName(id));
  4819. }
  4820. var container = ui.container.find("#togetherjs-chat-messages");
  4821. assert(container.length);
  4822. var popup = ui.container.find("#togetherjs-chat-notifier");
  4823. container.append(el);
  4824. ui.chat.scroll();
  4825. var doNotify = !! notify;
  4826. var section = popup.find("#togetherjs-chat-notifier-message");
  4827. if (notify && visibilityApi.hidden()) {
  4828. ui.container.find("#togetherjs-notification")[0].play();
  4829. }
  4830. if (id && section.data("message-id") == id) {
  4831. doNotify = true;
  4832. }
  4833. if (container.is(":visible")) {
  4834. doNotify = false;
  4835. }
  4836. if (doNotify) {
  4837. section.empty();
  4838. section.append(el.clone(true, true));
  4839. if (section.data("message-id") != id) {
  4840. section.data("message-id", id || "");
  4841. windowing.show(popup);
  4842. } else if (! popup.is(":visible")) {
  4843. windowing.show(popup);
  4844. }
  4845. if (typeof notify == "number") {
  4846. // This is the amount of time we're supposed to notify
  4847. if (this.hideTimeout) {
  4848. clearTimeout(this.hideTimeout);
  4849. this.hideTimeout = null;
  4850. }
  4851. this.hideTimeout = setTimeout((function () {
  4852. windowing.hide(popup);
  4853. this.hideTimeout = null;
  4854. }).bind(this), notify);
  4855. }
  4856. }
  4857. }),
  4858. scroll: deferForContainer(function () {
  4859. var container = ui.container.find("#togetherjs-chat-messages")[0];
  4860. container.scrollTop = container.scrollHeight;
  4861. })
  4862. };
  4863. session.on("display-window", function (id, win) {
  4864. if (id == "togetherjs-chat") {
  4865. ui.chat.scroll();
  4866. windowing.hide("#togetherjs-chat-notifier");
  4867. }
  4868. });
  4869. /* This class is bound to peers.Peer instances as peer.view.
  4870. The .update() method is regularly called by peer objects when info changes. */
  4871. ui.PeerView = util.Class({
  4872. constructor: function (peer) {
  4873. assert(peer.isSelf !== undefined, "PeerView instantiated with non-Peer object");
  4874. this.peer = peer;
  4875. this.dockClick = this.dockClick.bind(this);
  4876. },
  4877. /* Takes an element and sets any person-related attributes on the element
  4878. Different from updates, which use the class names we set here: */
  4879. setElement: function (el) {
  4880. var count = 0;
  4881. var classes = ["togetherjs-person", "togetherjs-person-status",
  4882. "togetherjs-person-name", "togetherjs-person-name-abbrev",
  4883. "togetherjs-person-bgcolor", "togetherjs-person-swatch",
  4884. "togetherjs-person-status", "togetherjs-person-role",
  4885. "togetherjs-person-url", "togetherjs-person-url-title",
  4886. "togetherjs-person-bordercolor"];
  4887. classes.forEach(function (cls) {
  4888. var els = el.find("." + cls);
  4889. els.addClass(this.peer.className(cls + "-"));
  4890. count += els.length;
  4891. }, this);
  4892. if (! count) {
  4893. console.warn("setElement(", el, ") doesn't contain any person items");
  4894. }
  4895. this.updateDisplay(el);
  4896. },
  4897. updateDisplay: deferForContainer(function (container) {
  4898. container = container || ui.container;
  4899. var abbrev = this.peer.name;
  4900. if (this.peer.isSelf) {
  4901. abbrev = "me";
  4902. }
  4903. container.find("." + this.peer.className("togetherjs-person-name-")).text(this.peer.name || "");
  4904. container.find("." + this.peer.className("togetherjs-person-name-abbrev-")).text(abbrev);
  4905. var avatarEl = container.find("." + this.peer.className("togetherjs-person-"));
  4906. if (this.peer.avatar) {
  4907. util.assertValidUrl(this.peer.avatar);
  4908. avatarEl.css({
  4909. backgroundImage: "url(" + this.peer.avatar + ")"
  4910. });
  4911. }
  4912. if (this.peer.idle == "inactive") {
  4913. avatarEl.addClass("togetherjs-person-inactive");
  4914. } else {
  4915. avatarEl.removeClass("togetherjs-person-inactive");
  4916. }
  4917. avatarEl.attr("title", this.peer.name);
  4918. if (this.peer.color) {
  4919. avatarEl.css({
  4920. borderColor: this.peer.color
  4921. });
  4922. avatarEl.find(".togetherjs-person-avatar-swatch").css({
  4923. borderTopColor: this.peer.color,
  4924. borderRightColor: this.peer.color
  4925. });
  4926. }
  4927. if (this.peer.color) {
  4928. var colors = container.find("." + this.peer.className("togetherjs-person-bgcolor-"));
  4929. colors.css({
  4930. backgroundColor: this.peer.color
  4931. });
  4932. colors = container.find("." + this.peer.className("togetherjs-person-bordercolor-"));
  4933. colors.css({
  4934. borderColor: this.peer.color
  4935. });
  4936. }
  4937. container.find("." + this.peer.className("togetherjs-person-role-"))
  4938. .text(this.peer.isCreator ? "Creator" : "Participant");
  4939. var urlName = this.peer.title || "";
  4940. if (this.peer.title) {
  4941. urlName += " (";
  4942. }
  4943. urlName += util.truncateCommonDomain(this.peer.url, location.href);
  4944. if (this.peer.title) {
  4945. urlName += ")";
  4946. }
  4947. container.find("." + this.peer.className("togetherjs-person-url-title-"))
  4948. .text(urlName);
  4949. var url = this.peer.url;
  4950. if (this.peer.urlHash) {
  4951. url += this.peer.urlHash;
  4952. }
  4953. container.find("." + this.peer.className("togetherjs-person-url-"))
  4954. .attr("href", url);
  4955. // FIXME: should have richer status:
  4956. container.find("." + this.peer.className("togetherjs-person-status-"))
  4957. .text(this.peer.idle == "active" ? "Active" : "Inactive");
  4958. if (this.peer.isSelf) {
  4959. // FIXME: these could also have consistent/reliable class names:
  4960. var selfName = $(".togetherjs-self-name");
  4961. selfName.each((function (index, el) {
  4962. el = $(el);
  4963. if (el.val() != this.peer.name) {
  4964. el.val(this.peer.name);
  4965. }
  4966. }).bind(this));
  4967. $("#togetherjs-menu-avatar").attr("src", this.peer.avatar);
  4968. if (! this.peer.name) {
  4969. $("#togetherjs-menu .togetherjs-person-name-self").text(this.peer.defaultName);
  4970. }
  4971. }
  4972. if (this.peer.url != session.currentUrl()) {
  4973. container.find("." + this.peer.className("togetherjs-person-"))
  4974. .addClass("togetherjs-person-other-url");
  4975. } else {
  4976. container.find("." + this.peer.className("togetherjs-person-"))
  4977. .removeClass("togetherjs-person-other-url");
  4978. }
  4979. if (this.peer.following) {
  4980. if (this.followCheckbox) {
  4981. this.followCheckbox.prop("checked", true);
  4982. }
  4983. } else {
  4984. if (this.followCheckbox) {
  4985. this.followCheckbox.prop("checked", false);
  4986. }
  4987. }
  4988. // FIXME: add some style based on following?
  4989. updateChatParticipantList();
  4990. this.updateFollow();
  4991. }),
  4992. update: function () {
  4993. if (! this.peer.isSelf) {
  4994. if (this.peer.status == "live") {
  4995. this.dock();
  4996. } else {
  4997. this.undock();
  4998. }
  4999. }
  5000. this.updateDisplay();
  5001. this.updateUrlDisplay();
  5002. },
  5003. updateUrlDisplay: function (force) {
  5004. var url = this.peer.url;
  5005. if ((! url) || (url == this._lastUpdateUrlDisplay && ! force)) {
  5006. return;
  5007. }
  5008. this._lastUpdateUrlDisplay = url;
  5009. var sameUrl = url == session.currentUrl();
  5010. ui.chat.urlChange({
  5011. peer: this.peer,
  5012. url: this.peer.url,
  5013. title: this.peer.title,
  5014. sameUrl: sameUrl
  5015. });
  5016. },
  5017. urlNudge: function () {
  5018. // FIXME: do something more distinct here
  5019. this.updateUrlDisplay(true);
  5020. },
  5021. notifyJoined: function () {
  5022. ui.chat.joinedSession({
  5023. peer: this.peer
  5024. });
  5025. },
  5026. // when there are too many participants in the dock, consolidate the participants to one avatar, and on mouseOver, the dock expands down to reveal the rest of the participants
  5027. // if there are X users in the session
  5028. // then hide the users in the dock
  5029. // and shrink the size of the dock
  5030. // and if you rollover the dock, it expands and reveals the rest of the participants in the dock
  5031. //if users hit X then show the participant button with the consol
  5032. dock: deferForContainer(function () {
  5033. var numberOfUsers = peers.getAllPeers().length;
  5034. // collapse the Dock if too many users
  5035. function CollapsedDock() {
  5036. // decrease/reset dock height
  5037. $("#togetherjs-dock").css("height", 260);
  5038. //replace participant button
  5039. $("#togetherjs-dock-participants").replaceWith("<button id='togetherjs-participantlist-button' class='togetherjs-button'><div class='togetherjs-tooltip togetherjs-dock-person-tooltip'><span class='togetherjs-person-name'>Participants</span><span class='togetherjs-person-tooltip-arrow-r'></span></div><div class='togetherjs-person togetherjs-person-status-overlay' title='Participant List' style='background-image: url("+TogetherJS.baseUrl+"/togetherjs/images/robot-avatar.png); border-color: rgb(255, 0, 0);'></div></button>");
  5040. // new full participant window created on toggle
  5041. $("#togetherjs-participantlist-button").click(function () {
  5042. windowing.toggle("#togetherjs-participantlist");
  5043. });
  5044. }
  5045. // FIXME: turned off for now
  5046. if( numberOfUsers >= 5 && false) {
  5047. CollapsedDock();
  5048. } else {
  5049. // reset
  5050. }
  5051. if (this.dockElement) {
  5052. return;
  5053. }
  5054. this.dockElement = templating.sub("dock-person", {
  5055. peer: this.peer
  5056. });
  5057. this.dockElement.attr("id", this.peer.className("togetherjs-dock-element-"));
  5058. ui.container.find("#togetherjs-dock-participants").append(this.dockElement);
  5059. this.dockElement.find(".togetherjs-person").animateDockEntry();
  5060. adjustDockSize(1);
  5061. this.detailElement = templating.sub("participant-window", {
  5062. peer: this.peer
  5063. });
  5064. var followId = this.peer.className("togetherjs-person-status-follow-");
  5065. this.detailElement.find('[for="togetherjs-person-status-follow"]').attr("for", followId);
  5066. this.detailElement.find('#togetherjs-person-status-follow').attr("id", followId);
  5067. this.detailElement.find(".togetherjs-follow").click(function () {
  5068. location.href = $(this).attr("href");
  5069. });
  5070. this.detailElement.find(".togetherjs-nudge").click((function () {
  5071. this.peer.nudge();
  5072. }).bind(this));
  5073. this.followCheckbox = this.detailElement.find("#" + followId);
  5074. this.followCheckbox.change(function () {
  5075. if (! this.checked) {
  5076. this.peer.unfollow();
  5077. }
  5078. // Following doesn't happen until the window is closed
  5079. // FIXME: should we tell the user this?
  5080. });
  5081. this.maybeHideDetailWindow = this.maybeHideDetailWindow.bind(this);
  5082. session.on("hide-window", this.maybeHideDetailWindow);
  5083. ui.container.append(this.detailElement);
  5084. this.dockElement.click((function () {
  5085. if (this.detailElement.is(":visible")) {
  5086. windowing.hide(this.detailElement);
  5087. } else {
  5088. windowing.show(this.detailElement, {bind: this.dockElement});
  5089. this.scrollTo();
  5090. this.cursor().element.animate({
  5091. opacity:0.3
  5092. }).animate({
  5093. opacity:1
  5094. }).animate({
  5095. opacity:0.3
  5096. }).animate({
  5097. opacity:1
  5098. });
  5099. }
  5100. }).bind(this));
  5101. this.updateFollow();
  5102. }),
  5103. undock: function () {
  5104. if (! this.dockElement) {
  5105. return;
  5106. }
  5107. this.dockElement.animateDockExit().promise().then((function () {
  5108. this.dockElement.remove();
  5109. this.dockElement = null;
  5110. this.detailElement.remove();
  5111. this.detailElement = null;
  5112. adjustDockSize(-1);
  5113. }).bind(this));
  5114. },
  5115. scrollTo: function () {
  5116. if (this.peer.url != session.currentUrl()) {
  5117. return;
  5118. }
  5119. var pos = this.peer.scrollPosition;
  5120. if (! pos) {
  5121. console.warn("Peer has no scroll position:", this.peer);
  5122. return;
  5123. }
  5124. pos = elementFinder.pixelForPosition(pos);
  5125. $("html, body").easeTo(pos);
  5126. },
  5127. updateFollow: function () {
  5128. if (! this.peer.url) {
  5129. return;
  5130. }
  5131. if (! this.detailElement) {
  5132. return;
  5133. }
  5134. var same = this.detailElement.find(".togetherjs-same-url");
  5135. var different = this.detailElement.find(".togetherjs-different-url");
  5136. if (this.peer.url == session.currentUrl()) {
  5137. same.show();
  5138. different.hide();
  5139. } else {
  5140. same.hide();
  5141. different.show();
  5142. }
  5143. },
  5144. maybeHideDetailWindow: function (windows) {
  5145. if (this.detailElement && windows[0] && windows[0][0] === this.detailElement[0]) {
  5146. if (this.followCheckbox[0].checked) {
  5147. this.peer.follow();
  5148. } else {
  5149. this.peer.unfollow();
  5150. }
  5151. }
  5152. },
  5153. dockClick: function () {
  5154. // FIXME: scroll to person
  5155. },
  5156. cursor: function () {
  5157. return require("cursor").getClient(this.peer.id);
  5158. },
  5159. destroy: function () {
  5160. // FIXME: should I get rid of the dockElement?
  5161. session.off("hide-window", this.maybeHideDetailWindow);
  5162. }
  5163. });
  5164. function updateChatParticipantList() {
  5165. var live = peers.getAllPeers(true);
  5166. if (live.length) {
  5167. ui.displayToggle("#togetherjs-chat-participants");
  5168. $("#togetherjs-chat-participant-list").text(
  5169. live.map(function (p) {return p.name;}).join(", "));
  5170. } else {
  5171. ui.displayToggle("#togetherjs-chat-no-participants");
  5172. }
  5173. }
  5174. function inviteHubUrl() {
  5175. var base = TogetherJS.config.get("inviteFromRoom");
  5176. assert(base);
  5177. return util.makeUrlAbsolute(base, session.hubUrl());
  5178. }
  5179. var inRefresh = false;
  5180. function refreshInvite() {
  5181. if (inRefresh) {
  5182. return;
  5183. }
  5184. inRefresh = true;
  5185. require(["who"], function (who) {
  5186. var def = who.getList(inviteHubUrl());
  5187. function addUser(user, before) {
  5188. var item = templating.sub("invite-user-item", {peer: user});
  5189. item.attr("data-clientid", user.id);
  5190. if (before) {
  5191. item.insertBefore(before);
  5192. } else {
  5193. $("#togetherjs-invite-users").append(item);
  5194. }
  5195. item.click(function() {
  5196. invite(user.clientId);
  5197. });
  5198. }
  5199. function refresh(users, finished) {
  5200. var sorted = [];
  5201. for (var id in users) {
  5202. if (users.hasOwnProperty(id)) {
  5203. sorted.push(users[id]);
  5204. }
  5205. }
  5206. sorted.sort(function (a, b) {
  5207. return a.name < b.name ? -1 : 1;
  5208. });
  5209. var pos = 0;
  5210. ui.container.find("#togetherjs-invite-users .togetherjs-menu-item").each(function () {
  5211. var $this = $(this);
  5212. if (finished && ! users[$this.attr("data-clientid")]) {
  5213. $this.remove();
  5214. return;
  5215. }
  5216. if (pos >= sorted.length) {
  5217. return;
  5218. }
  5219. while (pos < sorted.length && $this.attr("data-clientid") !== sorted[pos].id) {
  5220. addUser(sorted[pos], $this);
  5221. pos++;
  5222. }
  5223. while (pos < sorted.length && $this.attr("data-clientid") == sorted[pos].id) {
  5224. pos++;
  5225. }
  5226. });
  5227. for (var i=pos; i<sorted.length; i++) {
  5228. addUser(sorted[pos]);
  5229. }
  5230. }
  5231. def.then(function (users) {
  5232. refresh(users, true);
  5233. inRefresh = false;
  5234. });
  5235. def.progress(refresh);
  5236. });
  5237. }
  5238. session.hub.on("invite", function (msg) {
  5239. if (msg.forClientId && msg.clientId != peers.Self.id) {
  5240. return;
  5241. }
  5242. require(["who"], function (who) {
  5243. var peer = who.ExternalPeer(msg.userInfo.clientId, msg.userInfo);
  5244. ui.chat.invite({peer: peer, url: msg.url, forEveryone: ! msg.forClientId});
  5245. });
  5246. });
  5247. function invite(clientId) {
  5248. require(["who"], function (who) {
  5249. // FIXME: use the return value of this to give a signal that
  5250. // the invite has been successfully sent:
  5251. who.invite(inviteHubUrl(), clientId).then(function () {
  5252. hideMenu();
  5253. });
  5254. });
  5255. }
  5256. ui.showUrlChangeMessage = deferForContainer(function (peer, url) {
  5257. var window = templating.sub("url-change", {peer: peer});
  5258. ui.container.append(window);
  5259. windowing.show(window);
  5260. });
  5261. session.hub.on("url-change-nudge", function (msg) {
  5262. if (msg.to && msg.to != session.clientId) {
  5263. // Not directed to us
  5264. return;
  5265. }
  5266. msg.peer.urlNudge();
  5267. });
  5268. session.on("new-element", function (el) {
  5269. if (TogetherJS.config.get("toolName")) {
  5270. ui.updateToolName(el);
  5271. }
  5272. });
  5273. var setToolName = false;
  5274. ui.updateToolName = function (container) {
  5275. container = container || $(document.body);
  5276. var name = TogetherJS.config.get("toolName");
  5277. if (setToolName && ! name) {
  5278. name = "TogetherJS";
  5279. }
  5280. if (name) {
  5281. container.find(".togetherjs-tool-name").text(name);
  5282. setToolName = true;
  5283. }
  5284. };
  5285. TogetherJS.config.track("toolName", function (name) {
  5286. ui.updateToolName(ui.container);
  5287. });
  5288. return ui;
  5289. });
  5290. /* This Source Code Form is subject to the terms of the Mozilla Public
  5291. * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  5292. * You can obtain one at http://mozilla.org/MPL/2.0/. */
  5293. define('playback',["jquery", "util", "session", "storage", "require"], function ($, util, session, storage, require) {
  5294. var playback = util.Module("playback");
  5295. var assert = util.assert;
  5296. var ALWAYS_REPLAY = {
  5297. "cursor-update": true,
  5298. "scroll-update": true
  5299. };
  5300. playback.getLogs = function (url) {
  5301. if (url.search(/^local:/) === 0) {
  5302. return $.Deferred(function (def) {
  5303. storage.get("recording." + url.substr("local:".length)).then(function (logs) {
  5304. if (! logs) {
  5305. def.resolve(null);
  5306. return;
  5307. }
  5308. logs = parseLogs(logs);
  5309. def.resolve(logs);
  5310. }, function (error) {
  5311. def.reject(error);
  5312. });
  5313. });
  5314. }
  5315. return $.Deferred(function (def) {
  5316. $.ajax({
  5317. url: url,
  5318. dataType: "text"
  5319. }).then(
  5320. function (logs) {
  5321. logs = parseLogs(logs);
  5322. def.resolve(logs);
  5323. },
  5324. function (error) {
  5325. def.reject(error);
  5326. });
  5327. });
  5328. };
  5329. function parseLogs(logs) {
  5330. logs = logs.replace(/\r\n/g, '\n');
  5331. logs = logs.split(/\n/g);
  5332. var result = [];
  5333. for (var i=0; i<logs.length; i++) {
  5334. var line = logs[i];
  5335. line = line.replace(/^\s+/, "").replace(/\s+$/, "");
  5336. if (line.search(/\/\*/) === 0) {
  5337. var last = line.search(/\*\//);
  5338. if (last == -1) {
  5339. console.warn("bad line:", line);
  5340. continue;
  5341. }
  5342. line = line.substr(last+2);
  5343. }
  5344. line = line.replace(/^\s+/, "");
  5345. if (! line) {
  5346. continue;
  5347. }
  5348. line = JSON.parse(line);
  5349. result.push(line);
  5350. }
  5351. return Logs(result);
  5352. }
  5353. var Logs = util.Class({
  5354. constructor: function (logs, fromStorage) {
  5355. this.logs = logs;
  5356. this.fromStorage = fromStorage;
  5357. this.pos = 0;
  5358. },
  5359. play: function () {
  5360. this.start = Date.now();
  5361. if (this.pos >= this.logs.length) {
  5362. this.unload();
  5363. return;
  5364. }
  5365. if (this.pos !== 0) {
  5366. // First we need to play the hello
  5367. var toReplay = [];
  5368. var foundHello = false;
  5369. for (var i=this.pos-1; i>=0; i--) {
  5370. var item = this.logs[i];
  5371. if (ALWAYS_REPLAY[item.type]) {
  5372. toReplay.push(item);
  5373. }
  5374. if (item.type == "hello" || item.type == "hello-back") {
  5375. this.playItem(item);
  5376. foundHello = true;
  5377. break;
  5378. }
  5379. }
  5380. if (! foundHello) {
  5381. console.warn("No hello message found before position", this.pos);
  5382. }
  5383. toReplay.reverse();
  5384. for (i=0; i<toReplay.length; i++) {
  5385. this.playItem(toReplay[i]);
  5386. }
  5387. }
  5388. this.playOne();
  5389. },
  5390. cancel: function () {
  5391. if (this.playTimer) {
  5392. clearTimeout(this.playTimer);
  5393. this.playTimer = null;
  5394. }
  5395. this.start = null;
  5396. this.pos = 0;
  5397. this.unload();
  5398. },
  5399. pause: function () {
  5400. if (this.playTimer) {
  5401. clearTimeout(this.playTimer);
  5402. this.playTimer = null;
  5403. }
  5404. },
  5405. playOne: function () {
  5406. this.playTimer = null;
  5407. if (this.pos >= this.logs.length) {
  5408. this.unload();
  5409. return;
  5410. }
  5411. var item = this.logs[this.pos];
  5412. this.playItem(item);
  5413. this.pos++;
  5414. if (this.pos >= this.logs.length) {
  5415. this.unload();
  5416. return;
  5417. }
  5418. var next = this.logs[this.pos];
  5419. var pause = next.date - item.date;
  5420. this.playTimer = setTimeout(this.playOne.bind(this), pause);
  5421. if (this.fromStorage) {
  5422. this.savePos();
  5423. }
  5424. },
  5425. playItem: function (item) {
  5426. if (item.type == "hello") {
  5427. // We may need to pause here
  5428. if (item.url != (location.href+"").replace(/\#.*/, "")) {
  5429. this.pause();
  5430. }
  5431. }
  5432. try {
  5433. session._getChannel().onmessage(item);
  5434. } catch (e) {
  5435. console.warn("Could not play back message:", item, "error:", e);
  5436. }
  5437. },
  5438. save: function () {
  5439. this.fromStorage = true;
  5440. storage.set("playback.logs", this.logs);
  5441. this.savePos();
  5442. },
  5443. savePos: function () {
  5444. storage.set("playback.pos", this.pos);
  5445. },
  5446. unload: function () {
  5447. if (this.fromStorage) {
  5448. storage.set("playback.logs", undefined);
  5449. storage.set("playback.pos", undefined);
  5450. }
  5451. // FIXME: should do a bye message here
  5452. }
  5453. });
  5454. playback.getRunningLogs = function () {
  5455. return storage.get("playback.logs").then(function (value) {
  5456. if (! value) {
  5457. return null;
  5458. }
  5459. var logs = Logs(value, true);
  5460. return storage.get("playback.pos").then(function (pos) {
  5461. pos = pos || 0;
  5462. logs.pos = pos;
  5463. return logs;
  5464. });
  5465. });
  5466. };
  5467. return playback;
  5468. });
  5469. /* This Source Code Form is subject to the terms of the Mozilla Public
  5470. * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  5471. * You can obtain one at http://mozilla.org/MPL/2.0/. */
  5472. /*jshint evil:true */
  5473. define('chat',["require", "jquery", "util", "session", "ui", "templates", "playback", "storage", "peers", "windowing"], function (require, $, util, session, ui, templates, playback, storage, peers, windowing) {
  5474. var chat = util.Module("chat");
  5475. var assert = util.assert;
  5476. var Walkabout;
  5477. session.hub.on("chat", function (msg) {
  5478. ui.chat.text({
  5479. text: msg.text,
  5480. peer: msg.peer,
  5481. // FIXME: a little unsure of trusting this (maybe I should prefix it?)
  5482. messageId: msg.messageId,
  5483. notify: true
  5484. });
  5485. saveChatMessage({
  5486. text: msg.text,
  5487. date: Date.now(),
  5488. peerId: msg.peer.id,
  5489. messageId: msg.messageId
  5490. });
  5491. });
  5492. // FIXME: this doesn't really belong in this module:
  5493. session.hub.on("bye", function (msg) {
  5494. ui.chat.leftSession({
  5495. peer: msg.peer,
  5496. declinedJoin: msg.reason == "declined-join"
  5497. });
  5498. });
  5499. chat.submit = function (message) {
  5500. var parts = message.split(/ /);
  5501. if (parts[0].charAt(0) == "/") {
  5502. var name = parts[0].substr(1).toLowerCase();
  5503. var method = commands["command_" + name];
  5504. if (method) {
  5505. method.apply(null, parts.slice(1));
  5506. return;
  5507. }
  5508. }
  5509. var messageId = session.clientId + "-" + Date.now();
  5510. session.send({
  5511. type: "chat",
  5512. text: message,
  5513. messageId: messageId
  5514. });
  5515. ui.chat.text({
  5516. text: message,
  5517. peer: peers.Self,
  5518. messageId: messageId,
  5519. notify: false
  5520. });
  5521. saveChatMessage({
  5522. text: message,
  5523. date: Date.now(),
  5524. peerId: peers.Self.id,
  5525. messageId: messageId
  5526. });
  5527. };
  5528. var commands = {
  5529. command_help: function () {
  5530. var msg = util.trim(templates("help"));
  5531. ui.chat.system({
  5532. text: msg
  5533. });
  5534. },
  5535. command_test: function (args) {
  5536. if (! Walkabout) {
  5537. require(["walkabout"], (function (WalkaboutModule) {
  5538. Walkabout = WalkaboutModule;
  5539. this.command_test(args);
  5540. }).bind(this));
  5541. return;
  5542. }
  5543. args = util.trim(args || "").split(/\s+/g);
  5544. if (args[0] === "" || ! args.length) {
  5545. if (this._testCancel) {
  5546. args = ["cancel"];
  5547. } else {
  5548. args = ["start"];
  5549. }
  5550. }
  5551. if (args[0] == "cancel") {
  5552. ui.chat.system({
  5553. text: "Aborting test"
  5554. });
  5555. this._testCancel();
  5556. this._testCancel = null;
  5557. return;
  5558. }
  5559. if (args[0] == "start") {
  5560. var times = parseInt(args[1], 10);
  5561. if (isNaN(times) || ! times) {
  5562. times = 100;
  5563. }
  5564. ui.chat.system({
  5565. text: "Testing with walkabout.js"
  5566. });
  5567. var tmpl = $(templates("walkabout"));
  5568. var container = ui.container.find(".togetherjs-test-container");
  5569. container.empty();
  5570. container.append(tmpl);
  5571. container.show();
  5572. var statusContainer = container.find(".togetherjs-status");
  5573. statusContainer.text("starting...");
  5574. this._testCancel = Walkabout.runManyActions({
  5575. ondone: function () {
  5576. statusContainer.text("done");
  5577. statusContainer.one("click", function () {
  5578. container.hide();
  5579. });
  5580. this._testCancel = null;
  5581. },
  5582. onstatus: function (status) {
  5583. var note = "actions: " + status.actions.length + " running: " +
  5584. (status.times - status.remaining) + " / " + status.times;
  5585. statusContainer.text(note);
  5586. }
  5587. });
  5588. return;
  5589. }
  5590. if (args[0] == "show") {
  5591. if (this._testShow.length) {
  5592. this._testShow.forEach(function (item) {
  5593. if (item) {
  5594. item.remove();
  5595. }
  5596. }, this);
  5597. this._testShow = [];
  5598. } else {
  5599. var actions = Walkabout.findActions();
  5600. actions.forEach(function (action) {
  5601. this._testShow.push(action.show());
  5602. }, this);
  5603. }
  5604. return;
  5605. }
  5606. if (args[0] == "describe") {
  5607. Walkabout.findActions().forEach(function (action) {
  5608. ui.chat.system({
  5609. text: action.description()
  5610. });
  5611. }, this);
  5612. return;
  5613. }
  5614. ui.chat.system({
  5615. text: "Did not understand: " + args.join(" ")
  5616. });
  5617. },
  5618. _testCancel: null,
  5619. _testShow: [],
  5620. command_clear: function () {
  5621. ui.chat.clear();
  5622. },
  5623. command_exec: function () {
  5624. var expr = Array.prototype.slice.call(arguments).join(" ");
  5625. var result;
  5626. // We use this to force global eval (not in this scope):
  5627. var e = eval;
  5628. try {
  5629. result = e(expr);
  5630. } catch (error) {
  5631. ui.chat.system({
  5632. text: "Error: " + error
  5633. });
  5634. }
  5635. if (result !== undefined) {
  5636. ui.chat.system({
  5637. text: "" + result
  5638. });
  5639. }
  5640. },
  5641. command_record: function () {
  5642. ui.chat.system({
  5643. text: "When you see the robot appear, the recording will have started"
  5644. });
  5645. window.open(
  5646. session.recordUrl(), "_blank",
  5647. "left,width=" + ($(window).width() / 2));
  5648. },
  5649. playing: null,
  5650. command_playback: function (url) {
  5651. if (this.playing) {
  5652. this.playing.cancel();
  5653. this.playing.unload();
  5654. this.playing = null;
  5655. ui.chat.system({
  5656. text: "playback cancelled"
  5657. });
  5658. return;
  5659. }
  5660. if (! url) {
  5661. ui.chat.system({
  5662. text: "Nothing is playing"
  5663. });
  5664. return;
  5665. }
  5666. var logLoader = playback.getLogs(url);
  5667. logLoader.then(
  5668. (function (logs) {
  5669. if (! logs) {
  5670. ui.chat.system({
  5671. text: "No logs found."
  5672. });
  5673. return;
  5674. }
  5675. logs.save();
  5676. this.playing = logs;
  5677. logs.play();
  5678. }).bind(this),
  5679. function (error) {
  5680. ui.chat.system({
  5681. text: "Error fetching " + url + ":\n" + JSON.stringify(error, null, " ")
  5682. });
  5683. });
  5684. windowing.hide("#togetherjs-chat");
  5685. },
  5686. command_savelogs: function (name) {
  5687. session.send({
  5688. type: "get-logs",
  5689. forClient: session.clientId,
  5690. saveAs: name
  5691. });
  5692. function save(msg) {
  5693. if (msg.request.forClient == session.clientId && msg.request.saveAs == name) {
  5694. storage.set("recording." + name, msg.logs).then(function () {
  5695. session.hub.off("logs", save);
  5696. ui.chat.system({
  5697. text: "Saved as local:" + name
  5698. });
  5699. });
  5700. }
  5701. }
  5702. session.hub.on("logs", save);
  5703. },
  5704. command_baseurl: function (url) {
  5705. if (! url) {
  5706. storage.get("baseUrlOverride").then(function (b) {
  5707. if (b) {
  5708. ui.chat.system({
  5709. text: "Set to: " + b.baseUrl
  5710. });
  5711. } else {
  5712. ui.chat.system({
  5713. text: "No baseUrl override set"
  5714. });
  5715. }
  5716. });
  5717. return;
  5718. }
  5719. url = url.replace(/\/*$/, "");
  5720. ui.chat.system({
  5721. text: "If this goes wrong, do this in the console to reset:\n localStorage.setItem('togetherjs.baseUrlOverride', null)"
  5722. });
  5723. storage.set("baseUrlOverride", {
  5724. baseUrl: url,
  5725. expiresAt: Date.now() + (1000 * 60 * 60 * 24)
  5726. }).then(function () {
  5727. ui.chat.system({
  5728. text: "baseUrl overridden (to " + url + "), will last for one day."
  5729. });
  5730. });
  5731. },
  5732. command_config: function (variable, value) {
  5733. if (! (variable || value)) {
  5734. storage.get("configOverride").then(function (c) {
  5735. if (c) {
  5736. util.forEachAttr(c, function (value, attr) {
  5737. if (attr == "expiresAt") {
  5738. return;
  5739. }
  5740. ui.chat.system({
  5741. text: " " + attr + " = " + JSON.stringify(value)
  5742. });
  5743. });
  5744. ui.chat.system({
  5745. text: "Config expires at " + (new Date(c.expiresAt))
  5746. });
  5747. } else {
  5748. ui.chat.system({
  5749. text: "No config override"
  5750. });
  5751. }
  5752. });
  5753. return;
  5754. }
  5755. if (variable == "clear") {
  5756. storage.set("configOverride", undefined);
  5757. ui.chat.system({
  5758. text: "Clearing all overridden configuration"
  5759. });
  5760. return;
  5761. }
  5762. console.log("config", [variable, value]);
  5763. if (! (variable && value)) {
  5764. ui.chat.system({
  5765. text: "Error: must provide /config VAR VALUE"
  5766. });
  5767. return;
  5768. }
  5769. try {
  5770. value = JSON.parse(value);
  5771. } catch (e) {
  5772. ui.chat.system({
  5773. text: "Error: value (" + value + ") could not be parsed: " + e
  5774. });
  5775. return;
  5776. }
  5777. if (! TogetherJS._defaultConfiguration.hasOwnProperty(variable)) {
  5778. ui.chat.system({
  5779. text: "Warning: variable " + variable + " is unknown"
  5780. });
  5781. }
  5782. storage.get("configOverride").then(function (c) {
  5783. c = c || {};
  5784. c[variable] = value;
  5785. c.expiresAt = Date.now() + (1000 * 60 * 60 * 24);
  5786. storage.set("configOverride", c).then(function () {
  5787. ui.chat.system({
  5788. text: "Variable " + variable + " = " + JSON.stringify(value) + "\nValue will be set for one day."
  5789. });
  5790. });
  5791. });
  5792. }
  5793. };
  5794. // this section deal with saving/restoring chat history as long as session is alive
  5795. var chatStorageKey = "chatlog";
  5796. var maxLogMessages = 100;
  5797. function saveChatMessage(obj) {
  5798. assert(obj.peerId);
  5799. assert(obj.messageId);
  5800. assert(obj.date);
  5801. assert(typeof obj.text == "string");
  5802. loadChatLog().then(function (log) {
  5803. for (var i = log.length - 1; i >= 0; i--) {
  5804. if (log[i].messageId === obj.messageId) {
  5805. return;
  5806. }
  5807. }
  5808. log.push(obj);
  5809. if (log.length > maxLogMessages) {
  5810. log.splice(0, log.length - maxLogMessages);
  5811. }
  5812. storage.tab.set(chatStorageKey, log);
  5813. });
  5814. }
  5815. function loadChatLog() {
  5816. return storage.tab.get(chatStorageKey, []);
  5817. }
  5818. session.once("ui-ready", function () {
  5819. loadChatLog().then(function (log) {
  5820. if (! log) {
  5821. return;
  5822. }
  5823. for (var i = 0; i < log.length; i++) {
  5824. // peers should already be loaded from sessionStorage by the peers module
  5825. var currentPeer = peers.getPeer(log[i].peerId, null, true);
  5826. if (!currentPeer) {
  5827. // sometimes peers go away
  5828. continue;
  5829. }
  5830. ui.chat.text({
  5831. text: log[i].text,
  5832. date: log[i].date,
  5833. peer: currentPeer,
  5834. messageId: log[i].messageId
  5835. });
  5836. }
  5837. });
  5838. });
  5839. //delete chat log
  5840. session.on("close", function(){
  5841. storage.tab.set(chatStorageKey, undefined);
  5842. });
  5843. return chat;
  5844. });
  5845. /* This Source Code Form is subject to the terms of the Mozilla Public
  5846. * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  5847. * You can obtain one at http://mozilla.org/MPL/2.0/. */
  5848. define('console',["util"], function (util) {
  5849. var console = window.console || {log: function () {}};
  5850. var Console = util.Class({
  5851. constructor: function () {
  5852. this.messages = [];
  5853. this.level = this.levels.log;
  5854. },
  5855. messageLimit: 100,
  5856. levels: {
  5857. debug: 1,
  5858. // FIXME: I'm considering *not* wrapping console.log, and strictly keeping
  5859. // it as a debugging tool; also line numbers would be preserved
  5860. log: 2,
  5861. info: 3,
  5862. notify: 4,
  5863. warn: 5,
  5864. error: 6,
  5865. fatal: 7
  5866. },
  5867. // Gets set below:
  5868. maxLevel: 0,
  5869. consoleLevels: [
  5870. [],
  5871. console.debug || [],
  5872. console.log || [],
  5873. console.info || [],
  5874. console.notify || [],
  5875. console.warn || [],
  5876. console.error || [],
  5877. console.fatal || []
  5878. ],
  5879. levelNames: {},
  5880. setLevel: function (l) {
  5881. var number;
  5882. if (typeof l == "string") {
  5883. number = this.levels[l];
  5884. if (number === undefined) {
  5885. throw new Error("Tried to set Console level to unknown level string: " + l);
  5886. }
  5887. l = number;
  5888. }
  5889. if (typeof l == "function") {
  5890. number = this.consoleLevels.indexOf(l);
  5891. if (number == -1) {
  5892. throw new Error("Tried to set Console level based on unknown console function: " + l);
  5893. }
  5894. l = number;
  5895. }
  5896. if (typeof l == "number") {
  5897. if (l < 0) {
  5898. throw new Error("Console level must be 0 or larger: " + l);
  5899. } else if (l > this.maxLevel) {
  5900. throw new Error("Console level must be " + this.maxLevel + " or smaller: " + l);
  5901. }
  5902. }
  5903. this.level = l;
  5904. },
  5905. write: function (level) {
  5906. try {
  5907. this.messages.push([
  5908. Date.now(),
  5909. level,
  5910. this._stringify(Array.prototype.slice.call(arguments, 1))
  5911. ]);
  5912. } catch (e) {
  5913. console.warn("Error stringifying argument:", e);
  5914. }
  5915. if (level != "suppress" && this.level <= level) {
  5916. var method = console[this.levelNames[level]];
  5917. if (! method) {
  5918. method = console.log;
  5919. }
  5920. method.apply(console, Array.prototype.slice.call(arguments, 1));
  5921. }
  5922. },
  5923. suppressedWrite: function () {
  5924. this.write.apply(this, ["suppress"].concat(Array.prototype.slice.call(arguments)));
  5925. },
  5926. trace: function (level) {
  5927. level = level || 'log';
  5928. if (console.trace) {
  5929. level = "suppressedWrite";
  5930. }
  5931. try {
  5932. throw new Error();
  5933. } catch (e) {
  5934. // FIXME: trim this frame
  5935. var stack = e.stack;
  5936. stack = stack.replace(/^[^\n]*\n/, "");
  5937. this[level](stack);
  5938. }
  5939. if (console.trace) {
  5940. console.trace();
  5941. }
  5942. },
  5943. _browserInfo: function () {
  5944. // FIXME: add TogetherJS version and
  5945. return [
  5946. "TogetherJS base URL: " + TogetherJS.baseUrl,
  5947. "User Agent: " + navigator.userAgent,
  5948. "Page loaded: " + this._formatDate(TogetherJS.pageLoaded),
  5949. "Age: " + this._formatMinutes(Date.now() - TogetherJS.pageLoaded) + " minutes",
  5950. // FIXME: make this right:
  5951. //"Window: height: " + window.screen.height + " width: " + window.screen.width
  5952. "URL: " + location.href,
  5953. "------+------+----------------------------------------------"
  5954. ];
  5955. },
  5956. _stringify: function (args) {
  5957. var s = "";
  5958. for (var i=0; i<args.length; i++) {
  5959. if (s) {
  5960. s += " ";
  5961. }
  5962. s += this._stringifyItem(args[i]);
  5963. }
  5964. return s;
  5965. },
  5966. _stringifyItem: function (item) {
  5967. if (typeof item == "string") {
  5968. if (item === "") {
  5969. return '""';
  5970. }
  5971. return item;
  5972. }
  5973. if (typeof item == "object" && item.repr) {
  5974. try {
  5975. return item.repr();
  5976. } catch (e) {
  5977. console.warn("Error getting object repr:", item, e);
  5978. }
  5979. }
  5980. if (item !== null && typeof item == "object") {
  5981. // FIXME: this can drop lots of kinds of values, like a function or undefined
  5982. item = JSON.stringify(item);
  5983. }
  5984. return item.toString();
  5985. },
  5986. _formatDate: function (timestamp) {
  5987. return (new Date(timestamp)).toISOString();
  5988. },
  5989. _formatTime: function (timestamp) {
  5990. return ((timestamp - TogetherJS.pageLoaded) / 1000).toFixed(2);
  5991. },
  5992. _formatMinutes: function (milliseconds) {
  5993. var m = Math.floor(milliseconds / 1000 / 60);
  5994. var remaining = milliseconds - (m * 1000 * 60);
  5995. if (m > 10) {
  5996. // Over 10 minutes, just ignore the seconds
  5997. return m;
  5998. }
  5999. var seconds = Math.floor(remaining / 1000) + "";
  6000. m += ":";
  6001. seconds = lpad(seconds, 2, "0");
  6002. m += seconds;
  6003. if (m == "0:00") {
  6004. m += ((remaining / 1000).toFixed(3) + "").substr(1);
  6005. }
  6006. return m;
  6007. },
  6008. _formatLevel: function (l) {
  6009. if (l === "suppress") {
  6010. return "";
  6011. }
  6012. return this.levelNames[l];
  6013. },
  6014. toString: function () {
  6015. try {
  6016. var lines = this._browserInfo();
  6017. this.messages.forEach(function (m) {
  6018. lines.push(lpad(this._formatTime(m[0]), 6) + " " + rpad(this._formatLevel(m[1]), 6) + " " + lpadLines(m[2], 14));
  6019. }, this);
  6020. return lines.join("\n");
  6021. } catch (e) {
  6022. // toString errors can otherwise be swallowed:
  6023. console.warn("Error running console.toString():", e);
  6024. throw e;
  6025. }
  6026. },
  6027. submit: function (options) {
  6028. // FIXME: friendpaste is broken for this
  6029. // (and other pastebin sites aren't really Browser-accessible)
  6030. return util.Deferred(function (def) {
  6031. options = options || {};
  6032. var site = options.site || TogetherJS.config.get("pasteSite") || "https://www.friendpaste.com/";
  6033. var req = new XMLHttpRequest();
  6034. req.open("POST", site);
  6035. req.setRequestHeader("Content-Type", "application/json");
  6036. req.send(JSON.stringify({
  6037. "title": options.title || "TogetherJS log file",
  6038. "snippet": this.toString(),
  6039. "language": "text"
  6040. }));
  6041. req.onreadystatechange = function () {
  6042. if (req.readyState === 4) {
  6043. var data = JSON.parse(req.responseText);
  6044. }
  6045. };
  6046. });
  6047. }
  6048. });
  6049. function rpad(s, len, pad) {
  6050. s = s + "";
  6051. pad = pad || " ";
  6052. while (s.length < len) {
  6053. s += pad;
  6054. }
  6055. return s;
  6056. }
  6057. function lpad(s, len, pad) {
  6058. s = s + "";
  6059. pad = pad || " ";
  6060. while (s.length < len) {
  6061. s = pad + s;
  6062. }
  6063. return s;
  6064. }
  6065. function lpadLines(s, len, pad) {
  6066. var i;
  6067. s = s + "";
  6068. if (s.indexOf("\n") == -1) {
  6069. return s;
  6070. }
  6071. pad = pad || " ";
  6072. var fullPad = "";
  6073. for (i=0; i<len; i++) {
  6074. fullPad += pad;
  6075. }
  6076. s = s.split(/\n/g);
  6077. for (i=1; i<s.length; i++) {
  6078. s[i] = fullPad + s[i];
  6079. }
  6080. return s.join("\n");
  6081. }
  6082. // This is a factory that creates `Console.prototype.debug`, `.error` etc:
  6083. function logFunction(name, level) {
  6084. return function () {
  6085. this.write.apply(this, [level].concat(Array.prototype.slice.call(arguments)));
  6086. };
  6087. }
  6088. util.forEachAttr(Console.prototype.levels, function (value, name) {
  6089. Console.prototype[name] = logFunction(name, value);
  6090. Console.prototype.maxLevel = Math.max(Console.prototype.maxLevel, value);
  6091. });
  6092. util.forEachAttr(Console.prototype.levels, function (value, name) {
  6093. Console.prototype.levelNames[value] = name;
  6094. });
  6095. var appConsole = Console();
  6096. appConsole.ConsoleClass = Console;
  6097. return appConsole;
  6098. });
  6099. /* This Source Code Form is subject to the terms of the Mozilla Public
  6100. * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  6101. * You can obtain one at http://mozilla.org/MPL/2.0/. */
  6102. define('eventMaker',["jquery", "util"], function ($, util) {
  6103. var eventMaker = util.Module("eventMaker");
  6104. eventMaker.performClick = function (target) {
  6105. // FIXME: should accept other parameters, like Ctrl/Alt/etc
  6106. var event = document.createEvent("MouseEvents");
  6107. event.initMouseEvent(
  6108. "click", // type
  6109. true, // canBubble
  6110. true, // cancelable
  6111. window, // view
  6112. 0, // detail
  6113. 0, // screenX
  6114. 0, // screenY
  6115. 0, // clientX
  6116. 0, // clientY
  6117. false, // ctrlKey
  6118. false, // altKey
  6119. false, // shiftKey
  6120. false, // metaKey
  6121. 0, // button
  6122. null // relatedTarget
  6123. );
  6124. // FIXME: I'm not sure this custom attribute always propagates?
  6125. // seems okay in Firefox/Chrome, but I've had problems with
  6126. // setting attributes on keyboard events in the past.
  6127. event.togetherjsInternal = true;
  6128. target = $(target)[0];
  6129. var cancelled = target.dispatchEvent(event);
  6130. if (cancelled) {
  6131. return;
  6132. }
  6133. if (target.tagName == "A") {
  6134. var href = target.href;
  6135. if (href) {
  6136. location.href = href;
  6137. return;
  6138. }
  6139. }
  6140. // FIXME: should do button clicks (like a form submit)
  6141. // FIXME: should run .onclick() as well
  6142. };
  6143. eventMaker.fireChange = function (target) {
  6144. target = $(target)[0];
  6145. var event = document.createEvent("HTMLEvents");
  6146. event.initEvent("change", true, true);
  6147. target.dispatchEvent(event);
  6148. };
  6149. return eventMaker;
  6150. });
  6151. /* This Source Code Form is subject to the terms of the Mozilla Public
  6152. * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  6153. * You can obtain one at http://mozilla.org/MPL/2.0/. */
  6154. // Cursor viewing support
  6155. define('cursor',["jquery", "ui", "util", "session", "elementFinder", "tinycolor", "eventMaker", "peers", "templating"], function ($, ui, util, session, elementFinder, tinycolor, eventMaker, peers, templating) {
  6156. var assert = util.assert;
  6157. var cursor = util.Module("cursor");
  6158. var FOREGROUND_COLORS = ["#111", "#eee"];
  6159. var CURSOR_HEIGHT = 50;
  6160. var CURSOR_ANGLE = (35 / 180) * Math.PI;
  6161. var CURSOR_WIDTH = Math.ceil(Math.sin(CURSOR_ANGLE) * CURSOR_HEIGHT);
  6162. // Number of milliseconds after page load in which a scroll-update
  6163. // related hello-back message will be processed:
  6164. var SCROLL_UPDATE_CUTOFF = 2000;
  6165. session.hub.on("cursor-update", function (msg) {
  6166. if (msg.sameUrl) {
  6167. Cursor.getClient(msg.clientId).updatePosition(msg);
  6168. } else {
  6169. // FIXME: This should be caught even before the cursor-update message,
  6170. // when the peer goes to another URL
  6171. Cursor.getClient(msg.clientId).hideOtherUrl();
  6172. }
  6173. });
  6174. // FIXME: should check for a peer leaving and remove the cursor object
  6175. var Cursor = util.Class({
  6176. constructor: function (clientId) {
  6177. this.clientId = clientId;
  6178. this.element = templating.clone("cursor");
  6179. this.elementClass = "togetherjs-scrolled-normal";
  6180. this.element.addClass(this.elementClass);
  6181. this.updatePeer(peers.getPeer(clientId));
  6182. this.lastTop = this.lastLeft = null;
  6183. $(document.body).append(this.element);
  6184. this.element.animateCursorEntry();
  6185. this.keydownTimeout = null;
  6186. this.clearKeydown = this.clearKeydown.bind(this);
  6187. this.atOtherUrl = false;
  6188. },
  6189. // How long after receiving a setKeydown call that we should show the
  6190. // user typing. This should be more than MIN_KEYDOWN_TIME:
  6191. KEYDOWN_WAIT_TIME: 2000,
  6192. updatePeer: function (peer) {
  6193. // FIXME: can I use peer.setElement()?
  6194. this.element.css({color: peer.color});
  6195. var img = this.element.find("img.togetherjs-cursor-img");
  6196. img.attr("src", makeCursor(peer.color));
  6197. var name = this.element.find(".togetherjs-cursor-name");
  6198. var nameContainer = this.element.find(".togetherjs-cursor-container");
  6199. assert(name.length);
  6200. name.text(peer.name);
  6201. nameContainer.css({
  6202. backgroundColor: peer.color,
  6203. color: tinycolor.mostReadable(peer.color, FOREGROUND_COLORS)
  6204. });
  6205. var path = this.element.find("svg path");
  6206. path.attr("fill", peer.color);
  6207. // FIXME: should I just remove the element?
  6208. if (peer.status != "live") {
  6209. //this.element.hide();
  6210. this.element.find("svg").animate({
  6211. opacity: 0
  6212. }, 350);
  6213. this.element.find(".togetherjs-cursor-container").animate({
  6214. width: 34,
  6215. height: 20,
  6216. padding: 12,
  6217. margin: 0
  6218. }, 200).animate({
  6219. width: 0,
  6220. height: 0,
  6221. padding: 0,
  6222. opacity: 0
  6223. }, 200);
  6224. } else {
  6225. //this.element.show();
  6226. this.element.animate({
  6227. opacity:0.3
  6228. }).animate({
  6229. opacity:1
  6230. });
  6231. }
  6232. },
  6233. setClass: function (name) {
  6234. if (name != this.elementClass) {
  6235. this.element.removeClass(this.elementClass).addClass(name);
  6236. this.elementClass = name;
  6237. }
  6238. },
  6239. updatePosition: function (pos) {
  6240. var top, left;
  6241. if (this.atOtherUrl) {
  6242. this.element.show();
  6243. this.atOtherUrl = false;
  6244. }
  6245. if (pos.element) {
  6246. var target = $(elementFinder.findElement(pos.element));
  6247. var offset = target.offset();
  6248. top = offset.top + pos.offsetY;
  6249. left = offset.left + pos.offsetX;
  6250. } else {
  6251. // No anchor, just an absolute position
  6252. top = pos.top;
  6253. left = pos.left;
  6254. }
  6255. // These are saved for use by .refresh():
  6256. this.lastTop = top;
  6257. this.lastLeft = left;
  6258. this.setPosition(top, left);
  6259. },
  6260. hideOtherUrl: function () {
  6261. if (this.atOtherUrl) {
  6262. return;
  6263. }
  6264. this.atOtherUrl = true;
  6265. // FIXME: should show away status better:
  6266. this.element.hide();
  6267. },
  6268. // place Cursor rotate function down here FIXME: this doesnt do anything anymore. This is in the CSS as an animation
  6269. rotateCursorDown: function(){
  6270. var e = $(this.element).find('svg');
  6271. e.animate({borderSpacing: -150, opacity: 1}, {
  6272. step: function(now, fx) {
  6273. if (fx.prop == "borderSpacing") {
  6274. e.css('-webkit-transform', 'rotate('+now+'deg)')
  6275. .css('-moz-transform', 'rotate('+now+'deg)')
  6276. .css('-ms-transform', 'rotate('+now+'deg)')
  6277. .css('-o-transform', 'rotate('+now+'deg)')
  6278. .css('transform', 'rotate('+now+'deg)');
  6279. } else {
  6280. e.css(fx.prop, now);
  6281. }
  6282. },
  6283. duration: 500
  6284. }, 'linear').promise().then(function () {
  6285. e.css('-webkit-transform', '')
  6286. .css('-moz-transform', '')
  6287. .css('-ms-transform', '')
  6288. .css('-o-transform', '')
  6289. .css('transform', '')
  6290. .css("opacity", "");
  6291. });
  6292. },
  6293. setPosition: function (top, left) {
  6294. var wTop = $(window).scrollTop();
  6295. var height = $(window).height();
  6296. if (top < wTop) {
  6297. // FIXME: this is a totally arbitrary number, but is meant to be big enough
  6298. // to keep the cursor name from being off the top of the screen.
  6299. top = 25;
  6300. this.setClass("togetherjs-scrolled-above");
  6301. } else if (top > wTop + height - CURSOR_HEIGHT) {
  6302. top = height - CURSOR_HEIGHT - 5;
  6303. this.setClass("togetherjs-scrolled-below");
  6304. } else {
  6305. this.setClass("togetherjs-scrolled-normal");
  6306. }
  6307. this.element.css({
  6308. top: top,
  6309. left: left
  6310. });
  6311. },
  6312. refresh: function () {
  6313. if (this.lastTop !== null) {
  6314. this.setPosition(this.lastTop, this.lastLeft);
  6315. }
  6316. },
  6317. setKeydown: function () {
  6318. if (this.keydownTimeout) {
  6319. clearTimeout(this.keydownTimeout);
  6320. } else {
  6321. this.element.find(".togetherjs-cursor-typing").show().animateKeyboard();
  6322. }
  6323. this.keydownTimeout = setTimeout(this.clearKeydown, this.KEYDOWN_WAIT_TIME);
  6324. },
  6325. clearKeydown: function () {
  6326. this.keydownTimeout = null;
  6327. this.element.find(".togetherjs-cursor-typing").hide().stopKeyboardAnimation();
  6328. },
  6329. _destroy: function () {
  6330. this.element.remove();
  6331. this.element = null;
  6332. }
  6333. });
  6334. Cursor._cursors = {};
  6335. cursor.getClient = Cursor.getClient = function (clientId) {
  6336. var c = Cursor._cursors[clientId];
  6337. if (! c) {
  6338. c = Cursor._cursors[clientId] = Cursor(clientId);
  6339. }
  6340. return c;
  6341. };
  6342. Cursor.forEach = function (callback, context) {
  6343. context = context || null;
  6344. for (var a in Cursor._cursors) {
  6345. if (Cursor._cursors.hasOwnProperty(a)) {
  6346. callback.call(context, Cursor._cursors[a], a);
  6347. }
  6348. }
  6349. };
  6350. Cursor.destroy = function (clientId) {
  6351. Cursor._cursors[clientId]._destroy();
  6352. delete Cursor._cursors[clientId];
  6353. };
  6354. peers.on("new-peer identity-updated status-updated", function (peer) {
  6355. var c = Cursor.getClient(peer.id);
  6356. c.updatePeer(peer);
  6357. });
  6358. var lastTime = 0;
  6359. var MIN_TIME = 100;
  6360. var lastPosX = -1;
  6361. var lastPosY = -1;
  6362. var lastMessage = null;
  6363. function mousemove(event) {
  6364. var now = Date.now();
  6365. if (now - lastTime < MIN_TIME) {
  6366. return;
  6367. }
  6368. lastTime = now;
  6369. var pageX = event.pageX;
  6370. var pageY = event.pageY;
  6371. if (Math.abs(lastPosX - pageX) < 3 && Math.abs(lastPosY - pageY) < 3) {
  6372. // Not a substantial enough change
  6373. return;
  6374. }
  6375. lastPosX = pageX;
  6376. lastPosY = pageY;
  6377. var target = event.target;
  6378. var parent = $(target).closest(".togetherjs-window, .togetherjs-popup, #togetherjs-dock");
  6379. if (parent.length) {
  6380. target = parent[0];
  6381. } else if (elementFinder.ignoreElement(target)) {
  6382. target = null;
  6383. }
  6384. if ((! target) || target == document.documentElement || target == document.body) {
  6385. lastMessage = {
  6386. type: "cursor-update",
  6387. top: pageY,
  6388. left: pageX
  6389. };
  6390. session.send(lastMessage);
  6391. return;
  6392. }
  6393. target = $(target);
  6394. var offset = target.offset();
  6395. if (! offset) {
  6396. // FIXME: this really is walkabout.js's problem to fire events on the
  6397. // document instead of a specific element
  6398. console.warn("Could not get offset of element:", target[0]);
  6399. return;
  6400. }
  6401. var offsetX = pageX - offset.left;
  6402. var offsetY = pageY - offset.top;
  6403. lastMessage = {
  6404. type: "cursor-update",
  6405. element: elementFinder.elementLocation(target),
  6406. offsetX: Math.floor(offsetX),
  6407. offsetY: Math.floor(offsetY)
  6408. };
  6409. session.send(lastMessage);
  6410. }
  6411. function makeCursor(color) {
  6412. var canvas = $("<canvas></canvas>");
  6413. canvas.attr("height", CURSOR_HEIGHT);
  6414. canvas.attr("width", CURSOR_WIDTH);
  6415. var context = canvas[0].getContext('2d');
  6416. context.fillStyle = color;
  6417. context.moveTo(0, 0);
  6418. context.beginPath();
  6419. context.lineTo(0, CURSOR_HEIGHT/1.2);
  6420. context.lineTo(Math.sin(CURSOR_ANGLE/2) * CURSOR_HEIGHT / 1.5,
  6421. Math.cos(CURSOR_ANGLE/2) * CURSOR_HEIGHT / 1.5);
  6422. context.lineTo(Math.sin(CURSOR_ANGLE) * CURSOR_HEIGHT / 1.2,
  6423. Math.cos(CURSOR_ANGLE) * CURSOR_HEIGHT / 1.2);
  6424. context.lineTo(0, 0);
  6425. context.shadowColor = 'rgba(0,0,0,0.3)';
  6426. context.shadowBlur = 2;
  6427. context.shadowOffsetX = 1;
  6428. context.shadowOffsetY = 2;
  6429. context.strokeStyle = "#ffffff";
  6430. context.stroke();
  6431. context.fill();
  6432. return canvas[0].toDataURL("image/png");
  6433. }
  6434. var scrollTimeout = null;
  6435. var scrollTimeoutSet = 0;
  6436. var SCROLL_DELAY_TIMEOUT = 75;
  6437. var SCROLL_DELAY_LIMIT = 300;
  6438. function scroll() {
  6439. var now = Date.now();
  6440. if (scrollTimeout) {
  6441. if (now - scrollTimeoutSet < SCROLL_DELAY_LIMIT) {
  6442. clearTimeout(scrollTimeout);
  6443. } else {
  6444. // Just let it progress anyway
  6445. return;
  6446. }
  6447. }
  6448. scrollTimeout = setTimeout(_scrollRefresh, SCROLL_DELAY_TIMEOUT);
  6449. if (! scrollTimeoutSet) {
  6450. scrollTimeoutSet = now;
  6451. }
  6452. }
  6453. var lastScrollMessage = null;
  6454. function _scrollRefresh() {
  6455. scrollTimeout = null;
  6456. scrollTimeoutSet = 0;
  6457. Cursor.forEach(function (c) {
  6458. c.refresh();
  6459. });
  6460. lastScrollMessage = {
  6461. type: "scroll-update",
  6462. position: elementFinder.elementByPixel($(window).scrollTop())
  6463. };
  6464. session.send(lastScrollMessage);
  6465. }
  6466. // FIXME: do the same thing for cursor position? And give up on the
  6467. // ad hoc update-on-hello?
  6468. session.on("prepare-hello", function (helloMessage) {
  6469. if (lastScrollMessage) {
  6470. helloMessage.scrollPosition = lastScrollMessage.position;
  6471. }
  6472. });
  6473. session.hub.on("scroll-update", function (msg) {
  6474. msg.peer.scrollPosition = msg.position;
  6475. if (msg.peer.following) {
  6476. msg.peer.view.scrollTo();
  6477. }
  6478. });
  6479. // In case there are multiple peers, we track that we've accepted one of their
  6480. // hello-based scroll updates, just so we don't bounce around (we don't intelligently
  6481. // choose which one to use, just the first that comes in)
  6482. var acceptedScrollUpdate = false;
  6483. session.hub.on("hello-back hello", function (msg) {
  6484. if (msg.type == "hello") {
  6485. // Once a hello comes in, a bunch of hello-backs not intended for us will also
  6486. // come in, and we should ignore them
  6487. acceptedScrollUpdate = true;
  6488. }
  6489. if (! msg.scrollPosition) {
  6490. return;
  6491. }
  6492. msg.peer.scrollPosition = msg.scrollPosition;
  6493. if ((! acceptedScrollUpdate) &&
  6494. msg.sameUrl &&
  6495. Date.now() - session.timeHelloSent < SCROLL_UPDATE_CUTOFF) {
  6496. acceptedScrollUpdate = true;
  6497. msg.peer.view.scrollTo();
  6498. }
  6499. });
  6500. session.on("ui-ready", function () {
  6501. $(document).mousemove(mousemove);
  6502. document.addEventListener("click", documentClick, true);
  6503. document.addEventListener("keydown", documentKeydown, true);
  6504. $(window).scroll(scroll);
  6505. scroll();
  6506. });
  6507. session.on("close", function () {
  6508. Cursor.forEach(function (c, clientId) {
  6509. Cursor.destroy(clientId);
  6510. });
  6511. $(document).unbind("mousemove", mousemove);
  6512. document.removeEventListener("click", documentClick, true);
  6513. document.removeEventListener("keydown", documentKeydown, true);
  6514. $(window).unbind("scroll", scroll);
  6515. });
  6516. session.hub.on("hello", function (msg) {
  6517. // Immediately get our cursor onto this new person's screen:
  6518. if (lastMessage) {
  6519. session.send(lastMessage);
  6520. }
  6521. if (lastScrollMessage) {
  6522. session.send(lastScrollMessage);
  6523. }
  6524. });
  6525. function documentClick(event) {
  6526. if (event.togetherjsInternal) {
  6527. // This is an artificial internal event
  6528. return;
  6529. }
  6530. // FIXME: this might just be my imagination, but somehow I just
  6531. // really don't want to do anything at this stage of the event
  6532. // handling (since I'm catching every click), and I'll just do
  6533. // something real soon:
  6534. setTimeout(function () {
  6535. if (! TogetherJS.running) {
  6536. // This can end up running right after TogetherJS has been closed, often
  6537. // because TogetherJS was closed with a click...
  6538. return;
  6539. }
  6540. var element = event.target;
  6541. if (element == document.documentElement) {
  6542. // For some reason clicking on <body> gives the <html> element here
  6543. element = document.body;
  6544. }
  6545. if (elementFinder.ignoreElement(element)) {
  6546. return;
  6547. }
  6548. //Prevent click events on video objects to avoid conflicts with
  6549. //togetherjs's own video events
  6550. if (element.nodeName.toLowerCase() === 'video'){
  6551. return;
  6552. }
  6553. var dontShowClicks = TogetherJS.config.get("dontShowClicks");
  6554. var cloneClicks = TogetherJS.config.get("cloneClicks");
  6555. // If you dont want to clone the click for this element
  6556. // and you dont want to show the click for this element or you dont want to show any clicks
  6557. // then return to avoid sending a useless click
  6558. if ((! util.matchElement(element, cloneClicks)) && util.matchElement(element, dontShowClicks)) {
  6559. return;
  6560. }
  6561. var location = elementFinder.elementLocation(element);
  6562. var offset = $(element).offset();
  6563. var offsetX = event.pageX - offset.left;
  6564. var offsetY = event.pageY - offset.top;
  6565. session.send({
  6566. type: "cursor-click",
  6567. element: location,
  6568. offsetX: offsetX,
  6569. offsetY: offsetY
  6570. });
  6571. if (util.matchElement(element, dontShowClicks)) {
  6572. return;
  6573. }
  6574. displayClick({top: event.pageY, left: event.pageX}, peers.Self.color);
  6575. });
  6576. }
  6577. var CLICK_TRANSITION_TIME = 3000;
  6578. session.hub.on("cursor-click", function (pos) {
  6579. // When the click is calculated isn't always the same as how the
  6580. // last cursor update was calculated, so we force the cursor to
  6581. // the last location during a click:
  6582. if (! pos.sameUrl) {
  6583. // FIXME: if we *could have* done a local click, but we follow along
  6584. // later, we'll be in different states if that click was important.
  6585. // Mostly click cloning just won't work.
  6586. return;
  6587. }
  6588. Cursor.getClient(pos.clientId).updatePosition(pos);
  6589. var target = $(elementFinder.findElement(pos.element));
  6590. var offset = target.offset();
  6591. var top = offset.top + pos.offsetY;
  6592. var left = offset.left + pos.offsetX;
  6593. var cloneClicks = TogetherJS.config.get("cloneClicks");
  6594. if (util.matchElement(target, cloneClicks)) {
  6595. eventMaker.performClick(target);
  6596. }
  6597. var dontShowClicks = TogetherJS.config.get("dontShowClicks");
  6598. if (util.matchElement(target, dontShowClicks)) {
  6599. return;
  6600. }
  6601. displayClick({top: top, left: left}, pos.peer.color);
  6602. });
  6603. function displayClick(pos, color) {
  6604. // FIXME: should we hide the local click if no one else is going to see it?
  6605. // That means tracking who might be able to see our screen.
  6606. var element = templating.clone("click");
  6607. $(document.body).append(element);
  6608. element.css({
  6609. top: pos.top,
  6610. left: pos.left,
  6611. borderColor: color
  6612. });
  6613. setTimeout(function () {
  6614. element.addClass("togetherjs-clicking");
  6615. }, 100);
  6616. setTimeout(function () {
  6617. element.remove();
  6618. }, CLICK_TRANSITION_TIME);
  6619. }
  6620. var lastKeydown = 0;
  6621. var MIN_KEYDOWN_TIME = 500;
  6622. function documentKeydown(event) {
  6623. setTimeout(function () {
  6624. var now = Date.now();
  6625. if (now - lastKeydown < MIN_KEYDOWN_TIME) {
  6626. return;
  6627. }
  6628. lastKeydown = now;
  6629. // FIXME: is event.target interesting here? That is, *what* the
  6630. // user is typing into, not just that the user is typing? Also
  6631. // I'm assuming we don't care if the user it typing into a
  6632. // togetherjs-related field, since chat activity is as interesting
  6633. // as any other activity.
  6634. session.send({type: "keydown"});
  6635. });
  6636. }
  6637. session.hub.on("keydown", function (msg) {
  6638. // FIXME: when the cursor is hidden there's nothing to show with setKeydown().
  6639. var cursor = Cursor.getClient(msg.clientId);
  6640. cursor.setKeydown();
  6641. });
  6642. util.testExpose({Cursor: Cursor});
  6643. return cursor;
  6644. });
  6645. /* This Source Code Form is subject to the terms of the Mozilla Public
  6646. * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  6647. * You can obtain one at http://mozilla.org/MPL/2.0/. */
  6648. define('ot',["util"], function (util) {
  6649. var ot = util.Module("ot");
  6650. var assert = util.assert;
  6651. var StringSet = util.Class({
  6652. /* Set that only supports string items */
  6653. constructor: function () {
  6654. this._items = {};
  6655. this._count = 0;
  6656. },
  6657. contains: function (k) {
  6658. assert(typeof k == "string");
  6659. return this._items.hasOwnProperty(k);
  6660. },
  6661. add: function (k) {
  6662. assert(typeof k == "string");
  6663. if (this.contains(k)) {
  6664. return;
  6665. }
  6666. this._items[k] = null;
  6667. this._count++;
  6668. },
  6669. remove: function (k) {
  6670. assert(typeof k == "string");
  6671. if (! this.contains(k)) {
  6672. return;
  6673. }
  6674. delete this._items[k];
  6675. this._count++;
  6676. },
  6677. isEmpty: function () {
  6678. return ! this._count;
  6679. }
  6680. });
  6681. var Queue = util.Class({
  6682. constructor: function (size) {
  6683. this._q = [];
  6684. this._size = size;
  6685. this._deleted = 0;
  6686. },
  6687. _trim: function () {
  6688. if (this._size) {
  6689. if (this._q.length > this._size) {
  6690. this._q.splice(0, this._q.length - this._size);
  6691. this._deleted += this._q.length - this._size;
  6692. }
  6693. }
  6694. },
  6695. push: function (item) {
  6696. this._q.push(item);
  6697. this._trim();
  6698. },
  6699. last: function () {
  6700. return this._q[this._q.length-1];
  6701. },
  6702. walkBack: function (callback, context) {
  6703. var result = true;
  6704. for (var i=this._q.length-1; i >= 0; i--) {
  6705. var item = this._q[i];
  6706. result = callback.call(context, item, i + this._deleted);
  6707. if (result === false) {
  6708. return result;
  6709. } else if (! result) {
  6710. result = true;
  6711. }
  6712. }
  6713. return result;
  6714. },
  6715. walkForward: function (index, callback, context) {
  6716. var result = true;
  6717. for (var i=index; i<this._q.length; i++) {
  6718. var item = this._q[i-this._deleted];
  6719. result = callback.call(context, item, i);
  6720. if (result === false) {
  6721. return result;
  6722. } else if (! result) {
  6723. result = true;
  6724. }
  6725. }
  6726. return result;
  6727. },
  6728. insert: function (index, item) {
  6729. this._q.splice(index-this._deleted, 0, item);
  6730. }
  6731. });
  6732. var Change = util.Class({
  6733. constructor: function (version, clientId, delta, known, outOfOrder) {
  6734. this.version = version;
  6735. this.clientId = clientId;
  6736. this.delta = delta;
  6737. this.known = known;
  6738. this.outOfOrder = !! outOfOrder;
  6739. assert(typeof version == "number" && typeof clientId == "string",
  6740. "Bad Change():", version, clientId);
  6741. },
  6742. toString: function () {
  6743. var s = "[Change " + this.version + "." + this.clientId + ": ";
  6744. s += this.delta + " ";
  6745. if (this.outOfOrder) {
  6746. s += "(out of order) ";
  6747. }
  6748. var cids = [];
  6749. for (var a in this.known) {
  6750. if (this.known.hasOwnProperty(a)) {
  6751. cids.push(a);
  6752. }
  6753. }
  6754. cids.sort();
  6755. s += "{";
  6756. if (! cids.length) {
  6757. s += "nothing known";
  6758. } else {
  6759. cids.forEach(function (a, index) {
  6760. if (index) {
  6761. s += ";";
  6762. }
  6763. s += a + ":" + this.known[a];
  6764. }, this);
  6765. }
  6766. return s + "}]";
  6767. },
  6768. clone: function () {
  6769. return Change(this.version, this.clientId, this.delta.clone(), util.extend(this.known), this.outOfOrder);
  6770. },
  6771. isBefore: function (otherChange) {
  6772. assert(otherChange !== this, "Tried to compare a change to itself", this);
  6773. return otherChange.version > this.version ||
  6774. (otherChange.version == this.version && otherChange.clientId > this.clientId);
  6775. },
  6776. knowsAboutAll: function (versions) {
  6777. for (var clientId in versions) {
  6778. if (! versions.hasOwnProperty(clientId)) {
  6779. continue;
  6780. }
  6781. if (! versions[clientId]) {
  6782. continue;
  6783. }
  6784. if ((! this.known[clientId]) || this.known[clientId] < versions[clientId]) {
  6785. return false;
  6786. }
  6787. }
  6788. return true;
  6789. },
  6790. knowsAboutChange: function (change) {
  6791. return change.clientId == this.clientId ||
  6792. (this.known[change.clientId] && this.known[change.clientId] >= change.version);
  6793. },
  6794. knowsAboutVersion: function (version, clientId) {
  6795. if ((! version) || clientId == this.clientId) {
  6796. return true;
  6797. }
  6798. return this.known[clientId] && this.known[clientId] >= version;
  6799. },
  6800. maybeMissingChanges: function (mostRecentVersion, clientId) {
  6801. if (! mostRecentVersion) {
  6802. // No actual changes for clientId exist
  6803. return false;
  6804. }
  6805. if (! this.known[clientId]) {
  6806. // We don't even know about clientId, so we are definitely missing something
  6807. return true;
  6808. }
  6809. if (this.known[clientId] >= mostRecentVersion) {
  6810. // We know about all versions through mostRecentVersion
  6811. return false;
  6812. }
  6813. if ((clientId > this.clientId && this.known[clientId] >= this.version-1) ||
  6814. (clientId < this.clientId && this.known[clientId] == this.version)) {
  6815. // We know about all versions from clientId that could exist before this
  6816. // version
  6817. return false;
  6818. }
  6819. // We may or may not be missing something
  6820. return true;
  6821. }
  6822. });
  6823. /* SimpleHistory synchronizes peers by relying on the server to serialize
  6824. * the order of all updates. Each client maintains a queue of patches
  6825. * which have not yet been 'committed' (by being echoed back from the
  6826. * server). The client is responsible for transposing its own queue
  6827. * if 'earlier' patches are heard from the server.
  6828. *
  6829. * Let's say that A's edit "1" and B's edit "2" occur and get put in
  6830. * their respective SimpleHistory queues. The server happens to
  6831. * handle 1 first, then 2, so those are the order that all peers
  6832. * (both A and B) see the messages.
  6833. *
  6834. * A sees 1, and has 1 on its queue, so everything's fine. It
  6835. * updates the 'committed' text to match its current text and drops
  6836. * the patch from its queue. It then sees 2, but the basis number
  6837. * for 2 no longer matches the committed basis, so it throws it
  6838. * away.
  6839. *
  6840. * B sees 1, and has 2 on its queue. It does the OT transpose thing,
  6841. * updating the committed text to include 1 and the 'current' text
  6842. * to include 1+2. It updates its queue with the newly transposed
  6843. * version of 2 (call it 2prime) and updates 2prime's basis
  6844. * number. It them resends 2prime to the server. It then receives 2
  6845. * (the original) but the basis number no longer matches the
  6846. * committed basis, so it throws it away.
  6847. *
  6848. * Now the server sees 2prime and rebroadcasts it to both A and B.
  6849. *
  6850. * A is seeing it for the first time, and the basis number matches,
  6851. * so it applies it to the current and committed text.
  6852. *
  6853. * B sees that 2prime matches what's on the start of its queue,
  6854. * shifts it off, and updates the committed text to match the
  6855. * current text.
  6856. *
  6857. * Note that no one tries to keep an entire history of changes,
  6858. * which is the main difference with ot.History. Everyone applies
  6859. * the same patches in the same order.
  6860. */
  6861. ot.SimpleHistory = util.Class({
  6862. constructor: function(clientId, initState, initBasis) {
  6863. this.clientId = clientId;
  6864. this.committed = initState;
  6865. this.current = initState;
  6866. this.basis = initBasis;
  6867. this.queue = [];
  6868. this.deltaId = 1;
  6869. this.selection = null;
  6870. },
  6871. // Use a fake change to represent the selection.
  6872. // (This is the only bit that hard codes ot.TextReplace as the delta
  6873. // representation; override this in a subclass (or don't set the
  6874. // selection) if you are using a different delta representation.
  6875. setSelection: function(selection) {
  6876. if (selection) {
  6877. this.selection = ot.TextReplace(selection[0],
  6878. selection[1] - selection[0], '@');
  6879. } else {
  6880. this.selection = null;
  6881. }
  6882. },
  6883. // Decode the fake change to reconstruct the updated selection.
  6884. getSelection: function() {
  6885. if (! this.selection) {
  6886. return null;
  6887. }
  6888. return [this.selection.start, this.selection.start + this.selection.del];
  6889. },
  6890. // Add this delta to this client's queue.
  6891. add: function(delta) {
  6892. var change = {
  6893. id: this.clientId + '.' + (this.deltaId++),
  6894. delta: delta
  6895. };
  6896. if (! this.queue.length) {
  6897. change.basis = this.basis;
  6898. }
  6899. this.queue.push(change);
  6900. this.current = delta.apply(this.current);
  6901. return !!change.basis;
  6902. },
  6903. // Apply a delta received from the server.
  6904. // Return true iff the current text changed as a result.
  6905. commit: function(change) {
  6906. // ignore it if the basis doesn't match (this patch doesn't apply)
  6907. // if so, this delta is out of order; we expect the original client
  6908. // to retransmit an updated delta.
  6909. if (change.basis !== this.basis) {
  6910. return false; // 'current' text did not change
  6911. }
  6912. // is this the first thing on the queue?
  6913. if (this.queue.length && this.queue[0].id === change.id) {
  6914. assert(change.basis === this.queue[0].basis);
  6915. // good, apply this to commit state & remove it from queue
  6916. this.committed = this.queue.shift().delta.apply(this.committed);
  6917. this.basis++;
  6918. if (this.queue.length) {
  6919. this.queue[0].basis = this.basis;
  6920. }
  6921. return false; // 'current' text did not change
  6922. }
  6923. // Transpose all bits on the queue to put this patch first.
  6924. var inserted = change.delta;
  6925. this.queue = this.queue.map(function(qchange) {
  6926. var tt = qchange.delta.transpose(inserted);
  6927. inserted = tt[1];
  6928. return {
  6929. id: qchange.id,
  6930. delta: tt[0]
  6931. };
  6932. });
  6933. if (this.selection) {
  6934. // update the selection!
  6935. this.selection = this.selection.transpose(inserted)[0];
  6936. }
  6937. this.committed = change.delta.apply(this.committed);
  6938. this.basis++;
  6939. if (this.queue.length) {
  6940. this.queue[0].basis = this.basis;
  6941. }
  6942. // Update current by replaying queued changes starting from 'committed'
  6943. this.current = this.committed;
  6944. this.queue.forEach(function(qchange) {
  6945. this.current = qchange.delta.apply(this.current);
  6946. }.bind(this));
  6947. return true; // The 'current' text changed.
  6948. },
  6949. // Return the next change to transmit to the server, or null if there
  6950. // isn't one.
  6951. getNextToSend: function() {
  6952. var qchange = this.queue[0];
  6953. if (! qchange) {
  6954. /* nothing to send */
  6955. return null;
  6956. }
  6957. if (qchange.sent) {
  6958. /* already sent */
  6959. return null;
  6960. }
  6961. assert(qchange.basis);
  6962. qchange.sent = true;
  6963. return qchange;
  6964. }
  6965. });
  6966. ot.History = util.Class({
  6967. constructor: function (clientId, initState) {
  6968. this._history = Queue();
  6969. this._history.push({
  6970. clientId: "init", state: initState
  6971. });
  6972. this.clientId = clientId;
  6973. this.known = {};
  6974. this.mostRecentLocalChange = null;
  6975. },
  6976. add: function (change) {
  6977. // Simplest cast, it is our change:
  6978. if (change.clientId == this.clientId) {
  6979. this._history.push(change);
  6980. this.mostRecentLocalChange = change.version;
  6981. return change.delta;
  6982. }
  6983. assert((! this.known[change.clientId]) || this.known[change.clientId] < change.version,
  6984. "Got a change", change, "that appears older (or same as) a known change", this.known[change.clientId]);
  6985. // Second simplest case, we get a change that we can add to our
  6986. // history without modification:
  6987. var last = this._history.last();
  6988. if ((last.clientId == "init" || last.isBefore(change)) &&
  6989. change.knowsAboutAll(this.known) &&
  6990. change.knowsAboutVersion(this.mostRecentLocalChange, this.clientId)) {
  6991. this._history.push(change);
  6992. this.known[change.clientId] = change.version;
  6993. return change.delta;
  6994. }
  6995. // We must do work!
  6996. this.logHistory("//");
  6997. // First we check if we need to modify this change because we
  6998. // know about changes that it should know about (changes that
  6999. // preceed it that are in our local history).
  7000. var clientsToCheck = StringSet();
  7001. for (var clientId in this.known) {
  7002. if (! this.known.hasOwnProperty(clientId)) {
  7003. continue;
  7004. }
  7005. if (change.maybeMissingChanges(this.known[clientId], clientId)) {
  7006. clientsToCheck.add(clientId);
  7007. }
  7008. }
  7009. if (change.maybeMissingChanges(this.mostRecentLocalChange, this.clientId)) {
  7010. clientsToCheck.add(this.clientId);
  7011. }
  7012. if (! clientsToCheck.isEmpty()) {
  7013. var indexToCheckFrom = null;
  7014. this._history.walkBack(function (c, index) {
  7015. indexToCheckFrom = index;
  7016. if (c.clientId == "init") {
  7017. return false;
  7018. }
  7019. if (clientsToCheck.contains(c.clientId) &&
  7020. ! change.maybeMissingChanges(c.version, c.clientId)) {
  7021. clientsToCheck.remove(c.clientId);
  7022. if (clientsToCheck.isEmpty()) {
  7023. return false;
  7024. }
  7025. }
  7026. return true;
  7027. }, this);
  7028. this._history.walkForward(indexToCheckFrom, function (c, index) {
  7029. if (c.clientId == "init") {
  7030. return true;
  7031. }
  7032. if (change.isBefore(c)) {
  7033. return false;
  7034. }
  7035. if (! change.knowsAboutChange(c)) {
  7036. var presentDelta = this.promoteDelta(c.delta, index, change);
  7037. if (! presentDelta.equals(c.delta)) {
  7038. //console.log("->rebase delta rewrite", presentDelta+"");
  7039. }
  7040. this.logChange("->rebase", change, function () {
  7041. var result = change.delta.transpose(presentDelta);
  7042. change.delta = result[0];
  7043. change.known[c.clientId] = c.version;
  7044. }, "with:", c);
  7045. }
  7046. return true;
  7047. }, this);
  7048. }
  7049. // Next we insert the change into its proper location
  7050. var indexToInsert = null;
  7051. this._history.walkBack(function (c, index) {
  7052. if (c.clientId == "init" || c.isBefore(change)) {
  7053. indexToInsert = index+1;
  7054. return false;
  7055. }
  7056. return true;
  7057. }, this);
  7058. assert(indexToInsert);
  7059. this._history.insert(indexToInsert, change);
  7060. // Now we fix up any forward changes
  7061. var fixupDelta = change.delta;
  7062. this._history.walkForward(indexToInsert+1, function (c, index) {
  7063. if (! c.knowsAboutChange(change)) {
  7064. var origChange = c.clone();
  7065. this.logChange("^^fix", c, function () {
  7066. var fixupResult = c.delta.transpose(fixupDelta);
  7067. console.log(" ^^real");
  7068. var result = c.delta.transpose(fixupDelta);
  7069. c.delta = result[0];
  7070. c.known[change.clientId] = change.version;
  7071. fixupDelta = fixupResult[1];
  7072. }, "clone:", change.delta+"");
  7073. console.log("(trans)", fixupDelta+"");
  7074. assert(c.knowsAboutChange(change));
  7075. }
  7076. }, this);
  7077. // Finally we return the transformed delta that represents
  7078. // changes that should be made to the state:
  7079. this.logHistory("!!");
  7080. return fixupDelta;
  7081. },
  7082. promoteDelta: function (delta, deltaIndex, untilChange) {
  7083. this._history.walkForward(deltaIndex+1, function (c, index) {
  7084. if (untilChange.isBefore(c)) {
  7085. return false;
  7086. }
  7087. // FIXME: not sure if this clientId check here is right. Maybe
  7088. // if untilChange.knowsAbout(c)?
  7089. if (untilChange.knowsAboutChange(c)) {
  7090. var result = c.delta.transpose(delta);
  7091. delta = result[1];
  7092. }
  7093. return true;
  7094. });
  7095. return delta;
  7096. },
  7097. logHistory: function (prefix) {
  7098. prefix = prefix || "";
  7099. var postfix = Array.prototype.slice.call(arguments, 1);
  7100. console.log.apply(console, [prefix + "history", this.clientId, ":"].concat(postfix));
  7101. console.log(prefix + " state:", JSON.stringify(this.getStateSafe()));
  7102. var hstate;
  7103. this._history.walkForward(0, function (c, index) {
  7104. if (! index) {
  7105. assert(c.clientId == "init");
  7106. console.log(prefix + " init:", JSON.stringify(c.state));
  7107. hstate = c.state;
  7108. } else {
  7109. try {
  7110. hstate = c.delta.apply(hstate);
  7111. } catch (e) {
  7112. hstate = "Error: " + e;
  7113. }
  7114. console.log(prefix + " ", index, c+"", JSON.stringify(hstate));
  7115. }
  7116. });
  7117. },
  7118. logChange: function (prefix, change, callback) {
  7119. prefix = prefix || "before";
  7120. var postfix = Array.prototype.slice.call(arguments, 3);
  7121. console.log.apply(
  7122. console,
  7123. [prefix, this.clientId, ":", change+""].concat(postfix).concat([JSON.stringify(this.getStateSafe(true))]));
  7124. try {
  7125. callback();
  7126. } finally {
  7127. console.log(prefix + " after:", change+"", JSON.stringify(this.getStateSafe()));
  7128. }
  7129. },
  7130. addDelta: function (delta) {
  7131. var version = this._createVersion();
  7132. var change = Change(version, this.clientId, delta, util.extend(this.knownVersions));
  7133. this.add(change);
  7134. return change;
  7135. },
  7136. _createVersion: function () {
  7137. var max = 1;
  7138. for (var id in this.knownVersions) {
  7139. max = Math.max(max, this.knownVersions[id]);
  7140. }
  7141. max = Math.max(max, this.mostRecentLocalChange);
  7142. return max+1;
  7143. },
  7144. fault: function (change) {
  7145. throw new Error('Fault');
  7146. },
  7147. getState: function () {
  7148. var state;
  7149. this._history.walkForward(0, function (c) {
  7150. if (c.clientId == "init") {
  7151. // Initialization, has the state
  7152. state = c.state;
  7153. } else {
  7154. state = c.delta.apply(state);
  7155. }
  7156. }, this);
  7157. return state;
  7158. },
  7159. getStateSafe: function () {
  7160. try {
  7161. return this.getState();
  7162. } catch (e) {
  7163. return 'Error: ' + e;
  7164. }
  7165. }
  7166. });
  7167. ot.TextReplace = util.Class({
  7168. constructor: function (start, del, text) {
  7169. assert(typeof start == "number" && typeof del == "number" && typeof text == "string", start, del, text);
  7170. assert(start >=0 && del >= 0, start, del);
  7171. this.start = start;
  7172. this.del = del;
  7173. this.text = text;
  7174. },
  7175. toString: function () {
  7176. if (this.empty()) {
  7177. return '[no-op]';
  7178. }
  7179. if (! this.del) {
  7180. return '[insert ' + JSON.stringify(this.text) + ' @' + this.start + ']';
  7181. } else if (! this.text) {
  7182. return '[delete ' + this.del + ' chars @' + this.start + ']';
  7183. } else {
  7184. return '[replace ' + this.del + ' chars with ' + JSON.stringify(this.text) + ' @' + this.start + ']';
  7185. }
  7186. },
  7187. equals: function (other) {
  7188. return other.constructor === this.constructor &&
  7189. other.del === this.del &&
  7190. other.start === this.start &&
  7191. other.text === this.text;
  7192. },
  7193. clone: function (start, del, text) {
  7194. if (start === undefined) {
  7195. start = this.start;
  7196. }
  7197. if (del === undefined) {
  7198. del = this.del;
  7199. }
  7200. if (text === undefined) {
  7201. text = this.text;
  7202. }
  7203. return ot.TextReplace(start, del, text);
  7204. },
  7205. empty: function () {
  7206. return (! this.del) && (! this.text);
  7207. },
  7208. apply: function (text) {
  7209. if (this.empty()) {
  7210. return text;
  7211. }
  7212. if (this.start > text.length) {
  7213. console.trace();
  7214. throw new util.AssertionError("Start after end of text (" + JSON.stringify(text) + "/" + text.length + "): " + this);
  7215. }
  7216. if (this.start + this.del > text.length) {
  7217. throw new util.AssertionError("Start+del after end of text (" + JSON.stringify(text) + "/" + text.length + "): " + this);
  7218. }
  7219. return text.substr(0, this.start) + this.text + text.substr(this.start+this.del);
  7220. },
  7221. transpose: function (delta) {
  7222. /* Transform this delta as though the other delta had come before it.
  7223. Returns a [new_version_of_this, transformed_delta], where transformed_delta
  7224. satisfies:
  7225. result1 = new_version_of_this.apply(delta.apply(text));
  7226. result2 = transformed_delta.apply(this.apply(text));
  7227. assert(result1 == result2);
  7228. Does not modify this object.
  7229. */
  7230. var overlap;
  7231. assert(delta instanceof ot.TextReplace, "Transposing with non-TextReplace:", delta);
  7232. if (this.empty()) {
  7233. //console.log(" =this is empty");
  7234. return [this.clone(), delta.clone()];
  7235. }
  7236. if (delta.empty()) {
  7237. //console.log(" =other is empty");
  7238. return [this.clone(), delta.clone()];
  7239. }
  7240. if (delta.before(this)) {
  7241. //console.log(" =this after other");
  7242. return [this.clone(this.start + delta.text.length - delta.del),
  7243. delta.clone()];
  7244. } else if (this.before(delta)) {
  7245. //console.log(" =this before other");
  7246. return [this.clone(), delta.clone(delta.start + this.text.length - this.del)];
  7247. } else if (delta.sameRange(this)) {
  7248. //console.log(" =same range");
  7249. return [this.clone(this.start+delta.text.length, 0),
  7250. delta.clone(undefined, 0)];
  7251. } else if (delta.contains(this)) {
  7252. //console.log(" =other contains this");
  7253. return [this.clone(delta.start+delta.text.length, 0, this.text),
  7254. delta.clone(undefined, delta.del - this.del + this.text.length, delta.text + this.text)];
  7255. } else if (this.contains(delta)) {
  7256. //console.log(" =this contains other");
  7257. return [this.clone(undefined, this.del - delta.del + delta.text.length, delta.text + this.text),
  7258. delta.clone(this.start, 0, delta.text)];
  7259. } else if (this.overlapsStart(delta)) {
  7260. //console.log(" =this overlaps start of other");
  7261. overlap = this.start + this.del - delta.start;
  7262. return [this.clone(undefined, this.del - overlap),
  7263. delta.clone(this.start + this.text.length, delta.del - overlap)];
  7264. } else {
  7265. //console.log(" =this overlaps end of other");
  7266. assert(delta.overlapsStart(this), delta+"", "does not overlap start of", this+"", delta.before(this));
  7267. overlap = delta.start + delta.del - this.start;
  7268. return [this.clone(delta.start + delta.text.length, this.del - overlap),
  7269. delta.clone(undefined, delta.del - overlap)];
  7270. }
  7271. throw 'Should not happen';
  7272. },
  7273. before: function (other) {
  7274. return this.start + this.del <= other.start;
  7275. },
  7276. contains: function (other) {
  7277. return other.start >= this.start && other.start + other.del < this.start + this.del;
  7278. },
  7279. sameRange: function (other) {
  7280. return other.start == this.start && other.del == this.del;
  7281. },
  7282. overlapsStart: function (other) {
  7283. return this.start < other.start && this.start + this.del > other.start;
  7284. },
  7285. classMethods: {
  7286. /* Make a new ot.TextReplace that converts oldValue to newValue. */
  7287. fromChange: function(oldValue, newValue) {
  7288. assert(typeof oldValue == "string");
  7289. assert(typeof newValue == "string");
  7290. var commonStart = 0;
  7291. while (commonStart < newValue.length &&
  7292. newValue.charAt(commonStart) == oldValue.charAt(commonStart)) {
  7293. commonStart++;
  7294. }
  7295. var commonEnd = 0;
  7296. while (commonEnd < (newValue.length - commonStart) &&
  7297. commonEnd < (oldValue.length - commonStart) &&
  7298. newValue.charAt(newValue.length - commonEnd - 1) ==
  7299. oldValue.charAt(oldValue.length - commonEnd - 1)) {
  7300. commonEnd++;
  7301. }
  7302. var removed = oldValue.substr(commonStart, oldValue.length - commonStart - commonEnd);
  7303. var inserted = newValue.substr(commonStart, newValue.length - commonStart - commonEnd);
  7304. if (! (removed.length || inserted)) {
  7305. return null;
  7306. }
  7307. return this(commonStart, removed.length, inserted);
  7308. },
  7309. random: function (source, generator) {
  7310. var text, start, len;
  7311. var ops = ["ins", "del", "repl"];
  7312. if (! source.length) {
  7313. ops = ["ins"];
  7314. }
  7315. switch (generator.pick(ops)) {
  7316. case "ins":
  7317. if (! generator.number(2)) {
  7318. text = generator.string(1);
  7319. } else {
  7320. text = generator.string(generator.number(3)+1);
  7321. }
  7322. if (! generator.number(4)) {
  7323. start = 0;
  7324. } else if (! generator.number(3)) {
  7325. start = source.length-1;
  7326. } else {
  7327. start = generator.number(source.length);
  7328. }
  7329. return this(start, 0, text);
  7330. case "del":
  7331. if (! generator.number(20)) {
  7332. return this(0, source.length, "");
  7333. }
  7334. start = generator.number(source.length-1);
  7335. if (! generator.number(2)) {
  7336. len = 1;
  7337. } else {
  7338. len = generator.number(5)+1;
  7339. }
  7340. len = Math.min(len, source.length - start);
  7341. return this(start, len, "");
  7342. case "repl":
  7343. start = generator.number(source.length-1);
  7344. len = generator.number(5);
  7345. len = Math.min(len, source.length - start);
  7346. text = generator.string(generator.number(2)+1);
  7347. return this(start, len, text);
  7348. }
  7349. throw 'Unreachable';
  7350. }
  7351. }
  7352. });
  7353. return ot;
  7354. });
  7355. /* This Source Code Form is subject to the terms of the Mozilla Public
  7356. * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  7357. * You can obtain one at http://mozilla.org/MPL/2.0/. */
  7358. define('forms',["jquery", "util", "session", "elementFinder", "eventMaker", "templating", "ot"], function ($, util, session, elementFinder, eventMaker, templating, ot) {
  7359. var forms = util.Module("forms");
  7360. var assert = util.assert;
  7361. // This is how much larger the focus element is than the element it surrounds
  7362. // (this is padding on each side)
  7363. var FOCUS_BUFFER = 5;
  7364. var inRemoteUpdate = false;
  7365. function suppressSync(element) {
  7366. var ignoreForms = TogetherJS.config.get("ignoreForms");
  7367. if (ignoreForms === true) {
  7368. return true;
  7369. }
  7370. else {
  7371. return $(element).is(ignoreForms.join(","));
  7372. }
  7373. }
  7374. function maybeChange(event) {
  7375. // Called when we get an event that may or may not indicate a real change
  7376. // (like keyup in a textarea)
  7377. var tag = event.target.tagName;
  7378. if (tag == "TEXTAREA" || tag == "INPUT") {
  7379. change(event);
  7380. }
  7381. }
  7382. function change(event) {
  7383. sendData({
  7384. element: event.target,
  7385. value: getValue(event.target)
  7386. });
  7387. }
  7388. function sendData(attrs) {
  7389. var el = $(attrs.element);
  7390. assert(el);
  7391. var tracker = attrs.tracker;
  7392. var value = attrs.value;
  7393. if (inRemoteUpdate) {
  7394. return;
  7395. }
  7396. if (elementFinder.ignoreElement(el) ||
  7397. (elementTracked(el) && !tracker) ||
  7398. suppressSync(el)) {
  7399. return;
  7400. }
  7401. var location = elementFinder.elementLocation(el);
  7402. var msg = {
  7403. type: "form-update",
  7404. element: location
  7405. };
  7406. if (isText(el) || tracker) {
  7407. var history = el.data("togetherjsHistory");
  7408. if (history) {
  7409. if (history.current == value) {
  7410. return;
  7411. }
  7412. var delta = ot.TextReplace.fromChange(history.current, value);
  7413. assert(delta);
  7414. history.add(delta);
  7415. maybeSendUpdate(msg.element, history, tracker);
  7416. return;
  7417. } else {
  7418. msg.value = value;
  7419. msg.basis = 1;
  7420. el.data("togetherjsHistory", ot.SimpleHistory(session.clientId, value, 1));
  7421. }
  7422. } else {
  7423. msg.value = value;
  7424. }
  7425. session.send(msg);
  7426. }
  7427. function isCheckable(el) {
  7428. el = $(el);
  7429. var type = (el.prop("type") || "text").toLowerCase();
  7430. if (el.prop("tagName") == "INPUT" && ["radio", "checkbox"].indexOf(type) != -1) {
  7431. return true;
  7432. }
  7433. return false;
  7434. }
  7435. var editTrackers = {};
  7436. var liveTrackers = [];
  7437. TogetherJS.addTracker = function (TrackerClass, skipSetInit) {
  7438. assert(typeof TrackerClass === "function", "You must pass in a class");
  7439. assert(typeof TrackerClass.prototype.trackerName === "string",
  7440. "Needs a .prototype.trackerName string");
  7441. // Test for required instance methods.
  7442. "destroy update init makeInit tracked".split(/ /).forEach(function(m) {
  7443. assert(typeof TrackerClass.prototype[m] === "function",
  7444. "Missing required tracker method: "+m);
  7445. });
  7446. // Test for required class methods.
  7447. "scan tracked".split(/ /).forEach(function(m) {
  7448. assert(typeof TrackerClass[m] === "function",
  7449. "Missing required tracker class method: "+m);
  7450. });
  7451. editTrackers[TrackerClass.prototype.trackerName] = TrackerClass;
  7452. if (!skipSetInit) {
  7453. setInit();
  7454. }
  7455. };
  7456. var AceEditor = util.Class({
  7457. trackerName: "AceEditor",
  7458. constructor: function (el) {
  7459. this.element = $(el)[0];
  7460. assert($(this.element).hasClass("ace_editor"));
  7461. this._change = this._change.bind(this);
  7462. this._editor().document.on("change", this._change);
  7463. },
  7464. tracked: function (el) {
  7465. return this.element === $(el)[0];
  7466. },
  7467. destroy: function (el) {
  7468. this._editor().document.removeListener("change", this._change);
  7469. },
  7470. update: function (msg) {
  7471. this._editor().document.setValue(msg.value);
  7472. },
  7473. init: function (update, msg) {
  7474. this.update(update);
  7475. },
  7476. makeInit: function () {
  7477. return {
  7478. element: this.element,
  7479. tracker: this.trackerName,
  7480. value: this._editor().document.getValue()
  7481. };
  7482. },
  7483. _editor: function () {
  7484. return this.element.env;
  7485. },
  7486. _change: function (e) {
  7487. // FIXME: I should have an internal .send() function that automatically
  7488. // asserts !inRemoteUpdate, among other things
  7489. if (inRemoteUpdate) {
  7490. return;
  7491. }
  7492. sendData({
  7493. tracker: this.trackerName,
  7494. element: this.element,
  7495. value: this.getContent()
  7496. });
  7497. },
  7498. getContent: function() {
  7499. return this._editor().document.getValue();
  7500. }
  7501. });
  7502. AceEditor.scan = function () {
  7503. return $(".ace_editor");
  7504. };
  7505. AceEditor.tracked = function (el) {
  7506. return !! $(el).closest(".ace_editor").length;
  7507. };
  7508. TogetherJS.addTracker(AceEditor, true /* skip setInit */);
  7509. var CodeMirrorEditor = util.Class({
  7510. trackerName: "CodeMirrorEditor",
  7511. constructor: function (el) {
  7512. this.element = $(el)[0];
  7513. assert(this.element.CodeMirror);
  7514. this._change = this._change.bind(this);
  7515. this._editor().on("change", this._change);
  7516. },
  7517. tracked: function (el) {
  7518. return this.element === $(el)[0];
  7519. },
  7520. destroy: function (el) {
  7521. this._editor().off("change", this._change);
  7522. },
  7523. update: function (msg) {
  7524. this._editor().setValue(msg.value);
  7525. },
  7526. init: function (msg) {
  7527. if (msg.value) {
  7528. this.update(msg);
  7529. }
  7530. },
  7531. makeInit: function () {
  7532. return {
  7533. element: this.element,
  7534. tracker: this.trackerName,
  7535. value: this._editor().getValue()
  7536. };
  7537. },
  7538. _change: function (editor, change) {
  7539. if (inRemoteUpdate) {
  7540. return;
  7541. }
  7542. sendData({
  7543. tracker: this.trackerName,
  7544. element: this.element,
  7545. value: this.getContent()
  7546. });
  7547. },
  7548. _editor: function () {
  7549. return this.element.CodeMirror;
  7550. },
  7551. getContent: function() {
  7552. return this._editor().getValue();
  7553. }
  7554. });
  7555. CodeMirrorEditor.scan = function () {
  7556. var result = [];
  7557. var els = document.body.getElementsByTagName("*");
  7558. var _len = els.length;
  7559. for (var i=0; i<_len; i++) {
  7560. var el = els[i];
  7561. if (el.CodeMirror) {
  7562. result.push(el);
  7563. }
  7564. }
  7565. return $(result);
  7566. };
  7567. CodeMirrorEditor.tracked = function (el) {
  7568. el = $(el)[0];
  7569. while (el) {
  7570. if (el.CodeMirror) {
  7571. return true;
  7572. }
  7573. el = el.parentNode;
  7574. }
  7575. return false;
  7576. };
  7577. TogetherJS.addTracker(CodeMirrorEditor, true /* skip setInit */);
  7578. var CKEditor = util.Class({
  7579. trackerName: "CKEditor",
  7580. constructor: function (el) {
  7581. this.element = $(el)[0];
  7582. assert(CKEDITOR);
  7583. assert(CKEDITOR.dom.element.get(this.element));
  7584. this._change = this._change.bind(this);
  7585. // FIXME: change event is available since CKEditor 4.2
  7586. this._editor().on("change", this._change);
  7587. },
  7588. tracked: function (el) {
  7589. return this.element === $(el)[0];
  7590. },
  7591. destroy: function (el) {
  7592. this._editor().removeListener("change", this._change);
  7593. },
  7594. update: function (msg) {
  7595. //FIXME: use setHtml instead of setData to avoid frame reloading overhead
  7596. this._editor().editable().setHtml(msg.value);
  7597. },
  7598. init: function (update, msg) {
  7599. this.update(update);
  7600. },
  7601. makeInit: function () {
  7602. return {
  7603. element: this.element,
  7604. tracker: this.trackerName,
  7605. value: this.getContent()
  7606. };
  7607. },
  7608. _change: function (e) {
  7609. if (inRemoteUpdate) {
  7610. return;
  7611. }
  7612. sendData({
  7613. tracker: this.trackerName,
  7614. element: this.element,
  7615. value: this.getContent()
  7616. });
  7617. },
  7618. _editor: function () {
  7619. return CKEDITOR.dom.element.get(this.element).getEditor();
  7620. },
  7621. getContent: function () {
  7622. return this._editor().getData();
  7623. }
  7624. });
  7625. CKEditor.scan = function () {
  7626. var result = [];
  7627. if (typeof CKEDITOR == "undefined") {
  7628. return;
  7629. }
  7630. var editorInstance;
  7631. for (var instanceIdentifier in CKEDITOR.instances) {
  7632. editorInstance = document.getElementById(instanceIdentifier) || document.getElementsByName(instanceIdentifier)[0];
  7633. if (editorInstance) {
  7634. result.push(editorInstance);
  7635. }
  7636. }
  7637. return $(result);
  7638. };
  7639. CKEditor.tracked = function (el) {
  7640. if (typeof CKEDITOR == "undefined") {
  7641. return false;
  7642. }
  7643. el = $(el)[0];
  7644. return !! (CKEDITOR.dom.element.get(el) && CKEDITOR.dom.element.get(el).getEditor());
  7645. };
  7646. TogetherJS.addTracker(CKEditor, true /* skip setInit */);
  7647. //////////////////// BEGINNING OF TINYMCE ////////////////////////
  7648. var tinymceEditor = util.Class({
  7649. trackerName: "tinymceEditor",
  7650. constructor: function (el) {
  7651. this.element = $(el)[0];
  7652. assert($(this.element).attr('id').indexOf('mce_') != -1);
  7653. this._change = this._change.bind(this);
  7654. this._editor().on("input keyup cut paste change", this._change);
  7655. },
  7656. tracked: function (el) {
  7657. return this.element === $(el)[0];
  7658. },
  7659. destroy: function (el) {
  7660. this._editor().destory();
  7661. },
  7662. update: function (msg) {
  7663. this._editor().setContent(msg.value, {format: 'raw'});
  7664. },
  7665. init: function (update, msg) {
  7666. this.update(update);
  7667. },
  7668. makeInit: function () {
  7669. return {
  7670. element: this.element,
  7671. tracker: this.trackerName,
  7672. value: this.getContent()
  7673. };
  7674. },
  7675. _change: function (e) {
  7676. if (inRemoteUpdate) {
  7677. return;
  7678. }
  7679. sendData({
  7680. tracker: this.trackerName,
  7681. element: this.element,
  7682. value: this.getContent()
  7683. });
  7684. },
  7685. _editor: function () {
  7686. if (typeof tinymce == "undefined") {
  7687. return;
  7688. }
  7689. return $(this.element).data("tinyEditor");
  7690. },
  7691. getContent: function () {
  7692. return this._editor().getContent();
  7693. }
  7694. });
  7695. tinymceEditor.scan = function () {
  7696. //scan all the elements that contain tinyMCE editors
  7697. if (typeof tinymce == "undefined") {
  7698. return;
  7699. }
  7700. var result = [];
  7701. $(window.tinymce.editors).each(function (i, ed) {
  7702. result.push($('#'+ed.id));
  7703. //its impossible to retrieve a single editor from a container, so lets store it
  7704. $('#'+ed.id).data("tinyEditor", ed);
  7705. });
  7706. return $(result);
  7707. };
  7708. tinymceEditor.tracked = function (el) {
  7709. if (typeof tinymce == "undefined") {
  7710. return false;
  7711. }
  7712. el = $(el)[0];
  7713. return !!$(el).data("tinyEditor");
  7714. /*var flag = false;
  7715. $(window.tinymce.editors).each(function (i, ed) {
  7716. if (el.id == ed.id) {
  7717. flag = true;
  7718. }
  7719. });
  7720. return flag;*/
  7721. };
  7722. TogetherJS.addTracker(tinymceEditor, true);
  7723. ///////////////// END OF TINYMCE ///////////////////////////////////
  7724. function buildTrackers() {
  7725. assert(! liveTrackers.length);
  7726. util.forEachAttr(editTrackers, function (TrackerClass) {
  7727. var els = TrackerClass.scan();
  7728. if (els) {
  7729. $.each(els, function () {
  7730. var tracker = new TrackerClass(this);
  7731. $(this).data("togetherjsHistory", ot.SimpleHistory(session.clientId, tracker.getContent(), 1));
  7732. liveTrackers.push(tracker);
  7733. });
  7734. }
  7735. });
  7736. }
  7737. function destroyTrackers() {
  7738. liveTrackers.forEach(function (tracker) {
  7739. tracker.destroy();
  7740. });
  7741. liveTrackers = [];
  7742. }
  7743. function elementTracked(el) {
  7744. var result = false;
  7745. util.forEachAttr(editTrackers, function (TrackerClass) {
  7746. if (TrackerClass.tracked(el)) {
  7747. result = true;
  7748. }
  7749. });
  7750. return result;
  7751. }
  7752. function getTracker(el, name) {
  7753. el = $(el)[0];
  7754. for (var i=0; i<liveTrackers.length; i++) {
  7755. var tracker = liveTrackers[i];
  7756. if (tracker.tracked(el)) {
  7757. //FIXME: assert statement below throws an exception when data is submitted to the hub too fast
  7758. //in other words, name == tracker.trackerName instead of name == tracker when someone types too fast in the tracked editor
  7759. //commenting out this assert statement solves the problem
  7760. assert((! name) || name == tracker.trackerName, "Expected to map to a tracker type", name, "but got", tracker.trackerName);
  7761. return tracker;
  7762. }
  7763. }
  7764. return null;
  7765. }
  7766. var TEXT_TYPES = (
  7767. "color date datetime datetime-local email " +
  7768. "tel text time week").split(/ /g);
  7769. function isText(el) {
  7770. el = $(el);
  7771. var tag = el.prop("tagName");
  7772. var type = (el.prop("type") || "text").toLowerCase();
  7773. if (tag == "TEXTAREA") {
  7774. return true;
  7775. }
  7776. if (tag == "INPUT" && TEXT_TYPES.indexOf(type) != -1) {
  7777. return true;
  7778. }
  7779. return false;
  7780. }
  7781. function getValue(el) {
  7782. el = $(el);
  7783. if (isCheckable(el)) {
  7784. return el.prop("checked");
  7785. } else {
  7786. return el.val();
  7787. }
  7788. }
  7789. function getElementType(el) {
  7790. el = $(el)[0];
  7791. if (el.tagName == "TEXTAREA") {
  7792. return "textarea";
  7793. }
  7794. if (el.tagName == "SELECT") {
  7795. return "select";
  7796. }
  7797. if (el.tagName == "INPUT") {
  7798. return (el.getAttribute("type") || "text").toLowerCase();
  7799. }
  7800. return "?";
  7801. }
  7802. function setValue(el, value) {
  7803. el = $(el);
  7804. var changed = false;
  7805. if (isCheckable(el)) {
  7806. var checked = !! el.prop("checked");
  7807. value = !! value;
  7808. if (checked != value) {
  7809. changed = true;
  7810. el.prop("checked", value);
  7811. }
  7812. } else {
  7813. if (el.val() != value) {
  7814. changed = true;
  7815. el.val(value);
  7816. }
  7817. }
  7818. if (changed) {
  7819. eventMaker.fireChange(el);
  7820. }
  7821. }
  7822. /* Send the top of this history queue, if it hasn't been already sent. */
  7823. function maybeSendUpdate(element, history, tracker) {
  7824. var change = history.getNextToSend();
  7825. if (! change) {
  7826. /* nothing to send */
  7827. return;
  7828. }
  7829. var msg = {
  7830. type: "form-update",
  7831. element: element,
  7832. "server-echo": true,
  7833. replace: {
  7834. id: change.id,
  7835. basis: change.basis,
  7836. delta: {
  7837. start: change.delta.start,
  7838. del: change.delta.del,
  7839. text: change.delta.text
  7840. }
  7841. }
  7842. };
  7843. if (tracker) {
  7844. msg.tracker = tracker;
  7845. }
  7846. session.send(msg);
  7847. }
  7848. session.hub.on("form-update", function (msg) {
  7849. if (! msg.sameUrl) {
  7850. return;
  7851. }
  7852. var el = $(elementFinder.findElement(msg.element));
  7853. var tracker;
  7854. if (msg.tracker) {
  7855. tracker = getTracker(el, msg.tracker);
  7856. assert(tracker);
  7857. }
  7858. var focusedEl = el[0].ownerDocument.activeElement;
  7859. var focusedElSelection;
  7860. if (isText(focusedEl)) {
  7861. focusedElSelection = [focusedEl.selectionStart, focusedEl.selectionEnd];
  7862. }
  7863. var selection;
  7864. if (isText(el)) {
  7865. selection = [el[0].selectionStart, el[0].selectionEnd];
  7866. }
  7867. var value;
  7868. if (msg.replace) {
  7869. var history = el.data("togetherjsHistory");
  7870. if (!history) {
  7871. console.warn("form update received for uninitialized form element");
  7872. return;
  7873. }
  7874. history.setSelection(selection);
  7875. // make a real TextReplace object.
  7876. msg.replace.delta = ot.TextReplace(msg.replace.delta.start,
  7877. msg.replace.delta.del,
  7878. msg.replace.delta.text);
  7879. // apply this change to the history
  7880. var changed = history.commit(msg.replace);
  7881. var trackerName = null;
  7882. if (typeof tracker != "undefined") {
  7883. trackerName = tracker.trackerName;
  7884. }
  7885. maybeSendUpdate(msg.element, history, trackerName);
  7886. if (! changed) {
  7887. return;
  7888. }
  7889. value = history.current;
  7890. selection = history.getSelection();
  7891. } else {
  7892. value = msg.value;
  7893. }
  7894. inRemoteUpdate = true;
  7895. try {
  7896. if(tracker) {
  7897. tracker.update({value:value});
  7898. } else {
  7899. setValue(el, value);
  7900. }
  7901. if (isText(el)) {
  7902. el[0].selectionStart = selection[0];
  7903. el[0].selectionEnd = selection[1];
  7904. }
  7905. // return focus to original input:
  7906. if (focusedEl != el[0]) {
  7907. focusedEl.focus();
  7908. if (isText(focusedEl)) {
  7909. focusedEl.selectionStart = focusedElSelection[0];
  7910. focusedEl.selectionEnd = focusedElSelection[1];
  7911. }
  7912. }
  7913. } finally {
  7914. inRemoteUpdate = false;
  7915. }
  7916. });
  7917. var initSent = false;
  7918. function sendInit() {
  7919. initSent = true;
  7920. var msg = {
  7921. type: "form-init",
  7922. pageAge: Date.now() - TogetherJS.pageLoaded,
  7923. updates: []
  7924. };
  7925. var els = $("textarea, input, select");
  7926. els.each(function () {
  7927. if (elementFinder.ignoreElement(this) || elementTracked(this) ||
  7928. suppressSync(this)) {
  7929. return;
  7930. }
  7931. var el = $(this);
  7932. var value = getValue(el);
  7933. var upd = {
  7934. element: elementFinder.elementLocation(this),
  7935. //elementType: getElementType(el), // added in 5cbb88c9a but unused
  7936. value: value
  7937. };
  7938. if (isText(el)) {
  7939. var history = el.data("togetherjsHistory");
  7940. if (history) {
  7941. upd.value = history.committed;
  7942. upd.basis = history.basis;
  7943. }
  7944. }
  7945. msg.updates.push(upd);
  7946. });
  7947. liveTrackers.forEach(function (tracker) {
  7948. var init = tracker.makeInit();
  7949. assert(tracker.tracked(init.element));
  7950. var history = $(init.element).data("togetherjsHistory");
  7951. if (history) {
  7952. init.value = history.committed;
  7953. init.basis = history.basis;
  7954. }
  7955. init.element = elementFinder.elementLocation($(init.element));
  7956. msg.updates.push(init);
  7957. });
  7958. if (msg.updates.length) {
  7959. session.send(msg);
  7960. }
  7961. }
  7962. function setInit() {
  7963. var els = $("textarea, input, select");
  7964. els.each(function () {
  7965. if (elementTracked(this)) {
  7966. return;
  7967. }
  7968. if (elementFinder.ignoreElement(this)) {
  7969. return;
  7970. }
  7971. var el = $(this);
  7972. var value = getValue(el);
  7973. el.data("togetherjsHistory", ot.SimpleHistory(session.clientId, value, 1));
  7974. });
  7975. destroyTrackers();
  7976. buildTrackers();
  7977. }
  7978. session.on("reinitialize", setInit);
  7979. session.on("ui-ready", setInit);
  7980. session.on("close", destroyTrackers);
  7981. session.hub.on("form-init", function (msg) {
  7982. if (! msg.sameUrl) {
  7983. return;
  7984. }
  7985. if (initSent) {
  7986. // In a 3+-peer situation more than one client may init; in this case
  7987. // we're probably the other peer, and not the peer that needs the init
  7988. // A quick check to see if we should init...
  7989. var myAge = Date.now() - TogetherJS.pageLoaded;
  7990. if (msg.pageAge < myAge) {
  7991. // We've been around longer than the other person...
  7992. return;
  7993. }
  7994. }
  7995. // FIXME: need to figure out when to ignore inits
  7996. msg.updates.forEach(function (update) {
  7997. var el;
  7998. try {
  7999. el = elementFinder.findElement(update.element);
  8000. } catch (e) {
  8001. /* skip missing element */
  8002. console.warn(e);
  8003. return;
  8004. }
  8005. inRemoteUpdate = true;
  8006. try {
  8007. if (update.tracker) {
  8008. var tracker = getTracker(el, update.tracker);
  8009. assert(tracker);
  8010. tracker.init(update, msg);
  8011. } else {
  8012. setValue(el, update.value);
  8013. }
  8014. if (update.basis) {
  8015. var history = $(el).data("togetherjsHistory");
  8016. // don't overwrite history if we're already up to date
  8017. // (we might have outstanding queued changes we don't want to lose)
  8018. if (!(history && history.basis === update.basis &&
  8019. // if history.basis is 1, the form could have lingering
  8020. // edits from before togetherjs was launched. that's too bad,
  8021. // we need to erase them to resynchronize with the peer
  8022. // we just asked to join.
  8023. history.basis !== 1)) {
  8024. $(el).data("togetherjsHistory", ot.SimpleHistory(session.clientId, update.value, update.basis));
  8025. }
  8026. }
  8027. } finally {
  8028. inRemoteUpdate = false;
  8029. }
  8030. });
  8031. });
  8032. var lastFocus = null;
  8033. function focus(event) {
  8034. var target = event.target;
  8035. if (elementFinder.ignoreElement(target) || elementTracked(target)) {
  8036. blur(event);
  8037. return;
  8038. }
  8039. if (target != lastFocus) {
  8040. lastFocus = target;
  8041. session.send({type: "form-focus", element: elementFinder.elementLocation(target)});
  8042. }
  8043. }
  8044. function blur(event) {
  8045. var target = event.target;
  8046. if (lastFocus) {
  8047. lastFocus = null;
  8048. session.send({type: "form-focus", element: null});
  8049. }
  8050. }
  8051. var focusElements = {};
  8052. session.hub.on("form-focus", function (msg) {
  8053. if (! msg.sameUrl) {
  8054. return;
  8055. }
  8056. var current = focusElements[msg.peer.id];
  8057. if (current) {
  8058. current.remove();
  8059. current = null;
  8060. }
  8061. if (! msg.element) {
  8062. // A blur
  8063. return;
  8064. }
  8065. var element = elementFinder.findElement(msg.element);
  8066. var el = createFocusElement(msg.peer, element);
  8067. if (el) {
  8068. focusElements[msg.peer.id] = el;
  8069. }
  8070. });
  8071. function createFocusElement(peer, around) {
  8072. around = $(around);
  8073. var aroundOffset = around.offset();
  8074. if (! aroundOffset) {
  8075. console.warn("Could not get offset of element:", around[0]);
  8076. return null;
  8077. }
  8078. var el = templating.sub("focus", {peer: peer});
  8079. el = el.find(".togetherjs-focus");
  8080. el.css({
  8081. top: aroundOffset.top-FOCUS_BUFFER + "px",
  8082. left: aroundOffset.left-FOCUS_BUFFER + "px",
  8083. width: around.outerWidth() + (FOCUS_BUFFER*2) + "px",
  8084. height: around.outerHeight() + (FOCUS_BUFFER*2) + "px"
  8085. });
  8086. $(document.body).append(el);
  8087. return el;
  8088. }
  8089. session.on("ui-ready", function () {
  8090. $(document).on("change", change);
  8091. // note that textInput, keydown, and keypress aren't appropriate events
  8092. // to watch, since they fire *before* the element's value changes.
  8093. $(document).on("input keyup cut paste", maybeChange);
  8094. $(document).on("focusin", focus);
  8095. $(document).on("focusout", blur);
  8096. });
  8097. session.on("close", function () {
  8098. $(document).off("change", change);
  8099. $(document).off("input keyup cut paste", maybeChange);
  8100. $(document).off("focusin", focus);
  8101. $(document).off("focusout", blur);
  8102. });
  8103. session.hub.on("hello", function (msg) {
  8104. if (msg.sameUrl) {
  8105. setTimeout(function () {
  8106. sendInit();
  8107. if (lastFocus) {
  8108. session.send({type: "form-focus", element: elementFinder.elementLocation(lastFocus)});
  8109. }
  8110. });
  8111. }
  8112. });
  8113. return forms;
  8114. });
  8115. /* This Source Code Form is subject to the terms of the Mozilla Public
  8116. * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  8117. * You can obtain one at http://mozilla.org/MPL/2.0/. */
  8118. /* This module handles all the different UI that happens (sometimes in order) when
  8119. TogetherJS is started:
  8120. - Introduce the session when you've been invited
  8121. - Show any browser compatibility indicators
  8122. - Show the walkthrough the first time
  8123. - Show the share link window
  8124. When everything is done it fires session.emit("startup-ready")
  8125. */
  8126. define('startup',["util", "require", "jquery", "windowing", "storage"], function (util, require, $, windowing, storage) {
  8127. var assert = util.assert;
  8128. var startup = util.Module("startup");
  8129. // Avoid circular import:
  8130. var session = null;
  8131. var STEPS = [
  8132. "browserBroken",
  8133. "browserUnsupported",
  8134. "sessionIntro",
  8135. "walkthrough",
  8136. // Look in the share() below if you add anything after here:
  8137. "share"
  8138. ];
  8139. var currentStep = null;
  8140. startup.start = function () {
  8141. if (! session) {
  8142. require(["session"], function (sessionModule) {
  8143. session = sessionModule;
  8144. startup.start();
  8145. });
  8146. return;
  8147. }
  8148. var index = -1;
  8149. if (currentStep) {
  8150. index = STEPS.indexOf(currentStep);
  8151. }
  8152. index++;
  8153. if (index >= STEPS.length) {
  8154. session.emit("startup-ready");
  8155. return;
  8156. }
  8157. currentStep = STEPS[index];
  8158. handlers[currentStep](startup.start);
  8159. };
  8160. var handlers = {
  8161. browserBroken: function (next) {
  8162. if (window.WebSocket) {
  8163. next();
  8164. return;
  8165. }
  8166. windowing.show("#togetherjs-browser-broken", {
  8167. onClose: function () {
  8168. session.close();
  8169. }
  8170. });
  8171. if ($.browser.msie) {
  8172. $("#togetherjs-browser-broken-is-ie").show();
  8173. }
  8174. },
  8175. browserUnsupported: function (next) {
  8176. next();
  8177. },
  8178. sessionIntro: function (next) {
  8179. if ((! session.isClient) || ! session.firstRun) {
  8180. next();
  8181. return;
  8182. }
  8183. TogetherJS.config.close("suppressJoinConfirmation");
  8184. if (TogetherJS.config.get("suppressJoinConfirmation")) {
  8185. next();
  8186. return;
  8187. }
  8188. var cancelled = false;
  8189. windowing.show("#togetherjs-intro", {
  8190. onClose: function () {
  8191. if (! cancelled) {
  8192. next();
  8193. }
  8194. }
  8195. });
  8196. $("#togetherjs-intro .togetherjs-modal-dont-join").click(function () {
  8197. cancelled = true;
  8198. windowing.hide();
  8199. session.close("declined-join");
  8200. });
  8201. },
  8202. walkthrough: function (next) {
  8203. storage.settings.get("seenIntroDialog").then(function (seenIntroDialog) {
  8204. if (seenIntroDialog) {
  8205. next();
  8206. return;
  8207. }
  8208. require(["walkthrough"], function (walkthrough) {
  8209. walkthrough.start(true, function () {
  8210. storage.settings.set("seenIntroDialog", true);
  8211. next();
  8212. });
  8213. });
  8214. });
  8215. },
  8216. share: function (next) {
  8217. TogetherJS.config.close("suppressInvite");
  8218. if (session.isClient || (! session.firstRun) ||
  8219. TogetherJS.config.get("suppressInvite")) {
  8220. next();
  8221. return;
  8222. }
  8223. require(["windowing"], function (windowing) {
  8224. windowing.show("#togetherjs-share");
  8225. // FIXME: no way to detect when the window is closed
  8226. // If there was a next() step then it would not work
  8227. });
  8228. }
  8229. };
  8230. return startup;
  8231. });
  8232. /* This Source Code Form is subject to the terms of the Mozilla Public
  8233. * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  8234. * You can obtain one at http://mozilla.org/MPL/2.0/. */
  8235. define('templates-localized',[], function () {
  8236. return {
  8237. "interface": "__interface_html__",
  8238. walkthrough: "__walkthrough_html__",
  8239. names: "__names__"
  8240. };
  8241. });
  8242. /* This Source Code Form is subject to the terms of the Mozilla Public
  8243. * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  8244. * You can obtain one at http://mozilla.org/MPL/2.0/. */
  8245. define('videos',["jquery", "util", "session", "elementFinder"],
  8246. function ($, util, session, elementFinder) {
  8247. var listeners = [];
  8248. var TIME_UPDATE = 'timeupdate';
  8249. var MIRRORED_EVENTS = ['play', 'pause'];
  8250. var TOO_FAR_APART = 3000;
  8251. session.on("reinitialize", function () {
  8252. unsetListeners();
  8253. setupListeners();
  8254. });
  8255. session.on("ui-ready", setupListeners);
  8256. function setupListeners() {
  8257. var videos = $('video');
  8258. setupMirroredEvents(videos);
  8259. setupTimeSync(videos);
  8260. }
  8261. function setupMirroredEvents(videos) {
  8262. var currentListener;
  8263. MIRRORED_EVENTS.forEach(function (eventName) {
  8264. currentListener = makeEventSender(eventName);
  8265. videos.on(eventName, currentListener);
  8266. listeners.push({
  8267. name: eventName,
  8268. listener: currentListener
  8269. });
  8270. });
  8271. }
  8272. function makeEventSender(eventName) {
  8273. return function (event, options) {
  8274. var element = event.target;
  8275. options = options || {};
  8276. if (!options.silent) {
  8277. session.send({
  8278. type: ('video-'+eventName),
  8279. location: elementFinder.elementLocation(element),
  8280. position: element.currentTime
  8281. });
  8282. }
  8283. };
  8284. }
  8285. function setupTimeSync(videos) {
  8286. videos.each(function(i, video) {
  8287. var onTimeUpdate = makeTimeUpdater();
  8288. $(video).on(TIME_UPDATE, onTimeUpdate);
  8289. listeners.push({
  8290. name: TIME_UPDATE,
  8291. listener: onTimeUpdate
  8292. });
  8293. });
  8294. }
  8295. function makeTimeUpdater() {
  8296. var last = 0;
  8297. return function (event) {
  8298. var currentTime = event.target.currentTime;
  8299. if(areTooFarApart(currentTime, last)){
  8300. makeEventSender(TIME_UPDATE)(event);
  8301. }
  8302. last = currentTime;
  8303. };
  8304. }
  8305. function areTooFarApart(currentTime, lastTime) {
  8306. var secDiff = Math.abs(currentTime - lastTime);
  8307. var milliDiff = secDiff * 1000;
  8308. return milliDiff > TOO_FAR_APART;
  8309. }
  8310. session.on("close", unsetListeners);
  8311. function unsetListeners() {
  8312. var videos = $('video');
  8313. listeners.forEach(function (event) {
  8314. videos.off(event.name, event.listener);
  8315. });
  8316. listeners = [];
  8317. }
  8318. session.hub.on('video-timeupdate', function (msg) {
  8319. var element = $findElement(msg.location);
  8320. var oldTime = element.prop('currentTime');
  8321. var newTime = msg.position;
  8322. //to help throttle uneccesary position changes
  8323. if(areTooFarApart(oldTime, newTime)){
  8324. setTime(element, msg.position);
  8325. }
  8326. });
  8327. MIRRORED_EVENTS.forEach( function (eventName) {
  8328. session.hub.on("video-"+eventName, function (msg) {
  8329. var element = $findElement(msg.location);
  8330. setTime(element, msg.position);
  8331. element.trigger(eventName, {silent: true});
  8332. });
  8333. });
  8334. //Currently does not discriminate between visible and invisible videos
  8335. function $findElement(location) {
  8336. return $(elementFinder.findElement(location));
  8337. }
  8338. function setTime(video, time) {
  8339. video.prop('currentTime', time);
  8340. }
  8341. });
  8342. /* This Source Code Form is subject to the terms of the Mozilla Public
  8343. * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  8344. * You can obtain one at http://mozilla.org/MPL/2.0/. */
  8345. define('walkthrough',["util", "ui", "jquery", "windowing", "templates", "templating", "session", "peers"], function (util, ui, $, windowing, templates, templating, session, peers) {
  8346. var assert = util.assert;
  8347. var walkthrough = util.Module("walkthrough");
  8348. var onHideAll = null;
  8349. var container = null;
  8350. var slides = null;
  8351. walkthrough.start = function (firstTime, doneCallback) {
  8352. if (! container) {
  8353. container = $(templates("walkthrough"));
  8354. container.hide();
  8355. ui.container.append(container);
  8356. slides = container.find(".togetherjs-walkthrough-slide");
  8357. slides.hide();
  8358. var progress = $("#togetherjs-walkthrough-progress");
  8359. slides.each(function (index) {
  8360. var bullet = templating.sub("walkthrough-slide-progress");
  8361. progress.append(bullet);
  8362. bullet.click(function () {
  8363. show(index);
  8364. });
  8365. });
  8366. container.find("#togetherjs-walkthrough-previous").click(previous);
  8367. container.find("#togetherjs-walkthrough-next").click(next);
  8368. ui.prepareShareLink(container);
  8369. container.find(".togetherjs-self-name").bind("keyup", function (event) {
  8370. var val = $(event.target).val();
  8371. peers.Self.update({name: val});
  8372. });
  8373. container.find(".togetherjs-swatch").click(function () {
  8374. var picker = $("#togetherjs-pick-color");
  8375. if (picker.is(":visible")) {
  8376. picker.hide();
  8377. return;
  8378. }
  8379. picker.show();
  8380. picker.find(".togetherjs-swatch-active").removeClass("togetherjs-swatch-active");
  8381. picker.find(".togetherjs-swatch[data-color=\"" + peers.Self.color + "\"]").addClass("togetherjs-swatch-active");
  8382. var location = container.find(".togetherjs-swatch").offset();
  8383. picker.css({
  8384. top: location.top,
  8385. // The -7 comes out of thin air, but puts it in the right place:
  8386. left: location.left-7
  8387. });
  8388. });
  8389. if (session.isClient) {
  8390. container.find(".togetherjs-if-creator").remove();
  8391. container.find(".togetherjs-ifnot-creator").show();
  8392. } else {
  8393. container.find(".togetherjs-if-creator").show();
  8394. container.find(".togetherjs-ifnot-creator").remove();
  8395. }
  8396. TogetherJS.config.track("siteName", function (value) {
  8397. value = value || document.title;
  8398. container.find(".togetherjs-site-name").text(value);
  8399. });
  8400. ui.activateAvatarEdit(container, {
  8401. onSave: function () {
  8402. container.find("#togetherjs-avatar-when-saved").show();
  8403. container.find("#togetherjs-avatar-when-unsaved").hide();
  8404. },
  8405. onPending: function () {
  8406. container.find("#togetherjs-avatar-when-saved").hide();
  8407. container.find("#togetherjs-avatar-when-unsaved").show();
  8408. }
  8409. });
  8410. // This triggers substititions in the walkthrough:
  8411. peers.Self.update({});
  8412. session.emit("new-element", container);
  8413. }
  8414. assert(typeof firstTime == "boolean", "You must provide a firstTime boolean parameter");
  8415. if (firstTime) {
  8416. container.find(".togetherjs-walkthrough-firsttime").show();
  8417. container.find(".togetherjs-walkthrough-not-firsttime").hide();
  8418. } else {
  8419. container.find(".togetherjs-walkthrough-firsttime").hide();
  8420. container.find(".togetherjs-walkthrough-not-firsttime").show();
  8421. }
  8422. onHideAll = doneCallback;
  8423. show(0);
  8424. windowing.show(container);
  8425. };
  8426. function show(index) {
  8427. slides.hide();
  8428. $(slides[index]).show();
  8429. var bullets = container.find("#togetherjs-walkthrough-progress .togetherjs-walkthrough-slide-progress");
  8430. bullets.removeClass("togetherjs-active");
  8431. $(bullets[index]).addClass("togetherjs-active");
  8432. var $next = $("#togetherjs-walkthrough-next").removeClass("togetherjs-disabled");
  8433. var $previous = $("#togetherjs-walkthrough-previous").removeClass("togetherjs-disabled");
  8434. if (index == slides.length - 1) {
  8435. $next.addClass("togetherjs-disabled");
  8436. } else if (index === 0) {
  8437. $previous.addClass("togetherjs-disabled");
  8438. }
  8439. }
  8440. function previous() {
  8441. var index = getIndex();
  8442. index--;
  8443. if (index < 0) {
  8444. index = 0;
  8445. }
  8446. show(index);
  8447. }
  8448. function next() {
  8449. var index = getIndex();
  8450. index++;
  8451. if (index >= slides.length) {
  8452. index = slides.length-1;
  8453. }
  8454. show(index);
  8455. }
  8456. function getIndex() {
  8457. var active = slides.filter(":visible");
  8458. if (! active.length) {
  8459. return 0;
  8460. }
  8461. for (var i=0; i<slides.length; i++) {
  8462. if (slides[i] == active[0]) {
  8463. return i;
  8464. }
  8465. }
  8466. return 0;
  8467. }
  8468. walkthrough.stop = function () {
  8469. windowing.hide(container);
  8470. if (onHideAll) {
  8471. onHideAll();
  8472. onHideAll = null;
  8473. }
  8474. };
  8475. session.on("hide-window", function () {
  8476. if (onHideAll) {
  8477. onHideAll();
  8478. onHideAll = null;
  8479. }
  8480. });
  8481. return walkthrough;
  8482. });
  8483. /* This Source Code Form is subject to the terms of the Mozilla Public
  8484. * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  8485. * You can obtain one at http://mozilla.org/MPL/2.0/. */
  8486. // WebRTC support -- Note that this relies on parts of the interface code that usually goes in ui.js
  8487. define('webrtc',["require", "jquery", "util", "session", "ui", "peers", "storage", "windowing"], function (require, $, util, session, ui, peers, storage, windowing) {
  8488. var webrtc = util.Module("webrtc");
  8489. var assert = util.assert;
  8490. session.RTCSupported = !!(window.mozRTCPeerConnection ||
  8491. window.webkitRTCPeerConnection ||
  8492. window.RTCPeerConnection);
  8493. if (session.RTCSupported && $.browser.mozilla && parseInt($.browser.version, 10) <= 19) {
  8494. // In a few versions of Firefox (18 and 19) these APIs are present but
  8495. // not actually usable
  8496. // See: https://bugzilla.mozilla.org/show_bug.cgi?id=828839
  8497. // Because they could be pref'd on we'll do a quick check:
  8498. try {
  8499. (function () {
  8500. var conn = new window.mozRTCPeerConnection();
  8501. })();
  8502. } catch (e) {
  8503. session.RTCSupported = false;
  8504. }
  8505. }
  8506. var mediaConstraints = {
  8507. mandatory: {
  8508. OfferToReceiveAudio: true,
  8509. OfferToReceiveVideo: false
  8510. }
  8511. };
  8512. if (window.mozRTCPeerConnection) {
  8513. mediaConstraints.mandatory.MozDontOfferDataChannel = true;
  8514. }
  8515. var URL = window.webkitURL || window.URL;
  8516. var RTCSessionDescription = window.mozRTCSessionDescription || window.webkitRTCSessionDescription || window.RTCSessionDescription;
  8517. var RTCIceCandidate = window.mozRTCIceCandidate || window.webkitRTCIceCandidate || window.RTCIceCandidate;
  8518. function makePeerConnection() {
  8519. // Based roughly off: https://github.com/firebase/gupshup/blob/gh-pages/js/chat.js
  8520. if (window.webkitRTCPeerConnection) {
  8521. return new webkitRTCPeerConnection({
  8522. "iceServers": [{"url": "stun:stun.l.google.com:19302"}]
  8523. }, {
  8524. "optional": [{"DtlsSrtpKeyAgreement": true}]
  8525. });
  8526. }
  8527. if (window.mozRTCPeerConnection) {
  8528. return new mozRTCPeerConnection({
  8529. // Or stun:124.124.124..2 ?
  8530. "iceServers": [{"url": "stun:23.21.150.121"}]
  8531. }, {
  8532. "optional": []
  8533. });
  8534. }
  8535. throw new util.AssertionError("Called makePeerConnection() without supported connection");
  8536. }
  8537. function ensureCryptoLine(sdp) {
  8538. if (! window.mozRTCPeerConnection) {
  8539. return sdp;
  8540. }
  8541. var sdpLinesIn = sdp.split('\r\n');
  8542. var sdpLinesOut = [];
  8543. // Search for m line.
  8544. for (var i = 0; i < sdpLinesIn.length; i++) {
  8545. sdpLinesOut.push(sdpLinesIn[i]);
  8546. if (sdpLinesIn[i].search('m=') !== -1) {
  8547. sdpLinesOut.push("a=crypto:1 AES_CM_128_HMAC_SHA1_80 inline:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");
  8548. }
  8549. }
  8550. sdp = sdpLinesOut.join('\r\n');
  8551. return sdp;
  8552. }
  8553. function getUserMedia(options, success, failure) {
  8554. failure = failure || function (error) {
  8555. console.error("Error in getUserMedia:", error);
  8556. };
  8557. (navigator.getUserMedia ||
  8558. navigator.mozGetUserMedia ||
  8559. navigator.webkitGetUserMedia ||
  8560. navigator.msGetUserMedia).call(navigator, options, success, failure);
  8561. }
  8562. /****************************************
  8563. * getUserMedia Avatar support
  8564. */
  8565. session.on("ui-ready", function () {
  8566. $("#togetherjs-self-avatar").click(function () {
  8567. var avatar = peers.Self.avatar;
  8568. if (avatar) {
  8569. $preview.attr("src", avatar);
  8570. }
  8571. ui.displayToggle("#togetherjs-avatar-edit");
  8572. });
  8573. if (! session.RTCSupported) {
  8574. $("#togetherjs-avatar-edit-rtc").hide();
  8575. }
  8576. var avatarData = null;
  8577. var $preview = $("#togetherjs-self-avatar-preview");
  8578. var $accept = $("#togetherjs-self-avatar-accept");
  8579. var $cancel = $("#togetherjs-self-avatar-cancel");
  8580. var $takePic = $("#togetherjs-avatar-use-camera");
  8581. var $video = $("#togetherjs-avatar-video");
  8582. var $upload = $("#togetherjs-avatar-upload");
  8583. $takePic.click(function () {
  8584. if (! streaming) {
  8585. startStreaming();
  8586. return;
  8587. }
  8588. takePicture();
  8589. });
  8590. function savePicture(dataUrl) {
  8591. avatarData = dataUrl;
  8592. $preview.attr("src", avatarData);
  8593. $accept.attr("disabled", null);
  8594. }
  8595. $accept.click(function () {
  8596. peers.Self.update({avatar: avatarData});
  8597. ui.displayToggle("#togetherjs-no-avatar-edit");
  8598. // FIXME: these probably shouldn't be two elements:
  8599. $("#togetherjs-participants-other").show();
  8600. $accept.attr("disabled", "1");
  8601. });
  8602. $cancel.click(function () {
  8603. ui.displayToggle("#togetherjs-no-avatar-edit");
  8604. // FIXME: like above:
  8605. $("#togetherjs-participants-other").show();
  8606. });
  8607. var streaming = false;
  8608. function startStreaming() {
  8609. getUserMedia({
  8610. video: true,
  8611. audio: false
  8612. },
  8613. function(stream) {
  8614. streaming = true;
  8615. $video[0].src = URL.createObjectURL(stream);
  8616. $video[0].play();
  8617. },
  8618. function(err) {
  8619. // FIXME: should pop up help or something in the case of a user
  8620. // cancel
  8621. console.error("getUserMedia error:", err);
  8622. }
  8623. );
  8624. }
  8625. function takePicture() {
  8626. assert(streaming);
  8627. var height = $video[0].videoHeight;
  8628. var width = $video[0].videoWidth;
  8629. width = width * (session.AVATAR_SIZE / height);
  8630. height = session.AVATAR_SIZE;
  8631. var $canvas = $("<canvas>");
  8632. $canvas[0].height = session.AVATAR_SIZE;
  8633. $canvas[0].width = session.AVATAR_SIZE;
  8634. var context = $canvas[0].getContext("2d");
  8635. context.arc(session.AVATAR_SIZE/2, session.AVATAR_SIZE/2, session.AVATAR_SIZE/2, 0, Math.PI*2);
  8636. context.closePath();
  8637. context.clip();
  8638. context.drawImage($video[0], (session.AVATAR_SIZE - width) / 2, 0, width, height);
  8639. savePicture($canvas[0].toDataURL("image/png"));
  8640. }
  8641. $upload.on("change", function () {
  8642. var reader = new FileReader();
  8643. reader.onload = function () {
  8644. // FIXME: I don't actually know it's JPEG, but it's probably a
  8645. // good enough guess:
  8646. var url = "data:image/jpeg;base64," + util.blobToBase64(this.result);
  8647. convertImage(url, function (result) {
  8648. savePicture(result);
  8649. });
  8650. };
  8651. reader.onerror = function () {
  8652. console.error("Error reading file:", this.error);
  8653. };
  8654. reader.readAsArrayBuffer(this.files[0]);
  8655. });
  8656. function convertImage(imageUrl, callback) {
  8657. var $canvas = $("<canvas>");
  8658. $canvas[0].height = session.AVATAR_SIZE;
  8659. $canvas[0].width = session.AVATAR_SIZE;
  8660. var context = $canvas[0].getContext("2d");
  8661. var img = new Image();
  8662. img.src = imageUrl;
  8663. // Sometimes the DOM updates immediately to call
  8664. // naturalWidth/etc, and sometimes it doesn't; using setTimeout
  8665. // gives it a chance to catch up
  8666. setTimeout(function () {
  8667. var width = img.naturalWidth || img.width;
  8668. var height = img.naturalHeight || img.height;
  8669. width = width * (session.AVATAR_SIZE / height);
  8670. height = session.AVATAR_SIZE;
  8671. context.drawImage(img, 0, 0, width, height);
  8672. callback($canvas[0].toDataURL("image/png"));
  8673. });
  8674. }
  8675. });
  8676. /****************************************
  8677. * RTC support
  8678. */
  8679. function audioButton(selector) {
  8680. ui.displayToggle(selector);
  8681. if (selector == "#togetherjs-audio-incoming") {
  8682. $("#togetherjs-audio-button").addClass("togetherjs-animated").addClass("togetherjs-color-alert");
  8683. } else {
  8684. $("#togetherjs-audio-button").removeClass("togetherjs-animated").removeClass("togetherjs-color-alert");
  8685. }
  8686. }
  8687. session.on("ui-ready", function () {
  8688. $("#togetherjs-audio-button").click(function () {
  8689. if ($("#togetherjs-rtc-info").is(":visible")) {
  8690. windowing.hide();
  8691. return;
  8692. }
  8693. if (session.RTCSupported) {
  8694. enableAudio();
  8695. } else {
  8696. windowing.show("#togetherjs-rtc-not-supported");
  8697. }
  8698. });
  8699. if (! session.RTCSupported) {
  8700. audioButton("#togetherjs-audio-unavailable");
  8701. return;
  8702. }
  8703. audioButton("#togetherjs-audio-ready");
  8704. var audioStream = null;
  8705. var accepted = false;
  8706. var connected = false;
  8707. var $audio = $("#togetherjs-audio-element");
  8708. var offerSent = null;
  8709. var offerReceived = null;
  8710. var offerDescription = false;
  8711. var answerSent = null;
  8712. var answerReceived = null;
  8713. var answerDescription = false;
  8714. var _connection = null;
  8715. var iceCandidate = null;
  8716. function enableAudio() {
  8717. accepted = true;
  8718. storage.settings.get("dontShowRtcInfo").then(function (dontShow) {
  8719. if (! dontShow) {
  8720. windowing.show("#togetherjs-rtc-info");
  8721. }
  8722. });
  8723. if (! audioStream) {
  8724. startStreaming(connect);
  8725. return;
  8726. }
  8727. if (! connected) {
  8728. connect();
  8729. }
  8730. toggleMute();
  8731. }
  8732. ui.container.find("#togetherjs-rtc-info .togetherjs-dont-show-again").change(function () {
  8733. storage.settings.set("dontShowRtcInfo", this.checked);
  8734. });
  8735. function error() {
  8736. console.warn.apply(console, arguments);
  8737. var s = "";
  8738. for (var i=0; i<arguments.length; i++) {
  8739. if (s) {
  8740. s += " ";
  8741. }
  8742. var a = arguments[i];
  8743. if (typeof a == "string") {
  8744. s += a;
  8745. } else {
  8746. var repl;
  8747. try {
  8748. repl = JSON.stringify(a);
  8749. } catch (e) {
  8750. }
  8751. if (! repl) {
  8752. repl = "" + a;
  8753. }
  8754. s += repl;
  8755. }
  8756. }
  8757. audioButton("#togetherjs-audio-error");
  8758. // FIXME: this title doesn't seem to display?
  8759. $("#togetherjs-audio-error").attr("title", s);
  8760. }
  8761. function startStreaming(callback) {
  8762. getUserMedia(
  8763. {
  8764. video: false,
  8765. audio: true
  8766. },
  8767. function (stream) {
  8768. audioStream = stream;
  8769. attachMedia("#togetherjs-local-audio", stream);
  8770. if (callback) {
  8771. callback();
  8772. }
  8773. },
  8774. function (err) {
  8775. // FIXME: handle cancel case
  8776. if (err && err.code == 1) {
  8777. // User cancel
  8778. return;
  8779. }
  8780. error("getUserMedia error:", err);
  8781. }
  8782. );
  8783. }
  8784. function attachMedia(element, media) {
  8785. element = $(element)[0];
  8786. console.log("Attaching", media, "to", element);
  8787. if (window.mozRTCPeerConnection) {
  8788. element.mozSrcObject = media;
  8789. element.play();
  8790. } else {
  8791. element.autoplay = true;
  8792. element.src = URL.createObjectURL(media);
  8793. }
  8794. }
  8795. function getConnection() {
  8796. assert(audioStream);
  8797. if (_connection) {
  8798. return _connection;
  8799. }
  8800. try {
  8801. _connection = makePeerConnection();
  8802. } catch (e) {
  8803. error("Error creating PeerConnection:", e);
  8804. throw e;
  8805. }
  8806. _connection.onaddstream = function (event) {
  8807. console.log("got event", event, event.type);
  8808. attachMedia($audio, event.stream);
  8809. audioButton("#togetherjs-audio-active");
  8810. };
  8811. _connection.onstatechange = function () {
  8812. // FIXME: this doesn't seem to work:
  8813. // Actually just doesn't work on Firefox
  8814. console.log("state change", _connection.readyState);
  8815. if (_connection.readyState == "closed") {
  8816. audioButton("#togetherjs-audio-ready");
  8817. }
  8818. };
  8819. _connection.onicecandidate = function (event) {
  8820. if (event.candidate) {
  8821. session.send({
  8822. type: "rtc-ice-candidate",
  8823. candidate: {
  8824. sdpMLineIndex: event.candidate.sdpMLineIndex,
  8825. sdpMid: event.candidate.sdpMid,
  8826. candidate: event.candidate.candidate
  8827. }
  8828. });
  8829. }
  8830. };
  8831. _connection.addStream(audioStream);
  8832. return _connection;
  8833. }
  8834. function addIceCandidate() {
  8835. if (iceCandidate) {
  8836. console.log("adding ice", iceCandidate);
  8837. _connection.addIceCandidate(new RTCIceCandidate(iceCandidate));
  8838. }
  8839. }
  8840. function connect() {
  8841. var connection = getConnection();
  8842. if (offerReceived && (! offerDescription)) {
  8843. connection.setRemoteDescription(
  8844. new RTCSessionDescription({
  8845. type: "offer",
  8846. sdp: offerReceived
  8847. }),
  8848. function () {
  8849. offerDescription = true;
  8850. addIceCandidate();
  8851. connect();
  8852. },
  8853. function (err) {
  8854. error("Error doing RTC setRemoteDescription:", err);
  8855. }
  8856. );
  8857. return;
  8858. }
  8859. if (! (offerSent || offerReceived)) {
  8860. connection.createOffer(function (offer) {
  8861. console.log("made offer", offer);
  8862. offer.sdp = ensureCryptoLine(offer.sdp);
  8863. connection.setLocalDescription(
  8864. offer,
  8865. function () {
  8866. session.send({
  8867. type: "rtc-offer",
  8868. offer: offer.sdp
  8869. });
  8870. offerSent = offer;
  8871. audioButton("#togetherjs-audio-outgoing");
  8872. },
  8873. function (err) {
  8874. error("Error doing RTC setLocalDescription:", err);
  8875. },
  8876. mediaConstraints
  8877. );
  8878. }, function (err) {
  8879. error("Error doing RTC createOffer:", err);
  8880. });
  8881. } else if (! (answerSent || answerReceived)) {
  8882. // FIXME: I might have only needed this due to my own bugs, this might
  8883. // not actually time out
  8884. var timeout = setTimeout(function () {
  8885. if (! answerSent) {
  8886. error("createAnswer Timed out; reload or restart browser");
  8887. }
  8888. }, 2000);
  8889. connection.createAnswer(function (answer) {
  8890. answer.sdp = ensureCryptoLine(answer.sdp);
  8891. clearTimeout(timeout);
  8892. connection.setLocalDescription(
  8893. answer,
  8894. function () {
  8895. session.send({
  8896. type: "rtc-answer",
  8897. answer: answer.sdp
  8898. });
  8899. answerSent = answer;
  8900. },
  8901. function (err) {
  8902. clearTimeout(timeout);
  8903. error("Error doing RTC setLocalDescription:", err);
  8904. },
  8905. mediaConstraints
  8906. );
  8907. }, function (err) {
  8908. error("Error doing RTC createAnswer:", err);
  8909. });
  8910. }
  8911. }
  8912. function toggleMute() {
  8913. // FIXME: implement. Actually, wait for this to be implementable - currently
  8914. // muting of localStreams isn't possible
  8915. // FIXME: replace with hang-up?
  8916. }
  8917. session.hub.on("rtc-offer", function (msg) {
  8918. if (offerReceived || answerSent || answerReceived || offerSent) {
  8919. abort();
  8920. }
  8921. offerReceived = msg.offer;
  8922. if (! accepted) {
  8923. audioButton("#togetherjs-audio-incoming");
  8924. return;
  8925. }
  8926. function run() {
  8927. var connection = getConnection();
  8928. connection.setRemoteDescription(
  8929. new RTCSessionDescription({
  8930. type: "offer",
  8931. sdp: offerReceived
  8932. }),
  8933. function () {
  8934. offerDescription = true;
  8935. addIceCandidate();
  8936. connect();
  8937. },
  8938. function (err) {
  8939. error("Error doing RTC setRemoteDescription:", err);
  8940. }
  8941. );
  8942. }
  8943. if (! audioStream) {
  8944. startStreaming(run);
  8945. } else {
  8946. run();
  8947. }
  8948. });
  8949. session.hub.on("rtc-answer", function (msg) {
  8950. if (answerSent || answerReceived || offerReceived || (! offerSent)) {
  8951. abort();
  8952. // Basically we have to abort and try again. We'll expect the other
  8953. // client to restart when appropriate
  8954. session.send({type: "rtc-abort"});
  8955. return;
  8956. }
  8957. answerReceived = msg.answer;
  8958. assert(offerSent);
  8959. assert(audioStream);
  8960. var connection = getConnection();
  8961. connection.setRemoteDescription(
  8962. new RTCSessionDescription({
  8963. type: "answer",
  8964. sdp: answerReceived
  8965. }),
  8966. function () {
  8967. answerDescription = true;
  8968. // FIXME: I don't think this connect is ever needed?
  8969. connect();
  8970. },
  8971. function (err) {
  8972. error("Error doing RTC setRemoteDescription:", err);
  8973. }
  8974. );
  8975. });
  8976. session.hub.on("rtc-ice-candidate", function (msg) {
  8977. iceCandidate = msg.candidate;
  8978. if (offerDescription || answerDescription) {
  8979. addIceCandidate();
  8980. }
  8981. });
  8982. session.hub.on("rtc-abort", function (msg) {
  8983. abort();
  8984. if (! accepted) {
  8985. return;
  8986. }
  8987. if (! audioStream) {
  8988. startStreaming(function () {
  8989. connect();
  8990. });
  8991. } else {
  8992. connect();
  8993. }
  8994. });
  8995. session.hub.on("hello", function (msg) {
  8996. // FIXME: displayToggle should be set due to
  8997. // _connection.onstatechange, but that's not working, so
  8998. // instead:
  8999. audioButton("#togetherjs-audio-ready");
  9000. if (accepted && (offerSent || answerSent)) {
  9001. abort();
  9002. connect();
  9003. }
  9004. });
  9005. function abort() {
  9006. answerSent = answerReceived = offerSent = offerReceived = null;
  9007. answerDescription = offerDescription = false;
  9008. _connection = null;
  9009. $audio[0].removeAttribute("src");
  9010. }
  9011. });
  9012. return webrtc;
  9013. });
  9014. /* This Source Code Form is subject to the terms of the Mozilla Public
  9015. * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  9016. * You can obtain one at http://mozilla.org/MPL/2.0/. */
  9017. define('who',["util", "channels", "session", "ui"], function (util, channels, session, ui) {
  9018. var assert = util.assert;
  9019. var who = util.Module("who");
  9020. var MAX_RESPONSE_TIME = 5000;
  9021. var MAX_LATE_RESPONSE = 2000;
  9022. who.getList = function (hubUrl) {
  9023. return util.Deferred(function (def) {
  9024. var expected;
  9025. var channel = channels.WebSocketChannel(hubUrl);
  9026. var users = {};
  9027. var responded = 0;
  9028. var firstResponse = 0;
  9029. var lateResponseTimeout;
  9030. channel.onmessage = function (msg) {
  9031. if (msg.type == "init-connection") {
  9032. expected = msg.peerCount;
  9033. }
  9034. if (msg.type == "who") {
  9035. // Our message back to ourselves probably
  9036. firstResponse = setTimeout(function () {
  9037. close();
  9038. }, MAX_LATE_RESPONSE);
  9039. }
  9040. if (msg.type == "hello-back") {
  9041. if (! users[msg.clientId]) {
  9042. users[msg.clientId] = who.ExternalPeer(msg.clientId, msg);
  9043. responded++;
  9044. if (expected && responded >= expected) {
  9045. close();
  9046. } else {
  9047. def.notify(users);
  9048. }
  9049. }
  9050. }
  9051. console.log("users", users);
  9052. };
  9053. channel.send({
  9054. type: "who",
  9055. "server-echo": true,
  9056. clientId: null
  9057. });
  9058. var timeout = setTimeout(function () {
  9059. close();
  9060. }, MAX_RESPONSE_TIME);
  9061. function close() {
  9062. if (timeout) {
  9063. clearTimeout(timeout);
  9064. }
  9065. if (lateResponseTimeout) {
  9066. clearTimeout(lateResponseTimeout);
  9067. }
  9068. channel.close();
  9069. def.resolve(users);
  9070. }
  9071. });
  9072. };
  9073. who.invite = function (hubUrl, clientId) {
  9074. return util.Deferred(function (def) {
  9075. var channel = channels.WebSocketChannel(hubUrl);
  9076. var id = util.generateId();
  9077. channel.onmessage = function (msg) {
  9078. if (msg.type == "invite" && msg.inviteId == id) {
  9079. channel.close();
  9080. def.resolve();
  9081. }
  9082. };
  9083. var userInfo = session.makeHelloMessage(false);
  9084. delete userInfo.type;
  9085. userInfo.clientId = session.clientId;
  9086. channel.send({
  9087. type: "invite",
  9088. inviteId: id,
  9089. url: session.shareUrl(),
  9090. userInfo: userInfo,
  9091. forClientId: clientId,
  9092. clientId: null,
  9093. "server-echo": true
  9094. });
  9095. });
  9096. };
  9097. who.ExternalPeer = util.Class({
  9098. isSelf: false,
  9099. isExternal: true,
  9100. constructor: function (id, attrs) {
  9101. attrs = attrs || {};
  9102. assert(id);
  9103. this.id = id;
  9104. this.identityId = attrs.identityId || null;
  9105. this.status = attrs.status || "live";
  9106. this.idle = attrs.status || "active";
  9107. this.name = attrs.name || null;
  9108. this.avatar = attrs.avatar || null;
  9109. this.color = attrs.color || "#00FF00";
  9110. this.lastMessageDate = 0;
  9111. this.view = ui.PeerView(this);
  9112. },
  9113. className: function (prefix) {
  9114. prefix = prefix || "";
  9115. return prefix + util.safeClassName(this.id);
  9116. }
  9117. });
  9118. return who;
  9119. });
  9120. /* This Source Code Form is subject to the terms of the Mozilla Public
  9121. * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  9122. * You can obtain one at http:// mozilla.org/MPL/2.0/. */
  9123. define('youtubeVideos',["jquery", "util", "session", "elementFinder"],
  9124. function ($, util, session, elementFinder) {
  9125. // constant var to indicate whether two players are too far apart in sync
  9126. var TOO_FAR_APART = 3000;
  9127. // embedded youtube iframes
  9128. var youTubeIframes = [];
  9129. // youtube API load delay
  9130. var API_LOADING_DELAY = 2000;
  9131. session.on("reinitialize", function () {
  9132. if (TogetherJS.config.get("youtube")) {
  9133. prepareYouTube();
  9134. }
  9135. });
  9136. session.on("close", function () {
  9137. $(youTubeIframes).each(function (i, iframe) {
  9138. // detach players from iframes
  9139. $(iframe).removeData("togetherjs-player");
  9140. $(iframe).removeData("dontPublish");
  9141. $(iframe).removeData("currentVideoId");
  9142. // disable iframeAPI
  9143. $(iframe).removeAttr("enablejsapi");
  9144. // remove unique youtube iframe indicators
  9145. var id = $(iframe).attr("id") || "";
  9146. if (id.indexOf("youtube-player") === 0) {
  9147. // An id we added
  9148. $(iframe).removeAttr("id");
  9149. }
  9150. youTubeIframes = [];
  9151. });
  9152. });
  9153. $(function() {
  9154. TogetherJS.config.track("youtube", function (track, previous) {
  9155. if (track && ! previous) {
  9156. prepareYouTube();
  9157. // You can enable youtube dynamically, but can't turn it off:
  9158. TogetherJS.config.close("youtube");
  9159. }
  9160. });
  9161. });
  9162. var youtubeHooked = false;
  9163. function prepareYouTube() {
  9164. // setup iframes first
  9165. setupYouTubeIframes();
  9166. // this function should be global so it can be called when API is loaded
  9167. if (!youtubeHooked) {
  9168. youtubeHooked = true;
  9169. window.onYouTubeIframeAPIReady = (function(oldf) {
  9170. return function() {
  9171. // YouTube API is ready
  9172. $(youTubeIframes).each(function (i, iframe) {
  9173. var player = new YT.Player(iframe.id, { // get the reference to the already existing iframe
  9174. events: {
  9175. 'onReady': insertPlayer,
  9176. 'onStateChange': publishPlayerStateChange
  9177. }
  9178. });
  9179. });
  9180. if (oldf) {
  9181. return oldf();
  9182. }
  9183. };
  9184. })(window.onYouTubeIframeAPIReady);
  9185. }
  9186. if (window.YT === undefined) {
  9187. // load necessary API
  9188. // it calls onYouTubeIframeAPIReady automatically when the API finishes loading
  9189. var tag = document.createElement('script');
  9190. tag.src = "https://www.youtube.com/iframe_api";
  9191. var firstScriptTag = document.getElementsByTagName('script')[0];
  9192. firstScriptTag.parentNode.insertBefore(tag, firstScriptTag);
  9193. } else {
  9194. // manually invoke APIReady function when the API was already loaded by user
  9195. onYouTubeIframeAPIReady();
  9196. }
  9197. // give each youtube iframe a unique id and set its enablejsapi param to true
  9198. function setupYouTubeIframes() {
  9199. var iframes = $('iframe');
  9200. iframes.each(function (i, iframe) {
  9201. // if the iframe's unique id is already set, skip it
  9202. // FIXME: what if the user manually sets an iframe's id (i.e. "#my-youtube")?
  9203. // maybe we should set iframes everytime togetherjs is reinitialized?
  9204. var osrc = $(iframe).attr("src"), src = osrc;
  9205. if ((src || "").indexOf("youtube") != -1 && !$(iframe).attr("id")) {
  9206. $(iframe).attr("id", "youtube-player"+i);
  9207. $(iframe).attr("enablejsapi", 1);
  9208. // we also need to add ?enablejsapi to the iframe src.
  9209. if (!/[?&]enablejsapi=1(&|$)/.test(src)) {
  9210. src += (/[?]/.test(src)) ? '&' : '?';
  9211. src += 'enablejsapi=1';
  9212. }
  9213. // the youtube API seems to be unhappy unless the URL starts
  9214. // with https
  9215. if (!/^https[:]\/\//.test(src)) {
  9216. src = 'https://' + src.replace(/^(\w+[:])?\/\//, '');
  9217. }
  9218. if (src !== osrc) {
  9219. $(iframe).attr("src", src);
  9220. }
  9221. youTubeIframes[i] = iframe;
  9222. }
  9223. });
  9224. } // iframes are ready
  9225. function insertPlayer(event) {
  9226. // only when it is READY, attach a player to its iframe
  9227. var currentPlayer = event.target;
  9228. var currentIframe = currentPlayer.getIframe();
  9229. // check if a player is already attached in case of being reinitialized
  9230. if (!$(currentIframe).data("togetherjs-player")) {
  9231. $(currentIframe).data("togetherjs-player", currentPlayer);
  9232. // initialize its dontPublish flag as well
  9233. $(currentIframe).data("dontPublish", false);
  9234. // store its current video's id
  9235. var currentVideoId = getVideoIdFromUrl(currentPlayer.getVideoUrl());
  9236. $(currentIframe).data("currentVideoId", currentVideoId);
  9237. }
  9238. }
  9239. } // end of prepareYouTube
  9240. function publishPlayerStateChange(event) {
  9241. var target = event.target;
  9242. var currentIframe = target.getIframe();
  9243. //var currentPlayer = $(currentIframe).data("togetherjs-player");
  9244. var currentPlayer = target;
  9245. var currentTime = currentPlayer.getCurrentTime();
  9246. //var currentTime = target.k.currentTime;
  9247. var iframeLocation = elementFinder.elementLocation(currentIframe);
  9248. if ($(currentPlayer).data("seek")) {
  9249. $(currentPlayer).removeData("seek");
  9250. return;
  9251. }
  9252. // do not publish if playerState was changed by other users
  9253. if ($(currentIframe).data("dontPublish")) {
  9254. // make it false again so it can start publishing events of its own state changes
  9255. $(currentIframe).data("dontPublish", false);
  9256. return;
  9257. }
  9258. // notify other people that I changed the player state
  9259. if (event.data == YT.PlayerState.PLAYING) {
  9260. var currentVideoId = isDifferentVideoLoaded(currentIframe);
  9261. if (currentVideoId) {
  9262. // notify that I just loaded another video
  9263. publishDifferentVideoLoaded(iframeLocation, currentVideoId);
  9264. // update current video id
  9265. $(currentIframe).data("currentVideoId", currentVideoId);
  9266. } else {
  9267. session.send({
  9268. type: "playerStateChange",
  9269. element: iframeLocation,
  9270. playerState: 1,
  9271. playerTime: currentTime
  9272. });
  9273. }
  9274. } else if (event.data == YT.PlayerState.PAUSED) {
  9275. session.send({
  9276. type: "playerStateChange",
  9277. element: iframeLocation,
  9278. playerState: 2,
  9279. playerTime: currentTime
  9280. });
  9281. } else {
  9282. // do nothing when the state is buffering, cued, or ended
  9283. return;
  9284. }
  9285. }
  9286. function publishDifferentVideoLoaded(iframeLocation, videoId) {
  9287. session.send({
  9288. type: "differentVideoLoaded",
  9289. videoId: videoId,
  9290. element: iframeLocation
  9291. });
  9292. }
  9293. session.hub.on('playerStateChange', function (msg) {
  9294. var iframe = elementFinder.findElement(msg.element);
  9295. var player = $(iframe).data("togetherjs-player");
  9296. var currentTime = player.getCurrentTime();
  9297. var currentState = player.getPlayerState();
  9298. if (currentState != msg.playerState) {
  9299. $(iframe).data("dontPublish", true);
  9300. }
  9301. if (msg.playerState == 1) {
  9302. player.playVideo();
  9303. // seekTo() updates the video's time and plays it if it was already playing
  9304. // and pauses it if it was already paused
  9305. if (areTooFarApart(currentTime, msg.playerTime)) {
  9306. player.seekTo(msg.playerTime, true);
  9307. }
  9308. } else if (msg.playerState == 2) {
  9309. // When YouTube videos are advanced while playing,
  9310. // Chrome: pause -> pause -> play (onStateChange is called even when it is from pause to pause)
  9311. // FireFox: buffering -> play -> buffering -> play
  9312. // We must prevent advanced videos from going out of sync
  9313. player.pauseVideo();
  9314. if (areTooFarApart(currentTime, msg.playerTime)) {
  9315. // "seek" flag will help supress publishing unwanted state changes
  9316. $(player).data("seek", true);
  9317. player.seekTo(msg.playerTime, true);
  9318. }
  9319. }
  9320. });
  9321. // if a late user joins a channel, synchronize his videos
  9322. session.hub.on('hello', function () {
  9323. // wait a couple seconds to make sure the late user has finished loading API
  9324. setTimeout(synchronizeVideosOfLateGuest, API_LOADING_DELAY);
  9325. });
  9326. session.hub.on('synchronizeVideosOfLateGuest', function (msg) {
  9327. // XXX can this message arrive before we're initialized?
  9328. var iframe = elementFinder.findElement(msg.element);
  9329. var player = $(iframe).data("togetherjs-player");
  9330. // check if another video had been loaded to an existing iframe before I joined
  9331. var currentVideoId = getVideoIdFromUrl(player.getVideoUrl());
  9332. if (msg.videoId != currentVideoId) {
  9333. $(iframe).data("currentVideoId", msg.videoId);
  9334. player.loadVideoById(msg.videoId, msg.playerTime, 'default');
  9335. } else {
  9336. // if the video is only cued, I do not have to do anything to sync
  9337. if (msg.playerState != 5) {
  9338. player.seekTo(msg.playerTime, true).playVideo();
  9339. }
  9340. }
  9341. });
  9342. session.hub.on('differentVideoLoaded', function (msg) {
  9343. // load a new video if the host has loaded one
  9344. var iframe = elementFinder.findElement(msg.element);
  9345. var player = $(iframe).data("togetherjs-player");
  9346. player.loadVideoById(msg.videoId, 0, 'default');
  9347. $(iframe).data("currentVideoId", msg.videoId);
  9348. });
  9349. function synchronizeVideosOfLateGuest() {
  9350. youTubeIframes.forEach(function (iframe) {
  9351. var currentPlayer = $(iframe).data("togetherjs-player");
  9352. var currentVideoId = getVideoIdFromUrl(currentPlayer.getVideoUrl());
  9353. var currentState = currentPlayer.getPlayerState();
  9354. var currentTime = currentPlayer.getCurrentTime();
  9355. var iframeLocation = elementFinder.elementLocation(iframe);
  9356. session.send({
  9357. type: "synchronizeVideosOfLateGuest",
  9358. element: iframeLocation,
  9359. videoId: currentVideoId,
  9360. playerState: currentState, //this might be necessary later
  9361. playerTime: currentTime
  9362. });
  9363. });
  9364. }
  9365. function isDifferentVideoLoaded(iframe) {
  9366. var lastVideoId = $(iframe).data("currentVideoId");
  9367. var currentPlayer = $(iframe).data("togetherjs-player");
  9368. var currentVideoId = getVideoIdFromUrl(currentPlayer.getVideoUrl());
  9369. // since url forms of iframe src and player's video url are different,
  9370. // I have to compare the video ids
  9371. if (currentVideoId != lastVideoId) {
  9372. return currentVideoId;
  9373. } else {
  9374. return false;
  9375. }
  9376. }
  9377. // parses videoId from the url returned by getVideoUrl function
  9378. function getVideoIdFromUrl(videoUrl) {
  9379. var videoId = videoUrl.split('v=')[1];
  9380. //Chrome and Firefox have different positions for parameters
  9381. var ampersandIndex = videoId.indexOf('&');
  9382. if (ampersandIndex != -1) {
  9383. videoId = videoId.substring(0, ampersandIndex);
  9384. }
  9385. return videoId;
  9386. }
  9387. function areTooFarApart(myTime, theirTime) {
  9388. var secDiff = Math.abs(myTime - theirTime);
  9389. var milliDiff = secDiff * 1000;
  9390. return milliDiff > TOO_FAR_APART;
  9391. }
  9392. });
  9393. /* This Source Code Form is subject to the terms of the Mozilla Public
  9394. * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  9395. * You can obtain one at http://mozilla.org/MPL/2.0/. */
  9396. define('templates-de-DE',[], function () {
  9397. return {
  9398. "interface": "<% /*\n This is basically all the markup and interface for TogetherJS.\n Note all links should be like http://localhost:8080/togetherjs/*\n these links are rewritten with the location where TogetherJS was deployed.\n\n This file is inlined into togetherjs/templates.js\n*/ %>\n<div id=\"togetherjs-container\" class=\"togetherjs\">\n\n <!-- This is the main set of buttons: -->\n <div id=\"togetherjs-dock\" class=\"togetherjs-dock-right\">\n <div id=\"togetherjs-dock-anchor\" title=\"Konsole verschieben\">\n <span id=\"togetherjs-dock-anchor-horizontal\">\n <img src=\"http://localhost:8080/togetherjs/images/icn-handle-circle@2x.png\" alt=\"drag\">\n </span>\n <span id=\"togetherjs-dock-anchor-vertical\">\n <img src=\"http://localhost:8080/togetherjs/images/icn-handle-circle@2x.png\" alt=\"drag\">\n </span>\n </div>\n <div id=\"togetherjs-buttons\">\n <div style=\"display: none\">\n <button id=\"togetherjs-template-dock-person\" class=\"togetherjs-button togetherjs-dock-person\">\n <div class=\"togetherjs-tooltip togetherjs-dock-person-tooltip\">\n <span class=\"togetherjs-person-name\"></span>\n <span class=\"togetherjs-person-tooltip-arrow-r\"></span>\n </div>\n <div class=\"togetherjs-person togetherjs-person-status-overlay\"></div>\n </button>\n </div>\n <button id=\"togetherjs-profile-button\" class=\"togetherjs-button\" title=\"Persönliche Einstellungen\">\n <div class=\"togetherjs-person togetherjs-person-self\"></div>\n <div id=\"togetherjs-profile-arrow\"></div>\n </button>\n <button id=\"togetherjs-share-button\" class=\"togetherjs-button\" title=\"Freund hinzufügen\"></button>\n <button id=\"togetherjs-audio-button\" class=\"togetherjs-button\" title=\"Mikrofon einschalten\">\n <span id=\"togetherjs-audio-unavailable\" class=\"togetherjs-audio-set\" data-toggles=\".togetherjs-audio-set\">\n </span>\n <span id=\"togetherjs-audio-ready\" class=\"togetherjs-audio-set\" data-toggles=\".togetherjs-audio-set\" style=\"display: none\">\n </span>\n <span id=\"togetherjs-audio-outgoing\" class=\"togetherjs-audio-set\" data-toggles=\".togetherjs-audio-set\" style=\"display: none\">\n </span>\n <span id=\"togetherjs-audio-incoming\" class=\"togetherjs-audio-set\" data-toggles=\".togetherjs-audio-set\" style=\"display: none\">\n </span>\n <span id=\"togetherjs-audio-active\" class=\"togetherjs-audio-set\" data-toggles=\".togetherjs-audio-set\" style=\"display: none\">\n </span>\n <span id=\"togetherjs-audio-muted\" class=\"togetherjs-audio-set\" data-toggles=\".togetherjs-audio-set\" style=\"display: none\">\n </span>\n <span id=\"togetherjs-audio-error\" class=\"togetherjs-audio-set\" data-toggles=\".togetherjs-audio-set\" style=\"display: none\">\n </span>\n </button>\n <button id=\"togetherjs-chat-button\" class=\"togetherjs-button\" title=\"Nachrichten\"></button>\n <div id=\"togetherjs-dock-participants\"></div>\n </div>\n </div>\n\n <!-- The window for editing the avatar: -->\n <div id=\"togetherjs-avatar-edit\" class=\"togetherjs-modal\"\n style=\"display: none\">\n <header> Avatar ändern </header>\n <section>\n <div class=\"togetherjs-avatar-preview togetherjs-person togetherjs-person-self\"></div>\n <div id=\"togetherjs-avatar-buttons\">\n <input type=\"file\" class=\"togetherjs-upload-avatar\">\n <!--<button id=\"togetherjs-upload-avatar\" class=\"togetherjs-primary\">Upload a picture</button>-->\n <!--<button id=\"togetherjs-camera-avatar\" class=\"togetherjs-default\">Take a picture</button>-->\n </div>\n </section>\n <section class=\"togetherjs-buttons\">\n <button class=\"togetherjs-cancel togetherjs-dismiss\">Abbrechen</button>\n <span class=\"togetherjs-alt-text\">oder</span>\n <button class=\"togetherjs-avatar-save togetherjs-primary\">Speichern</button>\n </section>\n </div>\n\n <!-- The window for sharing the link: -->\n <div id=\"togetherjs-share\" class=\"togetherjs-window\"\n data-bind-to=\"#togetherjs-share-button\" style=\"display: none\">\n <header> Einen Freund einladen </header>\n <section>\n <div class=\"togetherjs-not-mobile\">\n <p>Diese Adresse an Freunde senden:</p>\n <input type=\"text\" class=\"togetherjs-share-link\">\n </div>\n <div class=\"togetherjs-only-mobile\">\n <p>Diese Adresse an Freunde senden:</p>\n <input type=\"text\" class=\"togetherjs-share-link\">\n <!-- <a class=\"togetherjs-share-link\" href=\"#\">Press your thumb here.</a> -->\n </div>\n </section>\n </div>\n\n <!-- Participant Full List view template: -->\n <div id=\"togetherjs-participantlist\" class=\"togetherjs-window\"\n data-bind-to=\"#togetherjs-participantlist-button\" style=\"display: none\">\n <header> Teilnehmer </header>\n <section>\n <div class=\"togetherjs-not-mobile\">\n <ul>\n <li id=\"togetherjs-participant-item\">\n <img class=\"togetherjs-person togetherjs-person-small\" src=\"http://localhost:8080/togetherjs/images/btn-menu-change-avatar.png\">\n <span class=\"tj-name togetherjs-person-name\">NAME</span>\n <span class=\"tj-status\">&#9679;</span>\n <p class=\"tj-urllocation\">Momentan auf: <a class=\"togetherjs-person-url togetherjs-person-url-title\" href=\"\">http://www.location.comwww.location.comwww.location.comasdfsafd</a></p>\n <p class=\"tj-follow\">Folgen:\n <label class=\"togetherjs-follow-question\" for=\"togetherjs-person-status-follow\">\n <input type=\"checkbox\" id=\"togetherjs-person-status-follow\">\n </label>\n </p>\n <section class=\"togetherjs-buttons\">\n <!-- Displayed when the peer is at a different URL: -->\n <div class=\"togetherjs-different-url\">\n <a class=\"togetherjs-nudge togetherjs-default tj-btn-sm\">Anstupsen</a>\n <a href=\"#\" class=\"togetherjs-follow togetherjs-person-url togetherjs-primary tj-btn-sm\">Teilnehmen</a>\n </div>\n <!-- Displayed when the peer is at your same URL: -->\n <div class=\"togetherjs-same-url\" style=\"display: none\">\n <span class=\"togetherjs-person-name\"></span> ist auf der selben Seite.\n </div>\n </section>\n </li>\n </ul>\n </section>\n </div>\n\n <!-- Participant detail template: -->\n <div id=\"togetherjs-template-participant-window\" class=\"togetherjs-window\" style=\"display: none\">\n <header><div class=\"togetherjs-person togetherjs-person-small\"></div><span class=\"togetherjs-person-name\"></span></header>\n\n <section class=\"togetherjs-participant-window-main\">\n <p class=\"togetherjs-participant-window-row\"><strong>Rolle:</strong>\n <span class=\"togetherjs-person-role\"></span>\n </p>\n\n <p class=\"togetherjs-participant-window-row\"><strong>Momentan auf:</strong>\n <a class=\"togetherjs-person-url togetherjs-person-url-title\"></a>\n </p>\n\n <p class=\"togetherjs-participant-window-row\"><strong>Status:</strong>\n <span class=\"togetherjs-person-status\"></span>\n </p>\n\n <p class=\"togetherjs-participant-window-row\"><strong class=\"togetherjs-float-left\">Diesem Teilnehmer folgen:</strong>\n <label class=\"togetherjs-follow-question togetherjs-float-left\" for=\"togetherjs-person-status-follow\">\n <input type=\"checkbox\" id=\"togetherjs-person-status-follow\">\n </label>\n <span class=\"togetherjs-clear\"></span>\n </p>\n\n </section>\n\n <section class=\"togetherjs-buttons\">\n <!-- Displayed when the peer is at a different URL: -->\n <div class=\"togetherjs-different-url\">\n <a class=\"togetherjs-nudge togetherjs-default\">Anstupsen</a>\n <a href=\"#\" class=\"togetherjs-follow togetherjs-person-url togetherjs-primary\">Teilnehmen</a>\n </div>\n <!-- Displayed when the peer is at your same URL: -->\n <div class=\"togetherjs-same-url\" style=\"display: none\">\n <span class=\"togetherjs-person-name\"></span> ist auf der selben Seite.\n </div>\n </section>\n </div>\n\n <!-- The chat screen: -->\n <div id=\"togetherjs-chat\" class=\"togetherjs-window\" data-bind-to=\"#togetherjs-chat-button\"\n style=\"display: none\">\n <header> Nachrichten </header>\n <section class=\"togetherjs-subtitle\">\n <div id=\"togetherjs-chat-participants\" data-toggles=\"#togetherjs-chat-no-participants\" style=\"display: none\">\n <span id=\"togetherjs-chat-participant-list\"></span>\n &amp; You\n </div>\n <div id=\"togetherjs-chat-no-participants\" data-toggles=\"#togetherjs-chat-participants\">\n Es ist sonst niemand hier.\n </div>\n </section>\n\n <div style=\"display: none\">\n\n <!-- Template for one message: -->\n <div id=\"togetherjs-template-chat-message\" class=\"togetherjs-chat-item togetherjs-chat-message\">\n <div class=\"togetherjs-person\"></div>\n <div class=\"togetherjs-timestamp\"><span class=\"togetherjs-time\">HH:MM</span> <span class=\"togetherjs-ampm\">AM/PM</span></div>\n <div class=\"togetherjs-person-name-abbrev\"></div>\n <div class=\"togetherjs-chat-content togetherjs-sub-content\"></div>\n </div>\n\n <!-- Template for when a person leaves: -->\n <div id=\"togetherjs-template-chat-left\" class=\"togetherjs-chat-item togetherjs-chat-left-item\">\n <div class=\"togetherjs-person\"></div>\n <div class=\"togetherjs-ifnot-declinedJoin\">\n <div class=\"togetherjs-inline-text\"><span class=\"togetherjs-person-name\"></span> hat die Sitzung verlassen.</div>\n </div>\n <div class=\"togetherjs-if-declinedJoin\">\n <div class=\"togetherjs-inline-text\"><span class=\"togetherjs-person-name\"></span> hat die Einladung abgelehnt.</div>\n </div>\n <div class=\"togetherjs-clear\"></div>\n </div>\n\n <!-- Template when a person joins the session: -->\n <div id=\"togetherjs-template-chat-joined\" class=\"togetherjs-chat-item togetherjs-chat-join-item\">\n <div class=\"togetherjs-person\"></div>\n <div class=\"togetherjs-inline-text\"><span class=\"togetherjs-person-name\"></span> nimmt an der Sitzung teil.</div>\n <div class=\"togetherjs-clear\"></div>\n </div>\n\n <!-- Template for system-derived messages: -->\n <div id=\"togetherjs-template-chat-system\" class=\"togetherjs-chat-item\">\n <span class=\"togetherjs-chat-content togetherjs-sub-content\"></span>\n </div>\n\n <!-- Template when a person joins the session: -->\n <!-- <div id=\"togetherjs-template-chat-joined\" class=\"togetherjs-chat-item togetherjs-chat-join-item\">\n <div class=\"togetherjs-person\"></div>\n <div class=\"togetherjs-inline-text\"><span class=\"togetherjs-person-name\"></span> joined the session.</div>\n <div class=\"togetherjs-clear\"></div>\n </div> -->\n\n <!-- Template for when someone goes to a new URL: -->\n <div id=\"togetherjs-template-url-change\" class=\"togetherjs-chat-item togetherjs-chat-url-change\">\n <div class=\"togetherjs-person\"></div>\n <div class=\"togetherjs-inline-text\">\n <div class=\"togetherjs-if-sameUrl\">\n <span class=\"togetherjs-person-name\"></span>\n ist auf der selben Seite.\n </div>\n <div class=\"togetherjs-ifnot-sameUrl\">\n <span class=\"togetherjs-person-name\"></span>\n ging auf: <a href=\"#\" class=\"togetherjs-person-url togetherjs-person-url-title\" target=\"_self\"></a>\n <section class=\"togetherjs-buttons togetherjs-buttons-notification-diff-url\">\n <!-- Displayed when the peer is at a different URL: -->\n <div class=\"togetherjs-different-url togetherjs-notification-diff-url\">\n <a class=\"togetherjs-nudge togetherjs-default\">Anstupsen</a>\n <a href=\"#\" class=\"togetherjs-follow togetherjs-person-url togetherjs-primary\">Teilnehmen</a>\n </div>\n </section>\n\n <!-- <div>\n <a class=\"togetherjs-nudge togetherjs-secondary\">Nudge them</a>\n <a href=\"\" class=\"togetherjs-person-url togetherjs-follow togetherjs-primary\">Join them</a>\n </div> -->\n\n </div>\n </div>\n <div class=\"togetherjs-clear\"></div>\n </div>\n </div>\n\n <section id=\"togetherjs-chat-messages\">\n <!-- FIX ME// need to have some dialogue that says something like - There are no chats yet! -->\n </section>\n <section id=\"togetherjs-chat-input-box\">\n <textarea id=\"togetherjs-chat-input\" placeholder=\"Nachricht hier eingeben\"></textarea>\n </section>\n </div>\n\n <!-- this is a kind of warning popped up when you (successfully) start RTC: -->\n <div id=\"togetherjs-rtc-info\" class=\"togetherjs-window\"\n data-bind-to=\"#togetherjs-audio-button\"\n style=\"display: none\">\n\n <header> Audiokonferenz </header>\n <section>\n <p>\n Das <strong>browser Mikrofon</strong> neben der Adresszeile oben aktivieren.\n </p>\n <p>\n Das Mikrofon über den browser zu nutzen, ist eine experimentelle Funktion.\n </p>\n <p>\n Mehr über <a href=\"https://github.com/mozilla/togetherjs/wiki/About-Audio-Chat-and-WebRTC\" target=\"_blank\">Audiokonferenz lesen</a>.\n </p>\n </section>\n\n <section class=\"togetherjs-buttons\">\n <label for=\"togetherjs-rtc-info-dismiss\" style=\"display: inline;\">\n <input class=\"togetherjs-dont-show-again\" id=\"togetherjs-rtc-info-dismiss\" type=\"checkbox\">\n Nicht erneut anzeigen.\n </label>\n <button class=\"togetherjs-default togetherjs-dismiss\" type=\"button\">Schließen</button>\n </section>\n </div>\n\n <!-- this is popped up when you hit the audio button, but RTC isn't\n supported: -->\n <div id=\"togetherjs-rtc-not-supported\" class=\"togetherjs-window\"\n data-bind-to=\"#togetherjs-audio-button\"\n style=\"display: none\">\n <header> Audiokonferenz </header>\n\n <section>\n <p>Für die Audiokonferenz wird ein <a href='https://github.com/mozilla/togetherjs/wiki/About-Audio-Chat-and-WebRTC' target='_blank'>neuerer browser</a> benötigt!</p>\n <p>\n Die Echtzeitaudiokonferenz benötigt einen neueren (oder anderen) browser.\n </p>\n <p>\n <a href='https://github.com/mozilla/togetherjs/wiki/About-Audio-Chat-and-WebRTC' target='_blank'>Liste ünterstützter browser und mehr Information</a>.\n </p>\n </section>\n\n <section class=\"togetherjs-buttons\">\n <div class=\"togetherjs-rtc-dialog-btn\">\n <button class=\"togetherjs-default togetherjs-dismiss\" type=\"button\">Schließen</button>\n </div>\n </section>\n </div>\n\n <!-- The popup when a chat message comes in and the #togetherjs-chat window isn't open -->\n <div id=\"togetherjs-chat-notifier\" class=\"togetherjs-notification\"\n data-bind-to=\"#togetherjs-chat-button\"\n style=\"display: none\">\n <img src=\"http://localhost:8080/togetherjs/images/notification-togetherjs-logo.png\" class=\"togetherjs-notification-logo\" alt=\"\">\n <img src=\"http://localhost:8080/togetherjs/images/notification-btn-close.png\" class=\"togetherjs-notification-closebtn togetherjs-dismiss\" alt=\"[close]\">\n <section id=\"togetherjs-chat-notifier-message\">\n </section>\n </div>\n\n <!-- The menu when you click on the profile: -->\n <div id=\"togetherjs-menu\" class=\"togetherjs-menu\" style=\"display: none\">\n <div class=\"togetherjs-menu-item togetherjs-menu-disabled\" id=\"togetherjs-menu-profile\">\n <img id=\"togetherjs-menu-avatar\">\n <span class=\"togetherjs-person-name-self\" id=\"togetherjs-self-name-display\" data-toggles=\"#togetherjs-menu .togetherjs-self-name\">[nickname]</span>\n <input class=\"togetherjs-self-name\" type=\"text\" data-toggles=\"#togetherjs-self-name-display\" style=\"display: none\" placeholder=\"Namen eingeben\">\n </div>\n <div class=\"togetherjs-menu-hr-avatar\"></div>\n <div class=\"togetherjs-menu-item\" id=\"togetherjs-menu-update-name\"><img src=\"http://localhost:8080/togetherjs/images/button-pencil.png\" alt=\"\"> Name ändern</div>\n <div class=\"togetherjs-menu-item\" id=\"togetherjs-menu-update-avatar\"><img src=\"http://localhost:8080/togetherjs/images/btn-menu-change-avatar.png\" alt=\"\"> Avatar ändern</div>\n <div class=\"togetherjs-menu-item\" id=\"togetherjs-menu-update-color\"><span class=\"togetherjs-person-bgcolor-self\"></span> Profilfarbe ändern</div>\n <div class=\"togetherjs-hr\"></div>\n <div class=\"togetherjs-menu-item\" id=\"togetherjs-menu-help\">Hilfe</div>\n <div class=\"togetherjs-menu-item\" id=\"togetherjs-menu-feedback\">Rückmeldung</div>\n <div id=\"togetherjs-invite\" style=\"display: none\">\n <div class=\"togetherjs-hr\"></div>\n <div id=\"togetherjs-invite-users\"></div>\n <div class=\"togetherjs-menu-item\" id=\"togetherjs-menu-refresh-invite\">Refresh users</div>\n <div class=\"togetherjs-menu-item\" id=\"togetherjs-menu-invite-anyone\">Jemanden einladen</div>\n </div>\n <div class=\"togetherjs-hr\"></div>\n <div class=\"togetherjs-menu-item\" id=\"togetherjs-menu-end\"><img src=\"http://localhost:8080/togetherjs/images/button-end-session.png\" alt=\"\"> Beende <span class=\"togetherjs-tool-name\">TogetherJS</span></div>\n </div>\n\n <!-- template for one person in the invite-users list -->\n <div style=\"display: none\">\n <div id=\"togetherjs-template-invite-user-item\" class=\"togetherjs-menu-item\">\n <!-- FIXME: should include avatar in some way -->\n <span class=\"togetherjs-person-name\"></span>\n </div>\n </div>\n\n <!-- A window version of #togetherjs-menu, for use on mobile -->\n <div id=\"togetherjs-menu-window\" class=\"togetherjs-window\" style=\"display: none\">\n <header>Einstellungen und Profil</header>\n <section>\n <div class=\"togetherjs-menu-item\">\n <img class=\"togetherjs-menu-avatar\">\n <span class=\"togetherjs-person-name-self\" id=\"togetherjs-self-name-display\"></span>\n </div>\n <div class=\"togetherjs-menu-hr-avatar\"></div>\n <div class=\"togetherjs-menu-item\" id=\"togetherjs-menu-update-name-button\"><img src=\"http://localhost:8080/togetherjs/images/button-pencil.png\" alt=\"\"> Name ändern</div>\n <div class=\"togetherjs-menu-item\" id=\"togetherjs-menu-update-avatar-button\"><img src=\"http://localhost:8080/togetherjs/images/btn-menu-change-avatar.png\" alt=\"\"> Avatar ändern</div>\n <div class=\"togetherjs-menu-item\" id=\"togetherjs-menu-update-color-button\"><span class=\"togetherjs-person-bgcolor-self\"></span> Profilfarbe ändern</div>\n <div class=\"togetherjs-hr\"></div>\n <div class=\"togetherjs-menu-item\" id=\"togetherjs-menu-help-button\">Hilfe</div>\n <div class=\"togetherjs-menu-item\" id=\"togetherjs-menu-feedback-button\">Rückmeldung</div>\n <div class=\"togetherjs-hr\"></div>\n <div class=\"togetherjs-menu-item\" id=\"togetherjs-menu-end-button\"><img src=\"http://localhost:8080/togetherjs/images/button-end-session.png\" alt=\"\"> TOOL_NAME beenden</div>\n </section>\n <section class=\"togetherjs-buttons\">\n <button class=\"togetherjs-dismiss togetherjs-primary\">OK</button>\n </section>\n </div>\n\n <!-- The name editor, for use on mobile -->\n <div id=\"togetherjs-edit-name-window\" class=\"togetherjs-window\" style=\"display: none\">\n <header>Name ändern</header>\n <section>\n <div>\n <input class=\"togetherjs-self-name\" type=\"text\" placeholder=\"Namen eingeben\">\n </div>\n </section>\n <section class=\"togetherjs-buttons\">\n <button class=\"togetherjs-dismiss togetherjs-primary\">OK</button>\n </section>\n </div>\n\n <div class=\"togetherjs-menu\" id=\"togetherjs-pick-color\" style=\"display: none\">\n <div class=\"togetherjs-triangle-up\"><img src=\"http://localhost:8080/togetherjs/images/icn-triangle-up.png\"></div>\n <div style=\"display: none\">\n <div id=\"togetherjs-template-swatch\" class=\"togetherjs-swatch\">\n </div>\n </div>\n </div>\n\n <!-- Invisible elements that handle the RTC audio: -->\n <audio id=\"togetherjs-audio-element\"></audio>\n <audio id=\"togetherjs-local-audio\" muted=\"true\" volume=\"0.3\"></audio>\n <audio id=\"togetherjs-notification\" src=\"http://localhost:8080/togetherjs/images/notification.ogg\"></audio>\n\n <!-- The intro screen for someone who joins a session the first time: -->\n <div id=\"togetherjs-intro\" class=\"togetherjs-modal\" style=\"display: none\">\n <header>TOOL_NAME Sitzungsteilnahme?</header>\n <section>\n <p>Ein Freund hat zur TOOL_SITE_LINK browser Sitzung zur Echtzeitzusammenarbeit eingeladen!</p>\n <p>An deren Sitzung teilnehmen?</p>\n </section>\n\n <section class=\"togetherjs-buttons\">\n <button class=\"togetherjs-destructive togetherjs-modal-dont-join\">Nein, ablehnen</button>\n <button class=\"togetherjs-primary togetherjs-dismiss\">Ja, teilnehmen</button>\n </section>\n </div>\n\n <!-- Shown when a web browser is completely incapable of running TogetherJS: -->\n <div id=\"togetherjs-browser-broken\" class=\"togetherjs-modal\" style=\"display: none\">\n <header> Entschuldigung </header>\n\n <section>\n <p>\n Entschuldigung, TOOL_NAME läuft nicht mit diesem browser. Bitte auf einen <a href='https://github.com/mozilla/togetherjs/wiki/Supported-Browsers#supported-browsers'>unterstützten browser</a> aktualisieren, um TOOL_NAME auszuprobieren.\n </p>\n\n <p id=\"togetherjs-browser-broken-is-ie\" style=\"display: none\">\n Um TogetherJS auf Internet Explorer zu bringen wird Hilfe benötigt! Diese <a href=\"https://github.com/mozilla/togetherjs/issues?labels=IE&milestone=&page=1&state=open\" target=\"_blank\">Liste an IE GitHub Themen</a> muss behoben werden, um IE einzusetzen.\n <a href=\"https://github.com/mozilla/togetherjs/wiki/Supported-Browsers#internet-explorer\">Internet Explorer</a> wird momentan nicht unterstütztis currently not supported. Um TogetherJS auszuprobieren empfehlen wir Firefox oder Chrome.\n </p>\n </section>\n\n <section class=\"togetherjs-buttons\">\n <button class=\"togetherjs-dismiss togetherjs-primary\">TOOL_NAME beenden</button>\n </section>\n\n </div>\n\n <!-- Shown when the browser has WebSockets, but is IE (i.e., IE10) -->\n <div id=\"togetherjs-browser-unsupported\" class=\"togetherjs-modal\" style=\"display: none\">\n <header> Nicht unterstützter Browser </header>\n\n <section>\n <p>\n Um TogetherJS auf Internet Explorer zu bringen wird Hilfe benötigt! Diese <a href=\"https://github.com/mozilla/togetherjs/issues?labels=IE&milestone=&page=1&state=open\" target=\"_blank\">Liste an IE GitHub Themen</a> muss behoben werden, um IE einzusetzen.\n <a href=\"https://github.com/mozilla/togetherjs/wiki/Supported-Browsers#internet-explorer\">Internet Explorer</a> wird momentan nicht unterstütztis currently not supported. Um TogetherJS auszuprobieren empfehlen wir Firefox oder Chrome.\n </p>\n\n <p>Warnung. TOOL_NAME kann weiterhin eingesetzt werden, allerdings werden wahrscheinlich einige Fehler auftreten.</p>\n\n </section>\n\n <section class=\"togetherjs-buttons\">\n <button class=\"togetherjs-dismiss togetherjs-primary\">TOOL_NAME beenden</button>\n <button class=\"togetherjs-dismiss togetherjs-secondary togetherjs-browser-unsupported-anyway\">TOOL_NAME trotzdem ausprobieren</button>\n </section>\n\n </div>\n\n <div id=\"togetherjs-confirm-end\" class=\"togetherjs-modal\" style=\"display: none\">\n <header> Sitzung beenden? </header>\n <section>\n <p>\n Soll die TOOL_NAME Sitzung wirklich beendet werden?\n </p>\n </section>\n <section class=\"togetherjs-buttons\">\n <button class=\"togetherjs-cancel togetherjs-dismiss\">Abbrechen</button>\n <span class=\"togetherjs-alt-text\">oder</span>\n <button id=\"togetherjs-end-session\" class=\"togetherjs-destructive\">Sitzung beenden</button>\n </section>\n </div>\n\n <div id=\"togetherjs-feedback-form\" class=\"togetherjs-modal\" style=\"display: none;\">\n <header> Rückmeldung </header>\n <iframe src=\"https://docs.google.com/a/mozilla.com/forms/d/1lVE7JyRo_tjakN0mLG1Cd9X9vseBX9wci153z9JcNEs/viewform?embedded=true\" width=\"400\" height=\"300\" frameborder=\"0\" marginheight=\"0\" marginwidth=\"0\">Formular lädt ...</iframe>\n <!-- <p><button class=\"togetherjs-modal-close\">Close</button></p> -->\n </div>\n\n <div style=\"display: none\">\n <!-- This is when you join a session and the other person has already changed to another URL: -->\n <div id=\"togetherjs-template-url-change\" class=\"togetherjs-modal\">\n <header> Zur neuen Adresse folgen ... </header>\n <section>\n <div class=\"togetherjs-person\"></div>\n Folgt\n <span class=\"togetherjs-person-name\"></span>\n nach <a href=\"\" class=\"togetherjs-person-url togetherjs-person-url-title\"></a>\n </section>\n </div>\n\n <!-- This is when someone invites you to their session: -->\n <div id=\"togetherjs-template-invite\" class=\"togetherjs-chat-item\">\n <div class=\"togetherjs-person\"></div>\n <div>\n <span class=\"togetherjs-person-name\"></span>\n hat eingeladen <strong class=\"togetherjs-if-forEveryone\">jemand</strong>\n <strong class=\"togetherjs-ifnot-forEveryone\">you</strong>\n nach <a href=\"\" data-togetherjs-subattr-href=\"href\" class=\"togetherjs-sub-hrefTitle\" target=\"_blank\"></a>\n </div>\n </div>\n\n </div>\n\n <!-- The pointer at the side of a window: -->\n <div id=\"togetherjs-window-pointer-right\" style=\"display: none\"></div>\n <div id=\"togetherjs-window-pointer-left\" style=\"display: none\"></div>\n\n <!-- The element that overlaps the background of the page during a modal dialog: -->\n <div id=\"togetherjs-modal-background\" style=\"display: none\"></div>\n\n <!-- Some miscellaneous templates -->\n <div style=\"display: none\">\n\n <!-- This is the cursor: -->\n <div id=\"togetherjs-template-cursor\" class=\"togetherjs-cursor togetherjs\">\n <!-- Note: images/cursor.svg is a copy of this (for editing): -->\n <!-- crossbrowser svg dropshadow http://demosthenes.info/blog/600/Creating-a-True-CrossBrowser-Drop-Shadow- -->\n <svg version=\"1.1\" id=\"Layer_1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" x=\"0px\" y=\"0px\"\n \t width=\"15px\" height=\"22.838px\" viewBox=\"96.344 146.692 15 22.838\" enable-background=\"new 96.344 146.692 15 22.838\"\n \t xml:space=\"preserve\">\n <path fill=\"#231F20\" d=\"M98.984,146.692c2.167,1.322,1.624,6.067,3.773,7.298c-0.072-0.488,2.512-0.931,3.097,0\n \tc0.503,0.337,1.104-0.846,2.653,0.443c0.555,0.593,3.258,2.179,1.001,8.851c-0.446,1.316,2.854,0.135,1.169,2.619\n \tc-3.748,5.521-9.455,2.787-9.062,1.746c1.06-2.809-6.889-4.885-4.97-9.896c0.834-2.559,2.898,0.653,2.923,0.29\n \tc-0.434-1.07-2.608-5.541-2.923-6.985C96.587,150.793,95.342,147.033,98.984,146.692z\"/>\n </svg>\n <!-- <img class=\"togetherjs-cursor-img\" src=\"http://localhost:8080/togetherjs/images/cursor.svg\"> -->\n <span class=\"togetherjs-cursor-container\">\n <span class=\"togetherjs-cursor-name\"></span>\n <span style=\"display:none\" class=\"togetherjs-cursor-typing\" id=\"togetherjs-cursor-typebox\">\n <span class=\"togetherjs-typing-ellipse-one\">&#9679;</span><span class=\"togetherjs-typing-ellipse-two\">&#9679;</span><span class=\"togetherjs-typing-ellipse-three\">&#9679;</span>\n </span>\n <!-- Displayed when the cursor is below the screen: -->\n <span class=\"togetherjs-cursor-down\">\n\n </span>\n <!-- Displayed when the cursor is above the screen: -->\n <span class=\"togetherjs-cursor-up\">\n\n </span>\n </span>\n </div>\n\n <!-- This is the element that goes around focused form elements: -->\n <div id=\"togetherjs-template-focus\">\n <div class=\"togetherjs-focus togetherjs-person-bordercolor\"></div>\n </div>\n\n <!-- This is a click: -->\n <div id=\"togetherjs-template-click\" class=\"togetherjs-click togetherjs\">\n </div>\n </div>\n</div>\n",
  9399. walkthrough: "<!--\n Any elements with .togetherjs-walkthrough-firsttime will only be\n displayed on during the first-time experience. Any elements with\n .togetherjs-walkthrough-not-firsttime will only be displayed when\n the walkthrough is accessed through the Help menu.\n\n Note you *cannot* use <section class=\"togetherjs-walkthrough-slide\n togetherjs-walkthrough-firsttime\">: the number of sections must be the\n same regardless.\n -->\n<div id=\"togetherjs-walkthrough\" class=\"togetherjs-modal togetherjs-modal-wide\">\n <header>TOOL_NAME wird genutzt!<button class=\"togetherjs-close\"></button></header>\n\n <div id=\"togetherjs-walkthrough-previous\"></div>\n <div id=\"togetherjs-walkthrough-next\"></div>\n\n <section class=\"togetherjs-walkthrough-slide\">\n <p class=\"togetherjs-walkthrough-main-image\"><img src=\"http://localhost:8080/togetherjs/images/walkthrough-images-intro.png\"></p>\n\t<p>TOOL_NAME ist ein einfacher Dienst zur Zusammenarbeit in Echtzeit auf SITE_NAME </p>\n </section>\n\n <section class=\"togetherjs-walkthrough-slide\">\n <div class=\"togetherjs-walkthrough-firsttime\">\n <div class=\"togetherjs-walkthrough-main-image\">\n <div class=\"togetherjs-walkthrough-avatar-section\">\n <div class=\"togetherjs-avatar-preview togetherjs-person togetherjs-person-self\"></div>\n <div class=\"togetherjs-avatar-upload-input\"><input type=\"file\" class=\"togetherjs-upload-avatar\"></div>\n </div>\n <input class=\"togetherjs-self-name\" type=\"text\" placeholder=\"Namen eingeben\">\n <div class=\"togetherjs-swatch togetherjs-person-bgcolor-self\"></div>\n <div class=\"togetherjs-save-settings\">\n <button class=\"togetherjs-avatar-save togetherjs-primary\">\n <span id=\"togetherjs-avatar-when-unsaved\">\"\"Speichern\"\"</span>\n <span id=\"togetherjs-avatar-when-saved\" style=\"display: none\">Gespeichert!</span>\n </button>\n </div>\n </div>\n <p>Avatar, Name und Profilfarbe oben anlegen. Um diese später zu ändern, die Profiltaste klicken.</p>\n </div>\n <div class=\"togetherjs-walkthrough-not-firsttime\">\n <p class=\"togetherjs-walkthrough-main-image\"><img src=\"http://localhost:8080/togetherjs/images/walkthrough-images-profile.png\"></p>\n <p>Avatar, Name und Profilfarbe über die Profil Taste ändern.</p>\n </div>\n </section>\n\n <section class=\"togetherjs-walkthrough-slide\">\n <p class=\"togetherjs-walkthrough-main-image togetherjs-ifnot-creator\"><img src=\"http://localhost:8080/togetherjs/images/walkthrough-images-invite.png\">\n </p>\n <p class=\"togetherjs-ifnot-creator\">Weitere Freunde können über Versendung des Einladungsverweis in der TOOL_NAME Konsole eingeladen werden.</p>\n <p class=\"togetherjs-walkthrough-main-image togetherjs-if-creator\">\n <span class=\"togetherjs-walkthrough-sendlink\">\n Diese Adresse an Freunde senden, um sie einzuladen.\n </span>\n <input type=\"text\" class=\"togetherjs-share-link\">\n </p>\n <p class=\"togetherjs-if-creator\">Die obige Adresse an Freunde schicken, um diese zur Sitzung einzuladen! Diese findet sich auch in der TOOL_NAME Konsole.</p>\n </section>\n\n <section class=\"togetherjs-walkthrough-slide\">\n <p class=\"togetherjs-walkthrough-main-image\"><img src=\"http://localhost:8080/togetherjs/images/walkthrough-images-participant.png\"></p>\n <p>Freunde, die an der TOOL_NAME Sitzung teilnehmen, erscheinen hier. Auf die Avatare klicken, um mehr zu sehen.</p>\n </section>\n\n <section class=\"togetherjs-walkthrough-slide\">\n <p class=\"togetherjs-walkthrough-main-image\"><img src=\"http://localhost:8080/togetherjs/images/walkthrough-images-chat.png\"></p>\n <p>Wenn Freunde an der TOOL_NAME Sitzung teilnehmen, können hier Nachrichten versandt werden!</p>\n </section>\n\n <section class=\"togetherjs-walkthrough-slide\">\n <p class=\"togetherjs-walkthrough-main-image\"><img src=\"http://localhost:8080/togetherjs/images/walkthrough-images-rtc.png\"></p>\n <p>Mikrofon klicken, um eine Audiokonferenz zu beginnen, falls vom browser unterstützt. Info zur <a href=\"https://github.com/mozilla/togetherjs/wiki/About-Audio-Chat-and-WebRTC\" target=\"_blank\">experimentellen Funktion</a>.</p>\n </section>\n\n <section class=\"togetherjs-walkthrough-slide\">\n <p class=\"togetherjs-walkthrough-main-image\"><img src=\"http://localhost:8080/togetherjs/images/walkthrough-images-logo.png\"></p>\n <p>TOOL_NAME ist einsatzbereit. Jetzt auf SITE_NAME zusammenarbeiten!</p>\n </section>\n\n <div style=\"display: none\">\n <!-- There is one of these created for each slide: -->\n <span id=\"togetherjs-template-walkthrough-slide-progress\" class=\"togetherjs-walkthrough-slide-progress\">&#9679;</span>\n </div>\n <section id=\"togetherjs-walkthrough-progress\">\n </section>\n\n <section class=\"togetherjs-buttons\">\n <button class=\"togetherjs-primary togetherjs-dismiss\">Bereit!</button>\n </section>\n\n</div><!-- /.togetherjs-modal -->\n",
  9400. names: "Freundlicher Fuchs, Brillianter Biber, Äugende Eule, Gesellige Giraffe, Wilder Wolf, Stiller Seehund, Wahnsinniger Wal, Curious Katze, Intelligent Leguan"
  9401. };
  9402. });
  9403. /* This Source Code Form is subject to the terms of the Mozilla Public
  9404. * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  9405. * You can obtain one at http://mozilla.org/MPL/2.0/. */
  9406. define('templates-en-US',[], function () {
  9407. return {
  9408. "interface": "<% /*\n This is basically all the markup and interface for TogetherJS.\n Note all links should be like http://localhost:8080/togetherjs/*\n these links are rewritten with the location where TogetherJS was deployed.\n\n This file is inlined into togetherjs/templates.js\n*/ %>\n<div id=\"togetherjs-container\" class=\"togetherjs\">\n\n <!-- This is the main set of buttons: -->\n <div id=\"togetherjs-dock\" class=\"togetherjs-dock-right\">\n <div id=\"togetherjs-dock-anchor\" title=\"Move the dock\">\n <span id=\"togetherjs-dock-anchor-horizontal\">\n <img src=\"http://localhost:8080/togetherjs/images/icn-handle-circle@2x.png\" alt=\"drag\">\n </span>\n <span id=\"togetherjs-dock-anchor-vertical\">\n <img src=\"http://localhost:8080/togetherjs/images/icn-handle-circle@2x.png\" alt=\"drag\">\n </span>\n </div>\n <div id=\"togetherjs-buttons\">\n <div style=\"display: none\">\n <button id=\"togetherjs-template-dock-person\" class=\"togetherjs-button togetherjs-dock-person\">\n <div class=\"togetherjs-tooltip togetherjs-dock-person-tooltip\">\n <span class=\"togetherjs-person-name\"></span>\n <span class=\"togetherjs-person-tooltip-arrow-r\"></span>\n </div>\n <div class=\"togetherjs-person togetherjs-person-status-overlay\"></div>\n </button>\n </div>\n <button id=\"togetherjs-profile-button\" class=\"togetherjs-button\" title=\"This is you\">\n <div class=\"togetherjs-person togetherjs-person-self\"></div>\n <div id=\"togetherjs-profile-arrow\"></div>\n </button>\n <button id=\"togetherjs-share-button\" class=\"togetherjs-button\" title=\"Add a friend\"></button>\n <button id=\"togetherjs-audio-button\" class=\"togetherjs-button\" title=\"Turn on microphone\">\n <span id=\"togetherjs-audio-unavailable\" class=\"togetherjs-audio-set\" data-toggles=\".togetherjs-audio-set\">\n </span>\n <span id=\"togetherjs-audio-ready\" class=\"togetherjs-audio-set\" data-toggles=\".togetherjs-audio-set\" style=\"display: none\">\n </span>\n <span id=\"togetherjs-audio-outgoing\" class=\"togetherjs-audio-set\" data-toggles=\".togetherjs-audio-set\" style=\"display: none\">\n </span>\n <span id=\"togetherjs-audio-incoming\" class=\"togetherjs-audio-set\" data-toggles=\".togetherjs-audio-set\" style=\"display: none\">\n </span>\n <span id=\"togetherjs-audio-active\" class=\"togetherjs-audio-set\" data-toggles=\".togetherjs-audio-set\" style=\"display: none\">\n </span>\n <span id=\"togetherjs-audio-muted\" class=\"togetherjs-audio-set\" data-toggles=\".togetherjs-audio-set\" style=\"display: none\">\n </span>\n <span id=\"togetherjs-audio-error\" class=\"togetherjs-audio-set\" data-toggles=\".togetherjs-audio-set\" style=\"display: none\">\n </span>\n </button>\n <button id=\"togetherjs-chat-button\" class=\"togetherjs-button\" title=\"Chat\"></button>\n <div id=\"togetherjs-dock-participants\"></div>\n </div>\n </div>\n\n <!-- The window for editing the avatar: -->\n <div id=\"togetherjs-avatar-edit\" class=\"togetherjs-modal\"\n style=\"display: none\">\n <header> Update avatar </header>\n <section>\n <div class=\"togetherjs-avatar-preview togetherjs-person togetherjs-person-self\"></div>\n <div id=\"togetherjs-avatar-buttons\">\n <input type=\"file\" class=\"togetherjs-upload-avatar\">\n <!--<button id=\"togetherjs-upload-avatar\" class=\"togetherjs-primary\">Upload a picture</button>-->\n <!--<button id=\"togetherjs-camera-avatar\" class=\"togetherjs-default\">Take a picture</button>-->\n </div>\n </section>\n <section class=\"togetherjs-buttons\">\n <button class=\"togetherjs-cancel togetherjs-dismiss\">Cancel</button>\n <span class=\"togetherjs-alt-text\">or</span>\n <button class=\"togetherjs-avatar-save togetherjs-primary\">Save</button>\n </section>\n </div>\n\n <!-- The window for sharing the link: -->\n <div id=\"togetherjs-share\" class=\"togetherjs-window\"\n data-bind-to=\"#togetherjs-share-button\" style=\"display: none\">\n <header> Invite a friend </header>\n <section>\n <div class=\"togetherjs-not-mobile\">\n <p>Copy and paste this link over IM or email:</p>\n <input type=\"text\" class=\"togetherjs-share-link\">\n </div>\n <div class=\"togetherjs-only-mobile\">\n <p>Copy and paste this link over IM or email:</p>\n <input type=\"text\" class=\"togetherjs-share-link\">\n <!-- <a class=\"togetherjs-share-link\" href=\"#\">Press your thumb here.</a> -->\n </div>\n </section>\n </div>\n\n <!-- Participant Full List view template: -->\n <div id=\"togetherjs-participantlist\" class=\"togetherjs-window\"\n data-bind-to=\"#togetherjs-participantlist-button\" style=\"display: none\">\n <header> Participants </header>\n <section>\n <div class=\"togetherjs-not-mobile\">\n <ul>\n <li id=\"togetherjs-participant-item\">\n <img class=\"togetherjs-person togetherjs-person-small\" src=\"http://localhost:8080/togetherjs/images/btn-menu-change-avatar.png\">\n <span class=\"tj-name togetherjs-person-name\">NAME</span>\n <span class=\"tj-status\">&#9679;</span>\n <p class=\"tj-urllocation\">Currently at: <a class=\"togetherjs-person-url togetherjs-person-url-title\" href=\"\">http://www.location.comwww.location.comwww.location.comasdfsafd</a></p>\n <p class=\"tj-follow\">Follow:\n <label class=\"togetherjs-follow-question\" for=\"togetherjs-person-status-follow\">\n <input type=\"checkbox\" id=\"togetherjs-person-status-follow\">\n </label>\n </p>\n <section class=\"togetherjs-buttons\">\n <!-- Displayed when the peer is at a different URL: -->\n <div class=\"togetherjs-different-url\">\n <a class=\"togetherjs-nudge togetherjs-default tj-btn-sm\">Nudge them</a>\n <a href=\"#\" class=\"togetherjs-follow togetherjs-person-url togetherjs-primary tj-btn-sm\">Join them</a>\n </div>\n <!-- Displayed when the peer is at your same URL: -->\n <div class=\"togetherjs-same-url\" style=\"display: none\">\n <span class=\"togetherjs-person-name\"></span> is on the same page as you.\n </div>\n </section>\n </li>\n </ul>\n </section>\n </div>\n\n <!-- Participant detail template: -->\n <div id=\"togetherjs-template-participant-window\" class=\"togetherjs-window\" style=\"display: none\">\n <header><div class=\"togetherjs-person togetherjs-person-small\"></div><span class=\"togetherjs-person-name\"></span></header>\n\n <section class=\"togetherjs-participant-window-main\">\n <p class=\"togetherjs-participant-window-row\"><strong>Role:</strong>\n <span class=\"togetherjs-person-role\"></span>\n </p>\n\n <p class=\"togetherjs-participant-window-row\"><strong>Currently at:</strong>\n <a class=\"togetherjs-person-url togetherjs-person-url-title\"></a>\n </p>\n\n <p class=\"togetherjs-participant-window-row\"><strong>Status:</strong>\n <span class=\"togetherjs-person-status\"></span>\n </p>\n\n <p class=\"togetherjs-participant-window-row\"><strong class=\"togetherjs-float-left\">Follow this participant:</strong>\n <label class=\"togetherjs-follow-question togetherjs-float-left\" for=\"togetherjs-person-status-follow\">\n <input type=\"checkbox\" id=\"togetherjs-person-status-follow\">\n </label>\n <span class=\"togetherjs-clear\"></span>\n </p>\n\n </section>\n\n <section class=\"togetherjs-buttons\">\n <!-- Displayed when the peer is at a different URL: -->\n <div class=\"togetherjs-different-url\">\n <a class=\"togetherjs-nudge togetherjs-default\">Nudge them</a>\n <a href=\"#\" class=\"togetherjs-follow togetherjs-person-url togetherjs-primary\">Join them</a>\n </div>\n <!-- Displayed when the peer is at your same URL: -->\n <div class=\"togetherjs-same-url\" style=\"display: none\">\n <span class=\"togetherjs-person-name\"></span> is on the same page as you.\n </div>\n </section>\n </div>\n\n <!-- The chat screen: -->\n <div id=\"togetherjs-chat\" class=\"togetherjs-window\" data-bind-to=\"#togetherjs-chat-button\"\n style=\"display: none\">\n <header> Chat </header>\n <section class=\"togetherjs-subtitle\">\n <div id=\"togetherjs-chat-participants\" data-toggles=\"#togetherjs-chat-no-participants\" style=\"display: none\">\n <span id=\"togetherjs-chat-participant-list\"></span>\n &amp; You\n </div>\n <div id=\"togetherjs-chat-no-participants\" data-toggles=\"#togetherjs-chat-participants\">\n No one else is here.\n </div>\n </section>\n\n <div style=\"display: none\">\n\n <!-- Template for one message: -->\n <div id=\"togetherjs-template-chat-message\" class=\"togetherjs-chat-item togetherjs-chat-message\">\n <div class=\"togetherjs-person\"></div>\n <div class=\"togetherjs-timestamp\"><span class=\"togetherjs-time\">HH:MM</span> <span class=\"togetherjs-ampm\">AM/PM</span></div>\n <div class=\"togetherjs-person-name-abbrev\"></div>\n <div class=\"togetherjs-chat-content togetherjs-sub-content\"></div>\n </div>\n\n <!-- Template for when a person leaves: -->\n <div id=\"togetherjs-template-chat-left\" class=\"togetherjs-chat-item togetherjs-chat-left-item\">\n <div class=\"togetherjs-person\"></div>\n <div class=\"togetherjs-ifnot-declinedJoin\">\n <div class=\"togetherjs-inline-text\"><span class=\"togetherjs-person-name\"></span> left the session.</div>\n </div>\n <div class=\"togetherjs-if-declinedJoin\">\n <div class=\"togetherjs-inline-text\"><span class=\"togetherjs-person-name\"></span> declined to join the session.</div>\n </div>\n <div class=\"togetherjs-clear\"></div>\n </div>\n\n <!-- Template when a person joins the session: -->\n <div id=\"togetherjs-template-chat-joined\" class=\"togetherjs-chat-item togetherjs-chat-join-item\">\n <div class=\"togetherjs-person\"></div>\n <div class=\"togetherjs-inline-text\"><span class=\"togetherjs-person-name\"></span> joined the session.</div>\n <div class=\"togetherjs-clear\"></div>\n </div>\n\n <!-- Template for system-derived messages: -->\n <div id=\"togetherjs-template-chat-system\" class=\"togetherjs-chat-item\">\n <span class=\"togetherjs-chat-content togetherjs-sub-content\"></span>\n </div>\n\n <!-- Template when a person joins the session: -->\n <!-- <div id=\"togetherjs-template-chat-joined\" class=\"togetherjs-chat-item togetherjs-chat-join-item\">\n <div class=\"togetherjs-person\"></div>\n <div class=\"togetherjs-inline-text\"><span class=\"togetherjs-person-name\"></span> joined the session.</div>\n <div class=\"togetherjs-clear\"></div>\n </div> -->\n\n <!-- Template for when someone goes to a new URL: -->\n <div id=\"togetherjs-template-url-change\" class=\"togetherjs-chat-item togetherjs-chat-url-change\">\n <div class=\"togetherjs-person\"></div>\n <div class=\"togetherjs-inline-text\">\n <div class=\"togetherjs-if-sameUrl\">\n <span class=\"togetherjs-person-name\"></span>\n is on the same page as you.\n </div>\n <div class=\"togetherjs-ifnot-sameUrl\">\n <span class=\"togetherjs-person-name\"></span>\n has gone to: <a href=\"#\" class=\"togetherjs-person-url togetherjs-person-url-title\" target=\"_self\"></a>\n <section class=\"togetherjs-buttons togetherjs-buttons-notification-diff-url\">\n <!-- Displayed when the peer is at a different URL: -->\n <div class=\"togetherjs-different-url togetherjs-notification-diff-url\">\n <a class=\"togetherjs-nudge togetherjs-default\">Nudge them</a>\n <a href=\"#\" class=\"togetherjs-follow togetherjs-person-url togetherjs-primary\">Join them</a>\n </div>\n </section>\n\n <!-- <div>\n <a class=\"togetherjs-nudge togetherjs-secondary\">Nudge them</a>\n <a href=\"\" class=\"togetherjs-person-url togetherjs-follow togetherjs-primary\">Join them</a>\n </div> -->\n\n </div>\n </div>\n <div class=\"togetherjs-clear\"></div>\n </div>\n </div>\n\n <section id=\"togetherjs-chat-messages\">\n <!-- FIX ME// need to have some dialogue that says something like - There are no chats yet! -->\n </section>\n <section id=\"togetherjs-chat-input-box\">\n <textarea id=\"togetherjs-chat-input\" placeholder=\"Type your message here\"></textarea>\n </section>\n </div>\n\n <!-- this is a kind of warning popped up when you (successfully) start RTC: -->\n <div id=\"togetherjs-rtc-info\" class=\"togetherjs-window\"\n data-bind-to=\"#togetherjs-audio-button\"\n style=\"display: none\">\n\n <header> Audio Chat </header>\n <section>\n <p>\n Activate your <strong>browser microphone</strong> near your URL bar above.\n </p>\n <p>\n Talking on your microphone through your web browser is an experimental feature.\n </p>\n <p>\n Read more about Audio Chat <a href=\"https://github.com/mozilla/togetherjs/wiki/About-Audio-Chat-and-WebRTC\" target=\"_blank\">here</a>.\n </p>\n </section>\n\n <section class=\"togetherjs-buttons\">\n <label for=\"togetherjs-rtc-info-dismiss\" style=\"display: inline;\">\n <input class=\"togetherjs-dont-show-again\" id=\"togetherjs-rtc-info-dismiss\" type=\"checkbox\">\n Don't show again.\n </label>\n <button class=\"togetherjs-default togetherjs-dismiss\" type=\"button\">Close</button>\n </section>\n </div>\n\n <!-- this is popped up when you hit the audio button, but RTC isn't\n supported: -->\n <div id=\"togetherjs-rtc-not-supported\" class=\"togetherjs-window\"\n data-bind-to=\"#togetherjs-audio-button\"\n style=\"display: none\">\n <header> Audio Chat </header>\n\n <section>\n <p>Audio chat requires you to use a <a href='https://github.com/mozilla/togetherjs/wiki/About-Audio-Chat-and-WebRTC' target='_blank'>newer browser</a>!</p>\n <p>\n Live audio chat requires a newer (or different) browser than you're using.\n </p>\n <p>\n See <a href='https://github.com/mozilla/togetherjs/wiki/About-Audio-Chat-and-WebRTC' target='_blank'>this page</a>for more information and a list of supported browsers.\n </p>\n </section>\n\n <section class=\"togetherjs-buttons\">\n <div class=\"togetherjs-rtc-dialog-btn\">\n <button class=\"togetherjs-default togetherjs-dismiss\" type=\"button\">Close</button>\n </div>\n </section>\n </div>\n\n <!-- The popup when a chat message comes in and the #togetherjs-chat window isn't open -->\n <div id=\"togetherjs-chat-notifier\" class=\"togetherjs-notification\"\n data-bind-to=\"#togetherjs-chat-button\"\n style=\"display: none\">\n <img src=\"http://localhost:8080/togetherjs/images/notification-togetherjs-logo.png\" class=\"togetherjs-notification-logo\" alt=\"\">\n <img src=\"http://localhost:8080/togetherjs/images/notification-btn-close.png\" class=\"togetherjs-notification-closebtn togetherjs-dismiss\" alt=\"[close]\">\n <section id=\"togetherjs-chat-notifier-message\">\n </section>\n </div>\n\n <!-- The menu when you click on the profile: -->\n <div id=\"togetherjs-menu\" class=\"togetherjs-menu\" style=\"display: none\">\n <div class=\"togetherjs-menu-item togetherjs-menu-disabled\" id=\"togetherjs-menu-profile\">\n <img id=\"togetherjs-menu-avatar\">\n <span class=\"togetherjs-person-name-self\" id=\"togetherjs-self-name-display\" data-toggles=\"#togetherjs-menu .togetherjs-self-name\">[nickname]</span>\n <input class=\"togetherjs-self-name\" type=\"text\" data-toggles=\"#togetherjs-self-name-display\" style=\"display: none\" placeholder=\"Enter your name\">\n </div>\n <div class=\"togetherjs-menu-hr-avatar\"></div>\n <div class=\"togetherjs-menu-item\" id=\"togetherjs-menu-update-name\"><img src=\"http://localhost:8080/togetherjs/images/button-pencil.png\" alt=\"\"> Update your name</div>\n <div class=\"togetherjs-menu-item\" id=\"togetherjs-menu-update-avatar\"><img src=\"http://localhost:8080/togetherjs/images/btn-menu-change-avatar.png\" alt=\"\"> Change avatar</div>\n <div class=\"togetherjs-menu-item\" id=\"togetherjs-menu-update-color\"><span class=\"togetherjs-person-bgcolor-self\"></span> Pick profile color</div>\n <div class=\"togetherjs-hr\"></div>\n <div class=\"togetherjs-menu-item\" id=\"togetherjs-menu-help\">Help</div>\n <div class=\"togetherjs-menu-item\" id=\"togetherjs-menu-feedback\">Feedback</div>\n <div id=\"togetherjs-invite\" style=\"display: none\">\n <div class=\"togetherjs-hr\"></div>\n <div id=\"togetherjs-invite-users\"></div>\n <div class=\"togetherjs-menu-item\" id=\"togetherjs-menu-refresh-invite\">Refresh users</div>\n <div class=\"togetherjs-menu-item\" id=\"togetherjs-menu-invite-anyone\">Invite anyone</div>\n </div>\n <div class=\"togetherjs-hr\"></div>\n <div class=\"togetherjs-menu-item\" id=\"togetherjs-menu-end\"><img src=\"http://localhost:8080/togetherjs/images/button-end-session.png\" alt=\"\"> End <span class=\"togetherjs-tool-name\">TogetherJS</span></div>\n </div>\n\n <!-- template for one person in the invite-users list -->\n <div style=\"display: none\">\n <div id=\"togetherjs-template-invite-user-item\" class=\"togetherjs-menu-item\">\n <!-- FIXME: should include avatar in some way -->\n <span class=\"togetherjs-person-name\"></span>\n </div>\n </div>\n\n <!-- A window version of #togetherjs-menu, for use on mobile -->\n <div id=\"togetherjs-menu-window\" class=\"togetherjs-window\" style=\"display: none\">\n <header>Settings and Profile</header>\n <section>\n <div class=\"togetherjs-menu-item\">\n <img class=\"togetherjs-menu-avatar\">\n <span class=\"togetherjs-person-name-self\" id=\"togetherjs-self-name-display\"></span>\n </div>\n <div class=\"togetherjs-menu-hr-avatar\"></div>\n <div class=\"togetherjs-menu-item\" id=\"togetherjs-menu-update-name-button\"><img src=\"http://localhost:8080/togetherjs/images/button-pencil.png\" alt=\"\"> Update your name</div>\n <div class=\"togetherjs-menu-item\" id=\"togetherjs-menu-update-avatar-button\"><img src=\"http://localhost:8080/togetherjs/images/btn-menu-change-avatar.png\" alt=\"\"> Change avatar</div>\n <div class=\"togetherjs-menu-item\" id=\"togetherjs-menu-update-color-button\"><span class=\"togetherjs-person-bgcolor-self\"></span> Pick profile color</div>\n <div class=\"togetherjs-hr\"></div>\n <div class=\"togetherjs-menu-item\" id=\"togetherjs-menu-help-button\">Help</div>\n <div class=\"togetherjs-menu-item\" id=\"togetherjs-menu-feedback-button\">Feedback</div>\n <div class=\"togetherjs-hr\"></div>\n <div class=\"togetherjs-menu-item\" id=\"togetherjs-menu-end-button\"><img src=\"http://localhost:8080/togetherjs/images/button-end-session.png\" alt=\"\"> End TOOL_NAME</div>\n </section>\n <section class=\"togetherjs-buttons\">\n <button class=\"togetherjs-dismiss togetherjs-primary\">OK</button>\n </section>\n </div>\n\n <!-- The name editor, for use on mobile -->\n <div id=\"togetherjs-edit-name-window\" class=\"togetherjs-window\" style=\"display: none\">\n <header>Update Name</header>\n <section>\n <div>\n <input class=\"togetherjs-self-name\" type=\"text\" placeholder=\"Enter your name\">\n </div>\n </section>\n <section class=\"togetherjs-buttons\">\n <button class=\"togetherjs-dismiss togetherjs-primary\">OK</button>\n </section>\n </div>\n\n <div class=\"togetherjs-menu\" id=\"togetherjs-pick-color\" style=\"display: none\">\n <div class=\"togetherjs-triangle-up\"><img src=\"http://localhost:8080/togetherjs/images/icn-triangle-up.png\"></div>\n <div style=\"display: none\">\n <div id=\"togetherjs-template-swatch\" class=\"togetherjs-swatch\">\n </div>\n </div>\n </div>\n\n <!-- Invisible elements that handle the RTC audio: -->\n <audio id=\"togetherjs-audio-element\"></audio>\n <audio id=\"togetherjs-local-audio\" muted=\"true\" volume=\"0.3\"></audio>\n <audio id=\"togetherjs-notification\" src=\"http://localhost:8080/togetherjs/images/notification.ogg\"></audio>\n\n <!-- The intro screen for someone who joins a session the first time: -->\n <div id=\"togetherjs-intro\" class=\"togetherjs-modal\" style=\"display: none\">\n <header>Join TOOL_NAME session?</header>\n <section>\n <p>Your friend has asked you to join their TOOL_SITE_LINK browser session to collaborate in real-time!</p>\n <p>Would you like to join their session?</p>\n </section>\n\n <section class=\"togetherjs-buttons\">\n <button class=\"togetherjs-destructive togetherjs-modal-dont-join\">No, don't join</button>\n <button class=\"togetherjs-primary togetherjs-dismiss\">Yes, join session</button>\n </section>\n </div>\n\n <!-- Shown when a web browser is completely incapable of running TogetherJS: -->\n <div id=\"togetherjs-browser-broken\" class=\"togetherjs-modal\" style=\"display: none\">\n <header> Sorry </header>\n\n <section>\n <p>\n We're sorry, TOOL_NAME doesn't work with this browser. Please <a href='https://github.com/mozilla/togetherjs/wiki/Supported-Browsers#supported-browsers'>upgrade to a supported browser</a> to try TOOL_NAME.\n </p>\n\n <p id=\"togetherjs-browser-broken-is-ie\" style=\"display: none\">\n We need your help fixing TogetherJS on Internet Explorer! Here are a list of IE <a href=\"https://github.com/mozilla/togetherjs/issues?labels=IE&milestone=&page=1&state=open\" target=\"_blank\">GitHub issues</a> we need fixed that you can work on.\n Internet Explorer <a href=\"https://github.com/mozilla/togetherjs/wiki/Supported-Browsers#internet-explorer\">is currently not supported</a>. If you do want to try out TogetherJS, we'd suggest using Firefox or Chrome.\n </p>\n </section>\n\n <section class=\"togetherjs-buttons\">\n <button class=\"togetherjs-dismiss togetherjs-primary\">End TOOL_NAME</button>\n </section>\n\n </div>\n\n <!-- Shown when the browser has WebSockets, but is IE (i.e., IE10) -->\n <div id=\"togetherjs-browser-unsupported\" class=\"togetherjs-modal\" style=\"display: none\">\n <header> Unsupported Browser </header>\n\n <section>\n <p>\n We need your help fixing TogetherJS on Internet Explorer! Here are a list of IE <a href=\"https://github.com/mozilla/togetherjs/issues?labels=IE&milestone=&page=1&state=open\" target=\"_blank\">GitHub issues</a> we need fixed that you can work on.\n Internet Explorer <a href=\"https://github.com/mozilla/togetherjs/wiki/Supported-Browsers#internet-explorer\">is currently not supported</a>. If you do want to try out TogetherJS, we'd suggest using Firefox or Chrome.\n </p>\n\n <p>You can continue to try to use TOOL_NAME, but you are likely to hit lots of bugs. So be warned.</p>\n\n </section>\n\n <section class=\"togetherjs-buttons\">\n <button class=\"togetherjs-dismiss togetherjs-primary\">End TOOL_NAME</button>\n <button class=\"togetherjs-dismiss togetherjs-secondary togetherjs-browser-unsupported-anyway\">Try TOOL_NAME Anyway</button>\n </section>\n\n </div>\n\n <div id=\"togetherjs-confirm-end\" class=\"togetherjs-modal\" style=\"display: none\">\n <header> End session? </header>\n <section>\n <p>\n Are you sure you'd like to end your TOOL_NAME session?\n </p>\n </section>\n <section class=\"togetherjs-buttons\">\n <button class=\"togetherjs-cancel togetherjs-dismiss\">Cancel</button>\n <span class=\"togetherjs-alt-text\">or</span>\n <button id=\"togetherjs-end-session\" class=\"togetherjs-destructive\">End session</button>\n </section>\n </div>\n\n <div id=\"togetherjs-feedback-form\" class=\"togetherjs-modal\" style=\"display: none;\">\n <header> Feedback </header>\n <iframe src=\"https://docs.google.com/a/mozilla.com/forms/d/1lVE7JyRo_tjakN0mLG1Cd9X9vseBX9wci153z9JcNEs/viewform?embedded=true\" width=\"400\" height=\"300\" frameborder=\"0\" marginheight=\"0\" marginwidth=\"0\">Loading form...</iframe>\n <!-- <p><button class=\"togetherjs-modal-close\">Close</button></p> -->\n </div>\n\n <div style=\"display: none\">\n <!-- This is when you join a session and the other person has already changed to another URL: -->\n <div id=\"togetherjs-template-url-change\" class=\"togetherjs-modal\">\n <header> Following to new URL... </header>\n <section>\n <div class=\"togetherjs-person\"></div>\n Following\n <span class=\"togetherjs-person-name\"></span>\n to <a href=\"\" class=\"togetherjs-person-url togetherjs-person-url-title\"></a>\n </section>\n </div>\n\n <!-- This is when someone invites you to their session: -->\n <div id=\"togetherjs-template-invite\" class=\"togetherjs-chat-item\">\n <div class=\"togetherjs-person\"></div>\n <div>\n <span class=\"togetherjs-person-name\"></span>\n has invited <strong class=\"togetherjs-if-forEveryone\">anyone</strong>\n <strong class=\"togetherjs-ifnot-forEveryone\">you</strong>\n to <a href=\"\" data-togetherjs-subattr-href=\"href\" class=\"togetherjs-sub-hrefTitle\" target=\"_blank\"></a>\n </div>\n </div>\n\n </div>\n\n <!-- The pointer at the side of a window: -->\n <div id=\"togetherjs-window-pointer-right\" style=\"display: none\"></div>\n <div id=\"togetherjs-window-pointer-left\" style=\"display: none\"></div>\n\n <!-- The element that overlaps the background of the page during a modal dialog: -->\n <div id=\"togetherjs-modal-background\" style=\"display: none\"></div>\n\n <!-- Some miscellaneous templates -->\n <div style=\"display: none\">\n\n <!-- This is the cursor: -->\n <div id=\"togetherjs-template-cursor\" class=\"togetherjs-cursor togetherjs\">\n <!-- Note: images/cursor.svg is a copy of this (for editing): -->\n <!-- crossbrowser svg dropshadow http://demosthenes.info/blog/600/Creating-a-True-CrossBrowser-Drop-Shadow- -->\n <svg version=\"1.1\" id=\"Layer_1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" x=\"0px\" y=\"0px\"\n \t width=\"15px\" height=\"22.838px\" viewBox=\"96.344 146.692 15 22.838\" enable-background=\"new 96.344 146.692 15 22.838\"\n \t xml:space=\"preserve\">\n <path fill=\"#231F20\" d=\"M98.984,146.692c2.167,1.322,1.624,6.067,3.773,7.298c-0.072-0.488,2.512-0.931,3.097,0\n \tc0.503,0.337,1.104-0.846,2.653,0.443c0.555,0.593,3.258,2.179,1.001,8.851c-0.446,1.316,2.854,0.135,1.169,2.619\n \tc-3.748,5.521-9.455,2.787-9.062,1.746c1.06-2.809-6.889-4.885-4.97-9.896c0.834-2.559,2.898,0.653,2.923,0.29\n \tc-0.434-1.07-2.608-5.541-2.923-6.985C96.587,150.793,95.342,147.033,98.984,146.692z\"/>\n </svg>\n <!-- <img class=\"togetherjs-cursor-img\" src=\"http://localhost:8080/togetherjs/images/cursor.svg\"> -->\n <span class=\"togetherjs-cursor-container\">\n <span class=\"togetherjs-cursor-name\"></span>\n <span style=\"display:none\" class=\"togetherjs-cursor-typing\" id=\"togetherjs-cursor-typebox\">\n <span class=\"togetherjs-typing-ellipse-one\">&#9679;</span><span class=\"togetherjs-typing-ellipse-two\">&#9679;</span><span class=\"togetherjs-typing-ellipse-three\">&#9679;</span>\n </span>\n <!-- Displayed when the cursor is below the screen: -->\n <span class=\"togetherjs-cursor-down\">\n\n </span>\n <!-- Displayed when the cursor is above the screen: -->\n <span class=\"togetherjs-cursor-up\">\n\n </span>\n </span>\n </div>\n\n <!-- This is the element that goes around focused form elements: -->\n <div id=\"togetherjs-template-focus\">\n <div class=\"togetherjs-focus togetherjs-person-bordercolor\"></div>\n </div>\n\n <!-- This is a click: -->\n <div id=\"togetherjs-template-click\" class=\"togetherjs-click togetherjs\">\n </div>\n </div>\n</div>\n",
  9409. walkthrough: "<!--\n Any elements with .togetherjs-walkthrough-firsttime will only be\n displayed on during the first-time experience. Any elements with\n .togetherjs-walkthrough-not-firsttime will only be displayed when\n the walkthrough is accessed through the Help menu.\n\n Note you *cannot* use <section class=\"togetherjs-walkthrough-slide\n togetherjs-walkthrough-firsttime\">: the number of sections must be the\n same regardless.\n -->\n<div id=\"togetherjs-walkthrough\" class=\"togetherjs-modal togetherjs-modal-wide\">\n <header>You're using TOOL_NAME!<button class=\"togetherjs-close\"></button></header>\n\n <div id=\"togetherjs-walkthrough-previous\"></div>\n <div id=\"togetherjs-walkthrough-next\"></div>\n\n <section class=\"togetherjs-walkthrough-slide\">\n <p class=\"togetherjs-walkthrough-main-image\"><img src=\"http://localhost:8080/togetherjs/images/walkthrough-images-intro.png\"></p>\n\t<p>TOOL_NAME is a service for your website that makes it easy to collaborate in real-time on SITE_NAME </p>\n </section>\n\n <section class=\"togetherjs-walkthrough-slide\">\n <div class=\"togetherjs-walkthrough-firsttime\">\n <div class=\"togetherjs-walkthrough-main-image\">\n <div class=\"togetherjs-walkthrough-avatar-section\">\n <div class=\"togetherjs-avatar-preview togetherjs-person togetherjs-person-self\"></div>\n <div class=\"togetherjs-avatar-upload-input\"><input type=\"file\" class=\"togetherjs-upload-avatar\"></div>\n </div>\n <input class=\"togetherjs-self-name\" type=\"text\" placeholder=\"Enter your name\">\n <div class=\"togetherjs-swatch togetherjs-person-bgcolor-self\"></div>\n <div class=\"togetherjs-save-settings\">\n <button class=\"togetherjs-avatar-save togetherjs-primary\">\n <span id=\"togetherjs-avatar-when-unsaved\">\"\"Save\"\"</span>\n <span id=\"togetherjs-avatar-when-saved\" style=\"display: none\">Saved!</span>\n </button>\n </div>\n </div>\n <p>Set up your avatar, name and user color above. If you'd like to update it later, you can click your Profile button.</p>\n </div>\n <div class=\"togetherjs-walkthrough-not-firsttime\">\n <p class=\"togetherjs-walkthrough-main-image\"><img src=\"http://localhost:8080/togetherjs/images/walkthrough-images-profile.png\"></p>\n <p>Change your avatar, name and user color using the Profile button.</p>\n </div>\n </section>\n\n <section class=\"togetherjs-walkthrough-slide\">\n <p class=\"togetherjs-walkthrough-main-image togetherjs-ifnot-creator\"><img src=\"http://localhost:8080/togetherjs/images/walkthrough-images-invite.png\">\n </p>\n <p class=\"togetherjs-ifnot-creator\">You can invite more friends to the session by sending the invite link in the TOOL_NAME dock.</p>\n <p class=\"togetherjs-walkthrough-main-image togetherjs-if-creator\">\n <span class=\"togetherjs-walkthrough-sendlink\">\n Copy and paste this link into IM or email to invite friends.\n </span>\n <input type=\"text\" class=\"togetherjs-share-link\">\n </p>\n <p class=\"togetherjs-if-creator\">Send the above link to a friend so they can join your session! You can find this invite link on the TOOL_NAME dock as well.</p>\n </section>\n\n <section class=\"togetherjs-walkthrough-slide\">\n <p class=\"togetherjs-walkthrough-main-image\"><img src=\"http://localhost:8080/togetherjs/images/walkthrough-images-participant.png\"></p>\n <p>Friends who join your TOOL_NAME session will appear here. You can click their avatars to see more.</p>\n </section>\n\n <section class=\"togetherjs-walkthrough-slide\">\n <p class=\"togetherjs-walkthrough-main-image\"><img src=\"http://localhost:8080/togetherjs/images/walkthrough-images-chat.png\"></p>\n <p>When your friends join you in your TOOL_NAME session, you can chat with them here!</p>\n </section>\n\n <section class=\"togetherjs-walkthrough-slide\">\n <p class=\"togetherjs-walkthrough-main-image\"><img src=\"http://localhost:8080/togetherjs/images/walkthrough-images-rtc.png\"></p>\n <p>If your browser supports it, click the microphone icon to begin an audio chat. Learn more about this experimental feature <a href=\"https://github.com/mozilla/togetherjs/wiki/About-Audio-Chat-and-WebRTC\" target=\"_blank\">here</a>.</p>\n </section>\n\n <section class=\"togetherjs-walkthrough-slide\">\n <p class=\"togetherjs-walkthrough-main-image\"><img src=\"http://localhost:8080/togetherjs/images/walkthrough-images-logo.png\"></p>\n <p>Alright, you're ready to use TOOL_NAME. Now start collaborating on SITE_NAME!</p>\n </section>\n\n <div style=\"display: none\">\n <!-- There is one of these created for each slide: -->\n <span id=\"togetherjs-template-walkthrough-slide-progress\" class=\"togetherjs-walkthrough-slide-progress\">&#9679;</span>\n </div>\n <section id=\"togetherjs-walkthrough-progress\">\n </section>\n\n <section class=\"togetherjs-buttons\">\n <button class=\"togetherjs-primary togetherjs-dismiss\">I'm ready!</button>\n </section>\n\n</div><!-- /.togetherjs-modal -->\n",
  9410. names: "Friendly Fox, Brilliant Beaver, Observant Owl, Gregarious Giraffe, Wild Wolf, Silent Seal, Wacky Whale, Curious Cat, Intelligent Iguana"
  9411. };
  9412. });
  9413. /* This Source Code Form is subject to the terms of the Mozilla Public
  9414. * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  9415. * You can obtain one at http://mozilla.org/MPL/2.0/. */
  9416. define('templates-es-BO',[], function () {
  9417. return {
  9418. "interface": "<% /*\n This is basically all the markup and interface for TogetherJS.\n Note all links should be like http://localhost:8080/togetherjs/*\n these links are rewritten with the location where TogetherJS was deployed.\n\n This file is inlined into togetherjs/templates.js\n*/ %>\n<div id=\"togetherjs-container\" class=\"togetherjs\">\n\n <!-- This is the main set of buttons: -->\n <div id=\"togetherjs-dock\" class=\"togetherjs-dock-right\">\n <div id=\"togetherjs-dock-anchor\" title=\"Mover el panel\">\n <span id=\"togetherjs-dock-anchor-horizontal\">\n <img src=\"http://localhost:8080/togetherjs/images/icn-handle-circle@2x.png\" alt=\"drag\">\n </span>\n <span id=\"togetherjs-dock-anchor-vertical\">\n <img src=\"http://localhost:8080/togetherjs/images/icn-handle-circle@2x.png\" alt=\"drag\">\n </span>\n </div>\n <div id=\"togetherjs-buttons\">\n <div style=\"display: none\">\n <button id=\"togetherjs-template-dock-person\" class=\"togetherjs-button togetherjs-dock-person\">\n <div class=\"togetherjs-tooltip togetherjs-dock-person-tooltip\">\n <span class=\"togetherjs-person-name\"></span>\n <span class=\"togetherjs-person-tooltip-arrow-r\"></span>\n </div>\n <div class=\"togetherjs-person togetherjs-person-status-overlay\"></div>\n </button>\n </div>\n <button id=\"togetherjs-profile-button\" class=\"togetherjs-button\" title=\"Este eres tú\">\n <div class=\"togetherjs-person togetherjs-person-self\"></div>\n <div id=\"togetherjs-profile-arrow\"></div>\n </button>\n <button id=\"togetherjs-share-button\" class=\"togetherjs-button\" title=\"Agrega un amigo\"></button>\n <button id=\"togetherjs-audio-button\" class=\"togetherjs-button\" title=\"Enciende tu microfono\">\n <span id=\"togetherjs-audio-unavailable\" class=\"togetherjs-audio-set\" data-toggles=\".togetherjs-audio-set\">\n </span>\n <span id=\"togetherjs-audio-ready\" class=\"togetherjs-audio-set\" data-toggles=\".togetherjs-audio-set\" style=\"display: none\">\n </span>\n <span id=\"togetherjs-audio-outgoing\" class=\"togetherjs-audio-set\" data-toggles=\".togetherjs-audio-set\" style=\"display: none\">\n </span>\n <span id=\"togetherjs-audio-incoming\" class=\"togetherjs-audio-set\" data-toggles=\".togetherjs-audio-set\" style=\"display: none\">\n </span>\n <span id=\"togetherjs-audio-active\" class=\"togetherjs-audio-set\" data-toggles=\".togetherjs-audio-set\" style=\"display: none\">\n </span>\n <span id=\"togetherjs-audio-muted\" class=\"togetherjs-audio-set\" data-toggles=\".togetherjs-audio-set\" style=\"display: none\">\n </span>\n <span id=\"togetherjs-audio-error\" class=\"togetherjs-audio-set\" data-toggles=\".togetherjs-audio-set\" style=\"display: none\">\n </span>\n </button>\n <button id=\"togetherjs-chat-button\" class=\"togetherjs-button\" title=\"Chat\"></button>\n <div id=\"togetherjs-dock-participants\"></div>\n </div>\n </div>\n\n <!-- The window for editing the avatar: -->\n <div id=\"togetherjs-avatar-edit\" class=\"togetherjs-modal\"\n style=\"display: none\">\n <header> Actualizar avatar </header>\n <section>\n <div class=\"togetherjs-avatar-preview togetherjs-person togetherjs-person-self\"></div>\n <div id=\"togetherjs-avatar-buttons\">\n <input type=\"file\" class=\"togetherjs-upload-avatar\">\n <!--<button id=\"togetherjs-upload-avatar\" class=\"togetherjs-primary\">Upload a picture</button>-->\n <!--<button id=\"togetherjs-camera-avatar\" class=\"togetherjs-default\">Take a picture</button>-->\n </div>\n </section>\n <section class=\"togetherjs-buttons\">\n <button class=\"togetherjs-cancel togetherjs-dismiss\">Cancelar</button>\n <span class=\"togetherjs-alt-text\">o</span>\n <button class=\"togetherjs-avatar-save togetherjs-primary\">Guardar</button>\n </section>\n </div>\n\n <!-- The window for sharing the link: -->\n <div id=\"togetherjs-share\" class=\"togetherjs-window\"\n data-bind-to=\"#togetherjs-share-button\" style=\"display: none\">\n <header> Invita a un amigo </header>\n <section>\n <div class=\"togetherjs-not-mobile\">\n <p>Copia y pega este link en tu mensajería o email:</p>\n <input type=\"text\" class=\"togetherjs-share-link\">\n </div>\n <div class=\"togetherjs-only-mobile\">\n <p>Copia y pega este link en tu mensajería o email:</p>\n <input type=\"text\" class=\"togetherjs-share-link\">\n <!-- <a class=\"togetherjs-share-link\" href=\"#\">Press your thumb here.</a> -->\n </div>\n </section>\n </div>\n\n <!-- Participant Full List view template: -->\n <div id=\"togetherjs-participantlist\" class=\"togetherjs-window\"\n data-bind-to=\"#togetherjs-participantlist-button\" style=\"display: none\">\n <header> Participantes </header>\n <section>\n <div class=\"togetherjs-not-mobile\">\n <ul>\n <li id=\"togetherjs-participant-item\">\n <img class=\"togetherjs-person togetherjs-person-small\" src=\"http://localhost:8080/togetherjs/images/btn-menu-change-avatar.png\">\n <span class=\"tj-name togetherjs-person-name\">NAME</span>\n <span class=\"tj-status\">&#9679;</span>\n <p class=\"tj-urllocation\">Actualmente en: <a class=\"togetherjs-person-url togetherjs-person-url-title\" href=\"\">http://www.location.comwww.location.comwww.location.comasdfsafd</a></p>\n <p class=\"tj-follow\">Sigue:\n <label class=\"togetherjs-follow-question\" for=\"togetherjs-person-status-follow\">\n <input type=\"checkbox\" id=\"togetherjs-person-status-follow\">\n </label>\n </p>\n <section class=\"togetherjs-buttons\">\n <!-- Displayed when the peer is at a different URL: -->\n <div class=\"togetherjs-different-url\">\n <a class=\"togetherjs-nudge togetherjs-default tj-btn-sm\">Zúmbalos</a>\n <a href=\"#\" class=\"togetherjs-follow togetherjs-person-url togetherjs-primary tj-btn-sm\">Únete a ellos</a>\n </div>\n <!-- Displayed when the peer is at your same URL: -->\n <div class=\"togetherjs-same-url\" style=\"display: none\">\n <span class=\"togetherjs-person-name\"></span> esta en la misma página que tú.\n </div>\n </section>\n </li>\n </ul>\n </section>\n </div>\n\n <!-- Participant detail template: -->\n <div id=\"togetherjs-template-participant-window\" class=\"togetherjs-window\" style=\"display: none\">\n <header><div class=\"togetherjs-person togetherjs-person-small\"></div><span class=\"togetherjs-person-name\"></span></header>\n\n <section class=\"togetherjs-participant-window-main\">\n <p class=\"togetherjs-participant-window-row\"><strong>Rol:</strong>\n <span class=\"togetherjs-person-role\"></span>\n </p>\n\n <p class=\"togetherjs-participant-window-row\"><strong>Actualmente en:</strong>\n <a class=\"togetherjs-person-url togetherjs-person-url-title\"></a>\n </p>\n\n <p class=\"togetherjs-participant-window-row\"><strong>Estado:</strong>\n <span class=\"togetherjs-person-status\"></span>\n </p>\n\n <p class=\"togetherjs-participant-window-row\"><strong class=\"togetherjs-float-left\">Sigue este participante:</strong>\n <label class=\"togetherjs-follow-question togetherjs-float-left\" for=\"togetherjs-person-status-follow\">\n <input type=\"checkbox\" id=\"togetherjs-person-status-follow\">\n </label>\n <span class=\"togetherjs-clear\"></span>\n </p>\n\n </section>\n\n <section class=\"togetherjs-buttons\">\n <!-- Displayed when the peer is at a different URL: -->\n <div class=\"togetherjs-different-url\">\n <a class=\"togetherjs-nudge togetherjs-default\">Zúmbalos</a>\n <a href=\"#\" class=\"togetherjs-follow togetherjs-person-url togetherjs-primary\">Únete a ellos</a>\n </div>\n <!-- Displayed when the peer is at your same URL: -->\n <div class=\"togetherjs-same-url\" style=\"display: none\">\n <span class=\"togetherjs-person-name\"></span> esta en la misma página que tú.\n </div>\n </section>\n </div>\n\n <!-- The chat screen: -->\n <div id=\"togetherjs-chat\" class=\"togetherjs-window\" data-bind-to=\"#togetherjs-chat-button\"\n style=\"display: none\">\n <header> Chat </header>\n <section class=\"togetherjs-subtitle\">\n <div id=\"togetherjs-chat-participants\" data-toggles=\"#togetherjs-chat-no-participants\" style=\"display: none\">\n <span id=\"togetherjs-chat-participant-list\"></span>\n &amp; Ustedes\n </div>\n <div id=\"togetherjs-chat-no-participants\" data-toggles=\"#togetherjs-chat-participants\">\n Nadie más está aquí.\n </div>\n </section>\n\n <div style=\"display: none\">\n\n <!-- Template for one message: -->\n <div id=\"togetherjs-template-chat-message\" class=\"togetherjs-chat-item togetherjs-chat-message\">\n <div class=\"togetherjs-person\"></div>\n <div class=\"togetherjs-timestamp\"><span class=\"togetherjs-time\">HH:MM</span> <span class=\"togetherjs-ampm\">AM/PM</span></div>\n <div class=\"togetherjs-person-name-abbrev\"></div>\n <div class=\"togetherjs-chat-content togetherjs-sub-content\"></div>\n </div>\n\n <!-- Template for when a person leaves: -->\n <div id=\"togetherjs-template-chat-left\" class=\"togetherjs-chat-item togetherjs-chat-left-item\">\n <div class=\"togetherjs-person\"></div>\n <div class=\"togetherjs-ifnot-declinedJoin\">\n <div class=\"togetherjs-inline-text\"><span class=\"togetherjs-person-name\"></span> dejo la sesión.</div>\n </div>\n <div class=\"togetherjs-if-declinedJoin\">\n <div class=\"togetherjs-inline-text\"><span class=\"togetherjs-person-name\"></span> rechazo unirse a la sesión.</div>\n </div>\n <div class=\"togetherjs-clear\"></div>\n </div>\n\n <!-- Template when a person joins the session: -->\n <div id=\"togetherjs-template-chat-joined\" class=\"togetherjs-chat-item togetherjs-chat-join-item\">\n <div class=\"togetherjs-person\"></div>\n <div class=\"togetherjs-inline-text\"><span class=\"togetherjs-person-name\"></span> se unió a la sesión.</div>\n <div class=\"togetherjs-clear\"></div>\n </div>\n\n <!-- Template for system-derived messages: -->\n <div id=\"togetherjs-template-chat-system\" class=\"togetherjs-chat-item\">\n <span class=\"togetherjs-chat-content togetherjs-sub-content\"></span>\n </div>\n\n <!-- Template when a person joins the session: -->\n <!-- <div id=\"togetherjs-template-chat-joined\" class=\"togetherjs-chat-item togetherjs-chat-join-item\">\n <div class=\"togetherjs-person\"></div>\n <div class=\"togetherjs-inline-text\"><span class=\"togetherjs-person-name\"></span> joined the session.</div>\n <div class=\"togetherjs-clear\"></div>\n </div> -->\n\n <!-- Template for when someone goes to a new URL: -->\n <div id=\"togetherjs-template-url-change\" class=\"togetherjs-chat-item togetherjs-chat-url-change\">\n <div class=\"togetherjs-person\"></div>\n <div class=\"togetherjs-inline-text\">\n <div class=\"togetherjs-if-sameUrl\">\n <span class=\"togetherjs-person-name\"></span>\n esta en la misma página que tú.\n </div>\n <div class=\"togetherjs-ifnot-sameUrl\">\n <span class=\"togetherjs-person-name\"></span>\n se fue a: <a href=\"#\" class=\"togetherjs-person-url togetherjs-person-url-title\" target=\"_self\"></a>\n <section class=\"togetherjs-buttons togetherjs-buttons-notification-diff-url\">\n <!-- Displayed when the peer is at a different URL: -->\n <div class=\"togetherjs-different-url togetherjs-notification-diff-url\">\n <a class=\"togetherjs-nudge togetherjs-default\">Zúmbalos</a>\n <a href=\"#\" class=\"togetherjs-follow togetherjs-person-url togetherjs-primary\">Únete a ellos</a>\n </div>\n </section>\n\n <!-- <div>\n <a class=\"togetherjs-nudge togetherjs-secondary\">Nudge them</a>\n <a href=\"\" class=\"togetherjs-person-url togetherjs-follow togetherjs-primary\">Join them</a>\n </div> -->\n\n </div>\n </div>\n <div class=\"togetherjs-clear\"></div>\n </div>\n </div>\n\n <section id=\"togetherjs-chat-messages\">\n <!-- FIX ME// need to have some dialogue that says something like - There are no chats yet! -->\n </section>\n <section id=\"togetherjs-chat-input-box\">\n <textarea id=\"togetherjs-chat-input\" placeholder=\"Escribe tu mensaje aquí\"></textarea>\n </section>\n </div>\n\n <!-- this is a kind of warning popped up when you (successfully) start RTC: -->\n <div id=\"togetherjs-rtc-info\" class=\"togetherjs-window\"\n data-bind-to=\"#togetherjs-audio-button\"\n style=\"display: none\">\n\n <header> Audio Chat </header>\n <section>\n <p>\n Activa tu <strong>microfono del navegador</strong> cerca de la barra de direcciones en la parte superior.\n </p>\n <p>\n Hablar con tu microfono sobre un navegador es una tecnología experimental.\n </p>\n <p>\n Lee más sobre Audio Chat <a href=\"https://github.com/mozilla/togetherjs/wiki/About-Audio-Chat-and-WebRTC\" target=\"_blank\">aqui</a>.\n </p>\n </section>\n\n <section class=\"togetherjs-buttons\">\n <label for=\"togetherjs-rtc-info-dismiss\" style=\"display: inline;\">\n <input class=\"togetherjs-dont-show-again\" id=\"togetherjs-rtc-info-dismiss\" type=\"checkbox\">\n No volver a mostrar.\n </label>\n <button class=\"togetherjs-default togetherjs-dismiss\" type=\"button\">Cerrar</button>\n </section>\n </div>\n\n <!-- this is popped up when you hit the audio button, but RTC isn't\n supported: -->\n <div id=\"togetherjs-rtc-not-supported\" class=\"togetherjs-window\"\n data-bind-to=\"#togetherjs-audio-button\"\n style=\"display: none\">\n <header> Audio Chat </header>\n\n <section>\n <p>Audio chat requiere que uses un <a href='https://github.com/mozilla/togetherjs/wiki/About-Audio-Chat-and-WebRTC' target='_blank'>navegador moderno!</a>!</p>\n <p>\n Audio chat en vivo necesita un nuevo (o diferente) navegador del que estás usando.\n </p>\n <p>\n Visita <a href='https://github.com/mozilla/togetherjs/wiki/About-Audio-Chat-and-WebRTC' target='_blank'>esta página</a> para más información y una lista de los navegadores soportados.\n </p>\n </section>\n\n <section class=\"togetherjs-buttons\">\n <div class=\"togetherjs-rtc-dialog-btn\">\n <button class=\"togetherjs-default togetherjs-dismiss\" type=\"button\">Cerrar</button>\n </div>\n </section>\n </div>\n\n <!-- The popup when a chat message comes in and the #togetherjs-chat window isn't open -->\n <div id=\"togetherjs-chat-notifier\" class=\"togetherjs-notification\"\n data-bind-to=\"#togetherjs-chat-button\"\n style=\"display: none\">\n <img src=\"http://localhost:8080/togetherjs/images/notification-togetherjs-logo.png\" class=\"togetherjs-notification-logo\" alt=\"\">\n <img src=\"http://localhost:8080/togetherjs/images/notification-btn-close.png\" class=\"togetherjs-notification-closebtn togetherjs-dismiss\" alt=\"[close]\">\n <section id=\"togetherjs-chat-notifier-message\">\n </section>\n </div>\n\n <!-- The menu when you click on the profile: -->\n <div id=\"togetherjs-menu\" class=\"togetherjs-menu\" style=\"display: none\">\n <div class=\"togetherjs-menu-item togetherjs-menu-disabled\" id=\"togetherjs-menu-profile\">\n <img id=\"togetherjs-menu-avatar\">\n <span class=\"togetherjs-person-name-self\" id=\"togetherjs-self-name-display\" data-toggles=\"#togetherjs-menu .togetherjs-self-name\">[nickname]</span>\n <input class=\"togetherjs-self-name\" type=\"text\" data-toggles=\"#togetherjs-self-name-display\" style=\"display: none\" placeholder=\"Ingresa tu nombre\">\n </div>\n <div class=\"togetherjs-menu-hr-avatar\"></div>\n <div class=\"togetherjs-menu-item\" id=\"togetherjs-menu-update-name\"><img src=\"http://localhost:8080/togetherjs/images/button-pencil.png\" alt=\"\"> Actualiza tu nombre</div>\n <div class=\"togetherjs-menu-item\" id=\"togetherjs-menu-update-avatar\"><img src=\"http://localhost:8080/togetherjs/images/btn-menu-change-avatar.png\" alt=\"\"> Cambiar avatar</div>\n <div class=\"togetherjs-menu-item\" id=\"togetherjs-menu-update-color\"><span class=\"togetherjs-person-bgcolor-self\"></span> Escoge un color de perfil</div>\n <div class=\"togetherjs-hr\"></div>\n <div class=\"togetherjs-menu-item\" id=\"togetherjs-menu-help\">Ayuda</div>\n <div class=\"togetherjs-menu-item\" id=\"togetherjs-menu-feedback\">Retroalimentación</div>\n <div id=\"togetherjs-invite\" style=\"display: none\">\n <div class=\"togetherjs-hr\"></div>\n <div id=\"togetherjs-invite-users\"></div>\n <div class=\"togetherjs-menu-item\" id=\"togetherjs-menu-refresh-invite\">Refresh users</div>\n <div class=\"togetherjs-menu-item\" id=\"togetherjs-menu-invite-anyone\">Invita a cualquiera</div>\n </div>\n <div class=\"togetherjs-hr\"></div>\n <div class=\"togetherjs-menu-item\" id=\"togetherjs-menu-end\"><img src=\"http://localhost:8080/togetherjs/images/button-end-session.png\" alt=\"\"> Fin <span class=\"togetherjs-tool-name\">TogetherJS</span></div>\n </div>\n\n <!-- template for one person in the invite-users list -->\n <div style=\"display: none\">\n <div id=\"togetherjs-template-invite-user-item\" class=\"togetherjs-menu-item\">\n <!-- FIXME: should include avatar in some way -->\n <span class=\"togetherjs-person-name\"></span>\n </div>\n </div>\n\n <!-- A window version of #togetherjs-menu, for use on mobile -->\n <div id=\"togetherjs-menu-window\" class=\"togetherjs-window\" style=\"display: none\">\n <header>Configuraciones y Perfil</header>\n <section>\n <div class=\"togetherjs-menu-item\">\n <img class=\"togetherjs-menu-avatar\">\n <span class=\"togetherjs-person-name-self\" id=\"togetherjs-self-name-display\"></span>\n </div>\n <div class=\"togetherjs-menu-hr-avatar\"></div>\n <div class=\"togetherjs-menu-item\" id=\"togetherjs-menu-update-name-button\"><img src=\"http://localhost:8080/togetherjs/images/button-pencil.png\" alt=\"\"> Actualiza tu nombre</div>\n <div class=\"togetherjs-menu-item\" id=\"togetherjs-menu-update-avatar-button\"><img src=\"http://localhost:8080/togetherjs/images/btn-menu-change-avatar.png\" alt=\"\"> Cambiar avatar</div>\n <div class=\"togetherjs-menu-item\" id=\"togetherjs-menu-update-color-button\"><span class=\"togetherjs-person-bgcolor-self\"></span> Escoge un color de perfil</div>\n <div class=\"togetherjs-hr\"></div>\n <div class=\"togetherjs-menu-item\" id=\"togetherjs-menu-help-button\">Ayuda</div>\n <div class=\"togetherjs-menu-item\" id=\"togetherjs-menu-feedback-button\">Retroalimentación</div>\n <div class=\"togetherjs-hr\"></div>\n <div class=\"togetherjs-menu-item\" id=\"togetherjs-menu-end-button\"><img src=\"http://localhost:8080/togetherjs/images/button-end-session.png\" alt=\"\"> Terminar TOOL_NAME</div>\n </section>\n <section class=\"togetherjs-buttons\">\n <button class=\"togetherjs-dismiss togetherjs-primary\">OK</button>\n </section>\n </div>\n\n <!-- The name editor, for use on mobile -->\n <div id=\"togetherjs-edit-name-window\" class=\"togetherjs-window\" style=\"display: none\">\n <header>Actualizar nombre</header>\n <section>\n <div>\n <input class=\"togetherjs-self-name\" type=\"text\" placeholder=\"Ingresa tu nombre\">\n </div>\n </section>\n <section class=\"togetherjs-buttons\">\n <button class=\"togetherjs-dismiss togetherjs-primary\">OK</button>\n </section>\n </div>\n\n <div class=\"togetherjs-menu\" id=\"togetherjs-pick-color\" style=\"display: none\">\n <div class=\"togetherjs-triangle-up\"><img src=\"http://localhost:8080/togetherjs/images/icn-triangle-up.png\"></div>\n <div style=\"display: none\">\n <div id=\"togetherjs-template-swatch\" class=\"togetherjs-swatch\">\n </div>\n </div>\n </div>\n\n <!-- Invisible elements that handle the RTC audio: -->\n <audio id=\"togetherjs-audio-element\"></audio>\n <audio id=\"togetherjs-local-audio\" muted=\"true\" volume=\"0.3\"></audio>\n <audio id=\"togetherjs-notification\" src=\"http://localhost:8080/togetherjs/images/notification.ogg\"></audio>\n\n <!-- The intro screen for someone who joins a session the first time: -->\n <div id=\"togetherjs-intro\" class=\"togetherjs-modal\" style=\"display: none\">\n <header>Unirse a la sesión TOOL_NAME?</header>\n <section>\n <p>Tu amigo a solicitado que lo unas en su TOOL_SITE_LINK sesión de navegador para colaborar en tiempo real!</p>\n <p>Deseas unirte a su sesión?</p>\n </section>\n\n <section class=\"togetherjs-buttons\">\n <button class=\"togetherjs-destructive togetherjs-modal-dont-join\">No, no unirse</button>\n <button class=\"togetherjs-primary togetherjs-dismiss\">Sí, unirse a la sesión</button>\n </section>\n </div>\n\n <!-- Shown when a web browser is completely incapable of running TogetherJS: -->\n <div id=\"togetherjs-browser-broken\" class=\"togetherjs-modal\" style=\"display: none\">\n <header> Lo siento </header>\n\n <section>\n <p>\n We're sorry, TOOL_NAME doesn't work with this browser. Please <a href='https://github.com/mozilla/togetherjs/wiki/Supported-Browsers#supported-browsers'>upgrade to a supported browser</a> to try TOOL_NAME.\n </p>\n\n <p id=\"togetherjs-browser-broken-is-ie\" style=\"display: none\">\n We need your help fixing TogetherJS on Internet Explorer! Here are a list of IE <a href=\"https://github.com/mozilla/togetherjs/issues?labels=IE&milestone=&page=1&state=open\" target=\"_blank\">GitHub issues</a> we need fixed that you can work on.\n Internet Explorer <a href=\"https://github.com/mozilla/togetherjs/wiki/Supported-Browsers#internet-explorer\">is currently not supported</a>. If you do want to try out TogetherJS, we'd suggest using Firefox or Chrome.\n </p>\n </section>\n\n <section class=\"togetherjs-buttons\">\n <button class=\"togetherjs-dismiss togetherjs-primary\">Terminar TOOL_NAME</button>\n </section>\n\n </div>\n\n <!-- Shown when the browser has WebSockets, but is IE (i.e., IE10) -->\n <div id=\"togetherjs-browser-unsupported\" class=\"togetherjs-modal\" style=\"display: none\">\n <header> Navegador sin soporte </header>\n\n <section>\n <p>\n We need your help fixing TogetherJS on Internet Explorer! Here are a list of IE <a href=\"https://github.com/mozilla/togetherjs/issues?labels=IE&milestone=&page=1&state=open\" target=\"_blank\">GitHub issues</a> we need fixed that you can work on.\n Internet Explorer <a href=\"https://github.com/mozilla/togetherjs/wiki/Supported-Browsers#internet-explorer\">is currently not supported</a>. If you do want to try out TogetherJS, we'd suggest using Firefox or Chrome.\n </p>\n\n <p>You can continue to try to use TOOL_NAME, but you are likely to hit lots of bugs. So be warned.</p>\n\n </section>\n\n <section class=\"togetherjs-buttons\">\n <button class=\"togetherjs-dismiss togetherjs-primary\">Terminar TOOL_NAME</button>\n <button class=\"togetherjs-dismiss togetherjs-secondary togetherjs-browser-unsupported-anyway\">Prueba TOOL_NAME de todas formas</button>\n </section>\n\n </div>\n\n <div id=\"togetherjs-confirm-end\" class=\"togetherjs-modal\" style=\"display: none\">\n <header> Terminar sesión? </header>\n <section>\n <p>\n Estás seguro de terminar tu sesión en TOOL_NAME?\n </p>\n </section>\n <section class=\"togetherjs-buttons\">\n <button class=\"togetherjs-cancel togetherjs-dismiss\">Cancelar</button>\n <span class=\"togetherjs-alt-text\">o</span>\n <button id=\"togetherjs-end-session\" class=\"togetherjs-destructive\">Terminar sesión</button>\n </section>\n </div>\n\n <div id=\"togetherjs-feedback-form\" class=\"togetherjs-modal\" style=\"display: none;\">\n <header> Retroalimentación </header>\n <iframe src=\"https://docs.google.com/a/mozilla.com/forms/d/1lVE7JyRo_tjakN0mLG1Cd9X9vseBX9wci153z9JcNEs/viewform?embedded=true\" width=\"400\" height=\"300\" frameborder=\"0\" marginheight=\"0\" marginwidth=\"0\">cargando desde...</iframe>\n <!-- <p><button class=\"togetherjs-modal-close\">Close</button></p> -->\n </div>\n\n <div style=\"display: none\">\n <!-- This is when you join a session and the other person has already changed to another URL: -->\n <div id=\"togetherjs-template-url-change\" class=\"togetherjs-modal\">\n <header> Siguiendo a new URL... </header>\n <section>\n <div class=\"togetherjs-person\"></div>\n Siguiendo\n <span class=\"togetherjs-person-name\"></span>\n para <a href=\"\" class=\"togetherjs-person-url togetherjs-person-url-title\"></a>\n </section>\n </div>\n\n <!-- This is when someone invites you to their session: -->\n <div id=\"togetherjs-template-invite\" class=\"togetherjs-chat-item\">\n <div class=\"togetherjs-person\"></div>\n <div>\n <span class=\"togetherjs-person-name\"></span>\n ha invitado <strong class=\"togetherjs-if-forEveryone\">cualquiera</strong>\n <strong class=\"togetherjs-ifnot-forEveryone\">tú</strong>\n para <a href=\"\" data-togetherjs-subattr-href=\"href\" class=\"togetherjs-sub-hrefTitle\" target=\"_blank\"></a>\n </div>\n </div>\n\n </div>\n\n <!-- The pointer at the side of a window: -->\n <div id=\"togetherjs-window-pointer-right\" style=\"display: none\"></div>\n <div id=\"togetherjs-window-pointer-left\" style=\"display: none\"></div>\n\n <!-- The element that overlaps the background of the page during a modal dialog: -->\n <div id=\"togetherjs-modal-background\" style=\"display: none\"></div>\n\n <!-- Some miscellaneous templates -->\n <div style=\"display: none\">\n\n <!-- This is the cursor: -->\n <div id=\"togetherjs-template-cursor\" class=\"togetherjs-cursor togetherjs\">\n <!-- Note: images/cursor.svg is a copy of this (for editing): -->\n <!-- crossbrowser svg dropshadow http://demosthenes.info/blog/600/Creating-a-True-CrossBrowser-Drop-Shadow- -->\n <svg version=\"1.1\" id=\"Layer_1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" x=\"0px\" y=\"0px\"\n \t width=\"15px\" height=\"22.838px\" viewBox=\"96.344 146.692 15 22.838\" enable-background=\"new 96.344 146.692 15 22.838\"\n \t xml:space=\"preserve\">\n <path fill=\"#231F20\" d=\"M98.984,146.692c2.167,1.322,1.624,6.067,3.773,7.298c-0.072-0.488,2.512-0.931,3.097,0\n \tc0.503,0.337,1.104-0.846,2.653,0.443c0.555,0.593,3.258,2.179,1.001,8.851c-0.446,1.316,2.854,0.135,1.169,2.619\n \tc-3.748,5.521-9.455,2.787-9.062,1.746c1.06-2.809-6.889-4.885-4.97-9.896c0.834-2.559,2.898,0.653,2.923,0.29\n \tc-0.434-1.07-2.608-5.541-2.923-6.985C96.587,150.793,95.342,147.033,98.984,146.692z\"/>\n </svg>\n <!-- <img class=\"togetherjs-cursor-img\" src=\"http://localhost:8080/togetherjs/images/cursor.svg\"> -->\n <span class=\"togetherjs-cursor-container\">\n <span class=\"togetherjs-cursor-name\"></span>\n <span style=\"display:none\" class=\"togetherjs-cursor-typing\" id=\"togetherjs-cursor-typebox\">\n <span class=\"togetherjs-typing-ellipse-one\">&#9679;</span><span class=\"togetherjs-typing-ellipse-two\">&#9679;</span><span class=\"togetherjs-typing-ellipse-three\">&#9679;</span>\n </span>\n <!-- Displayed when the cursor is below the screen: -->\n <span class=\"togetherjs-cursor-down\">\n\n </span>\n <!-- Displayed when the cursor is above the screen: -->\n <span class=\"togetherjs-cursor-up\">\n\n </span>\n </span>\n </div>\n\n <!-- This is the element that goes around focused form elements: -->\n <div id=\"togetherjs-template-focus\">\n <div class=\"togetherjs-focus togetherjs-person-bordercolor\"></div>\n </div>\n\n <!-- This is a click: -->\n <div id=\"togetherjs-template-click\" class=\"togetherjs-click togetherjs\">\n </div>\n </div>\n</div>\n",
  9419. walkthrough: "<!--\n Any elements with .togetherjs-walkthrough-firsttime will only be\n displayed on during the first-time experience. Any elements with\n .togetherjs-walkthrough-not-firsttime will only be displayed when\n the walkthrough is accessed through the Help menu.\n\n Note you *cannot* use <section class=\"togetherjs-walkthrough-slide\n togetherjs-walkthrough-firsttime\">: the number of sections must be the\n same regardless.\n -->\n<div id=\"togetherjs-walkthrough\" class=\"togetherjs-modal togetherjs-modal-wide\">\n <header>Estas usando TOOL_NAME!<button class=\"togetherjs-close\"></button></header>\n\n <div id=\"togetherjs-walkthrough-previous\"></div>\n <div id=\"togetherjs-walkthrough-next\"></div>\n\n <section class=\"togetherjs-walkthrough-slide\">\n <p class=\"togetherjs-walkthrough-main-image\"><img src=\"http://localhost:8080/togetherjs/images/walkthrough-images-intro.png\"></p>\n\t<p>TOOL_NAME es un servicio para tu sitio web que hace fácil colaborar en tiempo real en SITE_NAME </p>\n </section>\n\n <section class=\"togetherjs-walkthrough-slide\">\n <div class=\"togetherjs-walkthrough-firsttime\">\n <div class=\"togetherjs-walkthrough-main-image\">\n <div class=\"togetherjs-walkthrough-avatar-section\">\n <div class=\"togetherjs-avatar-preview togetherjs-person togetherjs-person-self\"></div>\n <div class=\"togetherjs-avatar-upload-input\"><input type=\"file\" class=\"togetherjs-upload-avatar\"></div>\n </div>\n <input class=\"togetherjs-self-name\" type=\"text\" placeholder=\"Ingresa tu nombre\">\n <div class=\"togetherjs-swatch togetherjs-person-bgcolor-self\"></div>\n <div class=\"togetherjs-save-settings\">\n <button class=\"togetherjs-avatar-save togetherjs-primary\">\n <span id=\"togetherjs-avatar-when-unsaved\">\"\"Guardar\"\"</span>\n <span id=\"togetherjs-avatar-when-saved\" style=\"display: none\">Guardado!</span>\n </button>\n </div>\n </div>\n <p>Set up your avatar, name and user color above. If you'd like to update it later, you can click your Profile button.</p>\n </div>\n <div class=\"togetherjs-walkthrough-not-firsttime\">\n <p class=\"togetherjs-walkthrough-main-image\"><img src=\"http://localhost:8080/togetherjs/images/walkthrough-images-profile.png\"></p>\n <p>Cambia tu avatar, nombre y color de usuario usando el boton de Perfil.</p>\n </div>\n </section>\n\n <section class=\"togetherjs-walkthrough-slide\">\n <p class=\"togetherjs-walkthrough-main-image togetherjs-ifnot-creator\"><img src=\"http://localhost:8080/togetherjs/images/walkthrough-images-invite.png\">\n </p>\n <p class=\"togetherjs-ifnot-creator\">Puedes invitar más amigos a la sesión enviándoles el link de invitación en el panel de TOOL_NAME.</p>\n <p class=\"togetherjs-walkthrough-main-image togetherjs-if-creator\">\n <span class=\"togetherjs-walkthrough-sendlink\">\n Copia y pega este link en tu mensajería o email para invitar amigos.\n </span>\n <input type=\"text\" class=\"togetherjs-share-link\">\n </p>\n <p class=\"togetherjs-if-creator\">Send the above link to a friend so they can join your session! You can find this invite link on the TOOL_NAME dock as well.</p>\n </section>\n\n <section class=\"togetherjs-walkthrough-slide\">\n <p class=\"togetherjs-walkthrough-main-image\"><img src=\"http://localhost:8080/togetherjs/images/walkthrough-images-participant.png\"></p>\n <p>Friends who join your TOOL_NAME session will appear here. You can click their avatars to see more.</p>\n </section>\n\n <section class=\"togetherjs-walkthrough-slide\">\n <p class=\"togetherjs-walkthrough-main-image\"><img src=\"http://localhost:8080/togetherjs/images/walkthrough-images-chat.png\"></p>\n <p>When your friends join you in your TOOL_NAME session, you can chat with them here!</p>\n </section>\n\n <section class=\"togetherjs-walkthrough-slide\">\n <p class=\"togetherjs-walkthrough-main-image\"><img src=\"http://localhost:8080/togetherjs/images/walkthrough-images-rtc.png\"></p>\n <p>Si tu navegador soporta, haz click en el ícono de microfono para iniciar audio chat. Aprende más sobre esta característica experimental <a href=\"https://github.com/mozilla/togetherjs/wiki/About-Audio-Chat-and-WebRTC\" target=\"_blank\">aquí</a>.</p>\n </section>\n\n <section class=\"togetherjs-walkthrough-slide\">\n <p class=\"togetherjs-walkthrough-main-image\"><img src=\"http://localhost:8080/togetherjs/images/walkthrough-images-logo.png\"></p>\n <p>Bien, estás listo para usar TOOL_NAME. Ahora empieza a colaborar en SITE_NAME!</p>\n </section>\n\n <div style=\"display: none\">\n <!-- There is one of these created for each slide: -->\n <span id=\"togetherjs-template-walkthrough-slide-progress\" class=\"togetherjs-walkthrough-slide-progress\">&#9679;</span>\n </div>\n <section id=\"togetherjs-walkthrough-progress\">\n </section>\n\n <section class=\"togetherjs-buttons\">\n <button class=\"togetherjs-primary togetherjs-dismiss\">Estoy listo!</button>\n </section>\n\n</div><!-- /.togetherjs-modal -->\n",
  9420. names: "Zorro Amigable, Castor Brillante, Buho Observador, Girafa Sociable, Lobo Salvaje, Foca Silenciosa, Ballena Graciosa, Gato Curioso, Iguana Inteligente"
  9421. };
  9422. });
  9423. /* This Source Code Form is subject to the terms of the Mozilla Public
  9424. * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  9425. * You can obtain one at http://mozilla.org/MPL/2.0/. */
  9426. define('templates-pl-PL',[], function () {
  9427. return {
  9428. "interface": "<% /*\n This is basically all the markup and interface for TogetherJS.\n Note all links should be like http://localhost:8080/togetherjs/*\n these links are rewritten with the location where TogetherJS was deployed.\n\n This file is inlined into togetherjs/templates.js\n*/ %>\n<div id=\"togetherjs-container\" class=\"togetherjs\">\n\n <!-- This is the main set of buttons: -->\n <div id=\"togetherjs-dock\" class=\"togetherjs-dock-right\">\n <div id=\"togetherjs-dock-anchor\" title=\"Przesuń pasek\">\n <span id=\"togetherjs-dock-anchor-horizontal\">\n <img src=\"http://localhost:8080/togetherjs/images/icn-handle-circle@2x.png\" alt=\"drag\">\n </span>\n <span id=\"togetherjs-dock-anchor-vertical\">\n <img src=\"http://localhost:8080/togetherjs/images/icn-handle-circle@2x.png\" alt=\"drag\">\n </span>\n </div>\n <div id=\"togetherjs-buttons\">\n <div style=\"display: none\">\n <button id=\"togetherjs-template-dock-person\" class=\"togetherjs-button togetherjs-dock-person\">\n <div class=\"togetherjs-tooltip togetherjs-dock-person-tooltip\">\n <span class=\"togetherjs-person-name\"></span>\n <span class=\"togetherjs-person-tooltip-arrow-r\"></span>\n </div>\n <div class=\"togetherjs-person togetherjs-person-status-overlay\"></div>\n </button>\n </div>\n <button id=\"togetherjs-profile-button\" class=\"togetherjs-button\" title=\"to Ty\">\n <div class=\"togetherjs-person togetherjs-person-self\"></div>\n <div id=\"togetherjs-profile-arrow\"></div>\n </button>\n <button id=\"togetherjs-share-button\" class=\"togetherjs-button\" title=\"Dodaj znajomego\"></button>\n <button id=\"togetherjs-audio-button\" class=\"togetherjs-button\" title=\"Włącz mikrofon\">\n <span id=\"togetherjs-audio-unavailable\" class=\"togetherjs-audio-set\" data-toggles=\".togetherjs-audio-set\">\n </span>\n <span id=\"togetherjs-audio-ready\" class=\"togetherjs-audio-set\" data-toggles=\".togetherjs-audio-set\" style=\"display: none\">\n </span>\n <span id=\"togetherjs-audio-outgoing\" class=\"togetherjs-audio-set\" data-toggles=\".togetherjs-audio-set\" style=\"display: none\">\n </span>\n <span id=\"togetherjs-audio-incoming\" class=\"togetherjs-audio-set\" data-toggles=\".togetherjs-audio-set\" style=\"display: none\">\n </span>\n <span id=\"togetherjs-audio-active\" class=\"togetherjs-audio-set\" data-toggles=\".togetherjs-audio-set\" style=\"display: none\">\n </span>\n <span id=\"togetherjs-audio-muted\" class=\"togetherjs-audio-set\" data-toggles=\".togetherjs-audio-set\" style=\"display: none\">\n </span>\n <span id=\"togetherjs-audio-error\" class=\"togetherjs-audio-set\" data-toggles=\".togetherjs-audio-set\" style=\"display: none\">\n </span>\n </button>\n <button id=\"togetherjs-chat-button\" class=\"togetherjs-button\" title=\"Czat\"></button>\n <div id=\"togetherjs-dock-participants\"></div>\n </div>\n </div>\n\n <!-- The window for editing the avatar: -->\n <div id=\"togetherjs-avatar-edit\" class=\"togetherjs-modal\"\n style=\"display: none\">\n <header> Zmień zdjęcie </header>\n <section>\n <div class=\"togetherjs-avatar-preview togetherjs-person togetherjs-person-self\"></div>\n <div id=\"togetherjs-avatar-buttons\">\n <input type=\"file\" class=\"togetherjs-upload-avatar\">\n <!--<button id=\"togetherjs-upload-avatar\" class=\"togetherjs-primary\">Upload a picture</button>-->\n <!--<button id=\"togetherjs-camera-avatar\" class=\"togetherjs-default\">Take a picture</button>-->\n </div>\n </section>\n <section class=\"togetherjs-buttons\">\n <button class=\"togetherjs-cancel togetherjs-dismiss\">Anuluj</button>\n <span class=\"togetherjs-alt-text\">lub</span>\n <button class=\"togetherjs-avatar-save togetherjs-primary\">Zapisz</button>\n </section>\n </div>\n\n <!-- The window for sharing the link: -->\n <div id=\"togetherjs-share\" class=\"togetherjs-window\"\n data-bind-to=\"#togetherjs-share-button\" style=\"display: none\">\n <header> Zaproś znajomego </header>\n <section>\n <div class=\"togetherjs-not-mobile\">\n <p>Skopiuj ten adres i wklej w komunikatorze lub wiadomości email:</p>\n <input type=\"text\" class=\"togetherjs-share-link\">\n </div>\n <div class=\"togetherjs-only-mobile\">\n <p>Skopiuj ten adres i wklej w komunikatorze lub wiadomości email:</p>\n <input type=\"text\" class=\"togetherjs-share-link\">\n <!-- <a class=\"togetherjs-share-link\" href=\"#\">Press your thumb here.</a> -->\n </div>\n </section>\n </div>\n\n <!-- Participant Full List view template: -->\n <div id=\"togetherjs-participantlist\" class=\"togetherjs-window\"\n data-bind-to=\"#togetherjs-participantlist-button\" style=\"display: none\">\n <header> Uczestnicy </header>\n <section>\n <div class=\"togetherjs-not-mobile\">\n <ul>\n <li id=\"togetherjs-participant-item\">\n <img class=\"togetherjs-person togetherjs-person-small\" src=\"http://localhost:8080/togetherjs/images/btn-menu-change-avatar.png\">\n <span class=\"tj-name togetherjs-person-name\">NAME</span>\n <span class=\"tj-status\">&#9679;</span>\n <p class=\"tj-urllocation\">Obecnie na: <a class=\"togetherjs-person-url togetherjs-person-url-title\" href=\"\">http://www.location.comwww.location.comwww.location.comasdfsafd</a></p>\n <p class=\"tj-follow\">Podążaj:\n <label class=\"togetherjs-follow-question\" for=\"togetherjs-person-status-follow\">\n <input type=\"checkbox\" id=\"togetherjs-person-status-follow\">\n </label>\n </p>\n <section class=\"togetherjs-buttons\">\n <!-- Displayed when the peer is at a different URL: -->\n <div class=\"togetherjs-different-url\">\n <a class=\"togetherjs-nudge togetherjs-default tj-btn-sm\">Zaczep ich</a>\n <a href=\"#\" class=\"togetherjs-follow togetherjs-person-url togetherjs-primary tj-btn-sm\">Dołącz</a>\n </div>\n <!-- Displayed when the peer is at your same URL: -->\n <div class=\"togetherjs-same-url\" style=\"display: none\">\n <span class=\"togetherjs-person-name\"></span> na tej samej stronie co Ty.\n </div>\n </section>\n </li>\n </ul>\n </section>\n </div>\n\n <!-- Participant detail template: -->\n <div id=\"togetherjs-template-participant-window\" class=\"togetherjs-window\" style=\"display: none\">\n <header><div class=\"togetherjs-person togetherjs-person-small\"></div><span class=\"togetherjs-person-name\"></span></header>\n\n <section class=\"togetherjs-participant-window-main\">\n <p class=\"togetherjs-participant-window-row\"><strong>Rola:</strong>\n <span class=\"togetherjs-person-role\"></span>\n </p>\n\n <p class=\"togetherjs-participant-window-row\"><strong>Obecnie na:</strong>\n <a class=\"togetherjs-person-url togetherjs-person-url-title\"></a>\n </p>\n\n <p class=\"togetherjs-participant-window-row\"><strong>Stan:</strong>\n <span class=\"togetherjs-person-status\"></span>\n </p>\n\n <p class=\"togetherjs-participant-window-row\"><strong class=\"togetherjs-float-left\">Podążaj za tą osobą:</strong>\n <label class=\"togetherjs-follow-question togetherjs-float-left\" for=\"togetherjs-person-status-follow\">\n <input type=\"checkbox\" id=\"togetherjs-person-status-follow\">\n </label>\n <span class=\"togetherjs-clear\"></span>\n </p>\n\n </section>\n\n <section class=\"togetherjs-buttons\">\n <!-- Displayed when the peer is at a different URL: -->\n <div class=\"togetherjs-different-url\">\n <a class=\"togetherjs-nudge togetherjs-default\">Zaczep ich</a>\n <a href=\"#\" class=\"togetherjs-follow togetherjs-person-url togetherjs-primary\">Dołącz</a>\n </div>\n <!-- Displayed when the peer is at your same URL: -->\n <div class=\"togetherjs-same-url\" style=\"display: none\">\n <span class=\"togetherjs-person-name\"></span> na tej samej stronie co Ty.\n </div>\n </section>\n </div>\n\n <!-- The chat screen: -->\n <div id=\"togetherjs-chat\" class=\"togetherjs-window\" data-bind-to=\"#togetherjs-chat-button\"\n style=\"display: none\">\n <header> Czat </header>\n <section class=\"togetherjs-subtitle\">\n <div id=\"togetherjs-chat-participants\" data-toggles=\"#togetherjs-chat-no-participants\" style=\"display: none\">\n <span id=\"togetherjs-chat-participant-list\"></span>\n &amp; Ty\n </div>\n <div id=\"togetherjs-chat-no-participants\" data-toggles=\"#togetherjs-chat-participants\">\n Nie ma tu nikogo innego.\n </div>\n </section>\n\n <div style=\"display: none\">\n\n <!-- Template for one message: -->\n <div id=\"togetherjs-template-chat-message\" class=\"togetherjs-chat-item togetherjs-chat-message\">\n <div class=\"togetherjs-person\"></div>\n <div class=\"togetherjs-timestamp\"><span class=\"togetherjs-time\">HH:MM</span> <span class=\"togetherjs-ampm\">AM/PM</span></div>\n <div class=\"togetherjs-person-name-abbrev\"></div>\n <div class=\"togetherjs-chat-content togetherjs-sub-content\"></div>\n </div>\n\n <!-- Template for when a person leaves: -->\n <div id=\"togetherjs-template-chat-left\" class=\"togetherjs-chat-item togetherjs-chat-left-item\">\n <div class=\"togetherjs-person\"></div>\n <div class=\"togetherjs-ifnot-declinedJoin\">\n <div class=\"togetherjs-inline-text\"><span class=\"togetherjs-person-name\"></span> left the session.</div>\n </div>\n <div class=\"togetherjs-if-declinedJoin\">\n <div class=\"togetherjs-inline-text\"><span class=\"togetherjs-person-name\"></span> odrzucił zaproszenie.</div>\n </div>\n <div class=\"togetherjs-clear\"></div>\n </div>\n\n <!-- Template when a person joins the session: -->\n <div id=\"togetherjs-template-chat-joined\" class=\"togetherjs-chat-item togetherjs-chat-join-item\">\n <div class=\"togetherjs-person\"></div>\n <div class=\"togetherjs-inline-text\"><span class=\"togetherjs-person-name\"></span> dołączył do sesji.</div>\n <div class=\"togetherjs-clear\"></div>\n </div>\n\n <!-- Template for system-derived messages: -->\n <div id=\"togetherjs-template-chat-system\" class=\"togetherjs-chat-item\">\n <span class=\"togetherjs-chat-content togetherjs-sub-content\"></span>\n </div>\n\n <!-- Template when a person joins the session: -->\n <!-- <div id=\"togetherjs-template-chat-joined\" class=\"togetherjs-chat-item togetherjs-chat-join-item\">\n <div class=\"togetherjs-person\"></div>\n <div class=\"togetherjs-inline-text\"><span class=\"togetherjs-person-name\"></span> joined the session.</div>\n <div class=\"togetherjs-clear\"></div>\n </div> -->\n\n <!-- Template for when someone goes to a new URL: -->\n <div id=\"togetherjs-template-url-change\" class=\"togetherjs-chat-item togetherjs-chat-url-change\">\n <div class=\"togetherjs-person\"></div>\n <div class=\"togetherjs-inline-text\">\n <div class=\"togetherjs-if-sameUrl\">\n <span class=\"togetherjs-person-name\"></span>\n na tej samej stronie co Ty.\n </div>\n <div class=\"togetherjs-ifnot-sameUrl\">\n <span class=\"togetherjs-person-name\"></span>\n przeszedł do: <a href=\"#\" class=\"togetherjs-person-url togetherjs-person-url-title\" target=\"_self\"></a>\n <section class=\"togetherjs-buttons togetherjs-buttons-notification-diff-url\">\n <!-- Displayed when the peer is at a different URL: -->\n <div class=\"togetherjs-different-url togetherjs-notification-diff-url\">\n <a class=\"togetherjs-nudge togetherjs-default\">Zaczep ich</a>\n <a href=\"#\" class=\"togetherjs-follow togetherjs-person-url togetherjs-primary\">Dołącz</a>\n </div>\n </section>\n\n <!-- <div>\n <a class=\"togetherjs-nudge togetherjs-secondary\">Nudge them</a>\n <a href=\"\" class=\"togetherjs-person-url togetherjs-follow togetherjs-primary\">Join them</a>\n </div> -->\n\n </div>\n </div>\n <div class=\"togetherjs-clear\"></div>\n </div>\n </div>\n\n <section id=\"togetherjs-chat-messages\">\n <!-- FIX ME// need to have some dialogue that says something like - There are no chats yet! -->\n </section>\n <section id=\"togetherjs-chat-input-box\">\n <textarea id=\"togetherjs-chat-input\" placeholder=\"Wpisz tutaj swoją wiadomość\"></textarea>\n </section>\n </div>\n\n <!-- this is a kind of warning popped up when you (successfully) start RTC: -->\n <div id=\"togetherjs-rtc-info\" class=\"togetherjs-window\"\n data-bind-to=\"#togetherjs-audio-button\"\n style=\"display: none\">\n\n <header> Rozmowa głosowa </header>\n <section>\n <p>\n Aktywuj swój <strong>mikrofon przeglądarki</strong> w pobliżu paska adresu, powyżej.\n </p>\n <p>\n Rozmowa głosowa przez przeglądarkę jest eksperymentalną funkjconalnością.\n </p>\n <p>\n Dowiedz wię więcej o rozmowie głosowej <a href=\"https://github.com/mozilla/togetherjs/wiki/About-Audio-Chat-and-WebRTC\" target=\"_blank\">tutaj</a>.\n </p>\n </section>\n\n <section class=\"togetherjs-buttons\">\n <label for=\"togetherjs-rtc-info-dismiss\" style=\"display: inline;\">\n <input class=\"togetherjs-dont-show-again\" id=\"togetherjs-rtc-info-dismiss\" type=\"checkbox\">\n Nie pokazuj ponownie.\n </label>\n <button class=\"togetherjs-default togetherjs-dismiss\" type=\"button\">Zamknij</button>\n </section>\n </div>\n\n <!-- this is popped up when you hit the audio button, but RTC isn't\n supported: -->\n <div id=\"togetherjs-rtc-not-supported\" class=\"togetherjs-window\"\n data-bind-to=\"#togetherjs-audio-button\"\n style=\"display: none\">\n <header> Rozmowa głosowa </header>\n\n <section>\n <p>Aby użyć rozmowy głosowej, potrzebujesz <a href='https://github.com/mozilla/togetherjs/wiki/About-Audio-Chat-and-WebRTC' target='_blank'>nowszej przeglądarki</a>!</p>\n <p>\n Rozmowa głosowa wymaga nowszej (lub innej) przeglądarki, niż ta, której używasz.\n </p>\n <p>\n Przeczytaj <a href='https://github.com/mozilla/togetherjs/wiki/About-Audio-Chat-and-WebRTC' target='_blank'>tą stronę</a> Aby dowiedzieć się więcej i poznać listę wspieranych przeglądarek.\n </p>\n </section>\n\n <section class=\"togetherjs-buttons\">\n <div class=\"togetherjs-rtc-dialog-btn\">\n <button class=\"togetherjs-default togetherjs-dismiss\" type=\"button\">Zamknij</button>\n </div>\n </section>\n </div>\n\n <!-- The popup when a chat message comes in and the #togetherjs-chat window isn't open -->\n <div id=\"togetherjs-chat-notifier\" class=\"togetherjs-notification\"\n data-bind-to=\"#togetherjs-chat-button\"\n style=\"display: none\">\n <img src=\"http://localhost:8080/togetherjs/images/notification-togetherjs-logo.png\" class=\"togetherjs-notification-logo\" alt=\"\">\n <img src=\"http://localhost:8080/togetherjs/images/notification-btn-close.png\" class=\"togetherjs-notification-closebtn togetherjs-dismiss\" alt=\"[close]\">\n <section id=\"togetherjs-chat-notifier-message\">\n </section>\n </div>\n\n <!-- The menu when you click on the profile: -->\n <div id=\"togetherjs-menu\" class=\"togetherjs-menu\" style=\"display: none\">\n <div class=\"togetherjs-menu-item togetherjs-menu-disabled\" id=\"togetherjs-menu-profile\">\n <img id=\"togetherjs-menu-avatar\">\n <span class=\"togetherjs-person-name-self\" id=\"togetherjs-self-name-display\" data-toggles=\"#togetherjs-menu .togetherjs-self-name\">[nickname]</span>\n <input class=\"togetherjs-self-name\" type=\"text\" data-toggles=\"#togetherjs-self-name-display\" style=\"display: none\" placeholder=\"Podaj swoje imię\">\n </div>\n <div class=\"togetherjs-menu-hr-avatar\"></div>\n <div class=\"togetherjs-menu-item\" id=\"togetherjs-menu-update-name\"><img src=\"http://localhost:8080/togetherjs/images/button-pencil.png\" alt=\"\"> Zmień swoje imię</div>\n <div class=\"togetherjs-menu-item\" id=\"togetherjs-menu-update-avatar\"><img src=\"http://localhost:8080/togetherjs/images/btn-menu-change-avatar.png\" alt=\"\"> Zmień zdjęcie</div>\n <div class=\"togetherjs-menu-item\" id=\"togetherjs-menu-update-color\"><span class=\"togetherjs-person-bgcolor-self\"></span> Wybierz kolor profilu</div>\n <div class=\"togetherjs-hr\"></div>\n <div class=\"togetherjs-menu-item\" id=\"togetherjs-menu-help\">Pomoc</div>\n <div class=\"togetherjs-menu-item\" id=\"togetherjs-menu-feedback\">Uwagi</div>\n <div id=\"togetherjs-invite\" style=\"display: none\">\n <div class=\"togetherjs-hr\"></div>\n <div id=\"togetherjs-invite-users\"></div>\n <div class=\"togetherjs-menu-item\" id=\"togetherjs-menu-refresh-invite\">Refresh users</div>\n <div class=\"togetherjs-menu-item\" id=\"togetherjs-menu-invite-anyone\">Zaproś kogokolwiek</div>\n </div>\n <div class=\"togetherjs-hr\"></div>\n <div class=\"togetherjs-menu-item\" id=\"togetherjs-menu-end\"><img src=\"http://localhost:8080/togetherjs/images/button-end-session.png\" alt=\"\"> Zakończ <span class=\"togetherjs-tool-name\">TogetherJS</span></div>\n </div>\n\n <!-- template for one person in the invite-users list -->\n <div style=\"display: none\">\n <div id=\"togetherjs-template-invite-user-item\" class=\"togetherjs-menu-item\">\n <!-- FIXME: should include avatar in some way -->\n <span class=\"togetherjs-person-name\"></span>\n </div>\n </div>\n\n <!-- A window version of #togetherjs-menu, for use on mobile -->\n <div id=\"togetherjs-menu-window\" class=\"togetherjs-window\" style=\"display: none\">\n <header>Ustawienia i Profil</header>\n <section>\n <div class=\"togetherjs-menu-item\">\n <img class=\"togetherjs-menu-avatar\">\n <span class=\"togetherjs-person-name-self\" id=\"togetherjs-self-name-display\"></span>\n </div>\n <div class=\"togetherjs-menu-hr-avatar\"></div>\n <div class=\"togetherjs-menu-item\" id=\"togetherjs-menu-update-name-button\"><img src=\"http://localhost:8080/togetherjs/images/button-pencil.png\" alt=\"\"> Zmień swoje imię</div>\n <div class=\"togetherjs-menu-item\" id=\"togetherjs-menu-update-avatar-button\"><img src=\"http://localhost:8080/togetherjs/images/btn-menu-change-avatar.png\" alt=\"\"> Zmień zdjęcie</div>\n <div class=\"togetherjs-menu-item\" id=\"togetherjs-menu-update-color-button\"><span class=\"togetherjs-person-bgcolor-self\"></span> Wybierz kolor profilu</div>\n <div class=\"togetherjs-hr\"></div>\n <div class=\"togetherjs-menu-item\" id=\"togetherjs-menu-help-button\">Pomoc</div>\n <div class=\"togetherjs-menu-item\" id=\"togetherjs-menu-feedback-button\">Uwagi</div>\n <div class=\"togetherjs-hr\"></div>\n <div class=\"togetherjs-menu-item\" id=\"togetherjs-menu-end-button\"><img src=\"http://localhost:8080/togetherjs/images/button-end-session.png\" alt=\"\"> Zakończ TOOL_NAME</div>\n </section>\n <section class=\"togetherjs-buttons\">\n <button class=\"togetherjs-dismiss togetherjs-primary\">OK</button>\n </section>\n </div>\n\n <!-- The name editor, for use on mobile -->\n <div id=\"togetherjs-edit-name-window\" class=\"togetherjs-window\" style=\"display: none\">\n <header>Zmień imię</header>\n <section>\n <div>\n <input class=\"togetherjs-self-name\" type=\"text\" placeholder=\"Podaj swoje imię\">\n </div>\n </section>\n <section class=\"togetherjs-buttons\">\n <button class=\"togetherjs-dismiss togetherjs-primary\">OK</button>\n </section>\n </div>\n\n <div class=\"togetherjs-menu\" id=\"togetherjs-pick-color\" style=\"display: none\">\n <div class=\"togetherjs-triangle-up\"><img src=\"http://localhost:8080/togetherjs/images/icn-triangle-up.png\"></div>\n <div style=\"display: none\">\n <div id=\"togetherjs-template-swatch\" class=\"togetherjs-swatch\">\n </div>\n </div>\n </div>\n\n <!-- Invisible elements that handle the RTC audio: -->\n <audio id=\"togetherjs-audio-element\"></audio>\n <audio id=\"togetherjs-local-audio\" muted=\"true\" volume=\"0.3\"></audio>\n <audio id=\"togetherjs-notification\" src=\"http://localhost:8080/togetherjs/images/notification.ogg\"></audio>\n\n <!-- The intro screen for someone who joins a session the first time: -->\n <div id=\"togetherjs-intro\" class=\"togetherjs-modal\" style=\"display: none\">\n <header>Dołączyć do sesji TOOL_NAME?</header>\n <section>\n <p>Twój znajomy zaprosił Cię do współpracy w czasie rzeczywistym na stronie TOOL_SITE_LINK!</p>\n <p>Czy chcesz dołączyć do jego sesji?</p>\n </section>\n\n <section class=\"togetherjs-buttons\">\n <button class=\"togetherjs-destructive togetherjs-modal-dont-join\">Nie dołączaj</button>\n <button class=\"togetherjs-primary togetherjs-dismiss\">Tak, dołącz do sesji</button>\n </section>\n </div>\n\n <!-- Shown when a web browser is completely incapable of running TogetherJS: -->\n <div id=\"togetherjs-browser-broken\" class=\"togetherjs-modal\" style=\"display: none\">\n <header> Przykro nam </header>\n\n <section>\n <p>\n Przykto nam, TOOL_NAME nie działa w tej przeglądarce. <a href='https://github.com/mozilla/togetherjs/wiki/Supported-Browsers#supported-browsers'>Zaktualizuj do wspieranej przeglądarki</a> żeby móc użyć TOOL_NAME.\n </p>\n\n <p id=\"togetherjs-browser-broken-is-ie\" style=\"display: none\">\n Potrzebujemy Twojej pomocy w przygotowaniu TogetherJS pod Internet Explorera! Tutaj znajdziesz listę <a href=\"https://github.com/mozilla/togetherjs/issues?labels=IE&milestone=&page=1&state=open\" target=\"_blank\">zgłoszeń GitHub</a> związanych z IE, które muszą być naprawione zanim będzie można używać narzędzia pod tą przeglądarką.\n Internet Explorer <a href=\"https://github.com/mozilla/togetherjs/wiki/Supported-Browsers#internet-explorer\">obecnie nie jest wspierany</a>. Jeśli mimo to chcesz wypróbować TogetherJS, polecamy przeglądarkę Firefox lub Chrome.\n </p>\n </section>\n\n <section class=\"togetherjs-buttons\">\n <button class=\"togetherjs-dismiss togetherjs-primary\">Zakończ TOOL_NAME</button>\n </section>\n\n </div>\n\n <!-- Shown when the browser has WebSockets, but is IE (i.e., IE10) -->\n <div id=\"togetherjs-browser-unsupported\" class=\"togetherjs-modal\" style=\"display: none\">\n <header> Niewspierana przeglądarka </header>\n\n <section>\n <p>\n Potrzebujemy Twojej pomocy w przygotowaniu TogetherJS pod Internet Explorera! Tutaj znajdziesz listę <a href=\"https://github.com/mozilla/togetherjs/issues?labels=IE&milestone=&page=1&state=open\" target=\"_blank\">zgłoszeń GitHub</a> związanych z IE, które muszą być naprawione zanim będzie można używać narzędzia pod tą przeglądarką.\n Internet Explorer <a href=\"https://github.com/mozilla/togetherjs/wiki/Supported-Browsers#internet-explorer\">obecnie nie jest wspierany</a>. Jeśli mimo to chcesz wypróbować TogetherJS, polecamy przeglądarkę Firefox lub Chrome.\n </p>\n\n <p>Możesz używać TOOL_NAME, ale uwaga: najprawdopodobniej natrafisz na wiele problemów z działaniem.</p>\n\n </section>\n\n <section class=\"togetherjs-buttons\">\n <button class=\"togetherjs-dismiss togetherjs-primary\">Zakończ TOOL_NAME</button>\n <button class=\"togetherjs-dismiss togetherjs-secondary togetherjs-browser-unsupported-anyway\">Mimo to, wybróbuj TOOL_NAME</button>\n </section>\n\n </div>\n\n <div id=\"togetherjs-confirm-end\" class=\"togetherjs-modal\" style=\"display: none\">\n <header> Zakończ sesję? </header>\n <section>\n <p>\n Czy na pewno chcesz zakończyć swoją sesję w TOOL_NAME?\n </p>\n </section>\n <section class=\"togetherjs-buttons\">\n <button class=\"togetherjs-cancel togetherjs-dismiss\">Anuluj</button>\n <span class=\"togetherjs-alt-text\">lub</span>\n <button id=\"togetherjs-end-session\" class=\"togetherjs-destructive\">Zakończ sesję</button>\n </section>\n </div>\n\n <div id=\"togetherjs-feedback-form\" class=\"togetherjs-modal\" style=\"display: none;\">\n <header> Uwagi </header>\n <iframe src=\"https://docs.google.com/a/mozilla.com/forms/d/1lVE7JyRo_tjakN0mLG1Cd9X9vseBX9wci153z9JcNEs/viewform?embedded=true\" width=\"400\" height=\"300\" frameborder=\"0\" marginheight=\"0\" marginwidth=\"0\">Ładowanie formularza...</iframe>\n <!-- <p><button class=\"togetherjs-modal-close\">Close</button></p> -->\n </div>\n\n <div style=\"display: none\">\n <!-- This is when you join a session and the other person has already changed to another URL: -->\n <div id=\"togetherjs-template-url-change\" class=\"togetherjs-modal\">\n <header> Podążam do nowej strony... </header>\n <section>\n <div class=\"togetherjs-person\"></div>\n Podążam\n <span class=\"togetherjs-person-name\"></span>\n do <a href=\"\" class=\"togetherjs-person-url togetherjs-person-url-title\"></a>\n </section>\n </div>\n\n <!-- This is when someone invites you to their session: -->\n <div id=\"togetherjs-template-invite\" class=\"togetherjs-chat-item\">\n <div class=\"togetherjs-person\"></div>\n <div>\n <span class=\"togetherjs-person-name\"></span>\n zaprosił <strong class=\"togetherjs-if-forEveryone\">kogokolwiek</strong>\n <strong class=\"togetherjs-ifnot-forEveryone\">Cię</strong>\n do <a href=\"\" data-togetherjs-subattr-href=\"href\" class=\"togetherjs-sub-hrefTitle\" target=\"_blank\"></a>\n </div>\n </div>\n\n </div>\n\n <!-- The pointer at the side of a window: -->\n <div id=\"togetherjs-window-pointer-right\" style=\"display: none\"></div>\n <div id=\"togetherjs-window-pointer-left\" style=\"display: none\"></div>\n\n <!-- The element that overlaps the background of the page during a modal dialog: -->\n <div id=\"togetherjs-modal-background\" style=\"display: none\"></div>\n\n <!-- Some miscellaneous templates -->\n <div style=\"display: none\">\n\n <!-- This is the cursor: -->\n <div id=\"togetherjs-template-cursor\" class=\"togetherjs-cursor togetherjs\">\n <!-- Note: images/cursor.svg is a copy of this (for editing): -->\n <!-- crossbrowser svg dropshadow http://demosthenes.info/blog/600/Creating-a-True-CrossBrowser-Drop-Shadow- -->\n <svg version=\"1.1\" id=\"Layer_1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" x=\"0px\" y=\"0px\"\n \t width=\"15px\" height=\"22.838px\" viewBox=\"96.344 146.692 15 22.838\" enable-background=\"new 96.344 146.692 15 22.838\"\n \t xml:space=\"preserve\">\n <path fill=\"#231F20\" d=\"M98.984,146.692c2.167,1.322,1.624,6.067,3.773,7.298c-0.072-0.488,2.512-0.931,3.097,0\n \tc0.503,0.337,1.104-0.846,2.653,0.443c0.555,0.593,3.258,2.179,1.001,8.851c-0.446,1.316,2.854,0.135,1.169,2.619\n \tc-3.748,5.521-9.455,2.787-9.062,1.746c1.06-2.809-6.889-4.885-4.97-9.896c0.834-2.559,2.898,0.653,2.923,0.29\n \tc-0.434-1.07-2.608-5.541-2.923-6.985C96.587,150.793,95.342,147.033,98.984,146.692z\"/>\n </svg>\n <!-- <img class=\"togetherjs-cursor-img\" src=\"http://localhost:8080/togetherjs/images/cursor.svg\"> -->\n <span class=\"togetherjs-cursor-container\">\n <span class=\"togetherjs-cursor-name\"></span>\n <span style=\"display:none\" class=\"togetherjs-cursor-typing\" id=\"togetherjs-cursor-typebox\">\n <span class=\"togetherjs-typing-ellipse-one\">&#9679;</span><span class=\"togetherjs-typing-ellipse-two\">&#9679;</span><span class=\"togetherjs-typing-ellipse-three\">&#9679;</span>\n </span>\n <!-- Displayed when the cursor is below the screen: -->\n <span class=\"togetherjs-cursor-down\">\n\n </span>\n <!-- Displayed when the cursor is above the screen: -->\n <span class=\"togetherjs-cursor-up\">\n\n </span>\n </span>\n </div>\n\n <!-- This is the element that goes around focused form elements: -->\n <div id=\"togetherjs-template-focus\">\n <div class=\"togetherjs-focus togetherjs-person-bordercolor\"></div>\n </div>\n\n <!-- This is a click: -->\n <div id=\"togetherjs-template-click\" class=\"togetherjs-click togetherjs\">\n </div>\n </div>\n</div>\n",
  9429. walkthrough: "<!--\n Any elements with .togetherjs-walkthrough-firsttime will only be\n displayed on during the first-time experience. Any elements with\n .togetherjs-walkthrough-not-firsttime will only be displayed when\n the walkthrough is accessed through the Help menu.\n\n Note you *cannot* use <section class=\"togetherjs-walkthrough-slide\n togetherjs-walkthrough-firsttime\">: the number of sections must be the\n same regardless.\n -->\n<div id=\"togetherjs-walkthrough\" class=\"togetherjs-modal togetherjs-modal-wide\">\n <header>Używasz TOOL_NAME!<button class=\"togetherjs-close\"></button></header>\n\n <div id=\"togetherjs-walkthrough-previous\"></div>\n <div id=\"togetherjs-walkthrough-next\"></div>\n\n <section class=\"togetherjs-walkthrough-slide\">\n <p class=\"togetherjs-walkthrough-main-image\"><img src=\"http://localhost:8080/togetherjs/images/walkthrough-images-intro.png\"></p>\n\t<p>TOOL_NAME jest usługą dla Twojej strony, która umożliwia łatwą współpracę w czasie rzeczywistym na stronie SITE_NAME </p>\n </section>\n\n <section class=\"togetherjs-walkthrough-slide\">\n <div class=\"togetherjs-walkthrough-firsttime\">\n <div class=\"togetherjs-walkthrough-main-image\">\n <div class=\"togetherjs-walkthrough-avatar-section\">\n <div class=\"togetherjs-avatar-preview togetherjs-person togetherjs-person-self\"></div>\n <div class=\"togetherjs-avatar-upload-input\"><input type=\"file\" class=\"togetherjs-upload-avatar\"></div>\n </div>\n <input class=\"togetherjs-self-name\" type=\"text\" placeholder=\"Podaj swoje imię\">\n <div class=\"togetherjs-swatch togetherjs-person-bgcolor-self\"></div>\n <div class=\"togetherjs-save-settings\">\n <button class=\"togetherjs-avatar-save togetherjs-primary\">\n <span id=\"togetherjs-avatar-when-unsaved\">\"\"Zapisz\"\"</span>\n <span id=\"togetherjs-avatar-when-saved\" style=\"display: none\">Zapisano!</span>\n </button>\n </div>\n </div>\n <p>Powyżej możesz ustawić swoje zdjęcie, imię i kolor. Możesz zrobić to też później, klikając przycisk swojego profilu.</p>\n </div>\n <div class=\"togetherjs-walkthrough-not-firsttime\">\n <p class=\"togetherjs-walkthrough-main-image\"><img src=\"http://localhost:8080/togetherjs/images/walkthrough-images-profile.png\"></p>\n <p>Zmień swoje zdjęcie, imię i kolor, używając przycisku 'Profil'.</p>\n </div>\n </section>\n\n <section class=\"togetherjs-walkthrough-slide\">\n <p class=\"togetherjs-walkthrough-main-image togetherjs-ifnot-creator\"><img src=\"http://localhost:8080/togetherjs/images/walkthrough-images-invite.png\">\n </p>\n <p class=\"togetherjs-ifnot-creator\">Możesz zaprosić więcej znajomych do sesji, wysyłając im link poprzez pasek TOOL_NAME.</p>\n <p class=\"togetherjs-walkthrough-main-image togetherjs-if-creator\">\n <span class=\"togetherjs-walkthrough-sendlink\">\n Skopiuj ten adres i wklej w komunikatorze lub wiadomości email, aby zaprosić znajomych.\n </span>\n <input type=\"text\" class=\"togetherjs-share-link\">\n </p>\n <p class=\"togetherjs-if-creator\">Prześlij powyższy link znajomemu, żeby mógł dołączyć do Twojej sesji! Możesz znaleźć link zapraszający również na pasku TOOL_NAME.</p>\n </section>\n\n <section class=\"togetherjs-walkthrough-slide\">\n <p class=\"togetherjs-walkthrough-main-image\"><img src=\"http://localhost:8080/togetherjs/images/walkthrough-images-participant.png\"></p>\n <p>Twoi znajomi, którzy dołączą do Twojej sesji TOOL_NAME będą widoczni tutaj. Możesz kliknąć ich zdjęcia aby zobaczyć więcej na ich temat.</p>\n </section>\n\n <section class=\"togetherjs-walkthrough-slide\">\n <p class=\"togetherjs-walkthrough-main-image\"><img src=\"http://localhost:8080/togetherjs/images/walkthrough-images-chat.png\"></p>\n <p>Kiedy Twoi znajomi dołączą do Ciebie w Twojej sesji TOOL_NAME, w tym miejscu będziesz mógł z nimi rozmawiać!</p>\n </section>\n\n <section class=\"togetherjs-walkthrough-slide\">\n <p class=\"togetherjs-walkthrough-main-image\"><img src=\"http://localhost:8080/togetherjs/images/walkthrough-images-rtc.png\"></p>\n <p>Jeśli Twoja przeglądarka wspiera tą technologię, kliknij ikonę mikrofonu, aby zacząć rozmowę głosową. Dowiedz się więcej na temat tej eksperymentalnej technologii <a href=\"https://github.com/mozilla/togetherjs/wiki/About-Audio-Chat-and-WebRTC\" target=\"_blank\">tutaj</a>.</p>\n </section>\n\n <section class=\"togetherjs-walkthrough-slide\">\n <p class=\"togetherjs-walkthrough-main-image\"><img src=\"http://localhost:8080/togetherjs/images/walkthrough-images-logo.png\"></p>\n <p>W porządku, możesz już używać TOOL_NAME. Zacznij współpracować nad SITE_NAME!</p>\n </section>\n\n <div style=\"display: none\">\n <!-- There is one of these created for each slide: -->\n <span id=\"togetherjs-template-walkthrough-slide-progress\" class=\"togetherjs-walkthrough-slide-progress\">&#9679;</span>\n </div>\n <section id=\"togetherjs-walkthrough-progress\">\n </section>\n\n <section class=\"togetherjs-buttons\">\n <button class=\"togetherjs-primary togetherjs-dismiss\">Jestem gotowy!</button>\n </section>\n\n</div><!-- /.togetherjs-modal -->\n",
  9430. names: "Przyjazny Pingwin, Czujna Czajka, Ryży Ryś, Przebiegła Pantera, Błogi Bóbr, Puchaty Puchacz, Bohaterski Borsuk, Mądra Mysz, Łasa Łasica"
  9431. };
  9432. });
  9433. /* This Source Code Form is subject to the terms of the Mozilla Public
  9434. * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  9435. * You can obtain one at http://mozilla.org/MPL/2.0/. */
  9436. define('templates-ru',[], function () {
  9437. return {
  9438. "interface": "<% /*\n This is basically all the markup and interface for TogetherJS.\n Note all links should be like http://localhost:8080/togetherjs/*\n these links are rewritten with the location where TogetherJS was deployed.\n\n This file is inlined into togetherjs/templates.js\n*/ %>\n<div id=\"togetherjs-container\" class=\"togetherjs\">\n\n <!-- This is the main set of buttons: -->\n <div id=\"togetherjs-dock\" class=\"togetherjs-dock-right\">\n <div id=\"togetherjs-dock-anchor\" title=\"Передвинуть панель\">\n <span id=\"togetherjs-dock-anchor-horizontal\">\n <img src=\"http://localhost:8080/togetherjs/images/icn-handle-circle@2x.png\" alt=\"drag\">\n </span>\n <span id=\"togetherjs-dock-anchor-vertical\">\n <img src=\"http://localhost:8080/togetherjs/images/icn-handle-circle@2x.png\" alt=\"drag\">\n </span>\n </div>\n <div id=\"togetherjs-buttons\">\n <div style=\"display: none\">\n <button id=\"togetherjs-template-dock-person\" class=\"togetherjs-button togetherjs-dock-person\">\n <div class=\"togetherjs-tooltip togetherjs-dock-person-tooltip\">\n <span class=\"togetherjs-person-name\"></span>\n <span class=\"togetherjs-person-tooltip-arrow-r\"></span>\n </div>\n <div class=\"togetherjs-person togetherjs-person-status-overlay\"></div>\n </button>\n </div>\n <button id=\"togetherjs-profile-button\" class=\"togetherjs-button\" title=\"Это Вы\">\n <div class=\"togetherjs-person togetherjs-person-self\"></div>\n <div id=\"togetherjs-profile-arrow\"></div>\n </button>\n <button id=\"togetherjs-share-button\" class=\"togetherjs-button\" title=\"Добавить друга\"></button>\n <button id=\"togetherjs-audio-button\" class=\"togetherjs-button\" title=\"Включить микрофон\">\n <span id=\"togetherjs-audio-unavailable\" class=\"togetherjs-audio-set\" data-toggles=\".togetherjs-audio-set\">\n </span>\n <span id=\"togetherjs-audio-ready\" class=\"togetherjs-audio-set\" data-toggles=\".togetherjs-audio-set\" style=\"display: none\">\n </span>\n <span id=\"togetherjs-audio-outgoing\" class=\"togetherjs-audio-set\" data-toggles=\".togetherjs-audio-set\" style=\"display: none\">\n </span>\n <span id=\"togetherjs-audio-incoming\" class=\"togetherjs-audio-set\" data-toggles=\".togetherjs-audio-set\" style=\"display: none\">\n </span>\n <span id=\"togetherjs-audio-active\" class=\"togetherjs-audio-set\" data-toggles=\".togetherjs-audio-set\" style=\"display: none\">\n </span>\n <span id=\"togetherjs-audio-muted\" class=\"togetherjs-audio-set\" data-toggles=\".togetherjs-audio-set\" style=\"display: none\">\n </span>\n <span id=\"togetherjs-audio-error\" class=\"togetherjs-audio-set\" data-toggles=\".togetherjs-audio-set\" style=\"display: none\">\n </span>\n </button>\n <button id=\"togetherjs-chat-button\" class=\"togetherjs-button\" title=\"Чат\"></button>\n <div id=\"togetherjs-dock-participants\"></div>\n </div>\n </div>\n\n <!-- The window for editing the avatar: -->\n <div id=\"togetherjs-avatar-edit\" class=\"togetherjs-modal\"\n style=\"display: none\">\n <header> Обновить аватар </header>\n <section>\n <div class=\"togetherjs-avatar-preview togetherjs-person togetherjs-person-self\"></div>\n <div id=\"togetherjs-avatar-buttons\">\n <input type=\"file\" class=\"togetherjs-upload-avatar\">\n <!--<button id=\"togetherjs-upload-avatar\" class=\"togetherjs-primary\">Upload a picture</button>-->\n <!--<button id=\"togetherjs-camera-avatar\" class=\"togetherjs-default\">Take a picture</button>-->\n </div>\n </section>\n <section class=\"togetherjs-buttons\">\n <button class=\"togetherjs-cancel togetherjs-dismiss\">Отмена</button>\n <span class=\"togetherjs-alt-text\">или</span>\n <button class=\"togetherjs-avatar-save togetherjs-primary\">Сохранить</button>\n </section>\n </div>\n\n <!-- The window for sharing the link: -->\n <div id=\"togetherjs-share\" class=\"togetherjs-window\"\n data-bind-to=\"#togetherjs-share-button\" style=\"display: none\">\n <header> Пригласить друга </header>\n <section>\n <div class=\"togetherjs-not-mobile\">\n <p>Скопируйте эту ссылку и приклейте её в IM или в имейл:</p>\n <input type=\"text\" class=\"togetherjs-share-link\">\n </div>\n <div class=\"togetherjs-only-mobile\">\n <p>Скопируйте эту ссылку и приклейте её в IM или в имейл:</p>\n <input type=\"text\" class=\"togetherjs-share-link\">\n <!-- <a class=\"togetherjs-share-link\" href=\"#\">Press your thumb here.</a> -->\n </div>\n </section>\n </div>\n\n <!-- Participant Full List view template: -->\n <div id=\"togetherjs-participantlist\" class=\"togetherjs-window\"\n data-bind-to=\"#togetherjs-participantlist-button\" style=\"display: none\">\n <header> Участники </header>\n <section>\n <div class=\"togetherjs-not-mobile\">\n <ul>\n <li id=\"togetherjs-participant-item\">\n <img class=\"togetherjs-person togetherjs-person-small\" src=\"http://localhost:8080/togetherjs/images/btn-menu-change-avatar.png\">\n <span class=\"tj-name togetherjs-person-name\">NAME</span>\n <span class=\"tj-status\">&#9679;</span>\n <p class=\"tj-urllocation\">Сейчас на: <a class=\"togetherjs-person-url togetherjs-person-url-title\" href=\"\">http://www.location.comwww.location.comwww.location.comasdfsafd</a></p>\n <p class=\"tj-follow\">Последовать:\n <label class=\"togetherjs-follow-question\" for=\"togetherjs-person-status-follow\">\n <input type=\"checkbox\" id=\"togetherjs-person-status-follow\">\n </label>\n </p>\n <section class=\"togetherjs-buttons\">\n <!-- Displayed when the peer is at a different URL: -->\n <div class=\"togetherjs-different-url\">\n <a class=\"togetherjs-nudge togetherjs-default tj-btn-sm\">Толкнуть друга</a>\n <a href=\"#\" class=\"togetherjs-follow togetherjs-person-url togetherjs-primary tj-btn-sm\">Присоединиться к нему</a>\n </div>\n <!-- Displayed when the peer is at your same URL: -->\n <div class=\"togetherjs-same-url\" style=\"display: none\">\n <span class=\"togetherjs-person-name\"></span> с Вами на одной странице.\n </div>\n </section>\n </li>\n </ul>\n </section>\n </div>\n\n <!-- Participant detail template: -->\n <div id=\"togetherjs-template-participant-window\" class=\"togetherjs-window\" style=\"display: none\">\n <header><div class=\"togetherjs-person togetherjs-person-small\"></div><span class=\"togetherjs-person-name\"></span></header>\n\n <section class=\"togetherjs-participant-window-main\">\n <p class=\"togetherjs-participant-window-row\"><strong>Роль:</strong>\n <span class=\"togetherjs-person-role\"></span>\n </p>\n\n <p class=\"togetherjs-participant-window-row\"><strong>Сейчас на:</strong>\n <a class=\"togetherjs-person-url togetherjs-person-url-title\"></a>\n </p>\n\n <p class=\"togetherjs-participant-window-row\"><strong>Статус:</strong>\n <span class=\"togetherjs-person-status\"></span>\n </p>\n\n <p class=\"togetherjs-participant-window-row\"><strong class=\"togetherjs-float-left\">Следовать за этим участником:</strong>\n <label class=\"togetherjs-follow-question togetherjs-float-left\" for=\"togetherjs-person-status-follow\">\n <input type=\"checkbox\" id=\"togetherjs-person-status-follow\">\n </label>\n <span class=\"togetherjs-clear\"></span>\n </p>\n\n </section>\n\n <section class=\"togetherjs-buttons\">\n <!-- Displayed when the peer is at a different URL: -->\n <div class=\"togetherjs-different-url\">\n <a class=\"togetherjs-nudge togetherjs-default\">Толкнуть друга</a>\n <a href=\"#\" class=\"togetherjs-follow togetherjs-person-url togetherjs-primary\">Присоединиться к нему</a>\n </div>\n <!-- Displayed when the peer is at your same URL: -->\n <div class=\"togetherjs-same-url\" style=\"display: none\">\n <span class=\"togetherjs-person-name\"></span> с Вами на одной странице.\n </div>\n </section>\n </div>\n\n <!-- The chat screen: -->\n <div id=\"togetherjs-chat\" class=\"togetherjs-window\" data-bind-to=\"#togetherjs-chat-button\"\n style=\"display: none\">\n <header> Чат </header>\n <section class=\"togetherjs-subtitle\">\n <div id=\"togetherjs-chat-participants\" data-toggles=\"#togetherjs-chat-no-participants\" style=\"display: none\">\n <span id=\"togetherjs-chat-participant-list\"></span>\n &amp; Вы\n </div>\n <div id=\"togetherjs-chat-no-participants\" data-toggles=\"#togetherjs-chat-participants\">\n Здесь больше никого нет.\n </div>\n </section>\n\n <div style=\"display: none\">\n\n <!-- Template for one message: -->\n <div id=\"togetherjs-template-chat-message\" class=\"togetherjs-chat-item togetherjs-chat-message\">\n <div class=\"togetherjs-person\"></div>\n <div class=\"togetherjs-timestamp\"><span class=\"togetherjs-time\">HH:MM</span> <span class=\"togetherjs-ampm\">AM/PM</span></div>\n <div class=\"togetherjs-person-name-abbrev\"></div>\n <div class=\"togetherjs-chat-content togetherjs-sub-content\"></div>\n </div>\n\n <!-- Template for when a person leaves: -->\n <div id=\"togetherjs-template-chat-left\" class=\"togetherjs-chat-item togetherjs-chat-left-item\">\n <div class=\"togetherjs-person\"></div>\n <div class=\"togetherjs-ifnot-declinedJoin\">\n <div class=\"togetherjs-inline-text\"><span class=\"togetherjs-person-name\"></span> покинул сеанс.</div>\n </div>\n <div class=\"togetherjs-if-declinedJoin\">\n <div class=\"togetherjs-inline-text\"><span class=\"togetherjs-person-name\"></span> отказался присоединиться к сеансу.</div>\n </div>\n <div class=\"togetherjs-clear\"></div>\n </div>\n\n <!-- Template when a person joins the session: -->\n <div id=\"togetherjs-template-chat-joined\" class=\"togetherjs-chat-item togetherjs-chat-join-item\">\n <div class=\"togetherjs-person\"></div>\n <div class=\"togetherjs-inline-text\"><span class=\"togetherjs-person-name\"></span> присоединился к сеансу.</div>\n <div class=\"togetherjs-clear\"></div>\n </div>\n\n <!-- Template for system-derived messages: -->\n <div id=\"togetherjs-template-chat-system\" class=\"togetherjs-chat-item\">\n <span class=\"togetherjs-chat-content togetherjs-sub-content\"></span>\n </div>\n\n <!-- Template when a person joins the session: -->\n <!-- <div id=\"togetherjs-template-chat-joined\" class=\"togetherjs-chat-item togetherjs-chat-join-item\">\n <div class=\"togetherjs-person\"></div>\n <div class=\"togetherjs-inline-text\"><span class=\"togetherjs-person-name\"></span> joined the session.</div>\n <div class=\"togetherjs-clear\"></div>\n </div> -->\n\n <!-- Template for when someone goes to a new URL: -->\n <div id=\"togetherjs-template-url-change\" class=\"togetherjs-chat-item togetherjs-chat-url-change\">\n <div class=\"togetherjs-person\"></div>\n <div class=\"togetherjs-inline-text\">\n <div class=\"togetherjs-if-sameUrl\">\n <span class=\"togetherjs-person-name\"></span>\n с Вами на одной странице.\n </div>\n <div class=\"togetherjs-ifnot-sameUrl\">\n <span class=\"togetherjs-person-name\"></span>\n отправился на: <a href=\"#\" class=\"togetherjs-person-url togetherjs-person-url-title\" target=\"_self\"></a>\n <section class=\"togetherjs-buttons togetherjs-buttons-notification-diff-url\">\n <!-- Displayed when the peer is at a different URL: -->\n <div class=\"togetherjs-different-url togetherjs-notification-diff-url\">\n <a class=\"togetherjs-nudge togetherjs-default\">Толкнуть друга</a>\n <a href=\"#\" class=\"togetherjs-follow togetherjs-person-url togetherjs-primary\">Присоединиться к нему</a>\n </div>\n </section>\n\n <!-- <div>\n <a class=\"togetherjs-nudge togetherjs-secondary\">Nudge them</a>\n <a href=\"\" class=\"togetherjs-person-url togetherjs-follow togetherjs-primary\">Join them</a>\n </div> -->\n\n </div>\n </div>\n <div class=\"togetherjs-clear\"></div>\n </div>\n </div>\n\n <section id=\"togetherjs-chat-messages\">\n <!-- FIX ME// need to have some dialogue that says something like - There are no chats yet! -->\n </section>\n <section id=\"togetherjs-chat-input-box\">\n <textarea id=\"togetherjs-chat-input\" placeholder=\"Напечатайте своё сообщение здесь\"></textarea>\n </section>\n </div>\n\n <!-- this is a kind of warning popped up when you (successfully) start RTC: -->\n <div id=\"togetherjs-rtc-info\" class=\"togetherjs-window\"\n data-bind-to=\"#togetherjs-audio-button\"\n style=\"display: none\">\n\n <header> Аудио-чат </header>\n <section>\n <p>\n Включите <strong>микрофон браузера</strong> возле адресной строки вверху.\n </p>\n <p>\n Разговор по микрофону через веб-браузер - экспериментальное свойство.\n </p>\n <p>\n Почитайте больше об аудио-чате <a href=\"https://github.com/mozilla/togetherjs/wiki/About-Audio-Chat-and-WebRTC\" target=\"_blank\">здесь</a>.\n </p>\n </section>\n\n <section class=\"togetherjs-buttons\">\n <label for=\"togetherjs-rtc-info-dismiss\" style=\"display: inline;\">\n <input class=\"togetherjs-dont-show-again\" id=\"togetherjs-rtc-info-dismiss\" type=\"checkbox\">\n Больше не показывать.\n </label>\n <button class=\"togetherjs-default togetherjs-dismiss\" type=\"button\">Закрыть</button>\n </section>\n </div>\n\n <!-- this is popped up when you hit the audio button, but RTC isn't\n supported: -->\n <div id=\"togetherjs-rtc-not-supported\" class=\"togetherjs-window\"\n data-bind-to=\"#togetherjs-audio-button\"\n style=\"display: none\">\n <header> Аудио-чат </header>\n\n <section>\n <p>Аудио-чат требует, чтобы Вы использовали<a href='https://github.com/mozilla/togetherjs/wiki/About-Audio-Chat-and-WebRTC' target='_blank'>браузер поновее</a>!</p>\n <p>\n Для живого аудио-чата требуется более новый (или другой) браузер, чем тот, который Вы используете.\n </p>\n <p>\n Посмотрите на <a href='https://github.com/mozilla/togetherjs/wiki/About-Audio-Chat-and-WebRTC' target='_blank'>'этой странице'</a> дополнительную информацию и список поддерживаемых браузеров.\n </p>\n </section>\n\n <section class=\"togetherjs-buttons\">\n <div class=\"togetherjs-rtc-dialog-btn\">\n <button class=\"togetherjs-default togetherjs-dismiss\" type=\"button\">Закрыть</button>\n </div>\n </section>\n </div>\n\n <!-- The popup when a chat message comes in and the #togetherjs-chat window isn't open -->\n <div id=\"togetherjs-chat-notifier\" class=\"togetherjs-notification\"\n data-bind-to=\"#togetherjs-chat-button\"\n style=\"display: none\">\n <img src=\"http://localhost:8080/togetherjs/images/notification-togetherjs-logo.png\" class=\"togetherjs-notification-logo\" alt=\"\">\n <img src=\"http://localhost:8080/togetherjs/images/notification-btn-close.png\" class=\"togetherjs-notification-closebtn togetherjs-dismiss\" alt=\"[close]\">\n <section id=\"togetherjs-chat-notifier-message\">\n </section>\n </div>\n\n <!-- The menu when you click on the profile: -->\n <div id=\"togetherjs-menu\" class=\"togetherjs-menu\" style=\"display: none\">\n <div class=\"togetherjs-menu-item togetherjs-menu-disabled\" id=\"togetherjs-menu-profile\">\n <img id=\"togetherjs-menu-avatar\">\n <span class=\"togetherjs-person-name-self\" id=\"togetherjs-self-name-display\" data-toggles=\"#togetherjs-menu .togetherjs-self-name\">[nickname]</span>\n <input class=\"togetherjs-self-name\" type=\"text\" data-toggles=\"#togetherjs-self-name-display\" style=\"display: none\" placeholder=\"Введите своё имя\">\n </div>\n <div class=\"togetherjs-menu-hr-avatar\"></div>\n <div class=\"togetherjs-menu-item\" id=\"togetherjs-menu-update-name\"><img src=\"http://localhost:8080/togetherjs/images/button-pencil.png\" alt=\"\"> Обновить своё имя</div>\n <div class=\"togetherjs-menu-item\" id=\"togetherjs-menu-update-avatar\"><img src=\"http://localhost:8080/togetherjs/images/btn-menu-change-avatar.png\" alt=\"\"> Изменить аватар</div>\n <div class=\"togetherjs-menu-item\" id=\"togetherjs-menu-update-color\"><span class=\"togetherjs-person-bgcolor-self\"></span> Выбрать цвет профиля</div>\n <div class=\"togetherjs-hr\"></div>\n <div class=\"togetherjs-menu-item\" id=\"togetherjs-menu-help\">Помощь</div>\n <div class=\"togetherjs-menu-item\" id=\"togetherjs-menu-feedback\">Отзывы и предложения</div>\n <div id=\"togetherjs-invite\" style=\"display: none\">\n <div class=\"togetherjs-hr\"></div>\n <div id=\"togetherjs-invite-users\"></div>\n <div class=\"togetherjs-menu-item\" id=\"togetherjs-menu-refresh-invite\">Refresh users</div>\n <div class=\"togetherjs-menu-item\" id=\"togetherjs-menu-invite-anyone\">Пригласить кого-нибудь</div>\n </div>\n <div class=\"togetherjs-hr\"></div>\n <div class=\"togetherjs-menu-item\" id=\"togetherjs-menu-end\"><img src=\"http://localhost:8080/togetherjs/images/button-end-session.png\" alt=\"\"> Закончить <span class=\"togetherjs-tool-name\">TogetherJS</span></div>\n </div>\n\n <!-- template for one person in the invite-users list -->\n <div style=\"display: none\">\n <div id=\"togetherjs-template-invite-user-item\" class=\"togetherjs-menu-item\">\n <!-- FIXME: should include avatar in some way -->\n <span class=\"togetherjs-person-name\"></span>\n </div>\n </div>\n\n <!-- A window version of #togetherjs-menu, for use on mobile -->\n <div id=\"togetherjs-menu-window\" class=\"togetherjs-window\" style=\"display: none\">\n <header>Установки и профиль</header>\n <section>\n <div class=\"togetherjs-menu-item\">\n <img class=\"togetherjs-menu-avatar\">\n <span class=\"togetherjs-person-name-self\" id=\"togetherjs-self-name-display\"></span>\n </div>\n <div class=\"togetherjs-menu-hr-avatar\"></div>\n <div class=\"togetherjs-menu-item\" id=\"togetherjs-menu-update-name-button\"><img src=\"http://localhost:8080/togetherjs/images/button-pencil.png\" alt=\"\"> Обновить своё имя</div>\n <div class=\"togetherjs-menu-item\" id=\"togetherjs-menu-update-avatar-button\"><img src=\"http://localhost:8080/togetherjs/images/btn-menu-change-avatar.png\" alt=\"\"> Изменить аватар</div>\n <div class=\"togetherjs-menu-item\" id=\"togetherjs-menu-update-color-button\"><span class=\"togetherjs-person-bgcolor-self\"></span> Выбрать цвет профиля</div>\n <div class=\"togetherjs-hr\"></div>\n <div class=\"togetherjs-menu-item\" id=\"togetherjs-menu-help-button\">Помощь</div>\n <div class=\"togetherjs-menu-item\" id=\"togetherjs-menu-feedback-button\">Отзывы и предложения</div>\n <div class=\"togetherjs-hr\"></div>\n <div class=\"togetherjs-menu-item\" id=\"togetherjs-menu-end-button\"><img src=\"http://localhost:8080/togetherjs/images/button-end-session.png\" alt=\"\"> Закончить TOOL_NAME</div>\n </section>\n <section class=\"togetherjs-buttons\">\n <button class=\"togetherjs-dismiss togetherjs-primary\">Хорошо</button>\n </section>\n </div>\n\n <!-- The name editor, for use on mobile -->\n <div id=\"togetherjs-edit-name-window\" class=\"togetherjs-window\" style=\"display: none\">\n <header>Обновить имя</header>\n <section>\n <div>\n <input class=\"togetherjs-self-name\" type=\"text\" placeholder=\"Введите своё имя\">\n </div>\n </section>\n <section class=\"togetherjs-buttons\">\n <button class=\"togetherjs-dismiss togetherjs-primary\">Хорошо</button>\n </section>\n </div>\n\n <div class=\"togetherjs-menu\" id=\"togetherjs-pick-color\" style=\"display: none\">\n <div class=\"togetherjs-triangle-up\"><img src=\"http://localhost:8080/togetherjs/images/icn-triangle-up.png\"></div>\n <div style=\"display: none\">\n <div id=\"togetherjs-template-swatch\" class=\"togetherjs-swatch\">\n </div>\n </div>\n </div>\n\n <!-- Invisible elements that handle the RTC audio: -->\n <audio id=\"togetherjs-audio-element\"></audio>\n <audio id=\"togetherjs-local-audio\" muted=\"true\" volume=\"0.3\"></audio>\n <audio id=\"togetherjs-notification\" src=\"http://localhost:8080/togetherjs/images/notification.ogg\"></audio>\n\n <!-- The intro screen for someone who joins a session the first time: -->\n <div id=\"togetherjs-intro\" class=\"togetherjs-modal\" style=\"display: none\">\n <header>Присоединиться к сеансу TOOL_NAME?</header>\n <section>\n <p>Ваш друг просит Вас присоединиться к его TOOL_SITE_LINK браузер-сеансу, чтобы сотрудничать с Вами в реальном времени!</p>\n <p>Хотели бы Вы присоединиться к его сеансу?</p>\n </section>\n\n <section class=\"togetherjs-buttons\">\n <button class=\"togetherjs-destructive togetherjs-modal-dont-join\">Нет, не присоединяться</button>\n <button class=\"togetherjs-primary togetherjs-dismiss\">Да, присоединиться к сеансу</button>\n </section>\n </div>\n\n <!-- Shown when a web browser is completely incapable of running TogetherJS: -->\n <div id=\"togetherjs-browser-broken\" class=\"togetherjs-modal\" style=\"display: none\">\n <header> Извините </header>\n\n <section>\n <p>\n Извините, TOOL_NAME не работает на этом браузере. Просьба <a href='https://github.com/mozilla/togetherjs/wiki/Supported-Browsers#supported-browsers'>обновиться до поддерживаемого браузера</a>, чтобы попробовать TOOL_NAME.\n </p>\n\n <p id=\"togetherjs-browser-broken-is-ie\" style=\"display: none\">\n Нам нужна Ваша помощь в починке TogetherJS на Internet Explorer! Вот список <a href=\"https://github.com/mozilla/togetherjs/issues?labels=IE&milestone=&page=1&state=open\" target=\"_blank\">ошибок на GitHub</a>, которые нам нужно починить, и над которыми Вы можете поработать.\n Internet Explorer <a href=\"https://github.com/mozilla/togetherjs/wiki/Supported-Browsers#internet-explorer\"> в настоящее время не поддерживается</a>. Если Вы действительно хотите испытать TogetherJS, советуем Вам использовать Firefox или Chrome.\n </p>\n </section>\n\n <section class=\"togetherjs-buttons\">\n <button class=\"togetherjs-dismiss togetherjs-primary\">Закончить TOOL_NAME</button>\n </section>\n\n </div>\n\n <!-- Shown when the browser has WebSockets, but is IE (i.e., IE10) -->\n <div id=\"togetherjs-browser-unsupported\" class=\"togetherjs-modal\" style=\"display: none\">\n <header> Браузер не поддерживается </header>\n\n <section>\n <p>\n Нам нужна Ваша помощь в починке TogetherJS на Internet Explorer! Вот список <a href=\"https://github.com/mozilla/togetherjs/issues?labels=IE&milestone=&page=1&state=open\" target=\"_blank\">ошибок на GitHub</a>, которые нам нужно починить, и над которыми Вы можете поработать.\n Internet Explorer <a href=\"https://github.com/mozilla/togetherjs/wiki/Supported-Browsers#internet-explorer\"> в настоящее время не поддерживается</a>. Если Вы действительно хотите испытать TogetherJS, советуем Вам использовать Firefox или Chrome.\n </p>\n\n <p>Вы можете продолжать пробовать использовать TOOL_NAME, но скорее всего Вы получите массу ошибок. Мы Вас предупредили.</p>\n\n </section>\n\n <section class=\"togetherjs-buttons\">\n <button class=\"togetherjs-dismiss togetherjs-primary\">Закончить TOOL_NAME</button>\n <button class=\"togetherjs-dismiss togetherjs-secondary togetherjs-browser-unsupported-anyway\">Всё равно попробовать TOOL_NAME</button>\n </section>\n\n </div>\n\n <div id=\"togetherjs-confirm-end\" class=\"togetherjs-modal\" style=\"display: none\">\n <header> Закончить сеанс? </header>\n <section>\n <p>\n Вы уверены, что хотите завершить свой сеанс TOOL_NAME?\n </p>\n </section>\n <section class=\"togetherjs-buttons\">\n <button class=\"togetherjs-cancel togetherjs-dismiss\">Отмена</button>\n <span class=\"togetherjs-alt-text\">или</span>\n <button id=\"togetherjs-end-session\" class=\"togetherjs-destructive\">Закончить сеанс</button>\n </section>\n </div>\n\n <div id=\"togetherjs-feedback-form\" class=\"togetherjs-modal\" style=\"display: none;\">\n <header> Отзывы и предложения </header>\n <iframe src=\"https://docs.google.com/a/mozilla.com/forms/d/1lVE7JyRo_tjakN0mLG1Cd9X9vseBX9wci153z9JcNEs/viewform?embedded=true\" width=\"400\" height=\"300\" frameborder=\"0\" marginheight=\"0\" marginwidth=\"0\">Загружаю форму ...</iframe>\n <!-- <p><button class=\"togetherjs-modal-close\">Close</button></p> -->\n </div>\n\n <div style=\"display: none\">\n <!-- This is when you join a session and the other person has already changed to another URL: -->\n <div id=\"togetherjs-template-url-change\" class=\"togetherjs-modal\">\n <header> Следую на новый URL... </header>\n <section>\n <div class=\"togetherjs-person\"></div>\n Следую\n <span class=\"togetherjs-person-name\"></span>\n на <a href=\"\" class=\"togetherjs-person-url togetherjs-person-url-title\"></a>\n </section>\n </div>\n\n <!-- This is when someone invites you to their session: -->\n <div id=\"togetherjs-template-invite\" class=\"togetherjs-chat-item\">\n <div class=\"togetherjs-person\"></div>\n <div>\n <span class=\"togetherjs-person-name\"></span>\n пригласил <strong class=\"togetherjs-if-forEveryone\">кого-либо</strong>\n <strong class=\"togetherjs-ifnot-forEveryone\">Вас</strong>\n на <a href=\"\" data-togetherjs-subattr-href=\"href\" class=\"togetherjs-sub-hrefTitle\" target=\"_blank\"></a>\n </div>\n </div>\n\n </div>\n\n <!-- The pointer at the side of a window: -->\n <div id=\"togetherjs-window-pointer-right\" style=\"display: none\"></div>\n <div id=\"togetherjs-window-pointer-left\" style=\"display: none\"></div>\n\n <!-- The element that overlaps the background of the page during a modal dialog: -->\n <div id=\"togetherjs-modal-background\" style=\"display: none\"></div>\n\n <!-- Some miscellaneous templates -->\n <div style=\"display: none\">\n\n <!-- This is the cursor: -->\n <div id=\"togetherjs-template-cursor\" class=\"togetherjs-cursor togetherjs\">\n <!-- Note: images/cursor.svg is a copy of this (for editing): -->\n <!-- crossbrowser svg dropshadow http://demosthenes.info/blog/600/Creating-a-True-CrossBrowser-Drop-Shadow- -->\n <svg version=\"1.1\" id=\"Layer_1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" x=\"0px\" y=\"0px\"\n \t width=\"15px\" height=\"22.838px\" viewBox=\"96.344 146.692 15 22.838\" enable-background=\"new 96.344 146.692 15 22.838\"\n \t xml:space=\"preserve\">\n <path fill=\"#231F20\" d=\"M98.984,146.692c2.167,1.322,1.624,6.067,3.773,7.298c-0.072-0.488,2.512-0.931,3.097,0\n \tc0.503,0.337,1.104-0.846,2.653,0.443c0.555,0.593,3.258,2.179,1.001,8.851c-0.446,1.316,2.854,0.135,1.169,2.619\n \tc-3.748,5.521-9.455,2.787-9.062,1.746c1.06-2.809-6.889-4.885-4.97-9.896c0.834-2.559,2.898,0.653,2.923,0.29\n \tc-0.434-1.07-2.608-5.541-2.923-6.985C96.587,150.793,95.342,147.033,98.984,146.692z\"/>\n </svg>\n <!-- <img class=\"togetherjs-cursor-img\" src=\"http://localhost:8080/togetherjs/images/cursor.svg\"> -->\n <span class=\"togetherjs-cursor-container\">\n <span class=\"togetherjs-cursor-name\"></span>\n <span style=\"display:none\" class=\"togetherjs-cursor-typing\" id=\"togetherjs-cursor-typebox\">\n <span class=\"togetherjs-typing-ellipse-one\">&#9679;</span><span class=\"togetherjs-typing-ellipse-two\">&#9679;</span><span class=\"togetherjs-typing-ellipse-three\">&#9679;</span>\n </span>\n <!-- Displayed when the cursor is below the screen: -->\n <span class=\"togetherjs-cursor-down\">\n\n </span>\n <!-- Displayed when the cursor is above the screen: -->\n <span class=\"togetherjs-cursor-up\">\n\n </span>\n </span>\n </div>\n\n <!-- This is the element that goes around focused form elements: -->\n <div id=\"togetherjs-template-focus\">\n <div class=\"togetherjs-focus togetherjs-person-bordercolor\"></div>\n </div>\n\n <!-- This is a click: -->\n <div id=\"togetherjs-template-click\" class=\"togetherjs-click togetherjs\">\n </div>\n </div>\n</div>\n",
  9439. walkthrough: "<!--\n Any elements with .togetherjs-walkthrough-firsttime will only be\n displayed on during the first-time experience. Any elements with\n .togetherjs-walkthrough-not-firsttime will only be displayed when\n the walkthrough is accessed through the Help menu.\n\n Note you *cannot* use <section class=\"togetherjs-walkthrough-slide\n togetherjs-walkthrough-firsttime\">: the number of sections must be the\n same regardless.\n -->\n<div id=\"togetherjs-walkthrough\" class=\"togetherjs-modal togetherjs-modal-wide\">\n <header>Вы используете TOOL_NAME!<button class=\"togetherjs-close\"></button></header>\n\n <div id=\"togetherjs-walkthrough-previous\"></div>\n <div id=\"togetherjs-walkthrough-next\"></div>\n\n <section class=\"togetherjs-walkthrough-slide\">\n <p class=\"togetherjs-walkthrough-main-image\"><img src=\"http://localhost:8080/togetherjs/images/walkthrough-images-intro.png\"></p>\n\t<p>TOOL_NAME - это служба для Вашего сайта, которая помогает Вам сотрудничать с другими в реальном времени на сайте SITE_NAME </p>\n </section>\n\n <section class=\"togetherjs-walkthrough-slide\">\n <div class=\"togetherjs-walkthrough-firsttime\">\n <div class=\"togetherjs-walkthrough-main-image\">\n <div class=\"togetherjs-walkthrough-avatar-section\">\n <div class=\"togetherjs-avatar-preview togetherjs-person togetherjs-person-self\"></div>\n <div class=\"togetherjs-avatar-upload-input\"><input type=\"file\" class=\"togetherjs-upload-avatar\"></div>\n </div>\n <input class=\"togetherjs-self-name\" type=\"text\" placeholder=\"Введите своё имя\">\n <div class=\"togetherjs-swatch togetherjs-person-bgcolor-self\"></div>\n <div class=\"togetherjs-save-settings\">\n <button class=\"togetherjs-avatar-save togetherjs-primary\">\n <span id=\"togetherjs-avatar-when-unsaved\">\"\"Сохранить\"\"</span>\n <span id=\"togetherjs-avatar-when-saved\" style=\"display: none\">Сохранено!</span>\n </button>\n </div>\n </div>\n <p>Задайте свои аватар, имя и пользовательский цвет вверху. При желании изменить их позднее, щёлкните по кнопке Профиль.</p>\n </div>\n <div class=\"togetherjs-walkthrough-not-firsttime\">\n <p class=\"togetherjs-walkthrough-main-image\"><img src=\"http://localhost:8080/togetherjs/images/walkthrough-images-profile.png\"></p>\n <p>Вы можете изменить свои аватар, имя и пользовательский цвет, используя кнопку Профиль.</p>\n </div>\n </section>\n\n <section class=\"togetherjs-walkthrough-slide\">\n <p class=\"togetherjs-walkthrough-main-image togetherjs-ifnot-creator\"><img src=\"http://localhost:8080/togetherjs/images/walkthrough-images-invite.png\">\n </p>\n <p class=\"togetherjs-ifnot-creator\">Вы можете пригласить ещё друзей на Ваш сеанс, послав им ссылку с приглашением на панели TOOL_NAME.</p>\n <p class=\"togetherjs-walkthrough-main-image togetherjs-if-creator\">\n <span class=\"togetherjs-walkthrough-sendlink\">\n Скопируйте и приклейте эту ссылку в IM или в имейл, чтобы пригласить друзей.<br>\n </span>\n <input type=\"text\" class=\"togetherjs-share-link\">\n </p>\n <p class=\"togetherjs-if-creator\">Пошлите другу ссылку вверху, чтобы он мог присоединиться к Вашему сеансу! Вы также можете найти эту ссылку с приглашением на панели TOOL_NAME.</p>\n </section>\n\n <section class=\"togetherjs-walkthrough-slide\">\n <p class=\"togetherjs-walkthrough-main-image\"><img src=\"http://localhost:8080/togetherjs/images/walkthrough-images-participant.png\"></p>\n <p>Друзья, которые присоединятся к Вашему сеансу TOOL_NAME, появятся здесь. Вы можете щёлкнуть по аватару друга, чтобы увидеть больше информации.</p>\n </section>\n\n <section class=\"togetherjs-walkthrough-slide\">\n <p class=\"togetherjs-walkthrough-main-image\"><img src=\"http://localhost:8080/togetherjs/images/walkthrough-images-chat.png\"></p>\n <p>Когда Ваши друзья присоединятся к Вашему сеансу TOOL_NAME, Вы сможете общаться с ними здесь!</p>\n </section>\n\n <section class=\"togetherjs-walkthrough-slide\">\n <p class=\"togetherjs-walkthrough-main-image\"><img src=\"http://localhost:8080/togetherjs/images/walkthrough-images-rtc.png\"></p>\n <p>Если Ваш браузер поддерживает аудио-чат, Вы можете запустить его нажатием на иконку микрофона. Узнайте побольше об этой экспериментальной функции <a href=\"https://github.com/mozilla/togetherjs/wiki/About-Audio-Chat-and-WebRTC\" target=\"_blank\">здесь</a>.</p>\n </section>\n\n <section class=\"togetherjs-walkthrough-slide\">\n <p class=\"togetherjs-walkthrough-main-image\"><img src=\"http://localhost:8080/togetherjs/images/walkthrough-images-logo.png\"></p>\n <p>Ну вот Вы и готовы использовать TOOL_NAME. Теперь начинайте сотрудничать на сайте SITE_NAME!</p>\n </section>\n\n <div style=\"display: none\">\n <!-- There is one of these created for each slide: -->\n <span id=\"togetherjs-template-walkthrough-slide-progress\" class=\"togetherjs-walkthrough-slide-progress\">&#9679;</span>\n </div>\n <section id=\"togetherjs-walkthrough-progress\">\n </section>\n\n <section class=\"togetherjs-buttons\">\n <button class=\"togetherjs-primary togetherjs-dismiss\">Я готов!</button>\n </section>\n\n</div><!-- /.togetherjs-modal -->\n",
  9440. names: "Лысый Лис, Большой Бобёр, Умная Сова, Жирный Жираф, Серый Волк, Толстый Тюлень, Рыба-кит, Кисьян Котяра, Известная Игуана"
  9441. };
  9442. });
  9443. TogetherJS.require = TogetherJS._requireObject = require;
  9444. TogetherJS._loaded = true;
  9445. require(["session"]);
  9446. }());