* *E$*>-*1*6*:*;*A+l+l++֑++y++3+" +0 + + +R ++++_+++s++`+z++e+ ++- +3!+#5"+_8#+O$+%+3+O4+?5+s6+7+s8+&9+:+3;+<+=+%>++?+1@+.IA+3VB+o[C+^D+nE+F+ӐG+tH+I+>J+HK+HL+M+N+!O+"P+(Q+.R+5S+]BT+2JU+XKV+pW+tX+Y+Z+`[+ޠ\+]+9^+_+l+m+$n+½r+s+t+&Su+Arv+[w+x+z y+ z+ {+ |+$" }+ ' ~+{) +0 +3 +? +G +J +5L +_O +\P +P +Qp +1x +j~ +~ + +. + + +S +V + +_ +2' +* + 0 +J4 +17 +F +~ + + +B +@E +J +_ +=r +v +} +~ + +Á + +Q + +z+ +7+Ě+a+˟+\+N+¤+Ħ++4++s+ +}+=+\H+'K+4+\ ++9,+*:+}=+m+)y+I+z+7+=+n@+G+O+R+gg+c++++,,,&,1,",,@,),( , ,6 ,J ,0 ,,5, ,3,`4, 5,"6,'7,08,O9,):,;,<, =,>,h?,@,TA,vB,C,DD,E,F,G,' H,\I,J,K,Z^L,^M,.zO,jP,zQ,1R,S,̮T,y,z,,,,,,v,yy,ʬ,J,,,,,<,O,BW,\,t,u,v,,~,},/,!,̯,,>,7,(8,S9,~:,E,G,G,PJ,@L,/M,N,`f,fs,", ,S,W,W,@,>,), ,,:,., ,,Ą#,#,#,£#,#,G#,ؽ#,L#,#,<#,#|.I#}.$$1D%:>e%;>:'<>z>'=>I'>>'c'?>Pk'@>y'B>*'C>'D>*[)E>v)F>w)G>)H>)I>)J>")K>)L>Z)M>>)O>)P>z)Q>)R>h)S>*T>$*X>(,Y>>,Z>,[>,\>,]>,^>i,_>,`>,a>,b>,c>-d>Z -e>'-f>2-g>+6-h>N-i>,c-j>c-k>Lk-l>(o-m>w-n>Gz-o>-p>-q>1-r>.s>&.t>[.u>s/v>/w>d/x>/y>/z>S0{>0|>X.0}>F^0>0d0>f0>0>0>y0O0O^0OC0O@0O0O0O0Os0O10O?0OC0O0O:0Os0O@0O0Ou0O#1O%1O41Oe61O81O2<1O=1OB1OH1OI1OpP1OX1OuZ1O*^1ON1OG1OZ1O72OE2OJ2O[a2Oh2FP,2GPi4`uT8c_T9c7U:c>U;c NUZU>cAeU@clUAcxUBcUCc_UDcUEcUFcUGcUHcUIcUJccUKcZVLch_VMcVNc-VOc#VPcVQc_XRcXSc|XTcXUcXVcHYWc>YXcO^Y {[ {[{$[{[{l[{[{մ[[
// Copyright (c) 2011 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. /** * This variable structure is here to document the structure that the template * expects to correctly populate the page. */ var moduleListDataFormat = { 'moduleList': [ { 'type': 'The type of module found', 'type_description': 'The type of module (string), defaults to blank for regular modules', 'status': 'The module status', 'location': 'The module path, not including filename', 'name': 'The name of the module', 'product_name': 'The name of the product the module belongs to', 'description': 'The module description', 'version': 'The module version', 'digital_signer': 'The signer of the digital certificate for the module', 'recommended_action': 'The help tips bitmask', 'possible_resolution': 'The help tips in string form', 'help_url': 'The link to the Help Center article' } ] }; /** * Takes the |moduleListData| input argument which represents data about * the currently available modules and populates the html jstemplate * with that data. It expects an object structure like the above. * @param {Object} moduleListData Information about available modules. */ function renderTemplate(moduleListData) { // This is the javascript code that processes the template: var input = new JsEvalContext(moduleListData); var output = $('modulesTemplate'); jstProcess(input, output); } /** * Asks the C++ ConflictsDOMHandler to get details about the available modules * and return detailed data about the configuration. The ConflictsDOMHandler * should reply to returnModuleList() (below). */ function requestModuleListData() { chrome.send('requestModuleList'); } /** * Called by the WebUI to re-populate the page with data representing the * current state of installed modules. * @param {Object} moduleListData Information about available modules. */ function returnModuleList(moduleListData) { renderTemplate(moduleListData); $('loading-message').style.visibility = 'hidden'; $('body-container').style.visibility = 'visible'; } // Get data and have it displayed upon loading. document.addEventListener('DOMContentLoaded', requestModuleListData);
LOADING_MESSAGE
// Copyright (c) 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. /** * Takes the |moduleListData| input argument which represents data about * the currently available modules and populates the html jstemplate * with that data. It expects an object structure like the above. * @param {Object} moduleListData Information about available modules */ function renderTemplate(moduleListData) { // This is the javascript code that processes the template: var input = new JsEvalContext(moduleListData); var output = $('flashInfoTemplate'); jstProcess(input, output); } /** * Asks the C++ FlashUIDOMHandler to get details about the Flash and return * the data in returnFlashInfo() (below). */ function requestFlashInfo() { chrome.send('requestFlashInfo'); } /** * Called by the WebUI to re-populate the page with data representing the * current state of Flash. * @param {Object} moduleListData Information about available modules. */ function returnFlashInfo(moduleListData) { $('loading-message').style.visibility = 'hidden'; $('body-container').style.visibility = 'visible'; renderTemplate(moduleListData); } // Get data and have it displayed upon loading. document.addEventListener('DOMContentLoaded', requestFlashInfo);
Loading...
/* Copyright (c) 2012 The Chromium Authors. All rights reserved. * Use of this source code is governed by a BSD-style license that can be * found in the LICENSE file. */ .key { font-weight: bold; width: 200px; } .value { margin-left: 10px; } // Copyright (c) 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. var nacl = nacl || {}; (function() { /** * Takes the |moduleListData| input argument which represents data about * the currently available modules and populates the html jstemplate * with that data. It expects an object structure like the above. * @param {Object} moduleListData Information about available modules */ function renderTemplate(moduleListData) { // Process the template. var input = new JsEvalContext(moduleListData); var output = $('naclInfoTemplate'); jstProcess(input, output); }; /** * Asks the C++ NaClUIDOMHandler to get details about the NaCl and return * the data in returnNaClInfo() (below). */ function requestNaClInfo() { chrome.send('requestNaClInfo'); }; /** * Called by the WebUI to re-populate the page with data representing the * current state of NaCl. * @param {Object} moduleListData Information about available modules */ nacl.returnNaClInfo = function(moduleListData) { $('loading-message').hidden = 'hidden'; $('body-container').hidden = ''; renderTemplate(moduleListData); }; // Get data and have it displayed upon loading. document.addEventListener('DOMContentLoaded', requestNaClInfo); })();

]{s6?UVVylln^ck^ٞU*%fF&u$/~ x!QIU,@4l_;g%p#psrO"zp9͜P(D3}Mމ E0Ağ"_>(Թ{qWF/3 pq9ܡԽ08ν;C{CWS뿾-PS|z Os|?9?⿆\oz?|t$c) FdAzW7mx6˸>LTdFeU=QKH]\^^ R@k0ῆ? P32YG f`?x}h&Fbjb_P^cWN֩G:/R@[pyg->$3ߠqn4yt1F#‽8d/ްoً߱^c/5ʳp1qqqqq1rr``ɽ/}p?p)>qc F6+7a%3Ž;$FaF]P,aNA>ݠ4]eXxM}I 'bf%{ntxNXxE)~@6& yG~2G ub<8r{XPx9Fr-;+J)X/cL~+Y )V t]J E^4aDэ÷@e\8MZ'80AH) .ER{fC8]j3҂/ptL1[†qcw7X2\JpZ,ʼnlZh,iɷ~GEX/^LV~Nr߸3㧰v]נ9ObI汶;;Yߵd.gi07%XXӪ-8 e=l3ohmG(^Kc2@d߅n[H^SΖp_KMCg)CzNKjlI:.Uϗ1Uwٮz+]2V*c,UmqÊ I+l h  yӡ[;K* }-hCz>Vaɒ RְszWM-Mڻϓ$+usq bf IΘ3;^K薦\y2*_2ѐ+GK@[Q})<XV!65kA>VK?#-^FAkk vj"(H,ߚ*%LzKֽQoǩ&]~KǪ Ԗ8ΎncFŒC1bf0-q-R2`;mq"j.eH6uL5*F8Q2q=fuHJB?p6(dؔҺW~,mxI,7rJkQAݰnؿ.ch,RđMZCs}7;,j*F: ]<]MFZL.=9ıoMf~\[ØRP.qs L d:xk,bɬƲy@WpX2ubVv-J.OBsyМb"JOGʺ>4ϱ[tνa6!IsgZ>9-h UЋTijx#./3%0^l"/jIRGBRis]Ѝ(n(fzyÿB"_PmWБ@Li3UiLN2jF CO5F *|4.*- 6 )`;%DbV3֚1pˇ4i;['dz.ɬӵ҈ծy(Yo"Wo5un-ҶoCSјƺ^+a\`O+[@6ʩ.-X t3dhI2ԡn0JTm8:B9h Η|͢uDWC;t`tz] vU.+D xfU`}5L |ʻ-OC.%Ґ 8d{h膲Vo4o/g meΞN2$*ö-26a8]=~(J:Iz " wfv`:U6+1O0< eyGp٫ȟg&؛EWߺF)޹|CF\ͰIXh3'2^o/M@<܍q<u`:Aex>t{@qx>s ȦQJbl-ph:'7D|BzЯh16OM3, F#JߓrVV4-p'`9Pj,,'u d!(%zxo@Z%b;]fuY! n *VtqKVf!BKXy`uʨrUui*5/@Nx4"O ˥<8CsU[;&(_]l%~iHR["kIǬ?e!z1Gg0А>ѳa ^-ų1[3?(3`WF޸y|>Wg \jxۚ/ *sDkL&2 ҉D䎼FGVܩ7; 1T;I~aa! L43M9tU[^xKhdgqRbD2M Fĩ%m&ucr t!UK0DW(a]*h_M|etV3o AtaKr谵2w*vy:c:S+ԔP 3`ĉ$~@xFLI2Wr0SJOW i%/n5bnd,S:MS2ر^n+x>86z}'@K?_H_0A\7%MR џoX6[Y[͟6%t|iJ?|^4_q­4_~048FW(E]:昖NԩB/ (U3Ȇ9'f! /R2u$òKhCQU5IH*TĖ!!GA9K!%rH rH~%E0Cr[NXZZIqlBF,)'{Y #yw)\rs椚[m橭\,l%CKl5 jNA 睘J`ֹ%TB3>6bWbyY*MKHF 6(wzOw6JxEET>2'9k F,Oqv񴚔>yܨj~NT 4Z+cȯn/OQW knj2)mR|v1mP6)ĵi# Иoi;RόI`ǦbdeX)=sU2C9NS5D˅U_myb"BOe;  0t+!6Q;肤76.c";QUE̙8eZ |fu5y;U|^LXRGɀ92KTIh.{g-f]#m~ry( M/d@ j@XkA\C渞N;TpW6w΄ډ[uO{ jN\]99ŭM]{^>;3X`_U*.M&;;n$LyryӻO='?ĝ.:S q?;/!nPM՛ΐ|y 8fRNNGj4u`44Ci"H#w&Jcv[ $40] PZGFKxþp(9Q0lH0e,E(фĞma}Xؚ*Tf0!p$:b`\MC'T_CךX2uU!>r $LI!LG]÷'o.;I 5bzGR]'8ck> azt;P:|pg ~en7"2:Dt)B:$N)|'AQKi| س{\":]o]÷qXcAn VEFkhxU)"7b Kp|RbBoB1 }G|-Шl跰3alAiU䘁D6 sfX 0֪c1,Iu-HҎ5Q܀c\I2W q #L%No8[,idV(]78C8*yq#D&ybPO Aa)Cj'u-^u÷R_E0co*F]<>-Yuw>)CkϦֽIJm\^[A0y G.#Bclzd j`?܂H kJ!/N>ª9RbALvvD.5C_/o/A<SfM:k:&إN!n#d8%iᲜ:V )x㈞{C+]FPac4G hIfՄ1MP|4Ie[MhPeF1msCVl =S5ثRs B}:\ Æ^xⅱ*;5ń%E2FR,(,RІjBIRΈe5@ CƟsͶ=S,~tذұoMeAWFWS]cpSDSa14{WyI-XP5}s$+?IQS<}TYq$\&onr! W@ta,Rכ2*diBLBOأ?HM6PSOJG2\{ &Y \&zGh{n4:>A6?8IRGйk%yQc!<ccl6pկa@ c-tAOȵQ!_ܙbgCIc+UY1hnF^0"#,bF81"'GPF#8 x"VM䫲R\S[,l` FI +N}ozS? y>"A4K@$_Q닚7&!BȀOMպzgмצdq"UVRAj(LtB@Rmq()VOo#gRAo FIpy)F&mjC/pdžyV'A)4jħ { dڸSIK1p7r)Wai71ve'7@mѷRqoJUB}>!c y`æ]Í-Ǜ'~r*VM:WN@p_~VP2p`GEDYfɤ0"O–Ojo9$UObQ.Ds,! KZ$fBKtQ8Kq(e&\"t/8|Ms( jcU)טUXo\'J$`2,d 9ʖIOrbcZk+d$lSF Zd 9vNJ5"ʫxF#@JuaR=Hy[Y m3kVlܺgFUWSD5X <4XB32,ȬD֢l%l3QKڜ ^%2.*;~9)H򎔽|'z#u=WT !fJȭT˕EK?pz`~%uZt-dז>& f^SOh=~#LX:Q,h=YC]x<( “BE]asa IJ^iF̚X !y\aTN@}GyU׆*5j)u 3ƢHyZdLY/=_Ϝ{ODjz)" I)DK2w!uj!~1 J̓0?'S'0_QYФ3D%I]j /ˌj6/4R/Iõ͌VuY:.˔[S˗KbelƽȡozIΑQɻ;r˪J=Ǯ*LGk+ԭ.<)qvPî^ şxՕ2_%zY:$@J=Bq1:i!{BO1ط5c2u'*\:8q26Ƒ>`!Y&+UH+<4 mG*X*=뷟 E6 VVzq߲=l5/iXY \lɞ9Tobn̪BXABpfb@T  ӑ 8C*>Z/!gP YI_GGկswl,߆6Ќl#Q`SNxCdy >TUe@$&Ҍ 4ҫ{4+3&sY``ʎcZHz~2xCX^&Ju5L,JT2뛕єQ|GˁP%=@C֛̗c)AۃlEC?FfTcEW9Sl}"i_2qR/.,3P=BȲS.7 X$%~3S,ewӥs ȴki7TF.h,jڝn?sѱ˸s$w98|N{7axdVB$duJmƉ|ڠUgsOL]BS.7DgJ-{Ly6.-Žtv6fPc5i>1xnۼ`:W28]WNHo9{C2y y61]@q4`卋 ^O>_6/6:|{h4 r:4m[?wVy>u6+wˇp'xfm[hUӾrZ+i:b+)lW LE_h;b %OIsw0+ ˚x{>}UyOtMZYN5>6!S6]lR^V_*|zeY'.4wx hrm;yzcN)x} 0rKotpF9]8MT94AGgpk̜Iw;lpS 3`2^o_gWz]9Sf}SM}D#f`XRl@<{WpI+"n_67H[ D ˅NRq+N/4ɖY! OE7YZI!5=7 l/@_⮡DO1$ovE0eMPe~(K%a5߹msyM~ *WȅLlsw6-bơ5X$UCh#&P2BYYCFM{40.4DSAë[kӵtLMEL=IS d>aomAU)z$0/v*6W>02Q6ZQDki?jې0U]Ϟ=}1\"'5q{-e T뵠Ή*Ʋ@ U~dΜ S*t+(‹.EQH'C A›dL6lh8v5;:'9]7^Qe )luMJ!0-EVp9!Mw`<5蛵-ǃG/3 {rn"͆gZ}g$Hv`M=Ѭh3:V2A3<\ػ Co[uK~ 쨪sKڬ`9Z`ؔҁnTNQ)Tʤֹk%8`ힰT+;KwI 0߷B݈wR[D](Ftle{TF=p= PAil6$8b:Szl]t&ʯu|ՃO\ :~E' =E>h Ӡ羱et7K{G?@7ZizdeTF8pF!TC]&oVb oP)o%VFlێgU^?`s, .㼟@ `jN)֢r3mNeQM3ݵ{_=Yl{{&sǀ=#K^kc<ܷap^/s뗞FD^HU]o0}WIVl{DUԩkQLrC;:Vwm'Z*M!<'&I`§?c0.Zz J2Ѡ`zI €QNR!v6%fd`D(El-\]\2!Ɏp{3ͧY 7%Q+zW؇PT8|&ɲ*e4 HIja.$q2B {R%%?p .!\EsN߸fG>p?>̷ ^1O|Y ZvtL >ӪBm0ǽӍພ UES֘2k+T`ieqtC:RPNzNT%6k{q6-|؍t\a5 I)tǖBi:P* bm0FNn4 QAkeU+_wQo}*&)v^AMex'M:ab'IN7bC 0)m;aGh=|S&/?no#ֲQ"w/Ü]sFݿrE.TyWA{Kʋ]ϻOA$B"%imx1`w{^  %2KWlF,ӌ{ЏcP@2iw#.@,nBFsYB#tx.&&D@HSIinXFcbe& {oXi,%QH@ȔQ"eYyɘ ܈nf$7Iol,YKR;\ʒḌ!4j) 8{0a.L.(vWAqֹ7%3L~bTPdp?d2&|dӀ&a[ƺe0m*K-pG+s#Q[C#wQI iO#1q宅e&uQ4KVHuH'4t2]C<0[1T*wA :̗*[0gBI`.M:dPYY# A )nm-^A>w=67/}Ԏz"&&%;<" Dg)ag IhS0%sb%9;3\.u|W3~,OlhDe'nXQx=rK-9Ḳt5,yÓAE(+E,ԯ5uYxaL8Jz$e•J۲V6$颗jo;^$NxXv} %0bxhX^dZtUE|ǹ |BC,Oْ2Hϲf FKBs͐SWu8,)rH4iV/ ʥn'?u?Sqt9> fS@d@(>MF3?x>?΂?} B*b\[FEFUr(h< {eg8g0;³;u揇H=q˿moot<tJŲ<Ԋ+\ q|6Mc7KuSJd9ܮV(𞩨8sP7Rg,lWOmjNαamBBiB,TM=O '/ӗk8?|?)~ҕ~)SYc7űIn2;~rY6&U3Ħot+=T]o@|X۾TGBPi#>W{wݙ{w}gIvgfg$ Lu6p0/˲^¤v66Y0hѬ0$A҂յIR!B(a O[KYA*< #VLg_g(J#8b%ljQ WhUAN!4IdNkWʑ!m3֝ѫ2CKl+wDp9EZ06b V(c++f(+_5Z2d3RM;KdL!U` b@,5=ADI~Wd/fҭ+έ3t>AcC.hƳfp[2Jv | PgQ8üT8Iȅa+9|C Ld+`#̴nrjU{! ;[.)qsUayg HKޢ/B#u='n `)NS–kI7iKxn!. (=dx_6 }W O».0xn׹لa2!o֨ӄ8yK:)"MLUs0C_֎ӫfY׸ \:nn'WT;hf6$y—D|Gl1SbljcB}uên7C>2H9pY7VKSHW4#`B*`{H-pXjY+ϸF# ߷硗eBm|03Q3)4|=3gT!nΝ3r>E*A.!ny(. c[a1I;A\NMDUYYD[`zާp6B/k _z"H"ԉ")R)3nrF-O 9sjIN=[U?!5O9ȥmeG!o3&VcBdZIaLG6oG6 [.W4^i.V΍m؃ed_eK1賳gN6g됫0fy>6S=q;~`ؿz'+{zvXL'T'7=]/*\gVf:;`IBKɁe8>ǑB}t2NRcvON坝ӟovۅ9HswUcntB..5QGnԀ6. }EIQg+p+LGb(z+bޑݓ)3wU/Oʧg~zԣ UnAkeµ4zsSUIgyl,v|7`AyC3sDA7\ G=n$+ D!x oo>a/x(M6ɇhf+4pY5Zs/]w4KomooS;kDdtJe͕qJGA-s;=ՙF%Vږ"6. RR[u.l{l"zo8oy[,.,:ps}<]Β8nzIt^8jkfͫ3 =_uW_}Eam_?ch!|kh!>&=.T}7Yrrݗg c:|]Gԛ Xo~_1[)Uk_ uSGV.'tIXR#rG{?v]vpL&0d Oņt#Ŗ[.FHE( )TQ9L& ֠7L(d"?ĞJNsX wJ (ezjR+j4Es`)|3X`?}2v%=:e>Pxܰl9S"=(HhkIQ۵Ğvg)2hqlS9nYƺUQR-,xP(tG/&Z]-<1ғ $k0q,KcN2u3i3O%_**1>[%1K<;(TcW,;ڷʌ_R@|bG>F#COhZ4-9h"gĜoZ+ſwןgs4ր H#ۑ41z^o~Cw樝(bwxD*>E&( o+h"zc~Ȋ[h'Z d1V;1Խ2p0(*Em!XsRYQJ?%0tܦv\3ZLV[VsNU&;\; KQɞB j1tZ\2B]*-<(G`y:4d'e|ΤҎ-K>ԤC^g5?ݭIb<xы>^FϒRrǢhr[r8Cfwj~JoS.zx"hG5*u0ZL?jc^wמAnGIRu*TŁD1$*' &lX gP׆]A I"OzgqxZSk&i_M7)}yaTJ v_C+"JLex\{?ͯ)՚Jʱ"NҭpM}qw:\.1|UJFv$M`f/U{TϿ:U5~T 2',k0mgjS?A0i<r`dWXy]ӵr">mT6bSkdK {E)y)Ѿ?`kPR,^h.-[_[Z4AxMy U0E@ mi\t#/^u:Ys6~_)D޵rQԩle&h(pzl ?Ew˃mŷb2W|ɛj`Qr-z#Uy@[3rz3d"Z2%X {q mzjBKFV21p .l>OX _+wxfFi- z:wl4.i) I6 Pٰ׋TbjI7U."ͥ ?]\_pfVf痓 Ǐ't?ڇx|O04MWO ˾b??~7 Z3d D0.1xPqLXELfwC TJr%#gd̀JvYl8ˁ}zIiGZ*Ԍyi-õ3伈J+  x*~jΕ ߺEՉ_vrytϧs% QN|Jg`g ?l&hxJMbц'bWoCTz=Rp~A>e% X a%=X?(T.0G( 3qmeiH$L7Yސl˜)v(셬4ńR@̃ c2΍jo(yG%I֩2LR `3+9S˄b^%j3bw}kt9sk\@4T==D*wwNMKƴݑQ 20Z5ƿ.2i?ږN0'Tz.|/ d1Va#X»E"i"[ E'll^.Ks5ZK]]s̸NcDG,t6VHk؆XW㒪%{+xTS X׌Lc@3uDZ)seg>5?;Y;&of6?+whl 7,1f Mr`ٟV5ا!~[Ѩ}܃[%S^ڒ͒Wch̼YԐ3+Zl癖[y߳:We[K*`#Kr9f:ɨ&%I 6TLD*=;׮#[58+v@UyƻF6}Ted ͽ5wޠCk˵w{~BU`=(xg &l(𸼊iPO9ʜ⯷u0|u=8oxFuZɩFf $ $XhŗfAGgg`=}c)c7 ^$ԩB0@Yhn=D!h)%Y@DBoy_􂔞UdibUBcuyz-Qr"1 ~MEڎ+1jCfKlLλ/d/ƧކoDRХԯW/9Úv]C>U⻏Ŵ6_BBD#~\TlQs+hXl( }^kz@; dƪ|wݢ.ZzdjIP!s 9ۣTH]|4AQz0sT7QD=K|>Z5Cb_sp%UjQu9aIƞPEZ2xJ.,CK`{/g7ߢߗ;Yӿ/iC [hjerxR)wijrW@uEAu\5Uo, |zyd"?X8N0KV`+쁳?H턻^yX# w|h.;ƥaU+ܩO+l,:\浸NE|YD) ,WSW]qYk_G'N`XUo6_qCvHbC. t%, S#,iu@@Sǻ~,jZk o_ jyսἳ҆yӀW2Ѡ`ɒ,OAU`kaN*s6%orqbAhDl-\BS:Y$Gp5^^B%dI'p  7%m ܐ+sl_0BE8S+\T8˲*e!-aieIRhVb%$N2[?B %'G$8+4`5xn?+ 'j Ȅ\3<>1>gHF n3iszLͿaQ8%rEcO_Դ44qH9%kұM>}~@48@47'Pߨ%Fij4fpu7tGfgU WMo7WLN k7ml# :9 r%J FH;Xi$;˻!m? KEiN;]^JF Z4{$Ep`um Bsz=`anA"HQ"7W2SFikA(GXM&g0rdm`bď:У D!J>Zv\(GhyFVdRF(Mu%)6*jv&Z@@(H-2rẉIo|~]2dN(^8v+?%d7ac߼јpڄ=a"H1 0YeP!yVal$$0G4<5E+Q$oބkTj bXwf{[#8< m?+$sm=ʹGXELq]P/#ٔ%ar(_t$垞/*4d?Ӡz**[ g~U*(ݥR19x摟Ih?^2rVrDS4M*΂*fѼ&͏ؓF}NEB1]ƈ=eytyzʏӫS2ksrτmnԬX~y4.F[mu-rju*4 q$;5R՗6+HDZ^U e[T/m;]X ΩI;v#4R30օ6#f+XSj|xz c5an>[KG͸Pۙ_|K|Bg¨Axo ;<';tKl7:]*{Ѷkhsdc8/ zZjtfHP8_^IlH+ɺwtK/NOOx KW07=MM۩ pJ;\:sGI-ɰGX&+6*r>,lvdSC*'XSG=v9t6F ?M?xVq3ݴhq;.%+Qm] Muʹ9b۷ LZ~}ZmsH_1K] ěcc:=7I[HfW߯EB8Wu{zzimgo{g>{{gvoЊܲL+Gj㠅n{ C껬 C:=!#q\Z񔜗攼̂@B>9wȜĐS f$U%S|z * B❗_zM&!ILIR &b3Ï 4'NFJ?*ͨp%Cr+N0[ԝpT3Gh4!#dh."> B0!I`^F9~E`29Aat9rJ1}`̑}d!iDօqnz G ޗ?r@xQNٽ37);qd@~J 0+Vu/gDO$ $SiH217A7#H HC:f+pVH\N E˻ ϗ i,SU%XB7`+L %f^#ҬcXm}dܑTOrVAvqVy0mfj|bx }J1ADM3P!*Xyf78ơ bH!T?`( `=Sn&>-ENC1bvH@A?ŋ8GRn\f;0MmĈ,ULe]ڑ1+Z8Qr@""M+ xL)%=E248 UU.S5gYF_O#mUd 6*)`H0̭,6%o7uPJ~?2, ,P*ծ^OM".ngy]_Kܽj kQWZkg|33Wk`w˸MjZ gX_%1-yt(S7e|^B<7Dg* $JjX҇b }Ѝ1TA*b42jQRKzܸJD)Ҧ{5M.{L"h~ VMYw} ;pM6ڄٮAM@$V˾^kV~go: MdC,ռ2.e"tI[m[тʌxPt+;Jr|<>DDzcn1.h"}bl3rk"@fc~ (~AmV`~\2rh-Xyb%ӯ;8 =Y!(*Pa5aNNMNWzx)d̙ޓr.ڕte;pUH =N ސ²6Lg ƞJ잪X5tUczVblA"&m.]?q/80Y͚ B5uP[lyH X$7XJ6 3[ \}sr njq~\^M*8 QT)!޻;bIwụ:MB%#Hyϐn!n9"-\|q|#$]M2;4jH q#3C70M2!T3{lR:TԌ*~&){L'{IPb~dLBz".tXKXz߫ zWdc$J StbHQ=ŖZ5V^Sornw<% dr^pD%J.!NE)͸ڂ.}{th^{uZUO{VDn {gP>IGD3#ߊ1-o~%|r^~ԣKUd3v?&L-l9o*o@[᰷IgV F٣9ܪb4g_D+___;=c|,yc7̮h!k;d7.'P/~j+o&*LjRqeRL@z1d"z2epYI6vrZe]@/SKo@W|T 8B"}IEBk8bZkQάq*$rz<|' MjUx~šJ5/u1e#ے$OG09|ilFȌ$ʴd5Ik-/_Re ¤4ZBi\_~]^"W%QF8g YU{1l.&C*`N$-jQ$$2Kʕv;8B+gsĖƝۇL"Y&=$AIv:m`[;L&dR,W_BnWLWe59ݨU^JJNӔI\'t0%'D2$N2e2Ff篥ڦ"S$fsԴ7YBD|^\.BW{`2.E`vI醫` I~[Z@ !E5$$eNB3QOGH-]\ 虫8k=2FpθYiʤWhYf)g'˓LH/й s -˖`2ZBTN6E7_*V%UT8-zҏijj=ݵb3K<Ӥ(i/S4QkFnɨ&)-'PX}4Hoq=N߿w 8R5Yim,l {lm%,(*LVgmSѻ +p,oRo/1{Am}Լtdg˫ly5&MZG#y+9HI<EhI 7C7WZ® 1~XZlpJq=3|ҫDEPYTom;V;E &hORPT3+Q[HQk##j.< Ic5m7fpU[m4c`Ь}XNn >JN϶&&aYEz/խiGuwOſ{}~~aM!K!e-5Y%y}} ^dUmjWM{4ɝyuj?t) aT=ԒypAt̡M.nnotuss ֧-Md#jՄҰsU(˼Gv]߾Eȁh-qmh AxW'+63|L؃XŁ؍0>wՂ0s3e⣃7Ie6F@rwO=|Л'TekU؍^FNl4qWn"Q?A(J *Y)\i!^%'6-^G:pi;ƾ*Rd8˗:v +2Kſ?zӪ;/â#KAq3|3,2R|ۑ!({7B@&mUTkOZ]q-Qrϫ=g*>} ҅C%H (k"%ty(i i'Jpt2&0 Q(qff*uR %x]njʽOD۩Vȿ <] 峕F;. Ri_KL$ g 2n~Kk/IhI}4USOi.^pZr򚦯uWա,K#NKa2Y֚bYS \v~)m*n.>2rKEo ΚmL߉j9:T0bO?QxxٓG4l7dy#vӳBm7j'sMWA 9g-!c0 ZQnH ;ICGqS1<+ ׆r1<'={ #JQڢ8 Av8iXj {+w|5ZC #FL/AfLLn"~mot-+ v~o4ݯ5'?B4^DT-Bٴ"x!NXÔW۹_, p1Fctz.=%]!s;_/u`}[np snAHՔ;"㟐t -  Xo6_q}96  XS2idq%MݑeI";i:YIqj_~,Zk(uZHE!R rh:CNU21|/\<|(8d"9iX+Nc9st9[\. Gp -Sn I( y[ϗ'`!vD?NWYuQDdH5=:dD|(+r`IGZh ߏ,x9?–IL e"zYt29z}((,?߰Z;q>.Z?;gr- z9[se1 l+Px/y? ŷR\3++.°fsd͗?˫ϋ[@ħ\΃y R!}k!r#ŚCfw( Oe  3$J XQ~ d;bsxR'+Ğ;ωj9y2<bcOI ٣z5ύs BCV0_Zd)ܱզR^a"$O5g7L5|g5aW*眤h>C*_R8Ƃ̰he YCJ񣖆/m Eh˚DCOg" B%ttfGWib$[# 4'̎五yM@ϴ3h_V7=7e`39zfed!$7ݢAĢJ u1%EǕ?sכnrayA{;.Z|-,pGA,A=P:' iPۻoΎƞ!!lxRţh9kP@EEE/bƣ@nqJ2K׎`o=/og,"Me|)$d^³=eȻ!;=XmPj\>H6ۃ\CmBd(,M='ynn"¥Zk{Ri-[w4 5~7itTxc4Pk8j4Ӥa-S_ߴσmʐ #L z[F3/W(2FJE#) N݁&+v(zg4\U|7kj3οR暰FMw^ I_UW'IJA7FhHkϑHey<8]&8rtnt-%$GT&럛^)E웝"%0(Fo4}=,]d 'Ρ}gb8Qm!topd7{+6 L-wc7XDWA꿞8=*&5iyȇ(m,KPjҝpm{.nnL$7\ӨU[k|~drȰi՘äkVC$vWMo7WLrUbrkb)SQr%"$W`w/ɒHZͼh:V- o~/f5+U} c]e6N` 4k%̷ ljT9j !H Sk J:O+Xh^/b-\nU8 bmk{a Ob=`QʘҼ/Z-ŜR.[cνJ%XQW%AGe";7*%_]LLqg_!mvmZ ]ICfeZxC(ìuKOߋ03T83!pOv{%2]iҾqO2no)S%;8oKMRdpD^]+*Ԗ^~ʄWkx$ڿI$\3^J˔͑wѪ[FQmgn[! ?/ΆV&[ _P*A!5t%&%}q޼D&y4eZP H_n<%_ {u{noᘦg]b|"$)QMh!1RFY 5A"7pָӹ'M^syd;KS,\ma# >k3An3o> ڙ4 *)e9N:'*vެ&89>D6<^:O'crVR?n1tb~T?3Y᫦kJo MMFQ=^^:+R}:@zι,L9,J䑡TbUS,$5>'MG_KƧyJaϴɎ! %%r늸)tc{d⸄4͐t'}z\<$/247Ϯ#&M+p6F8-'i0@̃տCѾ}~@hxh;;4k1f|;.#/="g>•aV7~EYmo_1 H  w8iϕ}=Z CQ+re-BR̾qIJ͇ Z3/;L`Z{)7>^|=,6YlEJo pe`HtܛLPAoUT2)Xy =0C&5aÊuQ)9|Mo淰z{vL%R%s|[1s-LjkT2_N&(525#Iq"烾r#XWyE@ޗINPJG~ JK?2s/a`?F|:l!MFD c+Kz^mtW`uyz1+?~bxbC@ N RآU&t,<gXL$|Qx%r˫)]/e!A[`d8xBxp-B-h{h^3au@=OMBh/ƴ^F"jܐBtݍoŸ~xV- "N=s7(m9/W-p̣B_40=ΨЅ2g%KnV?l즎87y9L;Z]6|F>b0j>ꁧ (cȼGbPYsb5m3H)$xD捹8Y=S/|w1<#~rAfe̿SdQ<&xX.^zwǞ4o2?>=>8Qt֧UVи 5ǿܘMab{ "key": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDQcByy+eN9jzazWF/DPn7NW47sW7lgmpk6eKc0BQM18q8hvEM3zNm2n7HkJv/R6fU+X5mtqkDuKvq5skF6qqUF4oEyaleWDFhd1xFwV7JV+/DU7bZ00w2+6gzqsabkerFpoP33ZRIw7OviJenP0c0uWqDWF8EGSyMhB3txqhOtiQIDAQAB", "name": "Bookmark Manager", "version": "0.1", "manifest_version": 2, "description": "Bookmark Manager", "icons": { // The favicon is loaded directly from resources.pak. }, "incognito" : "split", "permissions": [ "bookmarks", "bookmarkManagerPrivate", "metricsPrivate", "systemPrivate", "tabs", "chrome://favicon/", "chrome://resources/" ], "chrome_url_overrides": { "bookmarks": "main.html" }, "content_security_policy": "object-src 'none'; script-src chrome://resources 'self' blob: filesystem:" } { "key": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDqOhnwk4+HXVfGyaNsAQdU/js1Na56diW08oF1MhZiwzSnJsEaeuMN9od9q9N4ZdK3o1xXOSARrYdE+syV7Dl31nf6qz3A6K+D5NHe6sSB9yvYlIiN37jdWdrfxxE0pRYEVYZNTe3bzq3NkcYJlOdt1UPcpJB+isXpAGUKUvt7EQIDAQAB", "name": "Cloud Print", "version": "0.1", "description": "Cloud Print", "icons": { }, "app": { "launch": { "web_url": "https://www.google.com/cloudprint" }, "urls": [ "https://www.google.com/cloudprint/enable_chrome_connector" ] }, "permissions": [ "cloudPrintPrivate" ], "display_in_launcher": false } Inspectable pages
Inspectable pages

Vr0 .cc]2\KۭS:hRJ%qX$<@{i[`56z}WSO/quʤrK*kjn\+@D͝\eǗ3B =;E-:7@2G U&jgXos}*4d"IpeJ*QZT{w^8"NyG QL#4M402[j6#tOZ?ݨ*Ɨ'asR홒==lI~$;>W4JT ̋ȷ!*evF킠C܇E>[-02uHY&,?'wy`ŕ4xG@hd Six1kMsXy}CڇA0EI5^qځ6)3Ӓp{y%Zlgp>.x9B499ЄRlU[ St%X U@]T&Kb+[_eXxKPn6SpM]6-z2l:CaĸF_Fwrn=G^m_D]E)>5а:\ONjIIjI\?<0jh1^61Inأwt-{yy-*m(Hk[m5,(n#W?R.Qd #< bin * pdf * rtf * swfD * splE * crx * 001 * 7z4 * ace * arc * arj: * b64 * balz * bhx * bz * bz28 * bzip2 * cab * cpio@ * fat * gz6 * gzip * hfs * hqx * iso * lha< * lpaq1 * lpaq5 * lpaq8 * lzh; * lzma? * mim * ntfs * paq8f * paq8jd * paq8l * paq8o * pea * quad * r00 * r01 * r02 * r03 * r04 * r05 * r06 * r07 * r08 * r09 * r10 * r11 * r12 * r13 * r14 * r15 * r16 * r17 * r18 * r19 * r20 * r21 * r22 * r23 * r24 * r25 * r26 * r27 * r28 * r29 * rar * squashfs * swm * tar9 * taz * tbz * tbz2 * tgz7 * tpz * txz * tz * udf * uu * uue * vhd * vhdx * vmdk * wim= * wrc * xar * xxe * xz5 * z> * zip * zipx * zpaq * class * jar * jnlp * pl * py * pyc * pyw * rb * efi * torrent * msi * msp! * mst" * adeb * adpc * madd * mafe * magf * mamg * maqh * mari * masj * matk * mavl * mawm * mdan * mdbo * mdep * mdtq * mdwr * mdzs * ocxZ * ops[ * paf * pcd\ * pif * plg] * prf^ * prg_ * pst` * docx * docm * dott * dotm * docb * xlsx * xlsm * xltx * xltm * pptx * pptm * potx * ppam * ppsx * sldx * sldm * partial * xrm-ms * rels * svg * xml * xsl * ps1+ * ps1xml, * ps2- * ps2xml. * psc1/ * psc20 * url * website * js * jse * vb * vbe * vbs * vbscript * ws{ * wsc| * wsf3 * wsh} * msh% * msh1& * msh2( * mshxml* * msh1xml' * msh2xml) * ad * appB * applicationF * appref-ms * aspG * asxH * bas# * bat * cfgI * chiJ * chmK * cmdA * com * cplL * crta * dll * drv * eml * exe * fon * fxpM * gadget * grp * hlpN * hta$ * httO * infP * iniQ * insR * inx * isu * ispS * job * lnkT * localU * manifestV * mauW * mht * mhtml * mmcX * mofY * msc * msg * reg * rgs * scf1 * scr * sct2 * search-ms * shbt * shsu * sys * u3p * vsdv * vsmacrosw * vssx * vsty * vswz * xbap~ * xnk * cdr * dart * dc42 * diskcopy42 * dmg * dmgpart * dvdr * img * imgpart * ndif * smi * sparsebundle * sparseimage * toast * udif * action * as * cpgz * command * mpkg * pax * workflow * xip * pkg * deb * pet * pup * rpm * slp * out * run * bash * csh * ksh * sh * shar * tcsh * dex * apk *"  *PNG  IHDRatEXtSoftwareAdobe ImageReadyqe<IDATxڬS 0=,!"5)0+!D"BNd;}̲,㌱rl:`Br,Q"/q76VfQdža0bw}( A'̢z8aHqhWi~%ؿ:iL,MkJJ yƊ{$JRm=" (M&8$O i;IENDB`PNG  IHDR szzsRGBIDATX Wj"Q>3hc"Q6I [+l l"XQ0$*D%E4|2qegs`=;?wfTHRMQ].;~ 2~`@kwubZd\ -(5`8!=>>X666>Nڭ-{{{1AQSC@(Ύ16H@Uu5`eE竜++:O[5>xep$~[qoooi8.'$ yo%N+mvc"/_.1㋋ G/B)^Kh<Z`GFL-0nrtrrBT>ut#p&`0ȱ^&M&n[T@m:;;xL.* 3kkkLj)|>f,ŸJǍF"w_=%yjZ/} g@/%- 8a "U MӚnV!(;27 u]ff`nbVP znOOO 6{cXX8´cs#&d@ C‚3 j:܈ O$,8ڂl{񉶻^\#Ψ/8j跌h?f󄉐ا 3v~;{WCVFѮĸ?c=Y nIENDB`PNG  IHDRw2xbKGD pHYs  tIME: vTIDATx]{U@H!Bb&<E[Q=^.:"gHxȰ!C ytewU\T&<1&k NO?#5kg|_/S]s9s.$,dfND9h{{p$j8DCD\1Deh*Zcǎ?wvvj`0Z`v/=3[D.4-jZEp#W5M{0v9ZU^i *#?X.r>WI)/r<{qRcpǙ]G=N^4 ypǑN:2J(X[~:;9Q(*+M<>[hYfNVbU8!&hof_L{nè7n2iMe;ZZZKp$s9W$)Tl\!1J)s&/.54(]rpR9mkk{S)uqR2O//kyV H^VB&:e\tme#7MM[L1lYViDD3`Mtp G뺾y%U"ZkB#-5>W5VRv-w;ֲ4,\ |t|srΝ'P|9^Vz\p8LZK:Lpki,x*T5n>n۸a9cהj{Blw-yZRcQ%ej \ǧ<9fIJ7i`01NR_sCС hv[2+𾾾n{K-;GTiNaۈB{RW'bmwww#3Gګi'4&ɝjA%/N)e!ĩභDp4H$%":&J4J]e0+#rܩ>P(k/p\R>sxlUe`cGYG*˾Az/7Edz)\T(`uJ&"jMD4b+o&}kpGp8QMfބ=ڕB rWH?xF)uE xiBt]oRJ5< o!*W_\W*udַ&4)4.S.F8ÞF{wy/{Oї3wmhhĤ$D)ne$'YUןJ&kmms-B^6Nx<뤔w92Poo!g3kO444`{{pnx~XK\|0}>KHK疦D"if`/v8pց9ܩSZ쀛L<+v^|*3n$#CCC3m\S@ccc\EwwwԩS$`P%뉈hJ8WJyӦM$ ;,XRQ Dccs@GnB!I3BjH44_0neB|k#<.pS)qfx\Q 03<@D*0 DAmg|r jcFEӴ 71s𪑓BM\_epϲ/eր\_lY5y8pњ)ϩI}e=~Ej!*|NӴ;#"]T>KVf~P)21חeb߿jI4FCIJuJ_36̜@̬Y2B,,b (Wf~^J|V`H)ue"U=ًN"҈h8Jm(k795=:oppp!B7Qonn~.zQbَF;܃oa_*xډ]b~۷lrWgggYp]79!Sڥi'ѽDtr}|;8{0T*u3gM ,X',$s[Ra=v@K?DHz{֭_] ;Eدmܷlr+9yu]y BәŜl6RFUjM2_NDg8+ccvvvƙru%|ߒ!k' a8-[4m_R dr{d (8fv+e3ŬBx.XiU$!ċB8D ׻w(XJQJ]_D):Ǘa\D8eYt)i2D,;s[p 50BkRЛ^"Ե(+ @>gĻD|,NB5<9bƌ0W hSJ3EJ B\^ݮQ?!Kr}_D ҎfxL1| }:~Sz[4jc"-6m̙dy,|<:i?6ẻ#,P xځ_wB~3MCCCB,xT@D"{7BD.6mfMӖ 3@'@:)92(5ߋ"H@ӴМFADb!x|V.5D4d!YRpގCj|AAzF,8Y˂Re?2`\[ @W)( 4_J>72~ǁHyBKFR!Aݡ0u Qikr ,)_m ?ODǔ;D߾^\^)c-(S>W&Qz}@W^p JXbӅ{C@ӧO-STJ-Ηꯑ#nf^iJG"ǕR7Wpz RE.+WDti[e-*%ơ1p-{fE:Tfε͙n?O.O1UJ]_luQoo!n +QײRʕD1FNݢ^zV,>MFN=NbYB55ɹ`Y-.k-b{>]ig1cueV\SkUE4vuWOx٧6[J)D1_k af/L.)&!vM63b^6@lEMs=K Uew`}}}#dzڇ1{*v޵g1ߒֿd( Ėe>\F !.6R&m?i-Dt V"EDwI)˼7|:@<sݡhnn~o4M[;B`Pkjj:288N}t˘VJ}>[ѯIMXl]1SR}ɐP?BdK8')/G9HyaDv=H:`ؓ|BQ4 BW!(Ȉd{Wr&ܡQ]]]nMdPz5|;T|dt vi}-˲͒)Qqj;}b'"jD"Ye׮]2P%RgcO:tuumi0%AJyg"xhN,K);he[|JJpx@ZM.f^NnooeWk,Cܮ#^3p!_kέEj&iuRO8l¾#].^" >A)bN8JѺwb=̑/Rv[(vj1 UT4O8ݱcǟgΜicui$u]oJwdص3'~nEiUD/QJ<z ]ERj]ta޼ypST&r59ǹ:p ߿5[5Z${1þ29NTfdNG˺KsphJ&wr{ DVFE<BE*UJE5MRnD& rx`#@ԲΝ;7.\09vg˪IENDB`PNG  IHDR!'bKGD pHYs  tIME. IDATxyUƿS3=,1Y PDxX{mCDaSb (S[X} "IIH2H/$=]TR}W[KۢT*1 `̯'~fw_jhjŵև+zf`̎,d2cF1#@[~`?OD+,f~i#w͚5OϞ=;'%h=P24X<=a+^ EʶWo ymmmE^viOs1މelX,_XiVM9v'|eMMMokihg хZϋ, ݕH$vS9h{zz>DD| waLZ9b"B#{m2&&+3Qs f3L\H$bP-OBѓ07Fp8c1[vxڃj Pӵ֭bm]+JMFy78'i' [Ièﳮ Y06DtO<7Ąں뺵bٖQJ=#3f\ &,x]F׼7mt%3uzOR m~r o1en b|{+3F$fRĜںi5= ED#]NOe5Hø9iK$ׯ_bbZf>uv${x@Zj|wUj#g̘q@wFeE^^.N&h})۶n~n)F^-eȶbCCjM!hwfE[rq1*p֧ R/_ܔdc3888~bT˓POb1gokk{# X_&hkH)u[ W 5/۶'Q./vxk*d+IښWugb~cw$=hk=6I( .h4z?3u9 ^ kD 3WE"Uv8EVmێg":0'֗} Ng}gSMD;D?>d]Boö/M&q니&ѽZGG%2825LN4 N/#}j®]P$YO ѦL&㻍iv)89 =k:;;ꇂ?NOb1OϬ$Xfʔ)Oj[zۻVp}8}6K r h,Vɹ |@ I3fBDUi4aOwh+X,qڵd曙ijѢE;Q P(\*:+b`}Z _K ^óڶCߩGz e=$VID"p]99G44^ϚL&1 cE+*ݎJQOOχ Øfϫe}'&"_Y7nܸ3g)֨ C O{>^D~4_$zy/^tZ!-b744?䕙y(޽ Jr{@KKDteY >`D40JFRS>э~wf% R/ u"iC+s5{P(~cfN8[,X8qi;Ӽ9̱H$b3"Vuˮ^D cg<.\hO7,˪`m̏F8gG%Uooy< QD+3oH%r*D:6ftznJFDc+ X,+Њ|Ά)S γ,+u3/-Qy1sn `3JWbb.&x>$QhW9hw huNQJi1@k}.-Q57fM$766m(A'ZuXd290uzJ_<94` 566JD8h4X5۶XAjd=v"*z艆a'h>9Fl[u/{O{{kuGk}|KKiC:*ocJ aH6 ^ ن'|p2MD"1tSL9㗟x/ö.f4$"Ux2HX3_DD6RG&mׯ}iSdݦi>^!`DtIgggX00&+dYl΍t Ɋfzg/dris*,X'=Lr&RGIJf^N-dZUi^^J4+h-Zq㾓߿]d2yK }("r06lY֯\DB,o_@pƝw[D4v_1%J223h[h``p8XrR^-?q"0,5noo,nnKDv]M6ٱX)0<alvjGG+Z6WQ&!<gՓN{ |Rj(X`LBKDW lMBJ>MDW>io\7 %7X,-Z +W\yfm{3_ED1 /pR=XK v? ED RO{8+TZ⁙ϟ3gNf6W .%4;d~veY lwwCK77 $N`o"[3m r73R拵cIDt SYDd0 O(Mr7=^Yz3&MoxMtI1KD4:3b4T0x<3_͓7usXYhmm}u8K9^-̞=;~6rz@??k \Lg34k|ʯ-B""z8UV]1ڃJ^uIjC>^Rȅep`t"zBDtt2sZDt)SLR2B[,3/GfBrm9=-ٶel>eoq%g>yF)aj8ιxEhE=UQ3kZtM^\hqi^1x>Q)u5ϟe?c?7rxDO3/pRjho288P(Tv?7<iM|cC"`+XW3/&.Ժ|qoZ8000%h'0]^{{j$UlQɑ9_OWWמPgDtCL&oGG~7Jl&Ń+wPc Cn)˲Fa\Up%3ZXa/5a[u*|d2yawч+rN*Qߞ2JR1LG~0*L&sp˲d21sÎ:Yk\\ם̿((LDqO۶j'x9 / Jny/.,Z0Z|"Qu)JkI7a#ζ,|\Ș2Tr-VRJ]̗yI:R~V)羾Ù4f^S[̏ٶ}[2XzQX Z/ 2]܁*nFgj -5O{o]m7n GeX~ 0 9wDvX^X^F"SE`/WJ 644|tf>cfd%JQIm۞`eYE(LcDdUNpRJ1<.G-:ʫ(H^0JR.d2iVddm?IJQGJ&3MTn7紶>uTja7{iFxUpxi -ZeDbí/3fX[TL&s%ֹEkz6fɒ%0W OضݙH$vz ,P*:0*ɖe%Hp^af׭[w׿eiNDwxgAf / U6mڴ>  =}Nb1OsiDTj@kjj]!~~w„ _hQAR _8>uוRgtf]na? Ïk?SjaoWx ,m0N6=k-ZcSSC(aPC}׉DaB6A]pFԩ,Z,3x#Z랞Q75k;Eƍ 娫ADP"Jvtt8vXpaq~UN`ǚ*BGGc KiI&L8 u.Tʕ+b拘 Vk0\a5drQŸD:`rXj„ >/-0 B5gΜRt:=B5iϦRQReYrU𽻂}eY#u3U! ^2l) [ K566^1X,Rdq`T1 cmۏ~ۨefusY)2̵\RFzq4+4~hM0,0!Y'WJ8;H|cHhtR<[-@]]]-R&nD"+m~WY<53f8΋ JgwR떺6yyRJ1%C3mK$2x7ǻ5ʝh4=̗lctJimO(eWpORpטQ̥H-.{mAHh`1Ѥp8K]/VGG?-ˊ9s 3aZ)LZe}}}7W޳ ~Æ 7sU!rCxR*Q ZC0F̟tW>>0%Z[?uZݬX,Q)u~6=~4iҤ.el.;hgϞ`6l!8&גQS(W`zjooֲ,qb+p˩,^* f^֮]ff4DtD!m#_}"G[j4/k\׭ ޽L&cJfdɒ# dpg`:mZu}jh4Vo5Z뇉oms-Mom \3;+#n[drZ>kƱu_>I7F0 Y.+dqU~{+~:`_ SRJ$2yeYv\[ή#BFafϒh1$ ̷eٽ#ȕfzR/{SW8>],UV`.3]-FTFo^e]7 c.{JmkooݫˆJM$ iΜ9˲r]w?f8sn59"ȐtM&u5&R2 mgpccyc^J1Kf]=Uv1RF\8 aW-d)N_WraQ8,Hug1K뺧D_U^ADVb187W=3ݫbJ]޶#],H$r7̯Q{$*Tl6{wJ1sGCLڃŁghFYfcYs| g,3ߚ+gAZ~vc yyuZ(t\*:8綁#蕮Re=1*f^ɟbq]tzW7ijsEv7nJ"ڹp%Hf̨x<'(KkMDmwցˢQQKfΜ&J>FPn\TF!#STrz{{8BLVTuU';Ik=o%OIDAT#\3fcp,em-^n`f1YQ?Qּ-X,tBx9g:3 QHbQyt:}7J4wwwO:cqrYu^X,?}.+fo*l=@-CxV4:hcsKw  2qmmmXXW{otQwwm$,מ`uɊkFDL0-+$aa6Z8ϦDT5vbi1[񴥼/QbB660lZ5`E%{ڼ}>_"D"RZ^y+N 1svT'1t:}`,[+&+*y_%Abf~³7MMM ^I` 8Eym~m۝Ϊ".LCXhthxZfB%|Rwb&1i":=31јN çTHz;dO`0LDD".HbZij<|ͺu~?H#%pCCAa4Q3ffn&D Ahui`*uoܴiӵXlm+3fuf04` =us U㦶m*9ցcثC{ҏ4550[JMtʕ7gos[mC|#zzzx3gNƶKY!ZpR :i ͆a7%pRװf/ Ԓ%Kiiieͣ.TJ=,TFdriͮ6A[MDSZu䉆a0mo؜"=a< 8BcժUGΙ3'Sh ,n"F¶/(o{ݦM~슓?to[[[V$^?UJYL]]]{BG}B&Yu3(D3ޑHd;:2Sb` EOOOQD" 9G]1p(E5U) `k+6 3O,<:AoGRb~F'op^`b)N b*z0dL[ 744NDQDbճB6eD4_fӋͪN+ [zѳgZvgggɓ%ÊTJ/Њ*weaTJ$p8<Ұ$}Д[." Ge}0E{ٲ%<UDm0 c)njjrcX:Řy(!$EM. Fyv?H)yZ'ړ' /mŔ3{Ќhp8|slv&{֓)<,fۊ B?r`̼(ݵ㿓)ڶ}J/\H|).N$f19pk *XQa#M4MsQ<743)wF2XK,ُbC|q\@?Y #6ʟ~HlLL$p/K/Zrn@`w)r_C+>/\7`X˲dKo,"3 yqht?${i\;oZ5NڗhR Ef^}#/b(zǎx" ?x8.n3^UʢRC~4 USCޱz;0"D4+G }" ~zPl>Ff26 #;wZi7?!ئ6 HԾi"AQWkVJ=V"SFڱCDNaaIζeh[' 644`ǢHk~4 uY࣓ K`f*O+*ay ~cƖxL5_geLV @ZQ亮@}{ ZQd2˥3 >Q~IKG0ᜄ޷@+*d2Jh2iu@+*_gxZ5Nk@+*V^ F**^~h}OKKxLhEJ6%O/BVT@뽧oAVT@뽂'N_EJCEic0KUZVTVh^ " "=HhEeQ:hˠp8,ЊʢIKq@_~!Њ*[ZxZ ZQ^k\oo,ZfӐ ZQY4{3?#-1,Њ$D^|Qey'ЊZi@+*Wo+ Њʦ7%<ܒ[Eɸ_hEJ? vdg טt_t_D"ZQֶ>Q2/FK麺0eDQij=෹\YA /-mb2ffbm}x~"2?ODܟNbN=VXoX mu7\>k֬wR uL&tSSS@NL mYdcثIԇl~'|KDin&y^C[-xjڵCam~ `eY{T.% ^DDMm!8`/Fk$<OZ m]F˘ppٲeqWzC<]5cƌ1 yN0]'J) mӊJՂ &M 7Fڮ`wu_B[VTRc Ø GT-flʲm{<3DD63dbiiIyIENDB`ܽv0B(lEQI&!e9'}̌j-aSk9v¥);̷( B ;;n^]^-6:tircj^_W7{7z6& l6eSο?l|jʍbcqU5M}3z\neOFvjTNXly.xBzjRf:ZTthEgTO&dlqv]6MqYEgd_˓=*8. 9N-KkH 6"r>8_]2/'y15~^.nSbβb [̋QiHj^YTojhm1v^(F_)X*ĦKCaYʱ{ſbҔwɧKՃW"I}o@tTw2΄jbIwi(NA$iq ӠbK&+ ӿo-<,9 VܛP}W9Z8#AT;FTxre:;,J5;,W eˬ0H (r<4ZFa`,: (Е wi"<)?y[5rZ)e4GY̫"+cย\#kVbhr5I$Uf=<0 PŐ&rqϜӚ3:qmy|*/*lz aY  4Irit*q̊  n^E1)f*=r&ik9]t$|7a\Ȳ'3ryo@"?LQOBCfQ"5nj 14Tmis%ۿMޢ~[ߖ'"u4HIAR@]IÇgp^$ Y%M, Ё$_OzVЀ6TC4nr US(rN]H>㰺tu]=YKcU[>*dX$8Y#&y]BTu=_(1M=%Nz"vH=64woTL&4e_9lK ,b#<ЉR j$U<;_1M=^>M\9YH4̔+crQOE:;@ z}qG^}ŗ9,@:.au3f1KUZ|JĮ19i;SI2ߋq=LJ *~k;Ѐ8N !Zwwh mD*Nl=67IFrUc'Kxk^V3+ʜܠPUߛ%h [ns4#7ф%hkk(v[UzT :o~SڴUl12,5?;g ,nwYD2;όM4?3iKGO"2eVxYT 3 /ڶ Nzv ܁ޢl`}. %gb:%o[ /E#[l((wӔK|[MoRFx^ yG%Q􍤲XW?xEKx?gw4Wd'>H2aZ yTaPw OJl]5Cxc(X2I5Lգ_o Wx=bO8SU @mr"ρ)F9CR6:y*zI i7 ?z-WjDGsmLڱshеaZe0a\Ze JGPYZ߻zR$QnMmRT^j6Cސ O[gA5D&psXNB!:;/&dӥ-LT,@f7TcX{lV́C̕*Q޻E<Ͼ%C%0 .oz1X @0®v䭂 T-h.0Hd 6Q75#h[w6p!Q'C:h^O&9u>Km"B:g0Btմ]syKA:\%џU3_M[gV}<ݒaիմ9et\L͍JiNJd-ؑ.Tͬ=jڱU z(*Tz E=CӉ :+SG$`+U0y)t=osBdHKtZ@A+zlqu]70I(1 8h uˬ]P2?7 L@B!Iyʻ󺘏:Q?cW=nrPj%[?#r9pT\3)&d@FDV?g! a'\@#dxӗTdɛ)(jۑCnN&J:`g9&vq%^62_ʻ<>J:An`2bB^/bw8w_r~V 3TB%n(W9jcX= weV~+ g[7,?ViDz%'?跎Jr3+u^<IyJh$,:_~#7]5O-I`*UyD&J 0o.:hm4{֥*2qK%P㾶*0_X̮t )_vA$5ͼ<S^uLjc `O7wem:rd(GZ,Lկnl>-o4.P))1i6.:=Y:ʕ6N澼,WΦ%G~umvxR)@V3iB'+!r铏1O? Djx X(As*ymRXG'ؕ.b0Im3J,x&&כM5%Oh[[xjx |l[ߺ?{EyĐժϨ'_|>O?sssy??|ABwZ[#O}ԷQw|'t2E:k5{ccJmS ck0>*Vj@Z66v=~VƓ:/ ,/]?. f9w}{X5IrU4`%EUΛYonv]2,,6-zrw]{oley Oõ*wTP{Tlsږ2;֋F^8IYLY Eo@Iny4 mWI@BPDAށY(clAg:ArlHoS>;&c|?)n]rFk2:90><4#bu6Qm6噡 6M#H̦8H^y!hŴZfM^3P+ͽv1 ܶ_s]7E|b ybH$)ؾ7D{ˬ,nyO˄|:B+&0 $͔5.b@u`i4\ '#!bS u& 2@1nU#A{~7̘6j7֣A5iI/M9Qf]%7h"6:r\#Xbr1]i&g13[!+ȈԳEM)7 O~7ɧn=ht]KEq>w_'T>SkHdi  O 0ɞBA_73͞EH~;&0ï'w^+΀̽*X?Y0}ā y09)Ó Owx/Xu|^rǽLg,>Վܢ6 LGrT(suJnt 'P_;5^ƜY m|Y)8SL6Em 0\*]C?.q|4 sہr4!,'C4ffDD^񪶡,)u7M=}uxbAq˜&Y!d*{)I$vcU-/iݴŚsfp㣮g# |Ow #~<ۭ-,C_p1֜v5Ljw'))IWCn`o~/J_JL>Ru^ٯ9])0Vb2 9.hDv" cCWCRbHPf T)MCMNb"h IBF; 3aja (3Vv!Fj['詮@o".Ͷg38I5Ձj:.G49dK,?mTtvV6j7$*oq!-Vno/@)vs}y^x}" ԰*Rm;$61(~E%{2g+HWxҔ^nљ1nnWQ=Aߌ* ۈ7J4ɦ>xZ$؍u]Kg- {<>L5߱`|CȎN8>)́a0,bg}i'v+2xVdzkp/r}92v7|MSzdi19?8};]5eq QseߤJ2uբK+r9nɌ3WTL!ҵD8Z$ʝUZL&,2g}rPL"m'WT!sWm{DvkD7I9+c{!cnD9mx5mg z. W ~ OOcM'& bBx[4)#&\j͑1Q5'>Y=rn9׉-Uhx+}`w4Stfv|hw[lW {}vuv'rOQ>2ơ*4\0bNőtP+#zjM;S$Ze@}udZ!X*5ths4q3)"R栔c{4Z21//'ځ/h 쉀s_( :H?']Y*K3nJt ]YfJ TpiIpE+enۖKժ/!"W,FxqZ:-lwcyΚ0?\\q9 ?\zzȭԸV-ՙv]֟!8rt$岰~!HRJJjA'3Zp_){k-a?0Jg b + "b+WcdNpދI>gVɷ\KdҢbC>X㑞?*U28!Gy$66T;/ ƧabctF*gt@tvgG޾}9mm?x^~fO(7^?<:wgygԓ~>xǟ]vL9?<8>;{;U3`1\B'%T}xuT`;*항~5UwQ&ˌv?Z@0/Irr^Oyy1͵U^EnYu{ pOحA` 8O9tw%zA')m1 /5.>@I.L1m/O٪wt3$gjTͧYIMyE~dƉ0V\rשUL)?BGT<s#ăD+_F!u}sV6tV!~[,,\2fvQ͛ş+ Bܧ02AuOIeZ=SQȮ^%nmrv \{d{] {Qܫ3 uZxo㥪jn25Tۏ3\M=ejZ֥H1HFh3vUd"0VsA-L9Sa͹:"uC{e/nŎj:Ns^`O~R':)j.p8 Ž"3${.4Q ~Ume~#5+|G&,1pW!a$b"!aj~\+W]TΣ\*Eo ]l)n&#Ti`q*8Ul`"_|eOEz_:܋h*\@s (^)&;xs6).{U&vDs4)U%GkLC=OYmC-bEZ"/wlzϭvtoZEBL,'|5cոjMW̪^]& 4D-UMRku!XZCT7#ς[%!5`鮀yMX2ց2AI׶;-žr^zAƀ2R.Zҏ:;Sw1oMi %z\}`ZvBHdnUɰ5Z{ODmaaO8iǺFkX%kF޾Й#J6` ,@X† # &nt 7W'ؤK&y"zL1+Tշr06A1 eZûHcJ~Vgi`-*lpc?,c[> Vd=qQ` #C-3Xxg ꍬN{\+ [M舧P=Yom9&jMDAg^8ЃHt!n<yEi􇡽FD/O=dnx<z7xֱ&ܰu?|"i>Cx"&;VvI:{Gӷ>9_hc>rH2&*[#.A-N :P켛$&elnoئR-W?)34*16pX/Nrbܳp;4KWUV`(ѺҫIܑp=yWKAscY [?CJh Vi<5ƎҠƋG{M3+kՓEol@v[*扈'!wgė |髵0FQgHd6(TUtP5q5#QsTԳئftT>.Ǫa",)`aqx*n*+\Q(@E쨤ApDa>S6f0eh6c34~䫶.Swsr%FR ySY.{zRt4Y~#q\]a#_{0fȺyHǯ٫i5P}VPKmٔ:罘AyTn֫edIŝ1G^ 6DL|]HnFb 0.֎Ɋ\XQo;o躷cEڕw- &qu2܀Ţw@㟎'AOdyt ~F"E1yELxh|&uHڻkJOD<>R06ѩG A՝g!"P"ϡ^p#% O<*VyWLwcңnC(@[ Mۨd-)$nA9\om$h' "y|T~"rJ2ۏê8z ;k8&2(õ5 *~gԹp`.ŮfjeR2ݫ-N[<]DHJŤkFMX:5 PRpIjPT twrVq2yUgU2XuO߬ori̕cW>{ `K]x/q s?>psU,Y3'{MoG?-){-MՁ1RLVI LT˱Lxc06ui"xON0( 'P}N7╣tvW,f-!aĜi̽xCNxx8asjpUhkK ֙E5mLtk' J{TwZQE{yzBu?ZpR[V*'[Ģط/, deh=wJ ]_1߆_ss*EJ3<⬢?Y_nmٍY3lUcHD/sS܌&ϭet ~| &VVsW3&P::Oi CQq )xqV5H %|+K7~}҄'zv:]z#=.LO/32v2S~(lϓ[0A< n?7u7_x@̇}2\ao|+6fy o lwj/$;1oi0rcJSݸdf- 7WBX'7_KB[%z&([Bϯ_8t]8R6 ޭ2SE9RټXÑ]y|h$1.l>b$nF=X r:)!reiX@2T=g% r6y@{? TbuN!=&u٫<8h8If~~`2[ 2wB%%٧޽)4d? ܛoAW$,(&?AG?x񘟐Ƿ *tD_A3@퇇>eRhe`fLD7cm5byy'ts;a38;N+FXn ,x6|r;%?$ '䩾d Ѫ046czG*MVv'ԏ^`_! X қvXޢVgŏ^ػh^hp_e0c7$UBvR) pkQRQ+yrn ˣT#SAE JI6 =s>NYc)8u5b8-]MT:QZƻŤ C`o⚤ȴ4üd~ XGGhk`cM1}?alH}w?ĉ[f X`/5* װ%Ń abRmO| }}u|<Cb$g%ҷdmbi bC;Nzn@3X=wQnݛX EuɡLOmOqՉmOs!6m[[K= uSx Jml⾡}ܴ3s2E"C?$vG4)ePіigNNM>·D?,(Er!j[5 =]l1b%SBGP0ӊ¯VX>z3BIY{ £6;c R e[Wp}*dMit&`qʵ[DM& Q 39'ݙKENW:WkbLԪ3|=d<~y9 Z }MMK \/stdv5 CF{_NIv{p,/XBh(<-_*`(޷]Πj޴UЫ0"碚9J_fSde7HP_c Ԏ-MKswWŶ 4v v̵44Y;Y]N6[|ɴܘT"FFl^˜FvfC1xw$rw9I/(ѩx$u"Yo䨻t`иw5 Vڡs5T-Hj-R،tYZ[(!5[fmVAjKElHүX# +yj 8I},l&:S- JYhP0hW!:,;^V KJ'Uذn:%̊Q)s)y%cnfhJ8,;I;htlT)45ZWn48E_b̓1pvn8l(UR_yohLiaC2y!a哰sey0?Z꜠ R'W]ϬFސuYiPE'hf]sbވm%]Ej~dffj,ٛr\4Bޟ z@0;+4҄&~} ,#1x*_8oug*CGFP^XU&Q{Y Od.E4/˫kUϳfipiQM]~nv}{=eo(v̷oO9*%'n=ʧݛ @MAVd^Ry^-uBQ[mɟWa4U4RC "90WID5E&PK>麌~V4M]f7U,9XTO*z;ADgoˋE$!>9TP(h?zK^"H eJ`Ya9fII=۞LUU۾ظԢ}.)>gIWEntnƟ[<dž&:}6{i={og{|:*B M0-ɀ,@`RrX%Bh87l} w2ƦO3He"F?*A(qh#Ik'G%taP;O1V60^Y)kխ*WjQ`Z5*GݟK`ۋ@ɼWxeS=Hհe>YI'2 C I3:UUY0D'oIv!iI R]ux̬6;}?VSwRwαӟ Bw:b@s\vQ.&撹QzG@$+P~2}@چ!e@*F%[LwmT5z]Ja1 UsEIv 5{w8^ :׾7I뽫ZF3PYac"}Ic(w3њF~tǎܧ㾽Guo%VٖoRskZĖ+޹ޭUᝩ.Rᝨj mP[¡k$'0B TZ}sYjZ%>:T3XlѦ8w2&Q j(j'p;0U`5{e"ܭS m8ϘŐf/jcAToYU,vJw1mo _]AE11xK<4ףamˌAVAplN/W];U_Փq\h mOu5mg-j7$6>bsDV6%y_;:ⓨc`x2 :N!P7tbQgvHG绻J{Gޢ@S|٠ޏݼ^KuiClfe \LnOJ hŭ1\ !ZBQ1nqs*iYUX""VPi/L22kIǼ fB-~}*9>_аk+UԞU|KȘ3sΧ!jFd/B?+Q504~t4ݝj59~ `z,J!"iJ8@`lLB p[R$DU]K[5Y^AvXjUZ>O֩vl/f޳>Ȑ?1t)~ZBփB]cbTܶK?^L ;SbV&&WD`C;A*H3m{ۜݻM9JJUO-{7&-EƤ_oz^\J~"_r S;Τc_y3@oHZp.&?FxǸؐ Dr/8T#.+Gٷ lqo{ڨ"&P 3 ]Or̵=ꞷA' vΌ8^9)N񼺼,q3F\w#^E5S6S2 ]0:NΊόѣ2XN-M,/msTBmNuϾ:kV:6~R-+CAP5t|=bmζRh׌&J_EjGV|9)tgmzWfɈL41mhk5bVurV/}(rđPD|FÎ~ ~|wy߽QT2 zZEx"lvIhmu=+UyAJL3;g<]1hyܙx-M$UCudmwAR8m\!ٸq-E枴SHסWRp Ă2aJ/RȔ|+yNd(Ǹvi +2id4=%*f!16vmeae57@,߰BDIa+xM3wk3mCEa2\Q5_˥v tl׻v]M- Pŷn߃E]0mWnݻq8@k m 8e@;FlK;z/8[1Ut84TD &sZU;y+ G@#L0Z"Z댘!NQ3)(dUGN=ZaZ;=b7})ö8i}dRAB&حu( [ˣ@Æ O78r}S #4_/RbE(1Em[j1yW+Ρ?@]C`o"hoݛC@1&_t۲EqxksMzSED&(YF*Ԙ+ x8~ВM,JUl8vy}e&N$ixq ,MS&-T2Aѣ@2vDJ Q<ռDXH~fzevYTvY Ee-D)5=j6> pub6ٹԫ;OM f_e-WUM҇Hݨ~dwKDEئmfr`|R F`C *E~lmiܨ ]MQ@LC7O6fɀwx>,T[Czda\Ӝ <;q]PUh"ќR)GsƦUˆY/pEc`Yhמ9o6RgWUlpƃDa`I .|N*ps>-&;+D,^٪`󽼋W,;TqVU UN!VjS՘׏쎭/QUͻbr 1PgvdժcJ`Tfx-"]٩Cz: p O]O SP#.Ɉpd~GTȄ[ɫpKoWҝq YQ5' zis_pS~ㅋQg?Y%nmM~r*"ѕLVKfJTxc=c3S/y;0!ZŝۊQM㷇z#}o UK;rh?&3Ih-]'X3t 染n'A$y!םD+SAqv \rV>KaxpS,!n,Jv|AQmWg9H!H墢F_x?k*VvEKf4fH^0\P!zoRf_t'% F]&IGjyot=&!Bl$s:pu`.>JpZ?Mׯ裏UQ@_͆XRa\Lmq,v[K+T@z PK=$Tq-ђ8X]ZUΔ?ˍ| DKVcۭ :Ra-+[5\~qc?Z RC{1ꘐ#cKM_TwbPvz#`|jh7S~A$~9 \=E]`=g1u|S̸¢#@D8k(GGhT00o|ťF͙]m}`]4com>p=W@ X[p7-jY.f+8jaPωN\Չ0)^5s4)bS`m)t.}m%|z҉pu`'Bj-*/I 6:KMpIW׌K|sc.Z77q_I9g*s0c'j\NYJ?+fxp1UWS]]]W?q/i|u=7ryv(@HY:/ .sDp< 7fb^,؅,6N"鿄NB7#N+ _S`:G9M\;1K ֈ ""eGwِ] e!{휢ⷓhzklv'w' Caɲ/u(5yFo)8><$Othh:y6$,%t{wǪr()+֞x+$yOXrjlSuzDj$G\'vBjB>KNm$Vį)<Ɔd +cpvFAD3]Mk[M t)n% 8j)=RU%?˓G4ԅ2H3{ES+fq\L? M+/Lkp]v$4a';(`C.slPSfQ󺰨ؠ^n{ufR'-e1v.pg.zuGf#Ao_YϸBlMOq?5̣P޼>w7EaQp1N7>ߖahM-yo1ݩx/vEEviֺIAɷ_y>ƚ:X+sjOUqۑ 1 I٘{y[*?!eB-Ö@ W?&4% C*g)3!8kzqN( =u,QlT)Fg-)YAm*Yj3Y! U}U\X> 7zt pA8]VdU+Rm~MF:)-KpK'Ue؋uVmѰEäRe40nӰH2L;0:G8qMfjtg )9AK |d0ApWϸ PdqZ 3Oa~\VVNo- H׳7u;qԢ_.[$\iqn5&N!H`*8%k Q$qSآzi F\J)5oF4WŢ z!18HG6c 9M`3*ڿB -l %2wslڢE{ܓ7J:\SĕZSG}-{+UYN<OAPL:mfVyK/`@ Ĩ,>"W  ^pb$J/Om `[D*%;F<<$Hwd ><L+?=Fbޢ~[ߖsڠ@VryrvtwI9r_1fN97YԳ-. ! a䶪?Z˸-Klm*rNi9k۩濮qɦ̬.rt.*|Qik93-Yͯ8*(0doR]+R>u5YNEcj]WWvfQ?Lh;aCqĿ3 /Gbi&aV@r7(-87't -dRE-?S$!K"gC$g*cpmdrD#Ae}[^2k5ϵ(3XRq\,$Z @1U>tƝ"V#NTi寭-+㸁>("*쪔 I@r)vGt0|ŚȂ0];#Y'ΥS^UD^ i"!0l0iN4bbcp\Y Y,"qgF7F{~`,żlN #~E1)cp +7"|\ONz/VՙW  j[Rl}xH̾l_ԥ BE7R6@P o8Cy`Z {=;1Z3\$&/JtyX"T f &i<"Ǯ\$T;mlͽz J UzOrKcU8;wq:`mƜl|Id3s&titkuU^f0ey;zb\YFbDG 6AE3`MDdjQeS&jzYigj8 %Il0 ;W=kU;[TqҼ^UԲNC+-JS0E)$LV1+FYˠ"U`kL:D/dλ g5c&+jlP/E6xf3fT`Ƽ'$M*‘N( 4R^Md /.l dtuB :be'*E ,"0 %]&ZϞ lROd`4m:$NTE9۲^hc̢ͿZhd-:p)77SWү-μqvF,\|c2itqⶰu:TnVx\&:I z[ugEW{3jΞbhSUfMy%HXbd&yYTO WY㱟IK5 Ϯ8&-ҡB6~oLXluu\`Y0lqtdk'k \z:ɜe/>ʪO#@CC5K^@T`2NnyMl˲m%!k76UWDݜ.3 (&nTr S:MrNsN)K., hjO=llNv);R&z)ӞmN~)^}VX.W)70hD^ƫxU`Jh:/2񖨸B4ŏcFv[('P; #T8tꍪIٳw7 ]eo9"sfΤn6埢: J "8v9+^:nmח1'>:afڕsO "?B7j9\ge =J:$#Y1G4=%efeξ@uhZ]S}煎*qf;MpdUM,:=-]q "~l?wZ>DwCo0bqR* &Lcz$ut|dQ4.ٞNй4ao_R.: L,gVf/SmyM* go+/KF d+qFһ 5F[]xumH~_Aͥi݉gXZMHVQ7|E{ "G(Iۜ>o:;:4]=(zVdZ🇇UpmM|My}AzsiG+qV;EaWV4a=A3Xw"M#; UC~~Wrl:Z..-z`Fl.{<].õ(KoZY!R}"_O#z*uXEQ2wLeor¤l dyV-V^1dLN~# V*"v25JW2*L7 9oAѢKL*t0V 08D} t'O@.Ľ|Ǔ)2L"XLOhb+F)MA.##,w)@fέ!,PqPwX QOB08,^l;vIdפ)C_9oj0v?@l\i$eAqt洀TLVǹJ鴉Yu[]|=$Z<41i `?FԂF#1 Ot(ùN)0wwvJINxڣ<ڎм1w .Ͽx@RΠgO)wN$S/{ڷ1E)&y_[u5eS[QX%<"Ca yIQH];H$ˁ5=G=6Em.gTicXn$HVw+je1RH0mdrec6D XgxF ?AmsB-T`[1!(MzCX=/qSD+YxЬCӸ*l.|3(dcץ8%yj'fJCgb);w<͎O~5g m%ݸQ +ٻNʁނbNTrш FWcFuX$P憟I"|M.Eb|6owe*HY u`ëjqhE =X<(R- 7XbRcg1.eCGmNxHlZ}mEȝKdiv(BwFXagS˽;`0z!߳Pcͤ҂7[cD@6-ID+{~HBx0WmpCX1'#qD2 V_jtɞ*M_RvWG3gI %*@Fr*$@A9 vԹW$D1hٶtt>1n!OnQAp9hkph\Ag3s/`j zbT(#p}ymcM}wdxuiϥ9};ftGZs ʻda !vp!\3dyݪiS07ot[p[-[)[@U9@gsf$C=̋F@Pk8}CUTQusÿmעteQ(r2^ǮcdƬ-dHx`儌էn) C8k:4(x~ͅ-'UL$tywtb,:Tj4gnuO 1L$O͹5Il7 6Ä)Ak*؝vqD!M׈Z. ˒R 47ׯQ6ШQx`k<7U^"@V^3av# ֜;ib\U?MK ^B\fY9q;t[LSR7:sEȈTOJ(jՋ#go j̲~H=KNpX==Q$_lA^g`Qa"0"XovkӘ0b'~-OBt'8x vrwXhN?w+_-OA 7 Jl~Q>r#VCl|w9]9oO~'˯MP G1nӥVghŵmK[#!u-0`=&)WQ,.YڒA*pt1(Mh!ŗFDHb+B!4`PgP^aO>6hR(rX`,D%ߔ_?ђ]spl?[ޯ?4*v6DI|Z?sNówG_==;9?9lћ^x# y}zjjsbuv/n j,2D8W>qyzY2IμERl}Q[GGy~E-hS69`G[|46J>«1Vg};;F?bdoP˱,'ndXÉqJOR]7 }ՓMG ~257Eɮ~L8N4&;4''~R?'w%Ŕ~)sلt01#M 67 rDGEm),F5C?AtĻmw4zKE]r`7ƅn:H8p,>̨_}1:UEyp¶14k~II% ɒٮti5L=-@&w^݁p6?H1lȵ/:f2)a(ğԒHnFn{ҁdW맿gyX_: /W۾^>uL@VP63Ig=ӠA=PB=D^ah =a%X2<\o,=6);k9B"DxSFxѤ ]99^J2ht6Mi6GT츽/*R/! j05|_zU{ 6:lxw L)6Q 1oy[]rʐ+pl~|wZɚ FIo͉2+_l5]cQU+@9 執֯Wԛ዆XքMgAO~t$>,$/뷅;uP(>=@7^Z[×})]dpߺuír$bgi:iFNq6X).]4Ǧ!]Q{lY~̄lBWLGDXC8 ƆF ESc4úcf'o4E9s,50r2q燨ϫȊj*>{c+Qߟ+;lBw aNitepn;~><ڂy[{bߝzɪ,\*i$lTn:mp;l *GBoEl-Y(o"rpR$-ZgJT;جl8 1. eXpB֜u|ANٛWq}h'ǯݿ|w?*C{l-+C91x+6T7ݵ~ͨMӆ,@1CC/S|p-La۪͸FVbBs bNJΟcb}(&%Zr)дٳtYhsr|.ҽoӸ-;[!5 K d&Oڀ*7B7dTm`tCy<"^Zf V"G>#j.4l>kPvWNC֗TT"z*B(nrJ{¡m? B"(:ұ4 oaX~Z8v4fDҐʜ-٣ $VllG;f+_W Sf/$?-a{PgrꃰXJ|;Ã4/ IK nCuSNLe<'ιpSHxW 6A&f@ ksEɆ(zN%ɐ_lA02ggػ4AY$y m'GO-=<ӰjTi>`ywwH`:DbIx13|"ml 7j;<\j6EfM)uuxNJr>1=U[µ(B&j4 nG7(2b`Fn1:1)&m'Ԗ4\AMʞe\Q?Ӕ v(k=W3+Ê5FoP [Wpi}DX%PH"2Suv'VÃS34gx@8$Ѯ548))bc 븷SGh'vy.SğdI@)hdAY4Eu ܑ=5(2$lV|ueIʺ\sPN; p,?ꑂiN}ը0BdU=ٔ-/)*U3A/,=Rmze~vL\aA鶭|j ߝ2:lApV KM' p|uDx:!AKI&Tz 틍zDKIN+ӫU<v",b˶ Pȯ{hޤ)jw2X~D xU-ގe7OYyrMJk$n,r|{7cH\ʦe"L߈V([|F~p&`'ncv>dU3їZmx}7Tu#Aa #䇾sϖݭT DCyavQus3O"IZ굯EyH!e`7ؽV֐,4ϐ>s!{r]!/c`n' ޛƶt!ݛua*r\OVM |JSV $ /_ݩiBԙa޸1X3#0ZW[GQ44QjkP"; khvxEHDHjWL}[[%t:K᷒}wFI0 E53'rHۑxU=7G-){imRHcR&IN<ib3  Oz4K[EA*hW[F R2Rӥ$m0ϣa [hԁ_ٷ`d'W*3>s*]qA·^g9Hye\V30' ŠU[MΧ7WՈ i*5~q v~1cqGtxZq_ElS2yvBTag@ANj "6X 4UڸFLOF=Ƀi'V`-8-0l'BaϤ$UnVQLݒQgټ)q!t.GLݗ1ʢ/N1 $GF6\;kvPKg(E@v, q= I R9L*EbnnEXba߆iA{(a.ћ}=ɝ C68EjNR|#/-o*|QzۊPd:^*~` |75ZФS1PPctN$E+Z.S\\Wx V8;-$9#NkǀݔS*(pQ="iK.籛x+Ϋ@WTqfrzkuuYA5_^w;}U}ksHw <m $(ڎQ~DӷEA @^ne@PfjKdUVVVfVV+韾;kSoOf峓?gn&jJ:+vN<]<|_O'PGͳ*ߖզ$sz!OE~%SYhoMQVS<(|e" IlJX*v"uERzPVUu[rV=tz՛4+^M#$+)ĪHmk+fdjWc~7/~aR2[]Ga7??ȋ4.`Eq+k2 Gf9q^^V4PqnIw~w?a!#z%ul%f?;vtB3s>OOX=vBYmOMINMYl jQ\ Β:á2%:m:DVEV@lY_[Eα&׫↔;V` &lG5Zg9Z_=zt^EmTQVVr]QLjOl%@D^ ›}% m&ɮUZMU*8"=]Y lweoIUP߀,UEm*&qNˬ3vqe@l} )-h%WEKQkyzO==n%H?Cy =n%)*ӆ _x :>oϭuN>,y6.ݹ(ZwN2E!/7z@ﳴޜ[@1OϨ{)iX,M h}sf |Jl_[Uho|Yg[ru< EA% ghvWUTZlm^f鳓,vvfL>7ZrQ:q/ى^]g9 61\IXU(W|STL*(Wy"Vp٢jM[f1q5=Yj;%7`'}v e Ч >s[5 Ȗ?Π9G* 7壧[6haIuHvki") jd$zLic) Ӫc8;\xPʘ-t*,IEq(ۥ~lji>@ J*e\]&rZaPA9UWDdfi~_.ЗJz ke gldEU(]ԑR)3xq`T|12у.r,Iltd񬥹Z:^RPaQN?)ƒZU:1 *-<@!p~D ma;05ܼ ;\PӳAP`V)mhx % vcZnrǶprzsD7y|ɺɪl<I[lݢbwJۡmo!Uf C7P-^ UQV5+XM< ΋eRK C4Q#!$9ibcj ͵6 R`z0{ Y*=@Qd`[?> Tw@Et'T7f(;Nun9x14MGߟ:W~NlrqmaeuFC5=>N,FwtQ7&RuPM>Ԥ*Bx@t.~t$=6o+GJcJGg8ͮod )oPlP1k&%pȕ1H̨ Оwuw넝=9n!מ8#Z}|CSki'•<֯.zSu=-'Uà0Hl yL,$+L,ndjdw#q--x|S)=RH$T_hr^T\e ̒>f|LSr'S-Y8 ^s,oss,zFP_3XUocK#Oo":":6DGycR=Q= a;0;ّHCg$i{hCA,鮶KX;;xJuv4X&^/nCM}+g;4/zO\ p|v0Ec7 Dwt s܅j=n,;84&88#pF jtwu  Ve{tm55a]P;q?p*ۉ{! d^BΑIҹ>ƅ#IG ?8gdd1mE[KL[Wct71. ٝP6o@Vr]V ~f|P^F;9p"T8gm7} 4#)[+M[Xj[VtXwU[;~/m޵@gI#/ |fU䩶Nv|@o RoyLbdoJeMwi`n 2T:0!WR4\Ya |܏/h6aٖ u$oOGV;_ek o|Kj*[fz9mh%]R /}uvw: o\aL-p 8;uc9<|@i6͓.;*&{N_xfBjOfiN|V|.xQtߟY+r)X͉K#!VQ`48EFZFqKr<h-8.'y0撮\NwmsςUp; Gd3!$4kϺftYU"cLD>5[9ϟy}f\p;`%911z#7 ֱ3X'zn-[ͺ2W:08bgD^(@*5q|2 ;i.P\N A1V9-t6q%ph.GVkO+Eq5\3v/u&iv$!o>l.T'&ә93]B 8]I7b֝\!tuh[JMj'l!xT9e(d E4bb-BiBlA8l.x3z]LJ:5M (]~#uv&i DdhqκZ-/En2]>wf+RK=݅Ϥh~*$~.ֳD!ډzL D=%Qu '8 IsOOaCơ 7&ifUd6 :Z%~f| hD[b,3W$Y8Ȉ|^;o'g[XH WjdE"&↺4M&9OבƧU4ّXmM@נru5(0}޶0 _9|N=(fɚrӮeqж:Y۵:@FK=b-_iiu~ewzz" ׋8|ovjA:%žDÊ[Ng}'d0m|A$q"O/ęuU/݅tAw.r,`ϏĺZ9@p(z CMV}!%Qn P<-2I"=!7t,O{CE߃wCN)]Ivx]stjT;MhCZ=i̵$+:%t-euB\׀,a51+/uPׄwis_H?e-|: Kp'Nے5}Zgg&:Q59F[g[|ik}K)pj"z 8 dbXFrVAk 0%1oq!64Q;$ɿ<1<ԕ8'8l[rF UΆkwkX Vn;6P5Fg:J#4Ȟerpn+F eO/DoVZ`@.h['P㺠;)@c06nFyxI&O~vpER޿:g) Fsq*f'KbP0%4VLa-1TuyOZJϡԷ}F1(swGC_x213*fnacW27s ;$9Vց^VBl I0m:6Th@F ƵPK\34*,y||✆3pZa2͙:pg)ǮD7WyKWXBa.QĂ~dU #ՆeA2*ݙu aXP QE{妶'j]p̤,MYu3IϨU\m34Y^c싫2^>in4eT-zURկ23PH 5 3i|])@߃qW%ߝ[yI(n3nb+Jk ͤ<8@s΅:R0ƟqK FQS̜<9p,%-M#d8pTj!ݑMX VCuɈ &M]SR&Jd|<>|2{]<].\j(xs|2lW wk2?Sfp˶]R?Q<7Ot3dYS wdbNGaqKwp6>\fob'OqeZkK IXL~qFX[WTQokቕqU N&\>ɪo^ZY`CsIB=DI|0iANT4'@XBZe$Xo n\@ 2/!+3.=fJ[puyÝ^bUg5F)4Tz }ݔdPZAD\ƌ(+{/ұ9I~۬zGX%:O0\26be4\s-Jp8$_q%m4~T !y2r%3q_*Ӻ['\1i9'&FkH$79m/iEouW5w*=ʩþ|.ɳ>RN-ؠQ&ίI<*Y"Jru&~Ũ~d2gzş37b/w/rZ!U= tm5 U 8 IEV=S@~,7#ҙȭIk/@ԑ ꗎi`>SP UItUcDV7P+9I:)oYRC=ٜ>{2k/w}U|(k'4j;X,cY#6tޕA$7UVG3C]wϬhʩҦéέhj5E NX|bل2טf%6QDV٭z2fnԡ5۽9ѕE]ʓNq/k/룻Fq*2 f )w\" s.L=8\`S9`8\fHIM1RED"8)ن/o˸RDm z:&dC%z*0z|owTY{8tZКEEZ-{^>%[U6HXQ>JsĒF(%%ldX%ԑc, ?Dk[0t9){uQoAaqC͆XT]ooMQVS}d}m"VMVY )Zn w$^lgЁXk \lkE&NW//w<+Z_A+o!!Fx᫟UQ_-E m/6<@U&V^$qD :˯du)VRx-ZlXGWWLؠ {jҏj֖Z57o0;﷉"|b=.sO2sͩoE/: ɻ>&j%Ux{1 tm9s#`-px} aIUPG\e,!lW({bpH(v.;eeS ?r{4FycE*sMu@*q F;+85˥;àYe<ϻ<@@ͷAyz _MQ50l)pCc +%플wk=ZEk2 n;=Cx;BgNl.^w H>'(ԚBWwה'XmyJ&WqsC}= NLQ% :6^&DgOVRܤz %~zr?e# sznM8H\4.-RUA^QC:1~:ntc Ky1|wsgE_z=S)'R;o+mW9t+RK]_}Q]FG`йG5D| ۑtv!9>4rw=xb});[9 nmF tNfjC -<$b<}6afeR6La:|~pIow6KZHUu~iJe-Hj:1WȾ4: `:B(F_dO"4GS*oiVU!umK! &9s1 (6 E#,M%C 3R!<;fdGqX1e{ Yeq"e-}x0nަ㳓Dah}£TB=mq8< H{kXx7'R^A@Y(}Gc\UI# !Iٷ푯lAwS䡃vEKۻa`t6 Wxk7UQL ~_%Ehک N΂ESʷow샿l,ƎZLg};JLA:4܏OG:;R5vV)jݗEwvYƠ!1w`TZ8k|Wy) }W<)mq, ϠWG4vcJoi.0(ad=Wjѿ٫G<Iğ9)C4#;d*?J2n]PFgb1-{"k"<]>;C; Ì}Q}[B}QQ|ѠcX]oGK&IGM ?L\/FGpZ *RLqihZzT hkũ%:qi15 8DwG:ϥ;NtK >ΠGfr]oj~;͛֟8 g9p4g#Fн׉ ~HbHbHaa]cDz[c.s::.u˩WUgS.7ԥj ].Txꏡ6Цk0}O944CLz#3eI5/}R8bbl|R5oaҙ2v(."62Y؄T$W=>_]|/YnW܍3ѰU؁+pYp'l't/wuRI$gZiخP:.@sKk:aSl VhxU'VEY)]0-S.[|vSY:i"JfjJ tT#U0a]CVg>b2rFQ5Tyrs 죸5Y[:utbľsjQg=A=>x+_jR0sgIjK՗D>4Böööööööööööööööööööööööööööööl+QsQPEi\&v>^qzl穅 M6&1-$voV7!+Ӟ) ]7WcDNCGu%*^Qi ߾wolc{7Exc{/ƃٍGps,4^}-7C;g$4LA=4QYl-`Wumnkw> } fy!d.^!9&CJZHbyAB3xCXX0c4Ӷ A 5jXWEͱR"shRY3{6ȼ,NGa+4SQCl7eKv3o0=EEIINj HSwmY٘d{_n/llФvmwc_˅m`b _ލm<XzT헅%d_hH|E9(=#n\X@.4_=TUQn,h,GۢTUl("g<ޕ/"º$&*+yp3ST!Â_@3.\:SםFɖ:b0/Ħ: @,ƟF//XNL=~-Ao `!0LN`yo|A&``@$Юٌn 12 Po!+  7"Z0<0 ̌&6 e#2Zo6GSkX=@q̛:( l/ (^-+/ESg4TQQ_f/A u bGFla[V"A`@10|ޑ]s,4M\ۍ5.虚9**=vtr{EIs~q·H*C%>0@;';`?b~ ͡q*QѨfvHu͎k_f腮+܀!Ɍ&tQmGķc }\gglSk/~ dp+R:hhP/btA]ga4D p4@(28s CH^,^}hRp9 MKq!s‰/0Wj^z;i#Q2u{ E]m&OUpQa2uCA`9A  qiQ 4Ȫق*+0is qc4<ђL:7ENqc6¡ vmݧ;s yT;r'NYHQ'i.yXM뿘N pgۉ7 iV(JMP@aAf&L!٨ sK#䲤:xY eH24 B}ꍅlDbka%;ug.\hxnt6tA9u1 t[.{eRf81ykk/[AugVj|G4Q#8㨍՝׳E%-[,2J"%KސrlMtߨɠĕ6֝؍(8|BwHMDR ~:H! ҏc>V!=rW !N17YJRz!kuh;k7VQ⚳#Gifof8կɜg)I sj*ŹYơ뼮dCh|wW|wt-D)ycW'385q^9#tczb}6{Do޼Nsh#Q,O[aU,JHe Xo3݈ҥSgVq;[Xٚ@^3ԯݥݧq^]Jx~,Y8I5`l_ 9%%uJx'&:O0T >}mR/η!]]-*?$˞F(dPDȳY>HT(Q qpGz7n4/IڸKK;Y |ё[6 oh,0Uu![TE8sx'UjsU5<}l6O2v6m(p:,&]kb%\j[QDߜ|=MN=4LsN8/ #ISaWDRoʴJ$TTNuo9Ij< luHRDT2Z:'o;=Sٔ}|*=~.{?=::mG;8DyuQoB-ug֛ .pḪ75ho5lSNNY_[UzU[|*EպeIխ[{- Xk+w֊ 5` X^^|0 Z_, 6o:?VQ$:K(ɪH RmM|CVQ"׾D7uvشJ0_"vg) PmIUE`3?Kd5|qNBѥG2 l [6BnI)~݈|8?O- &w9;6E/ZtJdK2ݡxG?~[1& hڦPWqɓuayZ\0aeCM]şSC7|[?b>nmw].hW-խ͇lmt\A>ANu5ig\bw(FyϏHO-=qwYIߌ]Vu=ZW5= 'A'.charCodeAt(0) && keyCode <= 'Z'.charCodeAt(0)) || (keyCode >= '0'.charCodeAt(0) && keyCode <= '9'.charCodeAt(0)); } /** * Converts a keystroke event to string form, ignoring invalid extension * commands. * @param {!KeyboardEvent} e * @return {string} The keystroke as a string. */ function keystrokeToString(e) { var output = []; // TODO(devlin): Should this be i18n'd? if (cr.isMac && e.metaKey) output.push('Command'); if (cr.isChromeOS && e.metaKey) output.push('Search'); if (e.ctrlKey) output.push('Ctrl'); if (!e.ctrlKey && e.altKey) output.push('Alt'); if (e.shiftKey) output.push('Shift'); var keyCode = e.keyCode; if (isValidKeyCode(keyCode)) { if ((keyCode >= 'A'.charCodeAt(0) && keyCode <= 'Z'.charCodeAt(0)) || (keyCode >= '0'.charCodeAt(0) && keyCode <= '9'.charCodeAt(0))) { output.push(String.fromCharCode(keyCode)); } else { switch (keyCode) { case Key.Comma: output.push('Comma'); break; case Key.Del: output.push('Delete'); break; case Key.Down: output.push('Down'); break; case Key.End: output.push('End'); break; case Key.Home: output.push('Home'); break; case Key.Ins: output.push('Insert'); break; case Key.Left: output.push('Left'); break; case Key.MediaNextTrack: output.push('MediaNextTrack'); break; case Key.MediaPlayPause: output.push('MediaPlayPause'); break; case Key.MediaPrevTrack: output.push('MediaPrevTrack'); break; case Key.MediaStop: output.push('MediaStop'); break; case Key.PageDown: output.push('PageDown'); break; case Key.PageUp: output.push('PageUp'); break; case Key.Period: output.push('Period'); break; case Key.Right: output.push('Right'); break; case Key.Space: output.push('Space'); break; case Key.Tab: output.push('Tab'); break; case Key.Up: output.push('Up'); break; } } } return output.join('+'); } /** * Returns true if the event has valid modifiers. * @param {!KeyboardEvent} e The keyboard event to consider. * @return {boolean} True if the event is valid. */ function hasValidModifiers(e) { switch (getModifierPolicy(e.keyCode)) { case ModifierPolicy.REQUIRED: return hasModifier(e, false); case ModifierPolicy.NOT_ALLOWED: return !hasModifier(e, true); } assertNotReached(); } return { isValidKeyCode: isValidKeyCode, keystrokeToString: keystrokeToString, hasValidModifiers: hasValidModifiers, Key: Key, }; }); cr.define('extensions', function() { 'use strict'; /** * Creates a new list of extension commands. * @param {HTMLDivElement} div * @constructor * @extends {HTMLDivElement} */ function ExtensionCommandList(div) { div.__proto__ = ExtensionCommandList.prototype; return div; } ExtensionCommandList.prototype = { __proto__: HTMLDivElement.prototype, /** * While capturing, this records the current (last) keyboard event generated * by the user. Will be |null| after capture and during capture when no * keyboard event has been generated. * @type {KeyboardEvent}. * @private */ currentKeyEvent_: null, /** * While capturing, this keeps track of the previous selection so we can * revert back to if no valid assignment is made during capture. * @type {string}. * @private */ oldValue_: '', /** * While capturing, this keeps track of which element the user asked to * change. * @type {HTMLElement}. * @private */ capturingElement_: null, /** * Updates the extensions data for the overlay. * @param {!Array} data The extension * data. */ setData: function(data) { /** @private {!Array} */ this.data_ = data; this.textContent = ''; // Iterate over the extension data and add each item to the list. this.data_.forEach(this.createNodeForExtension_.bind(this)); }, /** * Synthesizes and initializes an HTML element for the extension command * metadata given in |extension|. * @param {chrome.developerPrivate.ExtensionInfo} extension A dictionary of * extension metadata. * @private */ createNodeForExtension_: function(extension) { if (extension.commands.length == 0 || extension.state == chrome.developerPrivate.ExtensionState.DISABLED) return; var template = $('template-collection-extension-commands').querySelector( '.extension-command-list-extension-item-wrapper'); var node = template.cloneNode(true); var title = node.querySelector('.extension-title'); title.textContent = extension.name; this.appendChild(node); // Iterate over the commands data within the extension and add each item // to the list. extension.commands.forEach( this.createNodeForCommand_.bind(this, extension.id)); }, /** * Synthesizes and initializes an HTML element for the extension command * metadata given in |command|. * @param {string} extensionId The associated extension's id. * @param {chrome.developerPrivate.Command} command A dictionary of * extension command metadata. * @private */ createNodeForCommand_: function(extensionId, command) { var template = $('template-collection-extension-commands').querySelector( '.extension-command-list-command-item-wrapper'); var node = template.cloneNode(true); node.id = this.createElementId_('command', extensionId, command.name); var description = node.querySelector('.command-description'); description.textContent = command.description; var shortcutNode = node.querySelector('.command-shortcut-text'); shortcutNode.addEventListener('mouseup', this.startCapture_.bind(this)); shortcutNode.addEventListener('focus', this.handleFocus_.bind(this)); shortcutNode.addEventListener('blur', this.handleBlur_.bind(this)); shortcutNode.addEventListener('keydown', this.handleKeyDown_.bind(this)); shortcutNode.addEventListener('keyup', this.handleKeyUp_.bind(this)); if (!command.isActive) { shortcutNode.textContent = loadTimeData.getString('extensionCommandsInactive'); var commandShortcut = node.querySelector('.command-shortcut'); commandShortcut.classList.add('inactive-keybinding'); } else { shortcutNode.textContent = command.keybinding; } var commandClear = node.querySelector('.command-clear'); commandClear.id = this.createElementId_( 'clear', extensionId, command.name); commandClear.title = loadTimeData.getString('extensionCommandsDelete'); commandClear.addEventListener('click', this.handleClear_.bind(this)); var select = node.querySelector('.command-scope'); select.id = this.createElementId_( 'setCommandScope', extensionId, command.name); select.hidden = false; // Add the 'In Chrome' option. var option = document.createElement('option'); option.textContent = loadTimeData.getString('extensionCommandsRegular'); select.appendChild(option); if (command.isExtensionAction || !command.isActive) { // Extension actions cannot be global, so we might as well disable the // combo box, to signify that, and if the command is inactive, it // doesn't make sense to allow the user to adjust the scope. select.disabled = true; } else { // Add the 'Global' option. option = document.createElement('option'); option.textContent = loadTimeData.getString('extensionCommandsGlobal'); select.appendChild(option); select.selectedIndex = command.scope == chrome.developerPrivate.CommandScope.GLOBAL ? 1 : 0; select.addEventListener( 'change', this.handleSetCommandScope_.bind(this)); } this.appendChild(node); }, /** * Starts keystroke capture to determine which key to use for a particular * extension command. * @param {Event} event The keyboard event to consider. * @private */ startCapture_: function(event) { if (this.capturingElement_) return; // Already capturing. chrome.developerPrivate.setShortcutHandlingSuspended(true); var shortcutNode = event.target; this.oldValue_ = shortcutNode.textContent; shortcutNode.textContent = loadTimeData.getString('extensionCommandsStartTyping'); shortcutNode.parentElement.classList.add('capturing'); var commandClear = shortcutNode.parentElement.querySelector('.command-clear'); commandClear.hidden = true; this.capturingElement_ = /** @type {HTMLElement} */(event.target); }, /** * Ends keystroke capture and either restores the old value or (if valid * value) sets the new value as active.. * @param {Event} event The keyboard event to consider. * @private */ endCapture_: function(event) { if (!this.capturingElement_) return; // Not capturing. chrome.developerPrivate.setShortcutHandlingSuspended(false); var shortcutNode = this.capturingElement_; var commandShortcut = shortcutNode.parentElement; commandShortcut.classList.remove('capturing'); commandShortcut.classList.remove('contains-chars'); // When the capture ends, the user may have not given a complete and valid // input (or even no input at all). Only a valid key event followed by a // valid key combination will cause a shortcut selection to be activated. // If no valid selection was made, however, revert back to what the // textbox had before to indicate that the shortcut registration was // canceled. if (!this.currentKeyEvent_ || !extensions.isValidKeyCode(this.currentKeyEvent_.keyCode)) shortcutNode.textContent = this.oldValue_; var commandClear = commandShortcut.querySelector('.command-clear'); if (this.oldValue_ == '') { commandShortcut.classList.remove('clearable'); commandClear.hidden = true; } else { commandShortcut.classList.add('clearable'); commandClear.hidden = false; } this.oldValue_ = ''; this.capturingElement_ = null; this.currentKeyEvent_ = null; }, /** * Handles focus event and adds visual indication for active shortcut. * @param {Event} event to consider. * @private */ handleFocus_: function(event) { var commandShortcut = event.target.parentElement; commandShortcut.classList.add('focused'); }, /** * Handles lost focus event and removes visual indication of active shortcut * also stops capturing on focus lost. * @param {Event} event to consider. * @private */ handleBlur_: function(event) { this.endCapture_(event); var commandShortcut = event.target.parentElement; commandShortcut.classList.remove('focused'); }, /** * The KeyDown handler. * @param {Event} event The keyboard event to consider. * @private */ handleKeyDown_: function(event) { event = /** @type {KeyboardEvent} */(event); if (event.keyCode == extensions.Key.Escape) { if (!this.capturingElement_) { // If we're not currently capturing, allow escape to propagate (so it // can close the overflow). return; } // Otherwise, escape cancels capturing. this.endCapture_(event); var parsed = this.parseElementId_('clear', event.target.parentElement.querySelector('.command-clear').id); chrome.developerPrivate.updateExtensionCommand({ extensionId: parsed.extensionId, commandName: parsed.commandName, keybinding: '' }); event.preventDefault(); event.stopPropagation(); return; } if (event.keyCode == extensions.Key.Tab) { // Allow tab propagation for keyboard navigation. return; } if (!this.capturingElement_) this.startCapture_(event); this.handleKey_(event); }, /** * The KeyUp handler. * @param {Event} event The keyboard event to consider. * @private */ handleKeyUp_: function(event) { event = /** @type {KeyboardEvent} */(event); if (event.keyCode == extensions.Key.Tab || event.keyCode == extensions.Key.Escape) { // We need to allow tab propagation for keyboard navigation, and escapes // are fully handled in handleKeyDown. return; } // We want to make it easy to change from Ctrl+Shift+ to just Ctrl+ by // releasing Shift, but we also don't want it to be easy to lose for // example Ctrl+Shift+F to Ctrl+ just because you didn't release Ctrl // as fast as the other two keys. Therefore, we process KeyUp until you // have a valid combination and then stop processing it (meaning that once // you have a valid combination, we won't change it until the next // KeyDown message arrives). if (!this.currentKeyEvent_ || !extensions.isValidKeyCode(this.currentKeyEvent_.keyCode)) { if (!event.ctrlKey && !event.altKey || ((cr.isMac || cr.isChromeOS) && !event.metaKey)) { // If neither Ctrl nor Alt is pressed then it is not a valid shortcut. // That means we're back at the starting point so we should restart // capture. this.endCapture_(event); this.startCapture_(event); } else { this.handleKey_(event); } } }, /** * A general key handler (used for both KeyDown and KeyUp). * @param {KeyboardEvent} event The keyboard event to consider. * @private */ handleKey_: function(event) { // While capturing, we prevent all events from bubbling, to prevent // shortcuts lacking the right modifier (F3 for example) from activating // and ending capture prematurely. event.preventDefault(); event.stopPropagation(); if (!extensions.hasValidModifiers(event)) return; var shortcutNode = this.capturingElement_; var keystroke = extensions.keystrokeToString(event); shortcutNode.textContent = keystroke; event.target.classList.add('contains-chars'); this.currentKeyEvent_ = event; if (extensions.isValidKeyCode(event.keyCode)) { var node = event.target; while (node && !node.id) node = node.parentElement; this.oldValue_ = keystroke; // Forget what the old value was. var parsed = this.parseElementId_('command', node.id); // Ending the capture must occur before calling // setExtensionCommandShortcut to ensure the shortcut is set. this.endCapture_(event); chrome.developerPrivate.updateExtensionCommand( {extensionId: parsed.extensionId, commandName: parsed.commandName, keybinding: keystroke}); } }, /** * A handler for the delete command button. * @param {Event} event The mouse event to consider. * @private */ handleClear_: function(event) { var parsed = this.parseElementId_('clear', event.target.id); chrome.developerPrivate.updateExtensionCommand( {extensionId: parsed.extensionId, commandName: parsed.commandName, keybinding: ''}); }, /** * A handler for the setting the scope of the command. * @param {Event} event The mouse event to consider. * @private */ handleSetCommandScope_: function(event) { var parsed = this.parseElementId_('setCommandScope', event.target.id); var element = document.getElementById( 'setCommandScope-' + parsed.extensionId + '-' + parsed.commandName); var scope = element.selectedIndex == 1 ? chrome.developerPrivate.CommandScope.GLOBAL : chrome.developerPrivate.CommandScope.CHROME; chrome.developerPrivate.updateExtensionCommand( {extensionId: parsed.extensionId, commandName: parsed.commandName, scope: scope}); }, /** * A utility function to create a unique element id based on a namespace, * extension id and a command name. * @param {string} namespace The namespace to prepend the id with. * @param {string} extensionId The extension ID to use in the id. * @param {string} commandName The command name to append the id with. * @private */ createElementId_: function(namespace, extensionId, commandName) { return namespace + '-' + extensionId + '-' + commandName; }, /** * A utility function to parse a unique element id based on a namespace, * extension id and a command name. * @param {string} namespace The namespace to prepend the id with. * @param {string} id The id to parse. * @return {{extensionId: string, commandName: string}} The parsed id. * @private */ parseElementId_: function(namespace, id) { var kExtensionIdLength = 32; return { extensionId: id.substring(namespace.length + 1, namespace.length + 1 + kExtensionIdLength), commandName: id.substring(namespace.length + 1 + kExtensionIdLength + 1) }; }, }; return { ExtensionCommandList: ExtensionCommandList }; }); // Copyright (c) 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. // // Copyright 2013 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. cr.define('extensions', function() { 'use strict'; /** * Clone a template within the extension error template collection. * @param {string} templateName The class name of the template to clone. * @return {HTMLElement} The clone of the template. */ function cloneTemplate(templateName) { return /** @type {HTMLElement} */($('template-collection-extension-error'). querySelector('.' + templateName).cloneNode(true)); } /** * Checks that an Extension ID follows the proper format (i.e., is 32 * characters long, is lowercase, and contains letters in the range [a, p]). * @param {string} id The Extension ID to test. * @return {boolean} Whether or not the ID is valid. */ function idIsValid(id) { return /^[a-p]{32}$/.test(id); } /** * @param {!Array<(ManifestError|RuntimeError)>} errors * @param {number} id * @return {number} The index of the error with |id|, or -1 if not found. */ function findErrorById(errors, id) { for (var i = 0; i < errors.length; ++i) { if (errors[i].id == id) return i; } return -1; } /** * Creates a new ExtensionError HTMLElement; this is used to show a * notification to the user when an error is caused by an extension. * @param {(RuntimeError|ManifestError)} error The error the element should * represent. * @constructor * @extends {HTMLElement} */ function ExtensionError(error) { var div = cloneTemplate('extension-error-metadata'); div.__proto__ = ExtensionError.prototype; div.decorate(error); return div; } ExtensionError.prototype = { __proto__: HTMLElement.prototype, /** * @param {(RuntimeError|ManifestError)} error The error the element should * represent. * @private */ decorate: function(error) { /** * The backing error. * @type {(ManifestError|RuntimeError)} */ this.error = error; var iconAltTextKey = 'extensionLogLevelWarn'; // Add an additional class for the severity level. if (error.type == chrome.developerPrivate.ErrorType.RUNTIME) { switch (error.severity) { case chrome.developerPrivate.ErrorLevel.LOG: this.classList.add('extension-error-severity-info'); iconAltTextKey = 'extensionLogLevelInfo'; break; case chrome.developerPrivate.ErrorLevel.WARN: this.classList.add('extension-error-severity-warning'); break; case chrome.developerPrivate.ErrorLevel.ERROR: this.classList.add('extension-error-severity-fatal'); iconAltTextKey = 'extensionLogLevelError'; break; default: assertNotReached(); } } else { // We classify manifest errors as "warnings". this.classList.add('extension-error-severity-warning'); } var iconNode = document.createElement('img'); iconNode.className = 'extension-error-icon'; iconNode.alt = loadTimeData.getString(iconAltTextKey); this.insertBefore(iconNode, this.firstChild); var messageSpan = this.querySelector('.extension-error-message'); messageSpan.textContent = error.message; var deleteButton = this.querySelector('.error-delete-button'); deleteButton.addEventListener('click', function(e) { this.dispatchEvent( new CustomEvent('deleteExtensionError', {bubbles: true, detail: this.error})); }.bind(this)); this.addEventListener('click', function(e) { if (e.target != deleteButton) this.requestActive_(); }.bind(this)); this.addEventListener('keydown', function(e) { if (e.key == 'Enter' && e.target != deleteButton) this.requestActive_(); }); }, /** * Bubble up an event to request to become active. * @private */ requestActive_: function() { this.dispatchEvent( new CustomEvent('highlightExtensionError', {bubbles: true, detail: this.error})); }, }; /** * A variable length list of runtime or manifest errors for a given extension. * @param {Array<(RuntimeError|ManifestError)>} errors The list of extension * errors with which to populate the list. * @param {string} extensionId The id of the extension. * @constructor * @extends {HTMLDivElement} */ function ExtensionErrorList(errors, extensionId) { var div = cloneTemplate('extension-error-list'); div.__proto__ = ExtensionErrorList.prototype; div.extensionId_ = extensionId; div.decorate(errors); return div; } /** * @param {!Element} root * @param {?Element} boundary * @constructor * @extends {cr.ui.FocusRow} */ ExtensionErrorList.FocusRow = function(root, boundary) { cr.ui.FocusRow.call(this, root, boundary); this.addItem('message', '.extension-error-message'); this.addItem('delete', '.error-delete-button'); }; ExtensionErrorList.FocusRow.prototype = { __proto__: cr.ui.FocusRow.prototype, }; ExtensionErrorList.prototype = { __proto__: HTMLDivElement.prototype, /** * Initializes the extension error list. * @param {Array<(RuntimeError|ManifestError)>} errors The list of errors. */ decorate: function(errors) { /** @private {!Array<(ManifestError|RuntimeError)>} */ this.errors_ = []; /** @private {!cr.ui.FocusGrid} */ this.focusGrid_ = new cr.ui.FocusGrid(); /** @private {Element} */ this.listContents_ = this.querySelector('.extension-error-list-contents'); errors.forEach(this.addError_, this); this.focusGrid_.ensureRowActive(); this.addEventListener('highlightExtensionError', function(e) { this.setActiveErrorNode_(e.target); }); this.addEventListener('deleteExtensionError', function(e) { this.removeError_(e.detail); }); this.querySelector('#extension-error-list-clear').addEventListener( 'click', function(e) { this.clear(true); }.bind(this)); /** * The callback for the extension changed event. * @private {function(chrome.developerPrivate.EventData):void} */ this.onItemStateChangedListener_ = function(data) { var type = chrome.developerPrivate.EventType; if ((data.event_type == type.ERRORS_REMOVED || data.event_type == type.ERROR_ADDED) && data.extensionInfo.id == this.extensionId_) { var newErrors = data.extensionInfo.runtimeErrors.concat( data.extensionInfo.manifestErrors); this.updateErrors_(newErrors); } }.bind(this); chrome.developerPrivate.onItemStateChanged.addListener( this.onItemStateChangedListener_); /** * The active error element in the list. * @private {?} */ this.activeError_ = null; this.setActiveError(0); }, /** * Adds an error to the list. * @param {(RuntimeError|ManifestError)} error The error to add. * @private */ addError_: function(error) { this.querySelector('#no-errors-span').hidden = true; this.errors_.push(error); var extensionError = new ExtensionError(error); this.listContents_.appendChild(extensionError); this.focusGrid_.addRow( new ExtensionErrorList.FocusRow(extensionError, this.listContents_)); }, /** * Removes an error from the list. * @param {(RuntimeError|ManifestError)} error The error to remove. * @private */ removeError_: function(error) { var index = 0; for (; index < this.errors_.length; ++index) { if (this.errors_[index].id == error.id) break; } assert(index != this.errors_.length); var errorList = this.querySelector('.extension-error-list-contents'); var wasActive = this.activeError_ && this.activeError_.error.id == error.id; this.errors_.splice(index, 1); var listElement = errorList.children[index]; var focusRow = this.focusGrid_.getRowForRoot(listElement); this.focusGrid_.removeRow(focusRow); this.focusGrid_.ensureRowActive(); focusRow.destroy(); // TODO(dbeam): in a world where this UI is actually used, we should // probably move the focus before removing |listElement|. listElement.parentNode.removeChild(listElement); if (wasActive) { index = Math.min(index, this.errors_.length - 1); this.setActiveError(index); // Gracefully handles the -1 case. } chrome.developerPrivate.deleteExtensionErrors({ extensionId: error.extensionId, errorIds: [error.id] }); if (this.errors_.length == 0) this.querySelector('#no-errors-span').hidden = false; }, /** * Updates the list of errors. * @param {!Array<(ManifestError|RuntimeError)>} newErrors The new list of * errors. * @private */ updateErrors_: function(newErrors) { this.errors_.forEach(function(error) { if (findErrorById(newErrors, error.id) == -1) this.removeError_(error); }, this); newErrors.forEach(function(error) { var index = findErrorById(this.errors_, error.id); if (index == -1) this.addError_(error); else this.errors_[index] = error; // Update the existing reference. }, this); }, /** * Called when the list is being removed. */ onRemoved: function() { chrome.developerPrivate.onItemStateChanged.removeListener( this.onItemStateChangedListener_); this.clear(false); }, /** * Sets the active error in the list. * @param {number} index The index to set to be active. */ setActiveError: function(index) { var errorList = this.querySelector('.extension-error-list-contents'); var item = errorList.children[index]; this.setActiveErrorNode_( item ? item.querySelector('.extension-error-metadata') : null); var node = null; if (index >= 0 && index < errorList.children.length) { node = errorList.children[index].querySelector( '.extension-error-metadata'); } this.setActiveErrorNode_(node); }, /** * Clears the list of all errors. * @param {boolean} deleteErrors Whether or not the errors should be deleted * on the backend. */ clear: function(deleteErrors) { if (this.errors_.length == 0) return; if (deleteErrors) { var ids = this.errors_.map(function(error) { return error.id; }); chrome.developerPrivate.deleteExtensionErrors({ extensionId: this.extensionId_, errorIds: ids }); } this.setActiveErrorNode_(null); this.errors_.length = 0; var errorList = this.querySelector('.extension-error-list-contents'); while (errorList.firstChild) errorList.removeChild(errorList.firstChild); }, /** * Sets the active error in the list. * @param {?} node The error to make active. * @private */ setActiveErrorNode_: function(node) { if (this.activeError_) this.activeError_.classList.remove('extension-error-active'); if (node) node.classList.add('extension-error-active'); this.activeError_ = node; this.dispatchEvent( new CustomEvent('activeExtensionErrorChanged', {bubbles: true, detail: node ? node.error : null})); }, }; return { ExtensionErrorList: ExtensionErrorList }; }); cr.define('extensions', function() { 'use strict'; var ExtensionType = chrome.developerPrivate.ExtensionType; /** * @param {string} name The name of the template to clone. * @return {!Element} The freshly cloned template. */ function cloneTemplate(name) { var node = $('templates').querySelector('.' + name).cloneNode(true); return assertInstanceof(node, Element); } /** * @extends {HTMLElement} * @constructor */ function ExtensionWrapper() { var wrapper = cloneTemplate('extension-list-item-wrapper'); wrapper.__proto__ = ExtensionWrapper.prototype; wrapper.initialize(); return wrapper; } ExtensionWrapper.prototype = { __proto__: HTMLElement.prototype, initialize: function() { var boundary = $('extension-settings-list'); /** @private {!extensions.FocusRow} */ this.focusRow_ = new extensions.FocusRow(this, boundary); }, /** @return {!cr.ui.FocusRow} */ getFocusRow: function() { return this.focusRow_; }, /** * Add an item to the focus row and listen for |eventType| events. * @param {string} focusType A tag used to identify equivalent elements when * changing focus between rows. * @param {string} query A query to select the element to set up. * @param {string=} opt_eventType The type of event to listen to. * @param {function(Event)=} opt_handler The function that should be called * by the event. * @private */ setupColumn: function(focusType, query, opt_eventType, opt_handler) { assert(this.focusRow_.addItem(focusType, query)); if (opt_eventType) { assert(opt_handler); this.querySelector(query).addEventListener(opt_eventType, opt_handler); } }, }; var ExtensionCommandsOverlay = extensions.ExtensionCommandsOverlay; /** * Compares two extensions for the order they should appear in the list. * @param {chrome.developerPrivate.ExtensionInfo} a The first extension. * @param {chrome.developerPrivate.ExtensionInfo} b The second extension. * returns {number} -1 if A comes before B, 1 if A comes after B, 0 if equal. */ function compareExtensions(a, b) { function compare(x, y) { return x < y ? -1 : (x > y ? 1 : 0); } function compareLocation(x, y) { if (x.location == y.location) return 0; if (x.location == chrome.developerPrivate.Location.UNPACKED) return -1; if (y.location == chrome.developerPrivate.Location.UNPACKED) return 1; return 0; } return compareLocation(a, b) || compare(a.name.toLowerCase(), b.name.toLowerCase()) || compare(a.id, b.id); } /** @interface */ function ExtensionListDelegate() {} ExtensionListDelegate.prototype = { /** * Called when the number of extensions in the list has changed. */ onExtensionCountChanged: assertNotReached, }; /** * Creates a new list of extensions. * @param {extensions.ExtensionListDelegate} delegate * @constructor * @extends {HTMLDivElement} */ function ExtensionList(delegate) { var div = document.createElement('div'); div.__proto__ = ExtensionList.prototype; div.initialize(delegate); return div; } ExtensionList.prototype = { __proto__: HTMLDivElement.prototype, /** * Indicates whether an embedded options page that was navigated to through * the '?options=' URL query has been shown to the user. This is necessary * to prevent showExtensionNodes_ from opening the options more than once. * @type {boolean} * @private */ optionsShown_: false, /** @private {!cr.ui.FocusGrid} */ focusGrid_: new cr.ui.FocusGrid(), /** * Indicates whether an uninstall dialog is being shown to prevent multiple * dialogs from being displayed. * @private {boolean} */ uninstallIsShowing_: false, /** * Indicates whether a permissions prompt is showing. * @private {boolean} */ permissionsPromptIsShowing_: false, /** * Whether or not any initial navigation (like scrolling to an extension, * or opening an options page) has occurred. * @private {boolean} */ didInitialNavigation_: false, /** * Whether or not incognito mode is available. * @private {boolean} */ incognitoAvailable_: false, /** * Whether or not the app info dialog is enabled. * @private {boolean} */ enableAppInfoDialog_: false, /** * Initializes the list. * @param {!extensions.ExtensionListDelegate} delegate */ initialize: function(delegate) { /** @private {!Array} */ this.extensions_ = []; /** @private {!extensions.ExtensionListDelegate} */ this.delegate_ = delegate; this.resetLoadFinished(); chrome.developerPrivate.onItemStateChanged.addListener( function(eventData) { var EventType = chrome.developerPrivate.EventType; switch (eventData.event_type) { case EventType.VIEW_REGISTERED: case EventType.VIEW_UNREGISTERED: case EventType.INSTALLED: case EventType.LOADED: case EventType.UNLOADED: case EventType.ERROR_ADDED: case EventType.ERRORS_REMOVED: case EventType.PREFS_CHANGED: if (eventData.extensionInfo) { this.updateOrCreateWrapper_(eventData.extensionInfo); this.focusGrid_.ensureRowActive(); } break; case EventType.UNINSTALLED: var index = this.getIndexOfExtension_(eventData.item_id); this.extensions_.splice(index, 1); this.removeWrapper_(getRequiredElement(eventData.item_id)); break; default: assertNotReached(); } if (eventData.event_type == EventType.UNLOADED) this.hideEmbeddedExtensionOptions_(eventData.item_id); if (eventData.event_type == EventType.INSTALLED || eventData.event_type == EventType.UNINSTALLED) { this.delegate_.onExtensionCountChanged(); } if (eventData.event_type == EventType.LOADED || eventData.event_type == EventType.UNLOADED || eventData.event_type == EventType.PREFS_CHANGED || eventData.event_type == EventType.UNINSTALLED) { // We update the commands overlay whenever an extension is added or // removed (other updates wouldn't affect command-ly things). We // need both UNLOADED and UNINSTALLED since the UNLOADED event results // in an extension losing active keybindings, and UNINSTALLED can // result in the "Keyboard shortcuts" link being removed. ExtensionCommandsOverlay.updateExtensionsData(this.extensions_); } }.bind(this)); }, /** * Resets the |loadFinished| promise so that it can be used again; this * is useful if the page updates and tests need to wait for it to finish. */ resetLoadFinished: function() { /** * |loadFinished| should be used for testing purposes and will be * fulfilled when this list has finished loading the first time. * @type {Promise} * */ this.loadFinished = new Promise(function(resolve, reject) { /** @private {function(?)} */ this.resolveLoadFinished_ = resolve; }.bind(this)); }, /** * Updates the extensions on the page. * @param {boolean} incognitoAvailable Whether or not incognito is allowed. * @param {boolean} enableAppInfoDialog Whether or not the app info dialog * is enabled. * @return {Promise} A promise that is resolved once the extensions data is * fully updated. */ updateExtensionsData: function(incognitoAvailable, enableAppInfoDialog) { // If we start to need more information about the extension configuration, // consider passing in the full object from the ExtensionSettings. this.incognitoAvailable_ = incognitoAvailable; this.enableAppInfoDialog_ = enableAppInfoDialog; /** @private {Promise} */ this.extensionsUpdated_ = new Promise(function(resolve, reject) { chrome.developerPrivate.getExtensionsInfo( {includeDisabled: true, includeTerminated: true}, function(extensions) { // Sort in order of unpacked vs. packed, followed by name, followed by // id. extensions.sort(compareExtensions); this.extensions_ = extensions; this.showExtensionNodes_(); // We keep the commands overlay's extension info in sync, so that we // don't duplicate the same querying logic there. ExtensionCommandsOverlay.updateExtensionsData(this.extensions_); resolve(); // |resolve| is async so it's necessary to use |then| here in order to // do work after other |then|s have finished. This is important so // elements are visible when these updates happen. this.extensionsUpdated_.then(function() { this.onUpdateFinished_(); this.resolveLoadFinished_(); }.bind(this)); }.bind(this)); }.bind(this)); return this.extensionsUpdated_; }, /** * Updates elements that need to be visible in order to update properly. * @private */ onUpdateFinished_: function() { // Cannot focus or highlight a extension if there are none, and we should // only scroll to a particular extension or open the options page once. if (this.extensions_.length == 0 || this.didInitialNavigation_) return; this.didInitialNavigation_ = true; assert(!this.hidden); assert(!this.parentElement.hidden); var idToHighlight = this.getIdQueryParam_(); if (idToHighlight) { var wrapper = $(idToHighlight); if (wrapper) { this.scrollToWrapper_(idToHighlight); var focusRow = wrapper.getFocusRow(); (focusRow.getFirstFocusable('enabled') || focusRow.getFirstFocusable('remove-enterprise') || focusRow.getFirstFocusable('website') || focusRow.getFirstFocusable('details')).focus(); } } var idToOpenOptions = this.getOptionsQueryParam_(); if (idToOpenOptions && $(idToOpenOptions)) this.showEmbeddedExtensionOptions_(idToOpenOptions, true); }, /** @return {number} The number of extensions being displayed. */ getNumExtensions: function() { return this.extensions_.length; }, /** * @param {string} id The id of the extension. * @return {number} The index of the extension with the given id. * @private */ getIndexOfExtension_: function(id) { for (var i = 0; i < this.extensions_.length; ++i) { if (this.extensions_[i].id == id) return i; } return -1; }, getIdQueryParam_: function() { return parseQueryParams(document.location)['id']; }, getOptionsQueryParam_: function() { return parseQueryParams(document.location)['options']; }, /** * Creates or updates all extension items from scratch. * @private */ showExtensionNodes_: function() { // Any node that is not updated will be removed. var seenIds = []; // Iterate over the extension data and add each item to the list. this.extensions_.forEach(function(extension) { seenIds.push(extension.id); this.updateOrCreateWrapper_(extension); }, this); this.focusGrid_.ensureRowActive(); // Remove extensions that are no longer installed. var wrappers = document.querySelectorAll( '.extension-list-item-wrapper[id]'); Array.prototype.forEach.call(wrappers, function(wrapper) { if (seenIds.indexOf(wrapper.id) < 0) this.removeWrapper_(wrapper); }, this); }, /** * Removes the wrapper from the DOM and updates the focused element if * needed. * @param {!Element} wrapper * @private */ removeWrapper_: function(wrapper) { // If focus is in the wrapper about to be removed, move it first. This // happens when clicking the trash can to remove an extension. if (wrapper.contains(document.activeElement)) { var wrappers = document.querySelectorAll( '.extension-list-item-wrapper[id]'); var index = Array.prototype.indexOf.call(wrappers, wrapper); assert(index != -1); var focusableWrapper = wrappers[index + 1] || wrappers[index - 1]; if (focusableWrapper) { var newFocusRow = focusableWrapper.getFocusRow(); newFocusRow.getEquivalentElement(document.activeElement).focus(); } } var focusRow = wrapper.getFocusRow(); this.focusGrid_.removeRow(focusRow); this.focusGrid_.ensureRowActive(); focusRow.destroy(); wrapper.parentNode.removeChild(wrapper); }, /** * Scrolls the page down to the extension node with the given id. * @param {string} extensionId The id of the extension to scroll to. * @private */ scrollToWrapper_: function(extensionId) { // Scroll offset should be calculated slightly higher than the actual // offset of the element being scrolled to, so that it ends up not all // the way at the top. That way it is clear that there are more elements // above the element being scrolled to. var wrapper = $(extensionId); var scrollFudge = 1.2; var scrollTop = wrapper.offsetTop - scrollFudge * wrapper.clientHeight; setScrollTopForDocument(document, scrollTop); }, /** * Synthesizes and initializes an HTML element for the extension metadata * given in |extension|. * @param {!chrome.developerPrivate.ExtensionInfo} extension A dictionary * of extension metadata. * @param {?Element} nextWrapper The newly created wrapper will be inserted * before |nextWrapper| if non-null (else it will be appended to the * wrapper list). * @private */ createWrapper_: function(extension, nextWrapper) { var wrapper = new ExtensionWrapper; wrapper.id = extension.id; // The 'Permissions' link. wrapper.setupColumn('details', '.permissions-link', 'click', function(e) { if (!this.permissionsPromptIsShowing_) { chrome.developerPrivate.showPermissionsDialog(extension.id, function() { this.permissionsPromptIsShowing_ = false; }.bind(this)); this.permissionsPromptIsShowing_ = true; } e.preventDefault(); }); wrapper.setupColumn('options', '.options-button', 'click', function(e) { this.showEmbeddedExtensionOptions_(extension.id, false); e.preventDefault(); }.bind(this)); // The 'Options' button or link, depending on its behaviour. // Set an href to get the correct mouse-over appearance (link, // footer) - but the actual link opening is done through developerPrivate // API with a preventDefault(). wrapper.querySelector('.options-link').href = extension.optionsPage ? extension.optionsPage.url : ''; wrapper.setupColumn('options', '.options-link', 'click', function(e) { chrome.developerPrivate.showOptions(extension.id); e.preventDefault(); }); // The 'View in Web Store/View Web Site' link. wrapper.setupColumn('website', '.site-link'); // The 'Launch' link. wrapper.setupColumn('launch', '.launch-link', 'click', function(e) { chrome.management.launchApp(extension.id); }); // The 'Reload' link. wrapper.setupColumn('localReload', '.reload-link', 'click', function(e) { chrome.developerPrivate.reload(extension.id, {failQuietly: true}); }); wrapper.setupColumn('errors', '.errors-link', 'click', function(e) { var extensionId = extension.id; assert(this.extensions_.length > 0); var newEx = this.extensions_.filter(function(e) { return e.state == chrome.developerPrivate.ExtensionState.ENABLED && e.id == extensionId; })[0]; var errors = newEx.manifestErrors.concat(newEx.runtimeErrors); extensions.ExtensionErrorOverlay.getInstance().setErrorsAndShowOverlay( errors, extensionId, newEx.name); }.bind(this)); wrapper.setupColumn('suspiciousLearnMore', '.suspicious-install-message .learn-more-link'); // The path, if provided by unpacked extension. wrapper.setupColumn('loadPath', '.load-path a:first-of-type', 'click', function(e) { chrome.developerPrivate.showPath(extension.id); e.preventDefault(); }); // The 'Show Browser Action' button. wrapper.setupColumn('showButton', '.show-button', 'click', function(e) { chrome.developerPrivate.updateExtensionConfiguration({ extensionId: extension.id, showActionButton: true }); }); // The 'allow in incognito' checkbox. wrapper.setupColumn('incognito', '.incognito-control input', 'change', function(e) { var butterBar = wrapper.querySelector('.butter-bar'); var checked = e.target.checked; butterBar.hidden = !checked || extension.type == ExtensionType.HOSTED_APP; chrome.developerPrivate.updateExtensionConfiguration({ extensionId: extension.id, incognitoAccess: e.target.checked }); }.bind(this)); // The 'collect errors' checkbox. This should only be visible if the // error console is enabled - we can detect this by the existence of the // |errorCollectionEnabled| property. wrapper.setupColumn('collectErrors', '.error-collection-control input', 'change', function(e) { chrome.developerPrivate.updateExtensionConfiguration({ extensionId: extension.id, errorCollection: e.target.checked }); }); // The 'allow on all urls' checkbox. This should only be visible if // active script restrictions are enabled. If they are not enabled, no // extensions should want all urls. wrapper.setupColumn('allUrls', '.all-urls-control input', 'click', function(e) { chrome.developerPrivate.updateExtensionConfiguration({ extensionId: extension.id, runOnAllUrls: e.target.checked }); }); // The 'allow file:// access' checkbox. wrapper.setupColumn('localUrls', '.file-access-control input', 'click', function(e) { chrome.developerPrivate.updateExtensionConfiguration({ extensionId: extension.id, fileAccess: e.target.checked }); }); // The 'Reload' terminated link. wrapper.setupColumn('terminatedReload', '.terminated-reload-link', 'click', function(e) { chrome.developerPrivate.reload(extension.id, {failQuietly: true}); }); // The 'Repair' corrupted link. wrapper.setupColumn('repair', '.corrupted-repair-button', 'click', function(e) { chrome.developerPrivate.repairExtension(extension.id); }); // The 'Enabled' checkbox. wrapper.setupColumn('enabled', '.enable-checkbox input', 'click', function(e) { var checked = e.target.checked; // TODO(devlin): What should we do if this fails? Ideally we want to // show some kind of error or feedback to the user if this fails. chrome.management.setEnabled(extension.id, checked); // This may seem counter-intuitive (to not set/clear the checkmark) // but this page will be updated asynchronously if the extension // becomes enabled/disabled. It also might not become enabled or // disabled, because the user might e.g. get prompted when enabling // and choose not to. e.preventDefault(); }); // 'Remove' button. var trash = cloneTemplate('trash'); trash.title = loadTimeData.getString('extensionUninstall'); wrapper.querySelector('.enable-controls').appendChild(trash); wrapper.setupColumn('remove-enterprise', '.trash', 'click', function(e) { trash.classList.add('open'); trash.classList.toggle('mouse-clicked', e.detail > 0); if (this.uninstallIsShowing_) return; this.uninstallIsShowing_ = true; chrome.management.uninstall(extension.id, {showConfirmDialog: true}, function() { // TODO(devlin): What should we do if the uninstall fails? this.uninstallIsShowing_ = false; if (trash.classList.contains('mouse-clicked')) trash.blur(); if (chrome.runtime.lastError) { // The uninstall failed (e.g. a cancel). Allow the trash to close. trash.classList.remove('open'); } else { // Leave the trash open if the uninstall succeded. Otherwise it can // half-close right before it's removed when the DOM is modified. } }.bind(this)); }.bind(this)); // Maintain the order that nodes should be in when creating as well as // when adding only one new wrapper. this.insertBefore(wrapper, nextWrapper); this.updateWrapper_(extension, wrapper); var nextRow = this.focusGrid_.getRowForRoot(nextWrapper); // May be null. this.focusGrid_.addRowBefore(wrapper.getFocusRow(), nextRow); }, /** * Updates an HTML element for the extension metadata given in |extension|. * @param {!chrome.developerPrivate.ExtensionInfo} extension A dictionary of * extension metadata. * @param {!Element} wrapper The extension wrapper element to update. * @private */ updateWrapper_: function(extension, wrapper) { var isActive = extension.state == chrome.developerPrivate.ExtensionState.ENABLED; wrapper.classList.toggle('inactive-extension', !isActive); wrapper.classList.remove('controlled', 'may-not-remove'); if (extension.controlledInfo) { wrapper.classList.add('controlled'); } else if (!extension.userMayModify || extension.mustRemainInstalled || extension.dependentExtensions.length > 0) { wrapper.classList.add('may-not-remove'); } var item = wrapper.querySelector('.extension-list-item'); item.style.backgroundImage = 'url(' + extension.iconUrl + ')'; this.setText_(wrapper, '.extension-title', extension.name); this.setText_(wrapper, '.extension-version', extension.version); this.setText_(wrapper, '.location-text', extension.locationText || ''); this.setText_(wrapper, '.blacklist-text', extension.blacklistText || ''); this.setText_(wrapper, '.extension-description', extension.description); // The 'Show Browser Action' button. this.updateVisibility_(wrapper, '.show-button', isActive && extension.actionButtonHidden); // The 'allow in incognito' checkbox. this.updateVisibility_(wrapper, '.incognito-control', isActive && this.incognitoAvailable_, function(item) { var incognito = item.querySelector('input'); incognito.disabled = !extension.incognitoAccess.isEnabled; incognito.checked = extension.incognitoAccess.isActive; }); var showButterBar = isActive && extension.incognitoAccess.isActive && extension.type != ExtensionType.HOSTED_APP; // The 'allow in incognito' butter bar. this.updateVisibility_(wrapper, '.butter-bar', showButterBar); // The 'collect errors' checkbox. This should only be visible if the // error console is enabled - we can detect this by the existence of the // |errorCollectionEnabled| property. this.updateVisibility_( wrapper, '.error-collection-control', isActive && extension.errorCollection.isEnabled, function(item) { item.querySelector('input').checked = extension.errorCollection.isActive; }); // The 'allow on all urls' checkbox. This should only be visible if // active script restrictions are enabled. If they are not enabled, no // extensions should want all urls. this.updateVisibility_( wrapper, '.all-urls-control', isActive && extension.runOnAllUrls.isEnabled, function(item) { item.querySelector('input').checked = extension.runOnAllUrls.isActive; }); // The 'allow file:// access' checkbox. this.updateVisibility_(wrapper, '.file-access-control', isActive && extension.fileAccess.isEnabled, function(item) { item.querySelector('input').checked = extension.fileAccess.isActive; }); // The 'Options' button or link, depending on its behaviour. var optionsEnabled = isActive && !!extension.optionsPage; this.updateVisibility_(wrapper, '.options-link', optionsEnabled && extension.optionsPage.openInTab); this.updateVisibility_(wrapper, '.options-button', optionsEnabled && !extension.optionsPage.openInTab); // The 'View in Web Store/View Web Site' link. var siteLinkEnabled = !!extension.homePage.url && !this.enableAppInfoDialog_; this.updateVisibility_(wrapper, '.site-link', siteLinkEnabled, function(item) { item.href = extension.homePage.url; item.textContent = loadTimeData.getString( extension.homePage.specified ? 'extensionSettingsVisitWebsite' : 'extensionSettingsVisitWebStore'); }); var isUnpacked = extension.location == chrome.developerPrivate.Location.UNPACKED; // The 'Reload' link. this.updateVisibility_(wrapper, '.reload-link', isActive && isUnpacked); // The 'Launch' link. this.updateVisibility_( wrapper, '.launch-link', isUnpacked && extension.type == ExtensionType.PLATFORM_APP && isActive); // The 'Errors' link. var hasErrors = extension.runtimeErrors.length > 0 || extension.manifestErrors.length > 0; this.updateVisibility_(wrapper, '.errors-link', hasErrors, function(item) { var Level = chrome.developerPrivate.ErrorLevel; var map = {}; map[Level.LOG] = {weight: 0, name: 'extension-error-info-icon'}; map[Level.WARN] = {weight: 1, name: 'extension-error-warning-icon'}; map[Level.ERROR] = {weight: 2, name: 'extension-error-fatal-icon'}; // Find the highest severity of all the errors; manifest errors all have // a 'warning' level severity. var highestSeverity = extension.runtimeErrors.reduce( function(prev, error) { return map[error.severity].weight > map[prev].weight ? error.severity : prev; }, extension.manifestErrors.length ? Level.WARN : Level.LOG); // Adjust the class on the icon. var icon = item.querySelector('.extension-error-icon'); // TODO(hcarmona): Populate alt text with a proper description since // this icon conveys the severity of the error. (info, warning, fatal). icon.alt = ''; icon.className = 'extension-error-icon'; // Remove other classes. icon.classList.add(map[highestSeverity].name); }); // The 'Reload' terminated link. var isTerminated = extension.state == chrome.developerPrivate.ExtensionState.TERMINATED; this.updateVisibility_(wrapper, '.terminated-reload-link', isTerminated); // The 'Repair' corrupted link. var canRepair = !isTerminated && extension.disableReasons.corruptInstall && extension.location == chrome.developerPrivate.Location.FROM_STORE; this.updateVisibility_(wrapper, '.corrupted-repair-button', canRepair); // The 'Enabled' checkbox. var isOK = !isTerminated && !canRepair; this.updateVisibility_(wrapper, '.enable-checkbox', isOK, function(item) { var enableCheckboxDisabled = !extension.userMayModify || extension.disableReasons.suspiciousInstall || extension.disableReasons.corruptInstall || extension.disableReasons.updateRequired || extension.dependentExtensions.length > 0 || extension.state == chrome.developerPrivate.ExtensionState.BLACKLISTED; item.querySelector('input').disabled = enableCheckboxDisabled; item.querySelector('input').checked = isActive; }); // Indicator for extensions controlled by policy. var controlNode = wrapper.querySelector('.enable-controls'); var indicator = controlNode.querySelector('.controlled-extension-indicator'); var needsIndicator = isOK && extension.controlledInfo; if (needsIndicator && !indicator) { indicator = new cr.ui.ControlledIndicator(); indicator.classList.add('controlled-extension-indicator'); var ControllerType = chrome.developerPrivate.ControllerType; var controlledByStr = ''; switch (extension.controlledInfo.type) { case ControllerType.POLICY: controlledByStr = 'policy'; break; case ControllerType.CHILD_CUSTODIAN: controlledByStr = 'child-custodian'; break; case ControllerType.SUPERVISED_USER_CUSTODIAN: controlledByStr = 'supervised-user-custodian'; break; } indicator.setAttribute('controlled-by', controlledByStr); var text = extension.controlledInfo.text; indicator.setAttribute('text' + controlledByStr, text); indicator.image.setAttribute('aria-label', text); controlNode.appendChild(indicator); wrapper.setupColumn('remove-enterprise', '[controlled-by] div'); } else if (!needsIndicator && indicator) { controlNode.removeChild(indicator); } // Developer mode //////////////////////////////////////////////////////// // First we have the id. var idLabel = wrapper.querySelector('.extension-id'); idLabel.textContent = ' ' + extension.id; // Then the path, if provided by unpacked extension. this.updateVisibility_(wrapper, '.load-path', isUnpacked, function(item) { item.querySelector('a:first-of-type').textContent = ' ' + extension.prettifiedPath; }); // Then the 'managed, cannot uninstall/disable' message. // We would like to hide managed installed message since this // extension is disabled. var isRequired = !extension.userMayModify || extension.mustRemainInstalled; this.updateVisibility_(wrapper, '.managed-message', isRequired && !extension.disableReasons.updateRequired); // Then the 'This isn't from the webstore, looks suspicious' message. var isSuspicious = extension.disableReasons.suspiciousInstall; this.updateVisibility_(wrapper, '.suspicious-install-message', !isRequired && isSuspicious); // Then the 'This is a corrupt extension' message. this.updateVisibility_(wrapper, '.corrupt-install-message', !isRequired && extension.disableReasons.corruptInstall); // Then the 'An update required by enterprise policy' message. Note that // a force-installed extension might be disabled due to being outdated // as well. this.updateVisibility_(wrapper, '.update-required-message', extension.disableReasons.updateRequired); // The 'following extensions depend on this extension' list. var hasDependents = extension.dependentExtensions.length > 0; wrapper.classList.toggle('developer-extras', hasDependents); this.updateVisibility_(wrapper, '.dependent-extensions-message', hasDependents, function(item) { var dependentList = item.querySelector('ul'); dependentList.textContent = ''; extension.dependentExtensions.forEach(function(dependentExtension) { var depNode = cloneTemplate('dependent-list-item'); depNode.querySelector('.dep-extension-title').textContent = dependentExtension.name; depNode.querySelector('.dep-extension-id').textContent = dependentExtension.id; dependentList.appendChild(depNode); }, this); }.bind(this)); // The active views. this.updateVisibility_(wrapper, '.active-views', extension.views.length > 0, function(item) { var link = item.querySelector('a'); // Link needs to be an only child before the list is updated. while (link.nextElementSibling) item.removeChild(link.nextElementSibling); // Link needs to be cleaned up if it was used before. link.textContent = ''; if (link.clickHandler) link.removeEventListener('click', link.clickHandler); extension.views.forEach(function(view, i) { if (view.type == chrome.developerPrivate.ViewType.EXTENSION_DIALOG || view.type == chrome.developerPrivate.ViewType.EXTENSION_POPUP) { return; } var displayName; if (view.url.startsWith('chrome-extension://')) { var pathOffset = 'chrome-extension://'.length + 32 + 1; displayName = view.url.substring(pathOffset); if (displayName == '_generated_background_page.html') displayName = loadTimeData.getString('backgroundPage'); } else { displayName = view.url; } var label = displayName + (view.incognito ? ' ' + loadTimeData.getString('viewIncognito') : '') + (view.renderProcessId == -1 ? ' ' + loadTimeData.getString('viewInactive') : '') + (view.isIframe ? ' ' + loadTimeData.getString('viewIframe') : ''); link.textContent = label; link.clickHandler = function(e) { chrome.developerPrivate.openDevTools({ extensionId: extension.id, renderProcessId: view.renderProcessId, renderViewId: view.renderViewId, incognito: view.incognito }); }; link.addEventListener('click', link.clickHandler); if (i < extension.views.length - 1) { link = link.cloneNode(true); item.appendChild(link); } wrapper.setupColumn('activeView', '.active-views a:last-of-type'); }); }); // The extension warnings (describing runtime issues). this.updateVisibility_(wrapper, '.extension-warnings', extension.runtimeWarnings.length > 0, function(item) { var warningList = item.querySelector('ul'); warningList.textContent = ''; extension.runtimeWarnings.forEach(function(warning) { var li = document.createElement('li'); warningList.appendChild(li).innerText = warning; }); }); // Install warnings. this.updateVisibility_(wrapper, '.install-warnings', extension.installWarnings.length > 0, function(item) { var installWarningList = item.querySelector('ul'); installWarningList.textContent = ''; if (extension.installWarnings) { extension.installWarnings.forEach(function(warning) { var li = document.createElement('li'); li.innerText = warning; installWarningList.appendChild(li); }); } }); if (location.hash.substr(1) == extension.id) { // Scroll beneath the fixed header so that the extension is not // obscured. var topScroll = wrapper.offsetTop - $('page-header').offsetHeight; var pad = parseInt(window.getComputedStyle(wrapper).marginTop, 10); if (!isNaN(pad)) topScroll -= pad / 2; setScrollTopForDocument(document, topScroll); } }, /** * Updates an element's textContent. * @param {Node} node Ancestor of the element specified by |query|. * @param {string} query A query to select an element in |node|. * @param {string} textContent * @private */ setText_: function(node, query, textContent) { node.querySelector(query).textContent = textContent; }, /** * Updates an element's visibility and calls |shownCallback| if it is * visible. * @param {Node} node Ancestor of the element specified by |query|. * @param {string} query A query to select an element in |node|. * @param {boolean} visible Whether the element should be visible or not. * @param {function(Element)=} opt_shownCallback Callback if the element is * visible. The element passed in will be the element specified by * |query|. * @private */ updateVisibility_: function(node, query, visible, opt_shownCallback) { var element = assertInstanceof(node.querySelector(query), Element); element.hidden = !visible; if (visible && opt_shownCallback) opt_shownCallback(element); }, /** * Opens the extension options overlay for the extension with the given id. * @param {string} extensionId The id of extension whose options page should * be displayed. * @param {boolean} scroll Whether the page should scroll to the extension * @private */ showEmbeddedExtensionOptions_: function(extensionId, scroll) { if (this.optionsShown_) return; // Get the extension from the given id. var extension = this.extensions_.filter(function(extension) { return extension.state == chrome.developerPrivate.ExtensionState.ENABLED && extension.id == extensionId; })[0]; if (!extension) return; if (scroll) this.scrollToWrapper_(extensionId); // Add the options query string. Corner case: the 'options' query string // will clobber the 'id' query string if the options link is clicked when // 'id' is in the URL, or if both query strings are in the URL. uber.replaceState({}, '?options=' + extensionId); var overlay = extensions.ExtensionOptionsOverlay.getInstance(); var shownCallback = function() { // This overlay doesn't get focused automatically as // is created after the overlay is shown. if (cr.ui.FocusOutlineManager.forDocument(document).visible) overlay.setInitialFocus(); }; overlay.setExtensionAndShow(extensionId, extension.name, extension.iconUrl, shownCallback); this.optionsShown_ = true; var self = this; $('overlay').addEventListener('cancelOverlay', function f() { self.optionsShown_ = false; $('overlay').removeEventListener('cancelOverlay', f); // Remove the options query string. uber.replaceState({}, ''); }); // TODO(dbeam): why do we need to focus before and // after its showing animation? Makes very little sense to me. overlay.setInitialFocus(); }, /** * Hides the extension options overlay for the extension with id * |extensionId|. If there is an overlay showing for a different extension, * nothing happens. * @param {string} extensionId ID of the extension to hide. * @private */ hideEmbeddedExtensionOptions_: function(extensionId) { if (!this.optionsShown_) return; var overlay = extensions.ExtensionOptionsOverlay.getInstance(); if (overlay.getExtensionId() == extensionId) overlay.close(); }, /** * Updates or creates a wrapper for |extension|. * @param {!chrome.developerPrivate.ExtensionInfo} extension The information * about the extension to update. * @private */ updateOrCreateWrapper_: function(extension) { var currIndex = this.getIndexOfExtension_(extension.id); if (currIndex != -1) { // If there is a current version of the extension, update it with the // new version. this.extensions_[currIndex] = extension; } else { // If the extension isn't found, push it back and sort. Technically, we // could optimize by inserting it at the right location, but since this // only happens on extension install, it's not worth it. this.extensions_.push(extension); this.extensions_.sort(compareExtensions); } var wrapper = $(extension.id); if (wrapper) { this.updateWrapper_(extension, wrapper); } else { var nextExt = this.extensions_[this.extensions_.indexOf(extension) + 1]; this.createWrapper_(extension, nextExt ? $(nextExt.id) : null); } } }; return { ExtensionList: ExtensionList, ExtensionListDelegate: ExtensionListDelegate }; }); // Copyright (c) 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. // // Copyright 2014 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. cr.define('cr.ui', function() { /** * A class to manage focus between given horizontally arranged elements. * * Pressing left cycles backward and pressing right cycles forward in item * order. Pressing Home goes to the beginning of the list and End goes to the * end of the list. * * If an item in this row is focused, it'll stay active (accessible via tab). * If no items in this row are focused, the row can stay active until focus * changes to a node inside |this.boundary_|. If |boundary| isn't specified, * any focus change deactivates the row. * * @param {!Element} root The root of this focus row. Focus classes are * applied to |root| and all added elements must live within |root|. * @param {?Element} boundary Focus events are ignored outside of this * element. * @param {cr.ui.FocusRow.Delegate=} opt_delegate An optional event delegate. * @constructor */ function FocusRow(root, boundary, opt_delegate) { /** @type {!Element} */ this.root = root; /** @private {!Element} */ this.boundary_ = boundary || document.documentElement; /** @type {cr.ui.FocusRow.Delegate|undefined} */ this.delegate = opt_delegate; /** @protected {!EventTracker} */ this.eventTracker = new EventTracker; } /** @interface */ FocusRow.Delegate = function() {}; FocusRow.Delegate.prototype = { /** * Called when a key is pressed while on a FocusRow's item. If true is * returned, further processing is skipped. * @param {!cr.ui.FocusRow} row The row that detected a keydown. * @param {!Event} e * @return {boolean} Whether the event was handled. */ onKeydown: assertNotReached, /** * @param {!cr.ui.FocusRow} row * @param {!Event} e */ onFocus: assertNotReached, }; /** @const {string} */ FocusRow.ACTIVE_CLASS = 'focus-row-active'; /** * Whether it's possible that |element| can be focused. * @param {Element} element * @return {boolean} Whether the item is focusable. */ FocusRow.isFocusable = function(element) { if (!element || element.disabled) return false; // We don't check that element.tabIndex >= 0 here because inactive rows set // a tabIndex of -1. function isVisible(element) { assertInstanceof(element, Element); var style = window.getComputedStyle(element); if (style.visibility == 'hidden' || style.display == 'none') return false; var parent = element.parentNode; if (!parent) return false; if (parent == element.ownerDocument || parent instanceof DocumentFragment) return true; return isVisible(parent); } return isVisible(element); }; FocusRow.prototype = { /** * Register a new type of focusable element (or add to an existing one). * * Example: an (X) button might be 'delete' or 'close'. * * When FocusRow is used within a FocusGrid, these types are used to * determine equivalent controls when Up/Down are pressed to change rows. * * Another example: mutually exclusive controls that hide eachother on * activation (i.e. Play/Pause) could use the same type (i.e. 'play-pause') * to indicate they're equivalent. * * @param {string} type The type of element to track focus of. * @param {string} query The selector of the element from this row's root. * @return {boolean} Whether a new item was added. */ addItem: function(type, query) { assert(type); var element = this.root.querySelector(query); if (!element) return false; element.setAttribute('focus-type', type); element.tabIndex = this.isActive() ? 0 : -1; this.eventTracker.add(element, 'blur', this.onBlur_.bind(this)); this.eventTracker.add(element, 'focus', this.onFocus_.bind(this)); this.eventTracker.add(element, 'keydown', this.onKeydown_.bind(this)); this.eventTracker.add(element, 'mousedown', this.onMousedown_.bind(this)); return true; }, /** Dereferences nodes and removes event handlers. */ destroy: function() { this.eventTracker.removeAll(); }, /** * @param {!Element} sampleElement An element for to find an equivalent for. * @return {!Element} An equivalent element to focus for |sampleElement|. * @protected */ getCustomEquivalent: function(sampleElement) { return assert(this.getFirstFocusable()); }, /** * @return {!Array} All registered elements (regardless of * focusability). */ getElements: function() { var elements = this.root.querySelectorAll('[focus-type]'); return Array.prototype.slice.call(elements); }, /** * Find the element that best matches |sampleElement|. * @param {!Element} sampleElement An element from a row of the same type * which previously held focus. * @return {!Element} The element that best matches sampleElement. */ getEquivalentElement: function(sampleElement) { if (this.getFocusableElements().indexOf(sampleElement) >= 0) return sampleElement; var sampleFocusType = this.getTypeForElement(sampleElement); if (sampleFocusType) { var sameType = this.getFirstFocusable(sampleFocusType); if (sameType) return sameType; } return this.getCustomEquivalent(sampleElement); }, /** * @param {string=} opt_type An optional type to search for. * @return {?Element} The first focusable element with |type|. */ getFirstFocusable: function(opt_type) { var filter = opt_type ? '="' + opt_type + '"' : ''; var elements = this.root.querySelectorAll('[focus-type' + filter + ']'); for (var i = 0; i < elements.length; ++i) { if (cr.ui.FocusRow.isFocusable(elements[i])) return elements[i]; } return null; }, /** @return {!Array} Registered, focusable elements. */ getFocusableElements: function() { return this.getElements().filter(cr.ui.FocusRow.isFocusable); }, /** * @param {!Element} element An element to determine a focus type for. * @return {string} The focus type for |element| or '' if none. */ getTypeForElement: function(element) { return element.getAttribute('focus-type') || ''; }, /** @return {boolean} Whether this row is currently active. */ isActive: function() { return this.root.classList.contains(FocusRow.ACTIVE_CLASS); }, /** * Enables/disables the tabIndex of the focusable elements in the FocusRow. * tabIndex can be set properly. * @param {boolean} active True if tab is allowed for this row. */ makeActive: function(active) { if (active == this.isActive()) return; this.getElements().forEach(function(element) { element.tabIndex = active ? 0 : -1; }); this.root.classList.toggle(FocusRow.ACTIVE_CLASS, active); }, /** * @param {!Event} e * @private */ onBlur_: function(e) { if (!this.boundary_.contains(/** @type {Element} */ (e.relatedTarget))) return; var currentTarget = /** @type {!Element} */ (e.currentTarget); if (this.getFocusableElements().indexOf(currentTarget) >= 0) this.makeActive(false); }, /** * @param {!Event} e * @private */ onFocus_: function(e) { if (this.delegate) this.delegate.onFocus(this, e); }, /** * @param {!Event} e A mousedown event. * @private */ onMousedown_: function(e) { // Only accept left mouse clicks. if (e.button) return; // Allow the element under the mouse cursor to be focusable. if (!e.currentTarget.disabled) e.currentTarget.tabIndex = 0; }, /** * @param {!Event} e The keydown event. * @private */ onKeydown_: function(e) { var elements = this.getFocusableElements(); var currentElement = /** @type {!Element} */ (e.currentTarget); var elementIndex = elements.indexOf(currentElement); assert(elementIndex >= 0); if (this.delegate && this.delegate.onKeydown(this, e)) return; if (hasKeyModifiers(e)) return; var index = -1; if (e.key == 'ArrowLeft') index = elementIndex + (isRTL() ? 1 : -1); else if (e.key == 'ArrowRight') index = elementIndex + (isRTL() ? -1 : 1); else if (e.key == 'Home') index = 0; else if (e.key == 'End') index = elements.length - 1; var elementToFocus = elements[index]; if (elementToFocus) { this.getEquivalentElement(elementToFocus).focus(); e.preventDefault(); } }, }; return { FocusRow: FocusRow, }; }); // // Copyright 2014 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. cr.define('cr.ui', function() { /** * A class to manage grid of focusable elements in a 2D grid. For example, * given this grid: * * focusable [focused] focusable (row: 0, col: 1) * focusable focusable focusable * focusable focusable focusable * * Pressing the down arrow would result in the focus moving down 1 row and * keeping the same column: * * focusable focusable focusable * focusable [focused] focusable (row: 1, col: 1) * focusable focusable focusable * * And pressing right or tab at this point would move the focus to: * * focusable focusable focusable * focusable focusable [focused] (row: 1, col: 2) * focusable focusable focusable * * @constructor * @implements {cr.ui.FocusRow.Delegate} */ function FocusGrid() { /** @type {!Array} */ this.rows = []; } FocusGrid.prototype = { /** @private {boolean} */ ignoreFocusChange_: false, /** @override */ onFocus: function(row, e) { if (this.ignoreFocusChange_) this.ignoreFocusChange_ = false; else this.lastFocused_ = e.currentTarget; this.rows.forEach(function(r) { r.makeActive(r == row); }); }, /** @override */ onKeydown: function(row, e) { var rowIndex = this.rows.indexOf(row); assert(rowIndex >= 0); var newRow = -1; if (e.key == 'ArrowUp') newRow = rowIndex - 1; else if (e.key == 'ArrowDown') newRow = rowIndex + 1; else if (e.key == 'PageUp') newRow = 0; else if (e.key == 'PageDown') newRow = this.rows.length - 1; var rowToFocus = this.rows[newRow]; if (rowToFocus) { this.ignoreFocusChange_ = true; rowToFocus.getEquivalentElement(this.lastFocused_).focus(); e.preventDefault(); return true; } return false; }, /** * Unregisters event handlers and removes all |this.rows|. */ destroy: function() { this.rows.forEach(function(row) { row.destroy(); }); this.rows.length = 0; }, /** * @param {!Element} target A target item to find in this grid. * @return {number} The row index. -1 if not found. */ getRowIndexForTarget: function(target) { for (var i = 0; i < this.rows.length; ++i) { if (this.rows[i].getElements().indexOf(target) >= 0) return i; } return -1; }, /** * @param {Element} root An element to search for. * @return {?cr.ui.FocusRow} The row with root of |root| or null. */ getRowForRoot: function(root) { for (var i = 0; i < this.rows.length; ++i) { if (this.rows[i].root == root) return this.rows[i]; } return null; }, /** * Adds |row| to the end of this list. * @param {!cr.ui.FocusRow} row The row that needs to be added to this grid. */ addRow: function(row) { this.addRowBefore(row, null); }, /** * Adds |row| before |nextRow|. If |nextRow| is not in the list or it's * null, |row| is added to the end. * @param {!cr.ui.FocusRow} row The row that needs to be added to this grid. * @param {cr.ui.FocusRow} nextRow The row that should follow |row|. */ addRowBefore: function(row, nextRow) { row.delegate = row.delegate || this; var nextRowIndex = nextRow ? this.rows.indexOf(nextRow) : -1; if (nextRowIndex == -1) this.rows.push(row); else this.rows.splice(nextRowIndex, 0, row); }, /** * Removes a row from the focus row. No-op if row is not in the grid. * @param {cr.ui.FocusRow} row The row that needs to be removed. */ removeRow: function(row) { var nextRowIndex = row ? this.rows.indexOf(row) : -1; if (nextRowIndex > -1) this.rows.splice(nextRowIndex, 1); }, /** * Makes sure that at least one row is active. Should be called once, after * adding all rows to FocusGrid. * @param {number=} preferredRow The row to select if no other row is * active. Selects the first item if this is beyond the range of the * grid. */ ensureRowActive: function(preferredRow) { if (this.rows.length == 0) return; for (var i = 0; i < this.rows.length; ++i) { if (this.rows[i].isActive()) return; } (this.rows[preferredRow || 0] || this.rows[0]).makeActive(true); }, }; return { FocusGrid: FocusGrid, }; }); // // Copyright (c) 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. /** * @fileoverview A collection of utility methods for UberPage and its contained * pages. */ cr.define('uber', function() { /** * Fixed position header elements on the page to be shifted by handleScroll. * @type {NodeList} */ var headerElements; /** * This should be called by uber content pages when DOM content has loaded. */ function onContentFrameLoaded() { headerElements = document.getElementsByTagName('header'); document.addEventListener('scroll', handleScroll); document.addEventListener('mousedown', handleMouseDownInFrame, true); invokeMethodOnParent('ready'); // Prevent the navigation from being stuck in a disabled state when a // content page is reloaded while an overlay is visible (crbug.com/246939). invokeMethodOnParent('stopInterceptingEvents'); // Trigger the scroll handler to tell the navigation if our page started // with some scroll (happens when you use tab restore). handleScroll(); window.addEventListener('message', handleWindowMessage); } /** * Handles scroll events on the document. This adjusts the position of all * headers and updates the parent frame when the page is scrolled. */ function handleScroll() { var scrollLeft = scrollLeftForDocument(document); var offset = scrollLeft * -1; for (var i = 0; i < headerElements.length; i++) { // As a workaround for http://crbug.com/231830, set the transform to // 'none' rather than 0px. headerElements[i].style.webkitTransform = offset ? 'translateX(' + offset + 'px)' : 'none'; } invokeMethodOnParent('adjustToScroll', scrollLeft); } /** * Tells the parent to focus the current frame if the mouse goes down in the * current frame (and it doesn't already have focus). * @param {Event} e A mousedown event. */ function handleMouseDownInFrame(e) { if (!e.isSynthetic && !document.hasFocus()) window.focus(); } /** * Handles 'message' events on window. * @param {Event} e The message event. */ function handleWindowMessage(e) { e = /** @type {!MessageEvent} */(e); if (e.data.method === 'frameSelected') { handleFrameSelected(); } else if (e.data.method === 'mouseWheel') { handleMouseWheel( /** @type {{deltaX: number, deltaY: number}} */(e.data.params)); } else if (e.data.method === 'mouseDown') { handleMouseDown(); } else if (e.data.method === 'popState') { handlePopState(e.data.params.state, e.data.params.path); } } /** * This is called when a user selects this frame via the navigation bar * frame (and is triggered via postMessage() from the uber page). */ function handleFrameSelected() { setScrollTopForDocument(document, 0); } /** * Called when a user mouse wheels (or trackpad scrolls) over the nav frame. * The wheel event is forwarded here and we scroll the body. * There's no way to figure out the actual scroll amount for a given delta. * It differs for every platform and even initWebKitWheelEvent takes a * pixel amount instead of a wheel delta. So we just choose something * reasonable and hope no one notices the difference. * @param {{deltaX: number, deltaY: number}} params A structure that holds * wheel deltas in X and Y. */ function handleMouseWheel(params) { window.scrollBy(-params.deltaX * 49 / 120, -params.deltaY * 49 / 120); } /** * Fire a synthetic mousedown on the body to dismiss transient things like * bubbles or menus that listen for mouse presses outside of their UI. We * dispatch a fake mousedown rather than a 'mousepressedinnavframe' so that * settings/history/extensions don't need to know about their embedder. */ function handleMouseDown() { var mouseEvent = new MouseEvent('mousedown'); mouseEvent.isSynthetic = true; document.dispatchEvent(mouseEvent); } /** * Called when the parent window restores some state saved by uber.pushState * or uber.replaceState. Simulates a popstate event. * @param {PopStateEvent} state A state object for replaceState and pushState. * @param {string} path The path the page navigated to. * @suppress {checkTypes} */ function handlePopState(state, path) { window.history.replaceState(state, '', path); window.dispatchEvent(new PopStateEvent('popstate', {state: state})); } /** * @return {boolean} Whether this frame has a parent. */ function hasParent() { return window != window.parent; } /** * Invokes a method on the parent window (UberPage). This is a convenience * method for API calls into the uber page. * @param {string} method The name of the method to invoke. * @param {?=} opt_params Optional property bag of parameters to pass to the * invoked method. */ function invokeMethodOnParent(method, opt_params) { if (!hasParent()) return; invokeMethodOnWindow(window.parent, method, opt_params, 'chrome://chrome'); } /** * Invokes a method on the target window. * @param {string} method The name of the method to invoke. * @param {?=} opt_params Optional property bag of parameters to pass to the * invoked method. * @param {string=} opt_url The origin of the target window. */ function invokeMethodOnWindow(targetWindow, method, opt_params, opt_url) { var data = {method: method, params: opt_params}; targetWindow.postMessage(data, opt_url ? opt_url : '*'); } /** * Updates the page's history state. If the page is embedded in a child, * forward the information to the parent for it to manage history for us. This * is a replacement of history.replaceState and history.pushState. * @param {Object} state A state object for replaceState and pushState. * @param {string} path The path the page navigated to. * @param {boolean} replace If true, navigate with replacement. */ function updateHistory(state, path, replace) { var historyFunction = replace ? window.history.replaceState : window.history.pushState; if (hasParent()) { // If there's a parent, always replaceState. The parent will do the actual // pushState. historyFunction = window.history.replaceState; invokeMethodOnParent('updateHistory', { state: state, path: path, replace: replace}); } historyFunction.call(window.history, state, '', '/' + path); } /** * Sets the current title for the page. If the page is embedded in a child, * forward the information to the parent. This is a replacement for setting * document.title. * @param {string} title The new title for the page. */ function setTitle(title) { document.title = title; invokeMethodOnParent('setTitle', {title: title}); } /** * Pushes new history state for the page. If the page is embedded in a child, * forward the information to the parent; when embedded, all history entries * are attached to the parent. This is a replacement of history.pushState. * @param {Object} state A state object for replaceState and pushState. * @param {string} path The path the page navigated to. */ function pushState(state, path) { updateHistory(state, path, false); } /** * Replaces the page's history state. If the page is embedded in a child, * forward the information to the parent; when embedded, all history entries * are attached to the parent. This is a replacement of history.replaceState. * @param {Object} state A state object for replaceState and pushState. * @param {string} path The path the page navigated to. */ function replaceState(state, path) { updateHistory(state, path, true); } return { invokeMethodOnParent: invokeMethodOnParent, invokeMethodOnWindow: invokeMethodOnWindow, onContentFrameLoaded: onContentFrameLoaded, pushState: pushState, replaceState: replaceState, setTitle: setTitle, }; }); // // Copyright 2016 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. cr.define('extensions', function() { 'use strict'; /** * @param {boolean} dragEnabled * @param {!EventTarget} target * @constructor * @implements cr.ui.DragWrapperDelegate */ function DragAndDropHandler(dragEnabled, target) { this.dragEnabled = dragEnabled; /** @private {!EventTarget} */ this.eventTarget_ = target; } // TODO(devlin): Un-chrome.send-ify this implementation. DragAndDropHandler.prototype = { /** @type {boolean} */ dragEnabled: false, /** @override */ shouldAcceptDrag: function(e) { // External Extension installation can be disabled globally, e.g. while a // different overlay is already showing. if (!this.dragEnabled) return false; // We can't access filenames during the 'dragenter' event, so we have to // wait until 'drop' to decide whether to do something with the file or // not. // See: http://www.w3.org/TR/2011/WD-html5-20110113/dnd.html#concept-dnd-p return !!e.dataTransfer.types && e.dataTransfer.types.indexOf('Files') > -1; }, /** @override */ doDragEnter: function() { chrome.send('startDrag'); this.eventTarget_.dispatchEvent( new CustomEvent('extension-drag-started')); }, /** @override */ doDragLeave: function() { this.fireDragEnded_(); chrome.send('stopDrag'); }, /** @override */ doDragOver: function(e) { e.preventDefault(); }, /** @override */ doDrop: function(e) { this.fireDragEnded_(); if (e.dataTransfer.files.length != 1) return; var toSend = ''; // Files lack a check if they're a directory, but we can find out through // its item entry. for (var i = 0; i < e.dataTransfer.items.length; ++i) { if (e.dataTransfer.items[i].kind == 'file' && e.dataTransfer.items[i].webkitGetAsEntry().isDirectory) { toSend = 'installDroppedDirectory'; break; } } // Only process files that look like extensions. Other files should // navigate the browser normally. if (!toSend && /\.(crx|user\.js|zip)$/i.test(e.dataTransfer.files[0].name)) { toSend = 'installDroppedFile'; } if (toSend) { e.preventDefault(); chrome.send(toSend); } }, /** @private */ fireDragEnded_: function() { this.eventTarget_.dispatchEvent(new CustomEvent('extension-drag-ended')); } }; return { DragAndDropHandler: DragAndDropHandler, }; }); // // Copyright 2014 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. /** * @typedef {{afterHighlight: string, * beforeHighlight: string, * highlight: string, * title: string}} */ var ExtensionHighlight; cr.define('extensions', function() { 'use strict'; /** * ExtensionCode is an element which displays code in a styled div, and is * designed to highlight errors. * @constructor * @extends {HTMLDivElement} */ function ExtensionCode(div) { div.__proto__ = ExtensionCode.prototype; return div; } ExtensionCode.prototype = { __proto__: HTMLDivElement.prototype, /** * Populate the content area of the code div with the given code. This will * highlight the erroneous section (if any). * @param {?ExtensionHighlight} code The 'highlight' strings represent the * three portions of the file's content to display - the portion which * is most relevant and should be emphasized (highlight), and the parts * both before and after this portion. The title is the error message, * which will be the mouseover hint for the highlighted region. These * may be empty. * @param {string} emptyMessage The message to display if the code * object is empty (e.g., 'could not load code'). */ populate: function(code, emptyMessage) { // Clear any remnant content, so we don't have multiple code listed. this.clear(); // If there's no code, then display an appropriate message. if (!code || (!code.highlight && !code.beforeHighlight && !code.afterHighlight)) { var span = document.createElement('span'); span.classList.add('extension-code-empty'); span.textContent = emptyMessage; this.appendChild(span); return; } var sourceDiv = document.createElement('div'); sourceDiv.classList.add('extension-code-source'); this.appendChild(sourceDiv); var lineCount = 0; var createSpan = function(source, isHighlighted) { lineCount += source.split('\n').length - 1; var span = document.createElement('span'); span.className = isHighlighted ? 'extension-code-highlighted-source' : 'extension-code-normal-source'; span.textContent = source; return span; }; if (code.beforeHighlight) sourceDiv.appendChild(createSpan(code.beforeHighlight, false)); if (code.highlight) { var highlightSpan = createSpan(code.highlight, true); highlightSpan.title = code.message; sourceDiv.appendChild(highlightSpan); } if (code.afterHighlight) sourceDiv.appendChild(createSpan(code.afterHighlight, false)); // Make the line numbers. This should be the number of line breaks + 1 // (the last line doesn't break, but should still be numbered). var content = ''; for (var i = 1; i < lineCount + 1; ++i) content += i + '\n'; var span = document.createElement('span'); span.textContent = content; var linesDiv = document.createElement('div'); linesDiv.classList.add('extension-code-line-numbers'); linesDiv.appendChild(span); this.insertBefore(linesDiv, this.firstChild); }, /** * Clears the content of the element. */ clear: function() { while (this.firstChild) this.removeChild(this.firstChild); }, /** * Scrolls to the error, if there is one. This cannot be called when the * div is hidden. */ scrollToError: function() { var errorSpan = this.querySelector('.extension-code-highlighted-source'); if (errorSpan) this.scrollTop = errorSpan.offsetTop - this.clientHeight / 2; } }; // Export return { ExtensionCode: ExtensionCode }; }); // // Copyright (c) 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. // // Copyright (c) 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. // // Copyright 2016 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. cr.define('extensions', function() { 'use strict'; /** * @enum {number} */ var Key = { Comma: 188, Del: 46, Down: 40, End: 35, Escape: 27, Home: 36, Ins: 45, Left: 37, MediaNextTrack: 176, MediaPlayPause: 179, MediaPrevTrack: 177, MediaStop: 178, PageDown: 34, PageUp: 33, Period: 190, Right: 39, Space: 32, Tab: 9, Up: 38, }; /** * Enum for whether we require modifiers of a keycode. * @enum {number} */ var ModifierPolicy = { NOT_ALLOWED: 0, REQUIRED: 1 }; /** * Gets the ModifierPolicy. Currently only "MediaNextTrack", "MediaPrevTrack", * "MediaStop", "MediaPlayPause" are required to be used without any modifier. * @param {number} keyCode * @return {ModifierPolicy} */ function getModifierPolicy(keyCode) { switch (keyCode) { case Key.MediaNextTrack: case Key.MediaPlayPause: case Key.MediaPrevTrack: case Key.MediaStop: return ModifierPolicy.NOT_ALLOWED; default: return ModifierPolicy.REQUIRED; } } /** * Returns whether the keyboard event has a key modifier, which could affect * how it's handled. * @param {!KeyboardEvent} e * @param {boolean} countShiftAsModifier Whether the 'Shift' key should be * counted as modifier. * @return {boolean} True if the event has any modifiers. */ function hasModifier(e, countShiftAsModifier) { return e.ctrlKey || e.altKey || // Meta key is only relevant on Mac and CrOS, where we treat Command // and Search (respectively) as modifiers. (cr.isMac && e.metaKey) || (cr.isChromeOS && e.metaKey) || (countShiftAsModifier && e.shiftKey); } /** * Checks whether the passed in |keyCode| is a valid extension command key. * @param {number} keyCode * @return {boolean} Whether the key is valid. */ function isValidKeyCode(keyCode) { if (keyCode == Key.Escape) return false; for (var k in Key) { if (Key[k] == keyCode) return true; } return (keyCode >= 'A'.charCodeAt(0) && keyCode <= 'Z'.charCodeAt(0)) || (keyCode >= '0'.charCodeAt(0) && keyCode <= '9'.charCodeAt(0)); } /** * Converts a keystroke event to string form, ignoring invalid extension * commands. * @param {!KeyboardEvent} e * @return {string} The keystroke as a string. */ function keystrokeToString(e) { var output = []; // TODO(devlin): Should this be i18n'd? if (cr.isMac && e.metaKey) output.push('Command'); if (cr.isChromeOS && e.metaKey) output.push('Search'); if (e.ctrlKey) output.push('Ctrl'); if (!e.ctrlKey && e.altKey) output.push('Alt'); if (e.shiftKey) output.push('Shift'); var keyCode = e.keyCode; if (isValidKeyCode(keyCode)) { if ((keyCode >= 'A'.charCodeAt(0) && keyCode <= 'Z'.charCodeAt(0)) || (keyCode >= '0'.charCodeAt(0) && keyCode <= '9'.charCodeAt(0))) { output.push(String.fromCharCode(keyCode)); } else { switch (keyCode) { case Key.Comma: output.push('Comma'); break; case Key.Del: output.push('Delete'); break; case Key.Down: output.push('Down'); break; case Key.End: output.push('End'); break; case Key.Home: output.push('Home'); break; case Key.Ins: output.push('Insert'); break; case Key.Left: output.push('Left'); break; case Key.MediaNextTrack: output.push('MediaNextTrack'); break; case Key.MediaPlayPause: output.push('MediaPlayPause'); break; case Key.MediaPrevTrack: output.push('MediaPrevTrack'); break; case Key.MediaStop: output.push('MediaStop'); break; case Key.PageDown: output.push('PageDown'); break; case Key.PageUp: output.push('PageUp'); break; case Key.Period: output.push('Period'); break; case Key.Right: output.push('Right'); break; case Key.Space: output.push('Space'); break; case Key.Tab: output.push('Tab'); break; case Key.Up: output.push('Up'); break; } } } return output.join('+'); } /** * Returns true if the event has valid modifiers. * @param {!KeyboardEvent} e The keyboard event to consider. * @return {boolean} True if the event is valid. */ function hasValidModifiers(e) { switch (getModifierPolicy(e.keyCode)) { case ModifierPolicy.REQUIRED: return hasModifier(e, false); case ModifierPolicy.NOT_ALLOWED: return !hasModifier(e, true); } assertNotReached(); } return { isValidKeyCode: isValidKeyCode, keystrokeToString: keystrokeToString, hasValidModifiers: hasValidModifiers, Key: Key, }; }); cr.define('extensions', function() { 'use strict'; /** * Creates a new list of extension commands. * @param {HTMLDivElement} div * @constructor * @extends {HTMLDivElement} */ function ExtensionCommandList(div) { div.__proto__ = ExtensionCommandList.prototype; return div; } ExtensionCommandList.prototype = { __proto__: HTMLDivElement.prototype, /** * While capturing, this records the current (last) keyboard event generated * by the user. Will be |null| after capture and during capture when no * keyboard event has been generated. * @type {KeyboardEvent}. * @private */ currentKeyEvent_: null, /** * While capturing, this keeps track of the previous selection so we can * revert back to if no valid assignment is made during capture. * @type {string}. * @private */ oldValue_: '', /** * While capturing, this keeps track of which element the user asked to * change. * @type {HTMLElement}. * @private */ capturingElement_: null, /** * Updates the extensions data for the overlay. * @param {!Array} data The extension * data. */ setData: function(data) { /** @private {!Array} */ this.data_ = data; this.textContent = ''; // Iterate over the extension data and add each item to the list. this.data_.forEach(this.createNodeForExtension_.bind(this)); }, /** * Synthesizes and initializes an HTML element for the extension command * metadata given in |extension|. * @param {chrome.developerPrivate.ExtensionInfo} extension A dictionary of * extension metadata. * @private */ createNodeForExtension_: function(extension) { if (extension.commands.length == 0 || extension.state == chrome.developerPrivate.ExtensionState.DISABLED) return; var template = $('template-collection-extension-commands').querySelector( '.extension-command-list-extension-item-wrapper'); var node = template.cloneNode(true); var title = node.querySelector('.extension-title'); title.textContent = extension.name; this.appendChild(node); // Iterate over the commands data within the extension and add each item // to the list. extension.commands.forEach( this.createNodeForCommand_.bind(this, extension.id)); }, /** * Synthesizes and initializes an HTML element for the extension command * metadata given in |command|. * @param {string} extensionId The associated extension's id. * @param {chrome.developerPrivate.Command} command A dictionary of * extension command metadata. * @private */ createNodeForCommand_: function(extensionId, command) { var template = $('template-collection-extension-commands').querySelector( '.extension-command-list-command-item-wrapper'); var node = template.cloneNode(true); node.id = this.createElementId_('command', extensionId, command.name); var description = node.querySelector('.command-description'); description.textContent = command.description; var shortcutNode = node.querySelector('.command-shortcut-text'); shortcutNode.addEventListener('mouseup', this.startCapture_.bind(this)); shortcutNode.addEventListener('focus', this.handleFocus_.bind(this)); shortcutNode.addEventListener('blur', this.handleBlur_.bind(this)); shortcutNode.addEventListener('keydown', this.handleKeyDown_.bind(this)); shortcutNode.addEventListener('keyup', this.handleKeyUp_.bind(this)); if (!command.isActive) { shortcutNode.textContent = loadTimeData.getString('extensionCommandsInactive'); var commandShortcut = node.querySelector('.command-shortcut'); commandShortcut.classList.add('inactive-keybinding'); } else { shortcutNode.textContent = command.keybinding; } var commandClear = node.querySelector('.command-clear'); commandClear.id = this.createElementId_( 'clear', extensionId, command.name); commandClear.title = loadTimeData.getString('extensionCommandsDelete'); commandClear.addEventListener('click', this.handleClear_.bind(this)); var select = node.querySelector('.command-scope'); select.id = this.createElementId_( 'setCommandScope', extensionId, command.name); select.hidden = false; // Add the 'In Chrome' option. var option = document.createElement('option'); option.textContent = loadTimeData.getString('extensionCommandsRegular'); select.appendChild(option); if (command.isExtensionAction || !command.isActive) { // Extension actions cannot be global, so we might as well disable the // combo box, to signify that, and if the command is inactive, it // doesn't make sense to allow the user to adjust the scope. select.disabled = true; } else { // Add the 'Global' option. option = document.createElement('option'); option.textContent = loadTimeData.getString('extensionCommandsGlobal'); select.appendChild(option); select.selectedIndex = command.scope == chrome.developerPrivate.CommandScope.GLOBAL ? 1 : 0; select.addEventListener( 'change', this.handleSetCommandScope_.bind(this)); } this.appendChild(node); }, /** * Starts keystroke capture to determine which key to use for a particular * extension command. * @param {Event} event The keyboard event to consider. * @private */ startCapture_: function(event) { if (this.capturingElement_) return; // Already capturing. chrome.developerPrivate.setShortcutHandlingSuspended(true); var shortcutNode = event.target; this.oldValue_ = shortcutNode.textContent; shortcutNode.textContent = loadTimeData.getString('extensionCommandsStartTyping'); shortcutNode.parentElement.classList.add('capturing'); var commandClear = shortcutNode.parentElement.querySelector('.command-clear'); commandClear.hidden = true; this.capturingElement_ = /** @type {HTMLElement} */(event.target); }, /** * Ends keystroke capture and either restores the old value or (if valid * value) sets the new value as active.. * @param {Event} event The keyboard event to consider. * @private */ endCapture_: function(event) { if (!this.capturingElement_) return; // Not capturing. chrome.developerPrivate.setShortcutHandlingSuspended(false); var shortcutNode = this.capturingElement_; var commandShortcut = shortcutNode.parentElement; commandShortcut.classList.remove('capturing'); commandShortcut.classList.remove('contains-chars'); // When the capture ends, the user may have not given a complete and valid // input (or even no input at all). Only a valid key event followed by a // valid key combination will cause a shortcut selection to be activated. // If no valid selection was made, however, revert back to what the // textbox had before to indicate that the shortcut registration was // canceled. if (!this.currentKeyEvent_ || !extensions.isValidKeyCode(this.currentKeyEvent_.keyCode)) shortcutNode.textContent = this.oldValue_; var commandClear = commandShortcut.querySelector('.command-clear'); if (this.oldValue_ == '') { commandShortcut.classList.remove('clearable'); commandClear.hidden = true; } else { commandShortcut.classList.add('clearable'); commandClear.hidden = false; } this.oldValue_ = ''; this.capturingElement_ = null; this.currentKeyEvent_ = null; }, /** * Handles focus event and adds visual indication for active shortcut. * @param {Event} event to consider. * @private */ handleFocus_: function(event) { var commandShortcut = event.target.parentElement; commandShortcut.classList.add('focused'); }, /** * Handles lost focus event and removes visual indication of active shortcut * also stops capturing on focus lost. * @param {Event} event to consider. * @private */ handleBlur_: function(event) { this.endCapture_(event); var commandShortcut = event.target.parentElement; commandShortcut.classList.remove('focused'); }, /** * The KeyDown handler. * @param {Event} event The keyboard event to consider. * @private */ handleKeyDown_: function(event) { event = /** @type {KeyboardEvent} */(event); if (event.keyCode == extensions.Key.Escape) { if (!this.capturingElement_) { // If we're not currently capturing, allow escape to propagate (so it // can close the overflow). return; } // Otherwise, escape cancels capturing. this.endCapture_(event); var parsed = this.parseElementId_('clear', event.target.parentElement.querySelector('.command-clear').id); chrome.developerPrivate.updateExtensionCommand({ extensionId: parsed.extensionId, commandName: parsed.commandName, keybinding: '' }); event.preventDefault(); event.stopPropagation(); return; } if (event.keyCode == extensions.Key.Tab) { // Allow tab propagation for keyboard navigation. return; } if (!this.capturingElement_) this.startCapture_(event); this.handleKey_(event); }, /** * The KeyUp handler. * @param {Event} event The keyboard event to consider. * @private */ handleKeyUp_: function(event) { event = /** @type {KeyboardEvent} */(event); if (event.keyCode == extensions.Key.Tab || event.keyCode == extensions.Key.Escape) { // We need to allow tab propagation for keyboard navigation, and escapes // are fully handled in handleKeyDown. return; } // We want to make it easy to change from Ctrl+Shift+ to just Ctrl+ by // releasing Shift, but we also don't want it to be easy to lose for // example Ctrl+Shift+F to Ctrl+ just because you didn't release Ctrl // as fast as the other two keys. Therefore, we process KeyUp until you // have a valid combination and then stop processing it (meaning that once // you have a valid combination, we won't change it until the next // KeyDown message arrives). if (!this.currentKeyEvent_ || !extensions.isValidKeyCode(this.currentKeyEvent_.keyCode)) { if (!event.ctrlKey && !event.altKey || ((cr.isMac || cr.isChromeOS) && !event.metaKey)) { // If neither Ctrl nor Alt is pressed then it is not a valid shortcut. // That means we're back at the starting point so we should restart // capture. this.endCapture_(event); this.startCapture_(event); } else { this.handleKey_(event); } } }, /** * A general key handler (used for both KeyDown and KeyUp). * @param {KeyboardEvent} event The keyboard event to consider. * @private */ handleKey_: function(event) { // While capturing, we prevent all events from bubbling, to prevent // shortcuts lacking the right modifier (F3 for example) from activating // and ending capture prematurely. event.preventDefault(); event.stopPropagation(); if (!extensions.hasValidModifiers(event)) return; var shortcutNode = this.capturingElement_; var keystroke = extensions.keystrokeToString(event); shortcutNode.textContent = keystroke; event.target.classList.add('contains-chars'); this.currentKeyEvent_ = event; if (extensions.isValidKeyCode(event.keyCode)) { var node = event.target; while (node && !node.id) node = node.parentElement; this.oldValue_ = keystroke; // Forget what the old value was. var parsed = this.parseElementId_('command', node.id); // Ending the capture must occur before calling // setExtensionCommandShortcut to ensure the shortcut is set. this.endCapture_(event); chrome.developerPrivate.updateExtensionCommand( {extensionId: parsed.extensionId, commandName: parsed.commandName, keybinding: keystroke}); } }, /** * A handler for the delete command button. * @param {Event} event The mouse event to consider. * @private */ handleClear_: function(event) { var parsed = this.parseElementId_('clear', event.target.id); chrome.developerPrivate.updateExtensionCommand( {extensionId: parsed.extensionId, commandName: parsed.commandName, keybinding: ''}); }, /** * A handler for the setting the scope of the command. * @param {Event} event The mouse event to consider. * @private */ handleSetCommandScope_: function(event) { var parsed = this.parseElementId_('setCommandScope', event.target.id); var element = document.getElementById( 'setCommandScope-' + parsed.extensionId + '-' + parsed.commandName); var scope = element.selectedIndex == 1 ? chrome.developerPrivate.CommandScope.GLOBAL : chrome.developerPrivate.CommandScope.CHROME; chrome.developerPrivate.updateExtensionCommand( {extensionId: parsed.extensionId, commandName: parsed.commandName, scope: scope}); }, /** * A utility function to create a unique element id based on a namespace, * extension id and a command name. * @param {string} namespace The namespace to prepend the id with. * @param {string} extensionId The extension ID to use in the id. * @param {string} commandName The command name to append the id with. * @private */ createElementId_: function(namespace, extensionId, commandName) { return namespace + '-' + extensionId + '-' + commandName; }, /** * A utility function to parse a unique element id based on a namespace, * extension id and a command name. * @param {string} namespace The namespace to prepend the id with. * @param {string} id The id to parse. * @return {{extensionId: string, commandName: string}} The parsed id. * @private */ parseElementId_: function(namespace, id) { var kExtensionIdLength = 32; return { extensionId: id.substring(namespace.length + 1, namespace.length + 1 + kExtensionIdLength), commandName: id.substring(namespace.length + 1 + kExtensionIdLength + 1) }; }, }; return { ExtensionCommandList: ExtensionCommandList }; }); cr.define('extensions', function() { 'use strict'; // The Extension Commands list object that will be used to show the commands // on the page. var ExtensionCommandList = extensions.ExtensionCommandList; /** * ExtensionCommandsOverlay class * Encapsulated handling of the 'Extension Commands' overlay page. * @constructor */ function ExtensionCommandsOverlay() { } cr.addSingletonGetter(ExtensionCommandsOverlay); ExtensionCommandsOverlay.prototype = { /** * Initialize the page. */ initializePage: function() { var overlay = $('overlay'); cr.ui.overlay.setupOverlay(overlay); cr.ui.overlay.globalInitialization(); overlay.addEventListener('cancelOverlay', this.handleDismiss_.bind(this)); this.extensionCommandList_ = new ExtensionCommandList( /**@type {HTMLDivElement} */($('extension-command-list'))); $('extension-commands-dismiss').addEventListener('click', function() { cr.dispatchSimpleEvent(overlay, 'cancelOverlay'); }); // The ExtensionList will update us with its data, so we don't need to // handle that here. }, /** * Handles a click on the dismiss button. * @param {Event} e The click event. */ handleDismiss_: function(e) { extensions.ExtensionSettings.showOverlay(null); }, }; /** * Called by the dom_ui_ to re-populate the page with data representing * the current state of extension commands. * @param {!Array} extensionsData */ ExtensionCommandsOverlay.updateExtensionsData = function(extensionsData) { var overlay = ExtensionCommandsOverlay.getInstance(); overlay.extensionCommandList_.setData(extensionsData); var hasAnyCommands = false; for (var i = 0; i < extensionsData.length; ++i) { if (extensionsData[i].commands.length > 0) { hasAnyCommands = true; break; } } // Make sure the config link is visible, since there are commands to show // and potentially configure. document.querySelector('.extension-commands-config').hidden = !hasAnyCommands; $('no-commands').hidden = hasAnyCommands; overlay.extensionCommandList_.classList.toggle( 'empty-extension-commands-list', !hasAnyCommands); }; // Export return { ExtensionCommandsOverlay: ExtensionCommandsOverlay }; }); // // Copyright 2013 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. /** @typedef {chrome.developerPrivate.RuntimeError} */ var RuntimeError; /** @typedef {chrome.developerPrivate.ManifestError} */ var ManifestError; cr.define('extensions', function() { 'use strict'; /** * Clear all the content of a given element. * @param {HTMLElement} element The element to be cleared. */ function clearElement(element) { while (element.firstChild) element.removeChild(element.firstChild); } /** * Get the url relative to the main extension url. If the url is * unassociated with the extension, this will be the full url. * @param {string} url The url to make relative. * @param {string} extensionUrl The url for the extension resources, in the * form "chrome-etxension:///". * @return {string} The url relative to the host. */ function getRelativeUrl(url, extensionUrl) { return url.substring(0, extensionUrl.length) == extensionUrl ? url.substring(extensionUrl.length) : url; } /** * The RuntimeErrorContent manages all content specifically associated with * runtime errors; this includes stack frames and the context url. * @constructor * @extends {HTMLDivElement} */ function RuntimeErrorContent() { var contentArea = $('template-collection-extension-error-overlay'). querySelector('.extension-error-overlay-runtime-content'). cloneNode(true); contentArea.__proto__ = RuntimeErrorContent.prototype; contentArea.init(); return contentArea; } /** * The name of the "active" class specific to extension errors (so as to * not conflict with other rules). * @type {string} * @const */ RuntimeErrorContent.ACTIVE_CLASS_NAME = 'extension-error-active'; /** * Determine whether or not we should display the url to the user. We don't * want to include any of our own code in stack traces. * @param {string} url The url in question. * @return {boolean} True if the url should be displayed, and false * otherwise (i.e., if it is an internal script). */ RuntimeErrorContent.shouldDisplayForUrl = function(url) { // All our internal scripts are in the 'extensions::' namespace. return !/^extensions::/.test(url); }; RuntimeErrorContent.prototype = { __proto__: HTMLDivElement.prototype, /** * The underlying error whose details are being displayed. * @type {?(RuntimeError|ManifestError)} * @private */ error_: null, /** * The URL associated with this extension, i.e. chrome-extension:///. * @type {?string} * @private */ extensionUrl_: null, /** * The node of the stack trace which is currently active. * @type {?HTMLElement} * @private */ currentFrameNode_: null, /** * Initialize the RuntimeErrorContent for the first time. */ init: function() { /** * The stack trace element in the overlay. * @type {HTMLElement} * @private */ this.stackTrace_ = /** @type {HTMLElement} */( this.querySelector('.extension-error-overlay-stack-trace-list')); assert(this.stackTrace_); /** * The context URL element in the overlay. * @type {HTMLElement} * @private */ this.contextUrl_ = /** @type {HTMLElement} */( this.querySelector('.extension-error-overlay-context-url')); assert(this.contextUrl_); }, /** * Sets the error for the content. * @param {(RuntimeError|ManifestError)} error The error whose content * should be displayed. * @param {string} extensionUrl The URL associated with this extension. */ setError: function(error, extensionUrl) { this.clearError(); this.error_ = error; this.extensionUrl_ = extensionUrl; this.contextUrl_.textContent = error.contextUrl ? getRelativeUrl(error.contextUrl, this.extensionUrl_) : loadTimeData.getString('extensionErrorOverlayContextUnknown'); this.initStackTrace_(); }, /** * Wipe content associated with a specific error. */ clearError: function() { this.error_ = null; this.extensionUrl_ = null; this.currentFrameNode_ = null; clearElement(this.stackTrace_); this.stackTrace_.hidden = true; }, /** * Makes |frame| active and deactivates the previously active frame (if * there was one). * @param {HTMLElement} frameNode The frame to activate. * @private */ setActiveFrame_: function(frameNode) { if (this.currentFrameNode_) { this.currentFrameNode_.classList.remove( RuntimeErrorContent.ACTIVE_CLASS_NAME); } this.currentFrameNode_ = frameNode; this.currentFrameNode_.classList.add( RuntimeErrorContent.ACTIVE_CLASS_NAME); }, /** * Initialize the stack trace element of the overlay. * @private */ initStackTrace_: function() { for (var i = 0; i < this.error_.stackTrace.length; ++i) { var frame = this.error_.stackTrace[i]; // Don't include any internal calls (e.g., schemaBindings) in the // stack trace. if (!RuntimeErrorContent.shouldDisplayForUrl(frame.url)) continue; var frameNode = document.createElement('li'); // Attach the index of the frame to which this node refers (since we // may skip some, this isn't a 1-to-1 match). frameNode.indexIntoTrace = i; // The description is a human-readable summation of the frame, in the // form ": (function)", e.g. // "myfile.js:25 (myFunction)". var description = getRelativeUrl(frame.url, assert(this.extensionUrl_)) + ':' + frame.lineNumber; if (frame.functionName) { var functionName = frame.functionName == '(anonymous function)' ? loadTimeData.getString('extensionErrorOverlayAnonymousFunction') : frame.functionName; description += ' (' + functionName + ')'; } frameNode.textContent = description; // When the user clicks on a frame in the stack trace, we should // highlight that overlay in the list, display the appropriate source // code with the line highlighted, and link the "Open DevTools" button // with that frame. frameNode.addEventListener('click', function(frame, frameNode, e) { this.setActiveFrame_(frameNode); // Request the file source with the section highlighted. extensions.ExtensionErrorOverlay.getInstance().requestFileSource( {extensionId: this.error_.extensionId, message: this.error_.message, pathSuffix: getRelativeUrl(frame.url, assert(this.extensionUrl_)), lineNumber: frame.lineNumber}); }.bind(this, frame, frameNode)); this.stackTrace_.appendChild(frameNode); } // Set the current stack frame to the first stack frame and show the // trace, if one exists. (We can't just check error.stackTrace, because // it's possible the trace was purely internal, and we don't show // internal frames.) if (this.stackTrace_.children.length > 0) { this.stackTrace_.hidden = false; this.setActiveFrame_(assertInstanceof(this.stackTrace_.firstChild, HTMLElement)); } }, /** * Open the developer tools for the active stack frame. */ openDevtools: function() { var stackFrame = this.error_.stackTrace[this.currentFrameNode_.indexIntoTrace]; chrome.developerPrivate.openDevTools( {renderProcessId: this.error_.renderProcessId || -1, renderViewId: this.error_.renderViewId || -1, url: stackFrame.url, lineNumber: stackFrame.lineNumber || 0, columnNumber: stackFrame.columnNumber || 0}); } }; /** * The ExtensionErrorOverlay will show the contents of a file which pertains * to the ExtensionError; this is either the manifest file (for manifest * errors) or a source file (for runtime errors). If possible, the portion * of the file which caused the error will be highlighted. * @constructor */ function ExtensionErrorOverlay() { /** * The content section for runtime errors; this is re-used for all * runtime errors and attached/detached from the overlay as needed. * @type {RuntimeErrorContent} * @private */ this.runtimeErrorContent_ = new RuntimeErrorContent(); } /** * The manifest filename. * @type {string} * @const * @private */ ExtensionErrorOverlay.MANIFEST_FILENAME_ = 'manifest.json'; /** * Determine whether or not chrome can load the source for a given file; this * can only be done if the file belongs to the extension. * @param {string} file The file to load. * @param {string} extensionUrl The url for the extension, in the form * chrome-extension:///. * @return {boolean} True if the file can be loaded, false otherwise. * @private */ ExtensionErrorOverlay.canLoadFileSource = function(file, extensionUrl) { return file.substr(0, extensionUrl.length) == extensionUrl || file.toLowerCase() == ExtensionErrorOverlay.MANIFEST_FILENAME_; }; cr.addSingletonGetter(ExtensionErrorOverlay); ExtensionErrorOverlay.prototype = { /** * The underlying error whose details are being displayed. * @type {?(RuntimeError|ManifestError)} * @private */ selectedError_: null, /** * Initialize the page. * @param {function(HTMLDivElement)} showOverlay The function to show or * hide the ExtensionErrorOverlay; this should take a single parameter * which is either the overlay Div if the overlay should be displayed, * or null if the overlay should be hidden. */ initializePage: function(showOverlay) { var overlay = $('overlay'); cr.ui.overlay.setupOverlay(overlay); cr.ui.overlay.globalInitialization(); overlay.addEventListener('cancelOverlay', this.handleDismiss_.bind(this)); $('extension-error-overlay-dismiss').addEventListener('click', function() { cr.dispatchSimpleEvent(overlay, 'cancelOverlay'); }); /** * The element of the full overlay. * @type {HTMLDivElement} * @private */ this.overlayDiv_ = /** @type {HTMLDivElement} */( $('extension-error-overlay')); /** * The portion of the overlay which shows the code relating to the error * and the corresponding line numbers. * @type {extensions.ExtensionCode} * @private */ this.codeDiv_ = new extensions.ExtensionCode($('extension-error-overlay-code')); /** * The function to show or hide the ExtensionErrorOverlay. * @param {boolean} isVisible Whether the overlay should be visible. */ this.setVisible = function(isVisible) { showOverlay(isVisible ? this.overlayDiv_ : null); if (isVisible) this.codeDiv_.scrollToError(); }; /** * The button to open the developer tools (only available for runtime * errors). * @type {HTMLButtonElement} * @private */ this.openDevtoolsButton_ = /** @type {HTMLButtonElement} */( $('extension-error-overlay-devtools-button')); this.openDevtoolsButton_.addEventListener('click', function() { this.runtimeErrorContent_.openDevtools(); }.bind(this)); }, /** * Handles a click on the dismiss ("OK" or close) buttons. * @param {Event} e The click event. * @private */ handleDismiss_: function(e) { this.setVisible(false); // There's a chance that the overlay receives multiple dismiss events; in // this case, handle it gracefully and return (since all necessary work // will already have been done). if (!this.selectedError_) return; // Remove all previous content. this.codeDiv_.clear(); this.overlayDiv_.querySelector('.extension-error-list').onRemoved(); this.clearRuntimeContent_(); this.selectedError_ = null; }, /** * Clears the current content. * @private */ clearRuntimeContent_: function() { if (this.runtimeErrorContent_.parentNode) { this.runtimeErrorContent_.parentNode.removeChild( this.runtimeErrorContent_); this.runtimeErrorContent_.clearError(); } this.openDevtoolsButton_.hidden = true; }, /** * Sets the active error for the overlay. * @param {?(ManifestError|RuntimeError)} error The error to make active. * @private */ setActiveError_: function(error) { this.selectedError_ = error; // If there is no error (this can happen if, e.g., the user deleted all // the errors), then clear the content. if (!error) { this.codeDiv_.populate( null, loadTimeData.getString('extensionErrorNoErrorsCodeMessage')); this.clearRuntimeContent_(); return; } var extensionUrl = 'chrome-extension://' + error.extensionId + '/'; // Set or hide runtime content. if (error.type == chrome.developerPrivate.ErrorType.RUNTIME) { this.runtimeErrorContent_.setError(error, extensionUrl); this.overlayDiv_.querySelector('.content-area').insertBefore( this.runtimeErrorContent_, this.codeDiv_.nextSibling); this.openDevtoolsButton_.hidden = false; this.openDevtoolsButton_.disabled = !error.canInspect; } else { this.clearRuntimeContent_(); } // Read the file source to populate the code section, or set it to null if // the file is unreadable. if (ExtensionErrorOverlay.canLoadFileSource(error.source, extensionUrl)) { // Use pathname instead of relativeUrl. var requestFileSourceArgs = {extensionId: error.extensionId, message: error.message}; switch (error.type) { case chrome.developerPrivate.ErrorType.MANIFEST: requestFileSourceArgs.pathSuffix = error.source; requestFileSourceArgs.manifestKey = error.manifestKey; requestFileSourceArgs.manifestSpecific = error.manifestSpecific; break; case chrome.developerPrivate.ErrorType.RUNTIME: // slice(1) because pathname starts with a /. var pathname = new URL(error.source).pathname.slice(1); requestFileSourceArgs.pathSuffix = pathname; requestFileSourceArgs.lineNumber = error.stackTrace && error.stackTrace[0] ? error.stackTrace[0].lineNumber : 0; break; default: assertNotReached(); } this.requestFileSource(requestFileSourceArgs); } else { this.onFileSourceResponse_(null); } }, /** * Associate an error with the overlay. This will set the error for the * overlay, and, if possible, will populate the code section of the overlay * with the relevant file, load the stack trace, and generate links for * opening devtools (the latter two only happen for runtime errors). * @param {Array<(RuntimeError|ManifestError)>} errors The error to show in * the overlay. * @param {string} extensionId The id of the extension. * @param {string} extensionName The name of the extension. */ setErrorsAndShowOverlay: function(errors, extensionId, extensionName) { document.querySelector( '#extension-error-overlay .extension-error-overlay-title'). textContent = extensionName; var errorsDiv = this.overlayDiv_.querySelector('.extension-error-list'); var extensionErrors = new extensions.ExtensionErrorList(errors, extensionId); errorsDiv.parentNode.replaceChild(extensionErrors, errorsDiv); extensionErrors.addEventListener('activeExtensionErrorChanged', function(e) { this.setActiveError_(e.detail); }.bind(this)); if (errors.length > 0) this.setActiveError_(errors[0]); this.setVisible(true); }, /** * Requests a file's source. * @param {chrome.developerPrivate.RequestFileSourceProperties} args The * arguments for the call. */ requestFileSource: function(args) { chrome.developerPrivate.requestFileSource( args, this.onFileSourceResponse_.bind(this)); }, /** * Set the code to be displayed in the code portion of the overlay. * @see ExtensionErrorOverlay.requestFileSourceResponse(). * @param {?chrome.developerPrivate.RequestFileSourceResponse} response The * response from the request file source call, which will be shown as * code. If |response| is null, then a "Could not display code" message * will be displayed instead. */ onFileSourceResponse_: function(response) { this.codeDiv_.populate( response, // ExtensionCode can handle a null response. loadTimeData.getString('extensionErrorOverlayNoCodeToDisplay')); this.setVisible(true); }, }; // Export return { ExtensionErrorOverlay: ExtensionErrorOverlay }; }); // // Copyright (c) 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. cr.define('extensions', function() { /** * @constructor * @extends {cr.ui.FocusManager} */ function ExtensionFocusManager() {} cr.addSingletonGetter(ExtensionFocusManager); ExtensionFocusManager.prototype = { __proto__: cr.ui.FocusManager.prototype, /** @override */ getFocusParent: function() { var overlay = extensions.ExtensionSettings.getCurrentOverlay(); return overlay || $('extension-settings'); }, }; return { ExtensionFocusManager: ExtensionFocusManager, }; }); // // Copyright 2015 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. cr.define('extensions', function() { /** * @param {!Element} root * @param {?Element} boundary * @constructor * @extends {cr.ui.FocusRow} */ function FocusRow(root, boundary) { cr.ui.FocusRow.call(this, root, boundary); } FocusRow.prototype = { __proto__: cr.ui.FocusRow.prototype, /** @override */ makeActive: function(active) { cr.ui.FocusRow.prototype.makeActive.call(this, active); // Only highlight if the row has focus. this.root.classList.toggle('extension-highlight', active && this.root.contains(document.activeElement)); }, }; return {FocusRow: FocusRow}; }); // // Copyright (c) 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. // // Copyright 2013 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. cr.define('extensions', function() { 'use strict'; /** * Clone a template within the extension error template collection. * @param {string} templateName The class name of the template to clone. * @return {HTMLElement} The clone of the template. */ function cloneTemplate(templateName) { return /** @type {HTMLElement} */($('template-collection-extension-error'). querySelector('.' + templateName).cloneNode(true)); } /** * Checks that an Extension ID follows the proper format (i.e., is 32 * characters long, is lowercase, and contains letters in the range [a, p]). * @param {string} id The Extension ID to test. * @return {boolean} Whether or not the ID is valid. */ function idIsValid(id) { return /^[a-p]{32}$/.test(id); } /** * @param {!Array<(ManifestError|RuntimeError)>} errors * @param {number} id * @return {number} The index of the error with |id|, or -1 if not found. */ function findErrorById(errors, id) { for (var i = 0; i < errors.length; ++i) { if (errors[i].id == id) return i; } return -1; } /** * Creates a new ExtensionError HTMLElement; this is used to show a * notification to the user when an error is caused by an extension. * @param {(RuntimeError|ManifestError)} error The error the element should * represent. * @constructor * @extends {HTMLElement} */ function ExtensionError(error) { var div = cloneTemplate('extension-error-metadata'); div.__proto__ = ExtensionError.prototype; div.decorate(error); return div; } ExtensionError.prototype = { __proto__: HTMLElement.prototype, /** * @param {(RuntimeError|ManifestError)} error The error the element should * represent. * @private */ decorate: function(error) { /** * The backing error. * @type {(ManifestError|RuntimeError)} */ this.error = error; var iconAltTextKey = 'extensionLogLevelWarn'; // Add an additional class for the severity level. if (error.type == chrome.developerPrivate.ErrorType.RUNTIME) { switch (error.severity) { case chrome.developerPrivate.ErrorLevel.LOG: this.classList.add('extension-error-severity-info'); iconAltTextKey = 'extensionLogLevelInfo'; break; case chrome.developerPrivate.ErrorLevel.WARN: this.classList.add('extension-error-severity-warning'); break; case chrome.developerPrivate.ErrorLevel.ERROR: this.classList.add('extension-error-severity-fatal'); iconAltTextKey = 'extensionLogLevelError'; break; default: assertNotReached(); } } else { // We classify manifest errors as "warnings". this.classList.add('extension-error-severity-warning'); } var iconNode = document.createElement('img'); iconNode.className = 'extension-error-icon'; iconNode.alt = loadTimeData.getString(iconAltTextKey); this.insertBefore(iconNode, this.firstChild); var messageSpan = this.querySelector('.extension-error-message'); messageSpan.textContent = error.message; var deleteButton = this.querySelector('.error-delete-button'); deleteButton.addEventListener('click', function(e) { this.dispatchEvent( new CustomEvent('deleteExtensionError', {bubbles: true, detail: this.error})); }.bind(this)); this.addEventListener('click', function(e) { if (e.target != deleteButton) this.requestActive_(); }.bind(this)); this.addEventListener('keydown', function(e) { if (e.key == 'Enter' && e.target != deleteButton) this.requestActive_(); }); }, /** * Bubble up an event to request to become active. * @private */ requestActive_: function() { this.dispatchEvent( new CustomEvent('highlightExtensionError', {bubbles: true, detail: this.error})); }, }; /** * A variable length list of runtime or manifest errors for a given extension. * @param {Array<(RuntimeError|ManifestError)>} errors The list of extension * errors with which to populate the list. * @param {string} extensionId The id of the extension. * @constructor * @extends {HTMLDivElement} */ function ExtensionErrorList(errors, extensionId) { var div = cloneTemplate('extension-error-list'); div.__proto__ = ExtensionErrorList.prototype; div.extensionId_ = extensionId; div.decorate(errors); return div; } /** * @param {!Element} root * @param {?Element} boundary * @constructor * @extends {cr.ui.FocusRow} */ ExtensionErrorList.FocusRow = function(root, boundary) { cr.ui.FocusRow.call(this, root, boundary); this.addItem('message', '.extension-error-message'); this.addItem('delete', '.error-delete-button'); }; ExtensionErrorList.FocusRow.prototype = { __proto__: cr.ui.FocusRow.prototype, }; ExtensionErrorList.prototype = { __proto__: HTMLDivElement.prototype, /** * Initializes the extension error list. * @param {Array<(RuntimeError|ManifestError)>} errors The list of errors. */ decorate: function(errors) { /** @private {!Array<(ManifestError|RuntimeError)>} */ this.errors_ = []; /** @private {!cr.ui.FocusGrid} */ this.focusGrid_ = new cr.ui.FocusGrid(); /** @private {Element} */ this.listContents_ = this.querySelector('.extension-error-list-contents'); errors.forEach(this.addError_, this); this.focusGrid_.ensureRowActive(); this.addEventListener('highlightExtensionError', function(e) { this.setActiveErrorNode_(e.target); }); this.addEventListener('deleteExtensionError', function(e) { this.removeError_(e.detail); }); this.querySelector('#extension-error-list-clear').addEventListener( 'click', function(e) { this.clear(true); }.bind(this)); /** * The callback for the extension changed event. * @private {function(chrome.developerPrivate.EventData):void} */ this.onItemStateChangedListener_ = function(data) { var type = chrome.developerPrivate.EventType; if ((data.event_type == type.ERRORS_REMOVED || data.event_type == type.ERROR_ADDED) && data.extensionInfo.id == this.extensionId_) { var newErrors = data.extensionInfo.runtimeErrors.concat( data.extensionInfo.manifestErrors); this.updateErrors_(newErrors); } }.bind(this); chrome.developerPrivate.onItemStateChanged.addListener( this.onItemStateChangedListener_); /** * The active error element in the list. * @private {?} */ this.activeError_ = null; this.setActiveError(0); }, /** * Adds an error to the list. * @param {(RuntimeError|ManifestError)} error The error to add. * @private */ addError_: function(error) { this.querySelector('#no-errors-span').hidden = true; this.errors_.push(error); var extensionError = new ExtensionError(error); this.listContents_.appendChild(extensionError); this.focusGrid_.addRow( new ExtensionErrorList.FocusRow(extensionError, this.listContents_)); }, /** * Removes an error from the list. * @param {(RuntimeError|ManifestError)} error The error to remove. * @private */ removeError_: function(error) { var index = 0; for (; index < this.errors_.length; ++index) { if (this.errors_[index].id == error.id) break; } assert(index != this.errors_.length); var errorList = this.querySelector('.extension-error-list-contents'); var wasActive = this.activeError_ && this.activeError_.error.id == error.id; this.errors_.splice(index, 1); var listElement = errorList.children[index]; var focusRow = this.focusGrid_.getRowForRoot(listElement); this.focusGrid_.removeRow(focusRow); this.focusGrid_.ensureRowActive(); focusRow.destroy(); // TODO(dbeam): in a world where this UI is actually used, we should // probably move the focus before removing |listElement|. listElement.parentNode.removeChild(listElement); if (wasActive) { index = Math.min(index, this.errors_.length - 1); this.setActiveError(index); // Gracefully handles the -1 case. } chrome.developerPrivate.deleteExtensionErrors({ extensionId: error.extensionId, errorIds: [error.id] }); if (this.errors_.length == 0) this.querySelector('#no-errors-span').hidden = false; }, /** * Updates the list of errors. * @param {!Array<(ManifestError|RuntimeError)>} newErrors The new list of * errors. * @private */ updateErrors_: function(newErrors) { this.errors_.forEach(function(error) { if (findErrorById(newErrors, error.id) == -1) this.removeError_(error); }, this); newErrors.forEach(function(error) { var index = findErrorById(this.errors_, error.id); if (index == -1) this.addError_(error); else this.errors_[index] = error; // Update the existing reference. }, this); }, /** * Called when the list is being removed. */ onRemoved: function() { chrome.developerPrivate.onItemStateChanged.removeListener( this.onItemStateChangedListener_); this.clear(false); }, /** * Sets the active error in the list. * @param {number} index The index to set to be active. */ setActiveError: function(index) { var errorList = this.querySelector('.extension-error-list-contents'); var item = errorList.children[index]; this.setActiveErrorNode_( item ? item.querySelector('.extension-error-metadata') : null); var node = null; if (index >= 0 && index < errorList.children.length) { node = errorList.children[index].querySelector( '.extension-error-metadata'); } this.setActiveErrorNode_(node); }, /** * Clears the list of all errors. * @param {boolean} deleteErrors Whether or not the errors should be deleted * on the backend. */ clear: function(deleteErrors) { if (this.errors_.length == 0) return; if (deleteErrors) { var ids = this.errors_.map(function(error) { return error.id; }); chrome.developerPrivate.deleteExtensionErrors({ extensionId: this.extensionId_, errorIds: ids }); } this.setActiveErrorNode_(null); this.errors_.length = 0; var errorList = this.querySelector('.extension-error-list-contents'); while (errorList.firstChild) errorList.removeChild(errorList.firstChild); }, /** * Sets the active error in the list. * @param {?} node The error to make active. * @private */ setActiveErrorNode_: function(node) { if (this.activeError_) this.activeError_.classList.remove('extension-error-active'); if (node) node.classList.add('extension-error-active'); this.activeError_ = node; this.dispatchEvent( new CustomEvent('activeExtensionErrorChanged', {bubbles: true, detail: node ? node.error : null})); }, }; return { ExtensionErrorList: ExtensionErrorList }; }); cr.define('extensions', function() { 'use strict'; var ExtensionType = chrome.developerPrivate.ExtensionType; /** * @param {string} name The name of the template to clone. * @return {!Element} The freshly cloned template. */ function cloneTemplate(name) { var node = $('templates').querySelector('.' + name).cloneNode(true); return assertInstanceof(node, Element); } /** * @extends {HTMLElement} * @constructor */ function ExtensionWrapper() { var wrapper = cloneTemplate('extension-list-item-wrapper'); wrapper.__proto__ = ExtensionWrapper.prototype; wrapper.initialize(); return wrapper; } ExtensionWrapper.prototype = { __proto__: HTMLElement.prototype, initialize: function() { var boundary = $('extension-settings-list'); /** @private {!extensions.FocusRow} */ this.focusRow_ = new extensions.FocusRow(this, boundary); }, /** @return {!cr.ui.FocusRow} */ getFocusRow: function() { return this.focusRow_; }, /** * Add an item to the focus row and listen for |eventType| events. * @param {string} focusType A tag used to identify equivalent elements when * changing focus between rows. * @param {string} query A query to select the element to set up. * @param {string=} opt_eventType The type of event to listen to. * @param {function(Event)=} opt_handler The function that should be called * by the event. * @private */ setupColumn: function(focusType, query, opt_eventType, opt_handler) { assert(this.focusRow_.addItem(focusType, query)); if (opt_eventType) { assert(opt_handler); this.querySelector(query).addEventListener(opt_eventType, opt_handler); } }, }; var ExtensionCommandsOverlay = extensions.ExtensionCommandsOverlay; /** * Compares two extensions for the order they should appear in the list. * @param {chrome.developerPrivate.ExtensionInfo} a The first extension. * @param {chrome.developerPrivate.ExtensionInfo} b The second extension. * returns {number} -1 if A comes before B, 1 if A comes after B, 0 if equal. */ function compareExtensions(a, b) { function compare(x, y) { return x < y ? -1 : (x > y ? 1 : 0); } function compareLocation(x, y) { if (x.location == y.location) return 0; if (x.location == chrome.developerPrivate.Location.UNPACKED) return -1; if (y.location == chrome.developerPrivate.Location.UNPACKED) return 1; return 0; } return compareLocation(a, b) || compare(a.name.toLowerCase(), b.name.toLowerCase()) || compare(a.id, b.id); } /** @interface */ function ExtensionListDelegate() {} ExtensionListDelegate.prototype = { /** * Called when the number of extensions in the list has changed. */ onExtensionCountChanged: assertNotReached, }; /** * Creates a new list of extensions. * @param {extensions.ExtensionListDelegate} delegate * @constructor * @extends {HTMLDivElement} */ function ExtensionList(delegate) { var div = document.createElement('div'); div.__proto__ = ExtensionList.prototype; div.initialize(delegate); return div; } ExtensionList.prototype = { __proto__: HTMLDivElement.prototype, /** * Indicates whether an embedded options page that was navigated to through * the '?options=' URL query has been shown to the user. This is necessary * to prevent showExtensionNodes_ from opening the options more than once. * @type {boolean} * @private */ optionsShown_: false, /** @private {!cr.ui.FocusGrid} */ focusGrid_: new cr.ui.FocusGrid(), /** * Indicates whether an uninstall dialog is being shown to prevent multiple * dialogs from being displayed. * @private {boolean} */ uninstallIsShowing_: false, /** * Indicates whether a permissions prompt is showing. * @private {boolean} */ permissionsPromptIsShowing_: false, /** * Whether or not any initial navigation (like scrolling to an extension, * or opening an options page) has occurred. * @private {boolean} */ didInitialNavigation_: false, /** * Whether or not incognito mode is available. * @private {boolean} */ incognitoAvailable_: false, /** * Whether or not the app info dialog is enabled. * @private {boolean} */ enableAppInfoDialog_: false, /** * Initializes the list. * @param {!extensions.ExtensionListDelegate} delegate */ initialize: function(delegate) { /** @private {!Array} */ this.extensions_ = []; /** @private {!extensions.ExtensionListDelegate} */ this.delegate_ = delegate; this.resetLoadFinished(); chrome.developerPrivate.onItemStateChanged.addListener( function(eventData) { var EventType = chrome.developerPrivate.EventType; switch (eventData.event_type) { case EventType.VIEW_REGISTERED: case EventType.VIEW_UNREGISTERED: case EventType.INSTALLED: case EventType.LOADED: case EventType.UNLOADED: case EventType.ERROR_ADDED: case EventType.ERRORS_REMOVED: case EventType.PREFS_CHANGED: if (eventData.extensionInfo) { this.updateOrCreateWrapper_(eventData.extensionInfo); this.focusGrid_.ensureRowActive(); } break; case EventType.UNINSTALLED: var index = this.getIndexOfExtension_(eventData.item_id); this.extensions_.splice(index, 1); this.removeWrapper_(getRequiredElement(eventData.item_id)); break; default: assertNotReached(); } if (eventData.event_type == EventType.UNLOADED) this.hideEmbeddedExtensionOptions_(eventData.item_id); if (eventData.event_type == EventType.INSTALLED || eventData.event_type == EventType.UNINSTALLED) { this.delegate_.onExtensionCountChanged(); } if (eventData.event_type == EventType.LOADED || eventData.event_type == EventType.UNLOADED || eventData.event_type == EventType.PREFS_CHANGED || eventData.event_type == EventType.UNINSTALLED) { // We update the commands overlay whenever an extension is added or // removed (other updates wouldn't affect command-ly things). We // need both UNLOADED and UNINSTALLED since the UNLOADED event results // in an extension losing active keybindings, and UNINSTALLED can // result in the "Keyboard shortcuts" link being removed. ExtensionCommandsOverlay.updateExtensionsData(this.extensions_); } }.bind(this)); }, /** * Resets the |loadFinished| promise so that it can be used again; this * is useful if the page updates and tests need to wait for it to finish. */ resetLoadFinished: function() { /** * |loadFinished| should be used for testing purposes and will be * fulfilled when this list has finished loading the first time. * @type {Promise} * */ this.loadFinished = new Promise(function(resolve, reject) { /** @private {function(?)} */ this.resolveLoadFinished_ = resolve; }.bind(this)); }, /** * Updates the extensions on the page. * @param {boolean} incognitoAvailable Whether or not incognito is allowed. * @param {boolean} enableAppInfoDialog Whether or not the app info dialog * is enabled. * @return {Promise} A promise that is resolved once the extensions data is * fully updated. */ updateExtensionsData: function(incognitoAvailable, enableAppInfoDialog) { // If we start to need more information about the extension configuration, // consider passing in the full object from the ExtensionSettings. this.incognitoAvailable_ = incognitoAvailable; this.enableAppInfoDialog_ = enableAppInfoDialog; /** @private {Promise} */ this.extensionsUpdated_ = new Promise(function(resolve, reject) { chrome.developerPrivate.getExtensionsInfo( {includeDisabled: true, includeTerminated: true}, function(extensions) { // Sort in order of unpacked vs. packed, followed by name, followed by // id. extensions.sort(compareExtensions); this.extensions_ = extensions; this.showExtensionNodes_(); // We keep the commands overlay's extension info in sync, so that we // don't duplicate the same querying logic there. ExtensionCommandsOverlay.updateExtensionsData(this.extensions_); resolve(); // |resolve| is async so it's necessary to use |then| here in order to // do work after other |then|s have finished. This is important so // elements are visible when these updates happen. this.extensionsUpdated_.then(function() { this.onUpdateFinished_(); this.resolveLoadFinished_(); }.bind(this)); }.bind(this)); }.bind(this)); return this.extensionsUpdated_; }, /** * Updates elements that need to be visible in order to update properly. * @private */ onUpdateFinished_: function() { // Cannot focus or highlight a extension if there are none, and we should // only scroll to a particular extension or open the options page once. if (this.extensions_.length == 0 || this.didInitialNavigation_) return; this.didInitialNavigation_ = true; assert(!this.hidden); assert(!this.parentElement.hidden); var idToHighlight = this.getIdQueryParam_(); if (idToHighlight) { var wrapper = $(idToHighlight); if (wrapper) { this.scrollToWrapper_(idToHighlight); var focusRow = wrapper.getFocusRow(); (focusRow.getFirstFocusable('enabled') || focusRow.getFirstFocusable('remove-enterprise') || focusRow.getFirstFocusable('website') || focusRow.getFirstFocusable('details')).focus(); } } var idToOpenOptions = this.getOptionsQueryParam_(); if (idToOpenOptions && $(idToOpenOptions)) this.showEmbeddedExtensionOptions_(idToOpenOptions, true); }, /** @return {number} The number of extensions being displayed. */ getNumExtensions: function() { return this.extensions_.length; }, /** * @param {string} id The id of the extension. * @return {number} The index of the extension with the given id. * @private */ getIndexOfExtension_: function(id) { for (var i = 0; i < this.extensions_.length; ++i) { if (this.extensions_[i].id == id) return i; } return -1; }, getIdQueryParam_: function() { return parseQueryParams(document.location)['id']; }, getOptionsQueryParam_: function() { return parseQueryParams(document.location)['options']; }, /** * Creates or updates all extension items from scratch. * @private */ showExtensionNodes_: function() { // Any node that is not updated will be removed. var seenIds = []; // Iterate over the extension data and add each item to the list. this.extensions_.forEach(function(extension) { seenIds.push(extension.id); this.updateOrCreateWrapper_(extension); }, this); this.focusGrid_.ensureRowActive(); // Remove extensions that are no longer installed. var wrappers = document.querySelectorAll( '.extension-list-item-wrapper[id]'); Array.prototype.forEach.call(wrappers, function(wrapper) { if (seenIds.indexOf(wrapper.id) < 0) this.removeWrapper_(wrapper); }, this); }, /** * Removes the wrapper from the DOM and updates the focused element if * needed. * @param {!Element} wrapper * @private */ removeWrapper_: function(wrapper) { // If focus is in the wrapper about to be removed, move it first. This // happens when clicking the trash can to remove an extension. if (wrapper.contains(document.activeElement)) { var wrappers = document.querySelectorAll( '.extension-list-item-wrapper[id]'); var index = Array.prototype.indexOf.call(wrappers, wrapper); assert(index != -1); var focusableWrapper = wrappers[index + 1] || wrappers[index - 1]; if (focusableWrapper) { var newFocusRow = focusableWrapper.getFocusRow(); newFocusRow.getEquivalentElement(document.activeElement).focus(); } } var focusRow = wrapper.getFocusRow(); this.focusGrid_.removeRow(focusRow); this.focusGrid_.ensureRowActive(); focusRow.destroy(); wrapper.parentNode.removeChild(wrapper); }, /** * Scrolls the page down to the extension node with the given id. * @param {string} extensionId The id of the extension to scroll to. * @private */ scrollToWrapper_: function(extensionId) { // Scroll offset should be calculated slightly higher than the actual // offset of the element being scrolled to, so that it ends up not all // the way at the top. That way it is clear that there are more elements // above the element being scrolled to. var wrapper = $(extensionId); var scrollFudge = 1.2; var scrollTop = wrapper.offsetTop - scrollFudge * wrapper.clientHeight; setScrollTopForDocument(document, scrollTop); }, /** * Synthesizes and initializes an HTML element for the extension metadata * given in |extension|. * @param {!chrome.developerPrivate.ExtensionInfo} extension A dictionary * of extension metadata. * @param {?Element} nextWrapper The newly created wrapper will be inserted * before |nextWrapper| if non-null (else it will be appended to the * wrapper list). * @private */ createWrapper_: function(extension, nextWrapper) { var wrapper = new ExtensionWrapper; wrapper.id = extension.id; // The 'Permissions' link. wrapper.setupColumn('details', '.permissions-link', 'click', function(e) { if (!this.permissionsPromptIsShowing_) { chrome.developerPrivate.showPermissionsDialog(extension.id, function() { this.permissionsPromptIsShowing_ = false; }.bind(this)); this.permissionsPromptIsShowing_ = true; } e.preventDefault(); }); wrapper.setupColumn('options', '.options-button', 'click', function(e) { this.showEmbeddedExtensionOptions_(extension.id, false); e.preventDefault(); }.bind(this)); // The 'Options' button or link, depending on its behaviour. // Set an href to get the correct mouse-over appearance (link, // footer) - but the actual link opening is done through developerPrivate // API with a preventDefault(). wrapper.querySelector('.options-link').href = extension.optionsPage ? extension.optionsPage.url : ''; wrapper.setupColumn('options', '.options-link', 'click', function(e) { chrome.developerPrivate.showOptions(extension.id); e.preventDefault(); }); // The 'View in Web Store/View Web Site' link. wrapper.setupColumn('website', '.site-link'); // The 'Launch' link. wrapper.setupColumn('launch', '.launch-link', 'click', function(e) { chrome.management.launchApp(extension.id); }); // The 'Reload' link. wrapper.setupColumn('localReload', '.reload-link', 'click', function(e) { chrome.developerPrivate.reload(extension.id, {failQuietly: true}); }); wrapper.setupColumn('errors', '.errors-link', 'click', function(e) { var extensionId = extension.id; assert(this.extensions_.length > 0); var newEx = this.extensions_.filter(function(e) { return e.state == chrome.developerPrivate.ExtensionState.ENABLED && e.id == extensionId; })[0]; var errors = newEx.manifestErrors.concat(newEx.runtimeErrors); extensions.ExtensionErrorOverlay.getInstance().setErrorsAndShowOverlay( errors, extensionId, newEx.name); }.bind(this)); wrapper.setupColumn('suspiciousLearnMore', '.suspicious-install-message .learn-more-link'); // The path, if provided by unpacked extension. wrapper.setupColumn('loadPath', '.load-path a:first-of-type', 'click', function(e) { chrome.developerPrivate.showPath(extension.id); e.preventDefault(); }); // The 'Show Browser Action' button. wrapper.setupColumn('showButton', '.show-button', 'click', function(e) { chrome.developerPrivate.updateExtensionConfiguration({ extensionId: extension.id, showActionButton: true }); }); // The 'allow in incognito' checkbox. wrapper.setupColumn('incognito', '.incognito-control input', 'change', function(e) { var butterBar = wrapper.querySelector('.butter-bar'); var checked = e.target.checked; butterBar.hidden = !checked || extension.type == ExtensionType.HOSTED_APP; chrome.developerPrivate.updateExtensionConfiguration({ extensionId: extension.id, incognitoAccess: e.target.checked }); }.bind(this)); // The 'collect errors' checkbox. This should only be visible if the // error console is enabled - we can detect this by the existence of the // |errorCollectionEnabled| property. wrapper.setupColumn('collectErrors', '.error-collection-control input', 'change', function(e) { chrome.developerPrivate.updateExtensionConfiguration({ extensionId: extension.id, errorCollection: e.target.checked }); }); // The 'allow on all urls' checkbox. This should only be visible if // active script restrictions are enabled. If they are not enabled, no // extensions should want all urls. wrapper.setupColumn('allUrls', '.all-urls-control input', 'click', function(e) { chrome.developerPrivate.updateExtensionConfiguration({ extensionId: extension.id, runOnAllUrls: e.target.checked }); }); // The 'allow file:// access' checkbox. wrapper.setupColumn('localUrls', '.file-access-control input', 'click', function(e) { chrome.developerPrivate.updateExtensionConfiguration({ extensionId: extension.id, fileAccess: e.target.checked }); }); // The 'Reload' terminated link. wrapper.setupColumn('terminatedReload', '.terminated-reload-link', 'click', function(e) { chrome.developerPrivate.reload(extension.id, {failQuietly: true}); }); // The 'Repair' corrupted link. wrapper.setupColumn('repair', '.corrupted-repair-button', 'click', function(e) { chrome.developerPrivate.repairExtension(extension.id); }); // The 'Enabled' checkbox. wrapper.setupColumn('enabled', '.enable-checkbox input', 'click', function(e) { var checked = e.target.checked; // TODO(devlin): What should we do if this fails? Ideally we want to // show some kind of error or feedback to the user if this fails. chrome.management.setEnabled(extension.id, checked); // This may seem counter-intuitive (to not set/clear the checkmark) // but this page will be updated asynchronously if the extension // becomes enabled/disabled. It also might not become enabled or // disabled, because the user might e.g. get prompted when enabling // and choose not to. e.preventDefault(); }); // 'Remove' button. var trash = cloneTemplate('trash'); trash.title = loadTimeData.getString('extensionUninstall'); wrapper.querySelector('.enable-controls').appendChild(trash); wrapper.setupColumn('remove-enterprise', '.trash', 'click', function(e) { trash.classList.add('open'); trash.classList.toggle('mouse-clicked', e.detail > 0); if (this.uninstallIsShowing_) return; this.uninstallIsShowing_ = true; chrome.management.uninstall(extension.id, {showConfirmDialog: true}, function() { // TODO(devlin): What should we do if the uninstall fails? this.uninstallIsShowing_ = false; if (trash.classList.contains('mouse-clicked')) trash.blur(); if (chrome.runtime.lastError) { // The uninstall failed (e.g. a cancel). Allow the trash to close. trash.classList.remove('open'); } else { // Leave the trash open if the uninstall succeded. Otherwise it can // half-close right before it's removed when the DOM is modified. } }.bind(this)); }.bind(this)); // Maintain the order that nodes should be in when creating as well as // when adding only one new wrapper. this.insertBefore(wrapper, nextWrapper); this.updateWrapper_(extension, wrapper); var nextRow = this.focusGrid_.getRowForRoot(nextWrapper); // May be null. this.focusGrid_.addRowBefore(wrapper.getFocusRow(), nextRow); }, /** * Updates an HTML element for the extension metadata given in |extension|. * @param {!chrome.developerPrivate.ExtensionInfo} extension A dictionary of * extension metadata. * @param {!Element} wrapper The extension wrapper element to update. * @private */ updateWrapper_: function(extension, wrapper) { var isActive = extension.state == chrome.developerPrivate.ExtensionState.ENABLED; wrapper.classList.toggle('inactive-extension', !isActive); wrapper.classList.remove('controlled', 'may-not-remove'); if (extension.controlledInfo) { wrapper.classList.add('controlled'); } else if (!extension.userMayModify || extension.mustRemainInstalled || extension.dependentExtensions.length > 0) { wrapper.classList.add('may-not-remove'); } var item = wrapper.querySelector('.extension-list-item'); item.style.backgroundImage = 'url(' + extension.iconUrl + ')'; this.setText_(wrapper, '.extension-title', extension.name); this.setText_(wrapper, '.extension-version', extension.version); this.setText_(wrapper, '.location-text', extension.locationText || ''); this.setText_(wrapper, '.blacklist-text', extension.blacklistText || ''); this.setText_(wrapper, '.extension-description', extension.description); // The 'Show Browser Action' button. this.updateVisibility_(wrapper, '.show-button', isActive && extension.actionButtonHidden); // The 'allow in incognito' checkbox. this.updateVisibility_(wrapper, '.incognito-control', isActive && this.incognitoAvailable_, function(item) { var incognito = item.querySelector('input'); incognito.disabled = !extension.incognitoAccess.isEnabled; incognito.checked = extension.incognitoAccess.isActive; }); var showButterBar = isActive && extension.incognitoAccess.isActive && extension.type != ExtensionType.HOSTED_APP; // The 'allow in incognito' butter bar. this.updateVisibility_(wrapper, '.butter-bar', showButterBar); // The 'collect errors' checkbox. This should only be visible if the // error console is enabled - we can detect this by the existence of the // |errorCollectionEnabled| property. this.updateVisibility_( wrapper, '.error-collection-control', isActive && extension.errorCollection.isEnabled, function(item) { item.querySelector('input').checked = extension.errorCollection.isActive; }); // The 'allow on all urls' checkbox. This should only be visible if // active script restrictions are enabled. If they are not enabled, no // extensions should want all urls. this.updateVisibility_( wrapper, '.all-urls-control', isActive && extension.runOnAllUrls.isEnabled, function(item) { item.querySelector('input').checked = extension.runOnAllUrls.isActive; }); // The 'allow file:// access' checkbox. this.updateVisibility_(wrapper, '.file-access-control', isActive && extension.fileAccess.isEnabled, function(item) { item.querySelector('input').checked = extension.fileAccess.isActive; }); // The 'Options' button or link, depending on its behaviour. var optionsEnabled = isActive && !!extension.optionsPage; this.updateVisibility_(wrapper, '.options-link', optionsEnabled && extension.optionsPage.openInTab); this.updateVisibility_(wrapper, '.options-button', optionsEnabled && !extension.optionsPage.openInTab); // The 'View in Web Store/View Web Site' link. var siteLinkEnabled = !!extension.homePage.url && !this.enableAppInfoDialog_; this.updateVisibility_(wrapper, '.site-link', siteLinkEnabled, function(item) { item.href = extension.homePage.url; item.textContent = loadTimeData.getString( extension.homePage.specified ? 'extensionSettingsVisitWebsite' : 'extensionSettingsVisitWebStore'); }); var isUnpacked = extension.location == chrome.developerPrivate.Location.UNPACKED; // The 'Reload' link. this.updateVisibility_(wrapper, '.reload-link', isActive && isUnpacked); // The 'Launch' link. this.updateVisibility_( wrapper, '.launch-link', isUnpacked && extension.type == ExtensionType.PLATFORM_APP && isActive); // The 'Errors' link. var hasErrors = extension.runtimeErrors.length > 0 || extension.manifestErrors.length > 0; this.updateVisibility_(wrapper, '.errors-link', hasErrors, function(item) { var Level = chrome.developerPrivate.ErrorLevel; var map = {}; map[Level.LOG] = {weight: 0, name: 'extension-error-info-icon'}; map[Level.WARN] = {weight: 1, name: 'extension-error-warning-icon'}; map[Level.ERROR] = {weight: 2, name: 'extension-error-fatal-icon'}; // Find the highest severity of all the errors; manifest errors all have // a 'warning' level severity. var highestSeverity = extension.runtimeErrors.reduce( function(prev, error) { return map[error.severity].weight > map[prev].weight ? error.severity : prev; }, extension.manifestErrors.length ? Level.WARN : Level.LOG); // Adjust the class on the icon. var icon = item.querySelector('.extension-error-icon'); // TODO(hcarmona): Populate alt text with a proper description since // this icon conveys the severity of the error. (info, warning, fatal). icon.alt = ''; icon.className = 'extension-error-icon'; // Remove other classes. icon.classList.add(map[highestSeverity].name); }); // The 'Reload' terminated link. var isTerminated = extension.state == chrome.developerPrivate.ExtensionState.TERMINATED; this.updateVisibility_(wrapper, '.terminated-reload-link', isTerminated); // The 'Repair' corrupted link. var canRepair = !isTerminated && extension.disableReasons.corruptInstall && extension.location == chrome.developerPrivate.Location.FROM_STORE; this.updateVisibility_(wrapper, '.corrupted-repair-button', canRepair); // The 'Enabled' checkbox. var isOK = !isTerminated && !canRepair; this.updateVisibility_(wrapper, '.enable-checkbox', isOK, function(item) { var enableCheckboxDisabled = !extension.userMayModify || extension.disableReasons.suspiciousInstall || extension.disableReasons.corruptInstall || extension.disableReasons.updateRequired || extension.dependentExtensions.length > 0 || extension.state == chrome.developerPrivate.ExtensionState.BLACKLISTED; item.querySelector('input').disabled = enableCheckboxDisabled; item.querySelector('input').checked = isActive; }); // Indicator for extensions controlled by policy. var controlNode = wrapper.querySelector('.enable-controls'); var indicator = controlNode.querySelector('.controlled-extension-indicator'); var needsIndicator = isOK && extension.controlledInfo; if (needsIndicator && !indicator) { indicator = new cr.ui.ControlledIndicator(); indicator.classList.add('controlled-extension-indicator'); var ControllerType = chrome.developerPrivate.ControllerType; var controlledByStr = ''; switch (extension.controlledInfo.type) { case ControllerType.POLICY: controlledByStr = 'policy'; break; case ControllerType.CHILD_CUSTODIAN: controlledByStr = 'child-custodian'; break; case ControllerType.SUPERVISED_USER_CUSTODIAN: controlledByStr = 'supervised-user-custodian'; break; } indicator.setAttribute('controlled-by', controlledByStr); var text = extension.controlledInfo.text; indicator.setAttribute('text' + controlledByStr, text); indicator.image.setAttribute('aria-label', text); controlNode.appendChild(indicator); wrapper.setupColumn('remove-enterprise', '[controlled-by] div'); } else if (!needsIndicator && indicator) { controlNode.removeChild(indicator); } // Developer mode //////////////////////////////////////////////////////// // First we have the id. var idLabel = wrapper.querySelector('.extension-id'); idLabel.textContent = ' ' + extension.id; // Then the path, if provided by unpacked extension. this.updateVisibility_(wrapper, '.load-path', isUnpacked, function(item) { item.querySelector('a:first-of-type').textContent = ' ' + extension.prettifiedPath; }); // Then the 'managed, cannot uninstall/disable' message. // We would like to hide managed installed message since this // extension is disabled. var isRequired = !extension.userMayModify || extension.mustRemainInstalled; this.updateVisibility_(wrapper, '.managed-message', isRequired && !extension.disableReasons.updateRequired); // Then the 'This isn't from the webstore, looks suspicious' message. var isSuspicious = extension.disableReasons.suspiciousInstall; this.updateVisibility_(wrapper, '.suspicious-install-message', !isRequired && isSuspicious); // Then the 'This is a corrupt extension' message. this.updateVisibility_(wrapper, '.corrupt-install-message', !isRequired && extension.disableReasons.corruptInstall); // Then the 'An update required by enterprise policy' message. Note that // a force-installed extension might be disabled due to being outdated // as well. this.updateVisibility_(wrapper, '.update-required-message', extension.disableReasons.updateRequired); // The 'following extensions depend on this extension' list. var hasDependents = extension.dependentExtensions.length > 0; wrapper.classList.toggle('developer-extras', hasDependents); this.updateVisibility_(wrapper, '.dependent-extensions-message', hasDependents, function(item) { var dependentList = item.querySelector('ul'); dependentList.textContent = ''; extension.dependentExtensions.forEach(function(dependentExtension) { var depNode = cloneTemplate('dependent-list-item'); depNode.querySelector('.dep-extension-title').textContent = dependentExtension.name; depNode.querySelector('.dep-extension-id').textContent = dependentExtension.id; dependentList.appendChild(depNode); }, this); }.bind(this)); // The active views. this.updateVisibility_(wrapper, '.active-views', extension.views.length > 0, function(item) { var link = item.querySelector('a'); // Link needs to be an only child before the list is updated. while (link.nextElementSibling) item.removeChild(link.nextElementSibling); // Link needs to be cleaned up if it was used before. link.textContent = ''; if (link.clickHandler) link.removeEventListener('click', link.clickHandler); extension.views.forEach(function(view, i) { if (view.type == chrome.developerPrivate.ViewType.EXTENSION_DIALOG || view.type == chrome.developerPrivate.ViewType.EXTENSION_POPUP) { return; } var displayName; if (view.url.startsWith('chrome-extension://')) { var pathOffset = 'chrome-extension://'.length + 32 + 1; displayName = view.url.substring(pathOffset); if (displayName == '_generated_background_page.html') displayName = loadTimeData.getString('backgroundPage'); } else { displayName = view.url; } var label = displayName + (view.incognito ? ' ' + loadTimeData.getString('viewIncognito') : '') + (view.renderProcessId == -1 ? ' ' + loadTimeData.getString('viewInactive') : '') + (view.isIframe ? ' ' + loadTimeData.getString('viewIframe') : ''); link.textContent = label; link.clickHandler = function(e) { chrome.developerPrivate.openDevTools({ extensionId: extension.id, renderProcessId: view.renderProcessId, renderViewId: view.renderViewId, incognito: view.incognito }); }; link.addEventListener('click', link.clickHandler); if (i < extension.views.length - 1) { link = link.cloneNode(true); item.appendChild(link); } wrapper.setupColumn('activeView', '.active-views a:last-of-type'); }); }); // The extension warnings (describing runtime issues). this.updateVisibility_(wrapper, '.extension-warnings', extension.runtimeWarnings.length > 0, function(item) { var warningList = item.querySelector('ul'); warningList.textContent = ''; extension.runtimeWarnings.forEach(function(warning) { var li = document.createElement('li'); warningList.appendChild(li).innerText = warning; }); }); // Install warnings. this.updateVisibility_(wrapper, '.install-warnings', extension.installWarnings.length > 0, function(item) { var installWarningList = item.querySelector('ul'); installWarningList.textContent = ''; if (extension.installWarnings) { extension.installWarnings.forEach(function(warning) { var li = document.createElement('li'); li.innerText = warning; installWarningList.appendChild(li); }); } }); if (location.hash.substr(1) == extension.id) { // Scroll beneath the fixed header so that the extension is not // obscured. var topScroll = wrapper.offsetTop - $('page-header').offsetHeight; var pad = parseInt(window.getComputedStyle(wrapper).marginTop, 10); if (!isNaN(pad)) topScroll -= pad / 2; setScrollTopForDocument(document, topScroll); } }, /** * Updates an element's textContent. * @param {Node} node Ancestor of the element specified by |query|. * @param {string} query A query to select an element in |node|. * @param {string} textContent * @private */ setText_: function(node, query, textContent) { node.querySelector(query).textContent = textContent; }, /** * Updates an element's visibility and calls |shownCallback| if it is * visible. * @param {Node} node Ancestor of the element specified by |query|. * @param {string} query A query to select an element in |node|. * @param {boolean} visible Whether the element should be visible or not. * @param {function(Element)=} opt_shownCallback Callback if the element is * visible. The element passed in will be the element specified by * |query|. * @private */ updateVisibility_: function(node, query, visible, opt_shownCallback) { var element = assertInstanceof(node.querySelector(query), Element); element.hidden = !visible; if (visible && opt_shownCallback) opt_shownCallback(element); }, /** * Opens the extension options overlay for the extension with the given id. * @param {string} extensionId The id of extension whose options page should * be displayed. * @param {boolean} scroll Whether the page should scroll to the extension * @private */ showEmbeddedExtensionOptions_: function(extensionId, scroll) { if (this.optionsShown_) return; // Get the extension from the given id. var extension = this.extensions_.filter(function(extension) { return extension.state == chrome.developerPrivate.ExtensionState.ENABLED && extension.id == extensionId; })[0]; if (!extension) return; if (scroll) this.scrollToWrapper_(extensionId); // Add the options query string. Corner case: the 'options' query string // will clobber the 'id' query string if the options link is clicked when // 'id' is in the URL, or if both query strings are in the URL. uber.replaceState({}, '?options=' + extensionId); var overlay = extensions.ExtensionOptionsOverlay.getInstance(); var shownCallback = function() { // This overlay doesn't get focused automatically as // is created after the overlay is shown. if (cr.ui.FocusOutlineManager.forDocument(document).visible) overlay.setInitialFocus(); }; overlay.setExtensionAndShow(extensionId, extension.name, extension.iconUrl, shownCallback); this.optionsShown_ = true; var self = this; $('overlay').addEventListener('cancelOverlay', function f() { self.optionsShown_ = false; $('overlay').removeEventListener('cancelOverlay', f); // Remove the options query string. uber.replaceState({}, ''); }); // TODO(dbeam): why do we need to focus before and // after its showing animation? Makes very little sense to me. overlay.setInitialFocus(); }, /** * Hides the extension options overlay for the extension with id * |extensionId|. If there is an overlay showing for a different extension, * nothing happens. * @param {string} extensionId ID of the extension to hide. * @private */ hideEmbeddedExtensionOptions_: function(extensionId) { if (!this.optionsShown_) return; var overlay = extensions.ExtensionOptionsOverlay.getInstance(); if (overlay.getExtensionId() == extensionId) overlay.close(); }, /** * Updates or creates a wrapper for |extension|. * @param {!chrome.developerPrivate.ExtensionInfo} extension The information * about the extension to update. * @private */ updateOrCreateWrapper_: function(extension) { var currIndex = this.getIndexOfExtension_(extension.id); if (currIndex != -1) { // If there is a current version of the extension, update it with the // new version. this.extensions_[currIndex] = extension; } else { // If the extension isn't found, push it back and sort. Technically, we // could optimize by inserting it at the right location, but since this // only happens on extension install, it's not worth it. this.extensions_.push(extension); this.extensions_.sort(compareExtensions); } var wrapper = $(extension.id); if (wrapper) { this.updateWrapper_(extension, wrapper); } else { var nextExt = this.extensions_[this.extensions_.indexOf(extension) + 1]; this.createWrapper_(extension, nextExt ? $(nextExt.id) : null); } } }; return { ExtensionList: ExtensionList, ExtensionListDelegate: ExtensionListDelegate }; }); // // Copyright (c) 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. cr.define('extensions', function() { /** * PackExtensionOverlay class * Encapsulated handling of the 'Pack Extension' overlay page. * @constructor */ function PackExtensionOverlay() { } cr.addSingletonGetter(PackExtensionOverlay); PackExtensionOverlay.prototype = { /** * Initialize the page. */ initializePage: function() { var overlay = $('overlay'); cr.ui.overlay.setupOverlay(overlay); cr.ui.overlay.globalInitialization(); overlay.addEventListener('cancelOverlay', this.handleDismiss_.bind(this)); $('pack-extension-dismiss').addEventListener('click', function() { cr.dispatchSimpleEvent(overlay, 'cancelOverlay'); }); $('pack-extension-commit').addEventListener('click', this.handleCommit_.bind(this)); $('browse-extension-dir').addEventListener('click', this.handleBrowseExtensionDir_.bind(this)); $('browse-private-key').addEventListener('click', this.handleBrowsePrivateKey_.bind(this)); }, /** * Handles a click on the dismiss button. * @param {Event} e The click event. */ handleDismiss_: function(e) { extensions.ExtensionSettings.showOverlay(null); }, /** * Handles a click on the pack button. * @param {Event} e The click event. */ handleCommit_: function(e) { var extensionPath = $('extension-root-dir').value; var privateKeyPath = $('extension-private-key').value; chrome.developerPrivate.packDirectory( extensionPath, privateKeyPath, 0, this.onPackResponse_.bind(this)); }, /** * Utility function which asks the C++ to show a platform-specific file * select dialog, and set the value property of |node| to the selected path. * @param {chrome.developerPrivate.SelectType} selectType * The type of selection to use. * @param {chrome.developerPrivate.FileType} fileType * The type of file to select. * @param {HTMLInputElement} node The node to set the value of. * @private */ showFileDialog_: function(selectType, fileType, node) { chrome.developerPrivate.choosePath(selectType, fileType, function(path) { // Last error is set if the user canceled the dialog. if (!chrome.runtime.lastError && path) node.value = path; }); }, /** * Handles the showing of the extension directory browser. * @param {Event} e Change event. * @private */ handleBrowseExtensionDir_: function(e) { this.showFileDialog_( chrome.developerPrivate.SelectType.FOLDER, chrome.developerPrivate.FileType.LOAD, /** @type {HTMLInputElement} */ ($('extension-root-dir'))); }, /** * Handles the showing of the extension private key file. * @param {Event} e Change event. * @private */ handleBrowsePrivateKey_: function(e) { this.showFileDialog_( chrome.developerPrivate.SelectType.FILE, chrome.developerPrivate.FileType.PEM, /** @type {HTMLInputElement} */ ($('extension-private-key'))); }, /** * Handles a response from a packDirectory call. * @param {chrome.developerPrivate.PackDirectoryResponse} response The * response of the pack call. * @private */ onPackResponse_: function(response) { /** @type {string} */ var alertTitle; /** @type {string} */ var alertOk; /** @type {string} */ var alertCancel; /** @type {function()} */ var alertOkCallback; /** @type {function()} */ var alertCancelCallback; var closeAlert = function() { extensions.ExtensionSettings.showOverlay(null); }; switch (response.status) { case chrome.developerPrivate.PackStatus.SUCCESS: alertTitle = loadTimeData.getString('packExtensionOverlay'); alertOk = loadTimeData.getString('ok'); alertOkCallback = closeAlert; // No 'Cancel' option. break; case chrome.developerPrivate.PackStatus.WARNING: alertTitle = loadTimeData.getString('packExtensionWarningTitle'); alertOk = loadTimeData.getString('packExtensionProceedAnyway'); alertCancel = loadTimeData.getString('cancel'); alertOkCallback = function() { chrome.developerPrivate.packDirectory( response.item_path, response.pem_path, response.override_flags, this.onPackResponse_.bind(this)); closeAlert(); }.bind(this); alertCancelCallback = closeAlert; break; case chrome.developerPrivate.PackStatus.ERROR: alertTitle = loadTimeData.getString('packExtensionErrorTitle'); alertOk = loadTimeData.getString('ok'); alertOkCallback = function() { extensions.ExtensionSettings.showOverlay( $('pack-extension-overlay')); }; // No 'Cancel' option. break; default: assertNotReached(); return; } alertOverlay.setValues(alertTitle, response.message, alertOk, alertCancel, alertOkCallback, alertCancelCallback); extensions.ExtensionSettings.showOverlay($('alertOverlay')); }, }; // Export return { PackExtensionOverlay: PackExtensionOverlay }; }); // // Copyright 2014 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. cr.define('extensions', function() { 'use strict'; /** * Construct an ExtensionLoadError around the given |div|. * @param {HTMLDivElement} div The HTML div for the extension load error. * @constructor * @extends {HTMLDivElement} */ function ExtensionLoadError(div) { div.__proto__ = ExtensionLoadError.prototype; div.init(); return div; } /** * Construct a Failure. * @param {string} filePath The path to the unpacked extension. * @param {string} error The reason the extension failed to load. * @param {ExtensionHighlight} manifest Three 'highlight' strings in * |manifest| represent three portions of the file's content to display - * the portion which is most relevant and should be emphasized * (highlight), and the parts both before and after this portion. These * may be empty. * @param {HTMLLIElement} listElement The HTML element used for displaying the * failure path for the additional failures UI. * @constructor * @extends {HTMLDivElement} */ function Failure(filePath, error, manifest, listElement) { this.path = filePath; this.error = error; this.manifest = manifest; this.listElement = listElement; } ExtensionLoadError.prototype = { __proto__: HTMLDivElement.prototype, /** * Initialize the ExtensionLoadError div. */ init: function() { /** * The element which displays the path of the extension. * @type {HTMLElement} * @private */ this.path_ = /** @type {HTMLElement} */( this.querySelector('#extension-load-error-path')); /** * The element which displays the reason the extension failed to load. * @type {HTMLElement} * @private */ this.reason_ = /** @type {HTMLElement} */( this.querySelector('#extension-load-error-reason')); /** * The element which displays the manifest code. * @type {extensions.ExtensionCode} * @private */ this.manifest_ = new extensions.ExtensionCode( this.querySelector('#extension-load-error-manifest')); /** * The element which displays information about additional errors. * @type {HTMLElement} * @private */ this.additional_ = /** @type {HTMLUListElement} */( this.querySelector('#extension-load-error-additional')); this.additional_.list = this.additional_.getElementsByTagName('ul')[0]; /** * An array of Failures for keeping track of multiple active failures. * @type {Array} * @private */ this.failures_ = []; this.querySelector('#extension-load-error-retry-button').addEventListener( 'click', function(e) { chrome.send('extensionLoaderRetry'); this.remove_(); }.bind(this)); this.querySelector('#extension-load-error-give-up-button'). addEventListener('click', function(e) { chrome.send('extensionLoaderIgnoreFailure'); this.remove_(); }.bind(this)); chrome.send('extensionLoaderDisplayFailures'); }, /** * Add a failure to failures_ array. If there is already a displayed * failure, display the additional failures element. * @param {Array} failures Array of failures containing paths, * errors, and manifests. * @private */ add_: function(failures) { // If a failure is already being displayed, unhide the last item. if (this.failures_.length > 0) this.failures_[this.failures_.length - 1].listElement.hidden = false; failures.forEach(function(failure) { var listItem = /** @type {HTMLLIElement} */( document.createElement('li')); listItem.textContent = failure.path; this.additional_.list.appendChild(listItem); this.failures_.push(new Failure(failure.path, failure.error, failure.manifest, listItem)); }.bind(this)); // Hide the last item because the UI is displaying its information. this.failures_[this.failures_.length - 1].listElement.hidden = true; this.show_(); }, /** * Remove a failure from |failures_| array. If this was the last failure, * hide the error UI. If this was the last additional failure, hide * the additional failures UI. * @private */ remove_: function() { this.additional_.list.removeChild( this.failures_[this.failures_.length - 1].listElement); this.failures_.pop(); if (this.failures_.length > 0) { this.failures_[this.failures_.length - 1].listElement.hidden = true; this.show_(); } else { this.hidden = true; } }, /** * Display the load error to the user. The last failure gets its manifest * and error displayed, while additional failures have their path names * displayed in the additional failures element. * @private */ show_: function() { assert(this.failures_.length >= 1); var failure = this.failures_[this.failures_.length - 1]; this.path_.textContent = failure.path; this.reason_.textContent = failure.error; failure.manifest.message = failure.error; this.manifest_.populate( failure.manifest, loadTimeData.getString('extensionLoadCouldNotLoadManifest')); this.hidden = false; this.manifest_.scrollToError(); this.additional_.hidden = this.failures_.length == 1; } }; /** * The ExtensionLoader is the class in charge of loading unpacked extensions. * @constructor */ function ExtensionLoader() { /** * The ExtensionLoadError to show any errors from loading an unpacked * extension. * @type {ExtensionLoadError} * @private */ this.loadError_ = new ExtensionLoadError( /** @type {HTMLDivElement} */($('extension-load-error'))); } cr.addSingletonGetter(ExtensionLoader); ExtensionLoader.prototype = { /** * Whether or not we are currently loading an unpacked extension. * @private {boolean} */ isLoading_: false, /** * Begin the sequence of loading an unpacked extension. If an error is * encountered, this object will get notified via notifyFailed(). */ loadUnpacked: function() { if (this.isLoading_) // Only one running load at a time. return; this.isLoading_ = true; chrome.developerPrivate.loadUnpacked({failQuietly: true}, function() { // Check lastError to avoid the log, but don't do anything with it - // error-handling is done on the C++ side. var lastError = chrome.runtime.lastError; this.isLoading_ = false; }.bind(this)); }, /** * Notify the ExtensionLoader that loading an unpacked extension failed. * Add the failure to failures_ and show the ExtensionLoadError. * @param {Array} failures Array of failures containing paths, * errors, and manifests. */ notifyFailed: function(failures) { this.loadError_.add_(failures); }, }; /** * A static forwarding function for ExtensionLoader.notifyFailed. * @param {Array} failures Array of failures containing paths, * errors, and manifests. * @see ExtensionLoader.notifyFailed */ ExtensionLoader.notifyLoadFailed = function(failures) { ExtensionLoader.getInstance().notifyFailed(failures); }; return { ExtensionLoader: ExtensionLoader }; }); // // Copyright 2014 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. // Returns the width of a scrollbar in logical pixels. function getScrollbarWidth() { // Create nested divs with scrollbars. var outer = document.createElement('div'); outer.style.width = '100px'; outer.style.overflow = 'scroll'; outer.style.visibility = 'hidden'; document.body.appendChild(outer); var inner = document.createElement('div'); inner.style.width = '101px'; outer.appendChild(inner); // The outer div's |clientWidth| and |offsetWidth| differ only by the width of // the vertical scrollbar. var scrollbarWidth = outer.offsetWidth - outer.clientWidth; outer.parentNode.removeChild(outer); return scrollbarWidth; } cr.define('extensions', function() { 'use strict'; /** * The ExtensionOptionsOverlay will show an extension's options page using * an element. * @constructor */ function ExtensionOptionsOverlay() {} cr.addSingletonGetter(ExtensionOptionsOverlay); ExtensionOptionsOverlay.prototype = { /** * The function that shows the given element in the overlay. * @type {?function(HTMLDivElement)} Function that receives the element to * show in the overlay. * @private */ showOverlay_: null, /** * The id of the extension that this options page display. * @type {string} * @private */ extensionId_: '', /** * Initialize the page. * @param {function(HTMLDivElement)} showOverlay The function to show or * hide the ExtensionOptionsOverlay; this should take a single parameter * which is either the overlay Div if the overlay should be displayed, * or null if the overlay should be hidden. */ initializePage: function(showOverlay) { var overlay = $('overlay'); cr.ui.overlay.setupOverlay(overlay); cr.ui.overlay.globalInitialization(); overlay.addEventListener('cancelOverlay', this.handleDismiss_.bind(this)); this.showOverlay_ = showOverlay; }, setInitialFocus: function() { this.getExtensionOptions_().focus(); }, /** * @return {?Element} * @private */ getExtensionOptions_: function() { return $('extension-options-overlay-guest').querySelector( 'extensionoptions'); }, /** * Handles a click on the close button. * @param {Event} event The click event. * @private */ handleDismiss_: function(event) { this.setVisible_(false); var extensionoptions = this.getExtensionOptions_(); if (extensionoptions) $('extension-options-overlay-guest').removeChild(extensionoptions); $('extension-options-overlay-icon').removeAttribute('src'); }, /** * Associate an extension with the overlay and display it. * @param {string} extensionId The id of the extension whose options page * should be displayed in the overlay. * @param {string} extensionName The name of the extension, which is used * as the header of the overlay. * @param {string} extensionIcon The URL of the extension's icon. * @param {function():void} shownCallback A function called when * showing completes. * @suppress {checkTypes} * TODO(vitalyp): remove the suppression after adding * chrome/renderer/resources/extensions/extension_options.js * to dependencies. */ setExtensionAndShow: function(extensionId, extensionName, extensionIcon, shownCallback) { var overlay = $('extension-options-overlay'); var overlayHeader = $('extension-options-overlay-header'); var overlayGuest = $('extension-options-overlay-guest'); var overlayStyle = window.getComputedStyle(overlay); $('extension-options-overlay-title').textContent = extensionName; $('extension-options-overlay-icon').src = extensionIcon; this.setVisible_(true); var extensionoptions = new window.ExtensionOptions(); extensionoptions.extension = extensionId; this.extensionId_ = extensionId; // The content's size needs to be restricted to the // bounds of the overlay window. The overlay gives a minWidth and // maxHeight, but the maxHeight does not include our header height (title // and close button), so we need to subtract that to get the maxHeight // for the extension options. var maxHeight = parseInt(overlayStyle.maxHeight, 10) - overlayHeader.offsetHeight; var minWidth = parseInt(overlayStyle.minWidth, 10); extensionoptions.onclose = function() { cr.dispatchSimpleEvent($('overlay'), 'cancelOverlay'); }.bind(this); // Track the current animation (used to grow/shrink the overlay content), // if any. Preferred size changes can fire more rapidly than the // animation speed, and multiple animations running at the same time has // undesirable effects. var animation = null; /** * Resize the overlay if the changes preferred size. * @param {{width: number, height: number}} evt */ extensionoptions.onpreferredsizechanged = function(evt) { var oldOverlayWidth = parseInt(overlayStyle.width, 10); var oldOverlayHeight = parseInt(overlayStyle.height, 10); var newOverlayWidth = evt.width; // |evt.height| is just the new overlay guest height, and does not // include the overlay header height, so it needs to be added. var newOverlayHeight = evt.height + overlayHeader.offsetHeight; // Make room for the vertical scrollbar, if there is one. if (newOverlayHeight > maxHeight) { newOverlayWidth += getScrollbarWidth(); } // Enforce |minWidth| and |maxHeight|. newOverlayWidth = Math.max(newOverlayWidth, minWidth); newOverlayHeight = Math.min(newOverlayHeight, maxHeight); // animationTime is the amount of time in ms that will be used to resize // the overlay. It is calculated by multiplying the pythagorean distance // between old and the new size (in px) with a constant speed of // 0.25 ms/px. var loading = document.documentElement.classList.contains('loading'); var animationTime = loading ? 0 : 0.25 * Math.sqrt(Math.pow(newOverlayWidth - oldOverlayWidth, 2) + Math.pow(newOverlayHeight - oldOverlayHeight, 2)); if (animation) animation.cancel(); // The header height must be added to the (old and new) preferred // heights to get the full overlay heights. animation = overlay.animate([ {width: oldOverlayWidth + 'px', height: oldOverlayHeight + 'px'}, {width: newOverlayWidth + 'px', height: newOverlayHeight + 'px'} ], { duration: animationTime, delay: 0 }); animation.onfinish = function(e) { animation = null; // The element is ready to place back in the // overlay. Make sure that it's sized to take up the full width/height // of the overlay. overlayGuest.style.position = ''; overlayGuest.style.left = ''; overlayGuest.style.width = newOverlayWidth + 'px'; // |newOverlayHeight| includes the header height, so it needs to be // subtracted to get the new guest height. overlayGuest.style.height = (newOverlayHeight - overlayHeader.offsetHeight) + 'px'; if (shownCallback) { shownCallback(); shownCallback = null; } }; }.bind(this); // Move the off screen until the overlay is ready. overlayGuest.style.position = 'fixed'; overlayGuest.style.left = window.outerWidth + 'px'; // Begin rendering at the default dimensions. This is also necessary to // cancel any width/height set on a previous render. // TODO(kalman): This causes a visual jag where the overlay guest shows // up briefly. It would be better to render this off-screen first, then // swap it in place. See crbug.com/408274. // This may also solve crbug.com/431001 (width is 0 on initial render). overlayGuest.style.width = ''; overlayGuest.style.height = ''; overlayGuest.appendChild(extensionoptions); }, /** * Dispatches a 'cancelOverlay' event on the $('overlay') element. */ close: function() { cr.dispatchSimpleEvent($('overlay'), 'cancelOverlay'); }, /** * Returns extension id that this options page set. * @return {string} */ getExtensionId: function() { return this.extensionId_; }, /** * Toggles the visibility of the ExtensionOptionsOverlay. * @param {boolean} isVisible Whether the overlay should be visible. * @private */ setVisible_: function(isVisible) { this.showOverlay_(isVisible ? /** @type {HTMLDivElement} */($('extension-options-overlay')) : null); } }; // Export return { ExtensionOptionsOverlay: ExtensionOptionsOverlay }; }); // // Used for observing function of the backend datasource for this page by // tests. var webuiResponded = false; cr.define('extensions', function() { var ExtensionList = extensions.ExtensionList; /** * ExtensionSettings class * @class * @constructor * @implements {extensions.ExtensionListDelegate} */ function ExtensionSettings() {} cr.addSingletonGetter(ExtensionSettings); ExtensionSettings.prototype = { /** * The drag-drop wrapper for installing external Extensions, if available. * null if external Extension installation is not available. * @type {cr.ui.DragWrapper} * @private */ dragWrapper_: null, /** * True if drag-drop is both available and currently enabled - it can be * temporarily disabled while overlays are showing. * @type {boolean} * @private */ dragEnabled_: false, /** * True if the page has finished the initial load. * @private {boolean} */ hasLoaded_: false, /** * Perform initial setup. */ initialize: function() { this.setLoading_(true); uber.onContentFrameLoaded(); cr.ui.FocusOutlineManager.forDocument(document); measureCheckboxStrings(); // Set the title. uber.setTitle(loadTimeData.getString('extensionSettings')); var extensionList = new ExtensionList(this); extensionList.id = 'extension-settings-list'; var wrapper = $('extension-list-wrapper'); wrapper.insertBefore(extensionList, wrapper.firstChild); // Get the initial profile state, and register to be notified of any // future changes. chrome.developerPrivate.getProfileConfiguration( this.update_.bind(this)); chrome.developerPrivate.onProfileStateChanged.addListener( this.update_.bind(this)); var extensionLoader = extensions.ExtensionLoader.getInstance(); $('toggle-dev-on').addEventListener('change', function(e) { this.updateDevControlsVisibility_(true); chrome.developerPrivate.updateProfileConfiguration( {inDeveloperMode: e.target.checked}); var suffix = $('toggle-dev-on').checked ? 'Enabled' : 'Disabled'; chrome.send('metricsHandler:recordAction', ['Options_ToggleDeveloperMode_' + suffix]); }.bind(this)); window.addEventListener('resize', function() { this.updateDevControlsVisibility_(false); }.bind(this)); // Set up the three dev mode buttons (load unpacked, pack and update). $('load-unpacked').addEventListener('click', function(e) { chrome.send('metricsHandler:recordAction', ['Options_LoadUnpackedExtension']); extensionLoader.loadUnpacked(); }); $('pack-extension').addEventListener('click', this.handlePackExtension_.bind(this)); $('update-extensions-now').addEventListener('click', this.handleUpdateExtensionNow_.bind(this)); if (!loadTimeData.getBoolean('offStoreInstallEnabled')) { var dragTarget = document.documentElement; /** @private {extensions.DragAndDropHandler} */ this.dragWrapperHandler_ = new extensions.DragAndDropHandler(true, dragTarget); dragTarget.addEventListener('extension-drag-started', function() { ExtensionSettings.showOverlay($('drop-target-overlay')); }); dragTarget.addEventListener('extension-drag-ended', function() { var overlay = ExtensionSettings.getCurrentOverlay(); if (overlay && overlay.id === 'drop-target-overlay') ExtensionSettings.showOverlay(null); }); this.dragWrapper_ = new cr.ui.DragWrapper(dragTarget, this.dragWrapperHandler_); } extensions.PackExtensionOverlay.getInstance().initializePage(); // Hook up the configure commands link to the overlay. var link = document.querySelector('.extension-commands-config'); link.addEventListener('click', this.handleExtensionCommandsConfig_.bind(this)); // Initialize the Commands overlay. extensions.ExtensionCommandsOverlay.getInstance().initializePage(); extensions.ExtensionErrorOverlay.getInstance().initializePage( extensions.ExtensionSettings.showOverlay); extensions.ExtensionOptionsOverlay.getInstance().initializePage( extensions.ExtensionSettings.showOverlay); // Add user action logging for bottom links. var moreExtensionLink = document.getElementsByClassName('more-extensions-link'); for (var i = 0; i < moreExtensionLink.length; i++) { moreExtensionLink[i].addEventListener('click', function(e) { chrome.send('metricsHandler:recordAction', ['Options_GetMoreExtensions']); }); } // Initialize the kiosk overlay. if (cr.isChromeOS) { var kioskOverlay = extensions.KioskAppsOverlay.getInstance(); kioskOverlay.initialize(); $('add-kiosk-app').addEventListener('click', function() { ExtensionSettings.showOverlay($('kiosk-apps-page')); kioskOverlay.didShowPage(); }); extensions.KioskDisableBailoutConfirm.getInstance().initialize(); } cr.ui.overlay.setupOverlay($('drop-target-overlay')); cr.ui.overlay.globalInitialization(); extensions.ExtensionFocusManager.getInstance().initialize(); var path = document.location.pathname; if (path.length > 1) { // Skip starting slash and remove trailing slash (if any). var overlayName = path.slice(1).replace(/\/$/, ''); if (overlayName == 'configureCommands') this.showExtensionCommandsConfigUi_(); } }, /** * [Re]-Populates the page with data representing the current state of * installed extensions. * @param {chrome.developerPrivate.ProfileInfo} profileInfo * @private */ update_: function(profileInfo) { // We only set the page to be loading if we haven't already finished an // initial load, because otherwise the updates are all incremental and // don't need to display the interstitial spinner. if (!this.hasLoaded_) this.setLoading_(true); webuiResponded = true; /** @const */ var supervised = profileInfo.isSupervised; var developerModeControlledByPolicy = profileInfo.isDeveloperModeControlledByPolicy; var pageDiv = $('extension-settings'); pageDiv.classList.toggle('profile-is-supervised', supervised); pageDiv.classList.toggle('showing-banner', supervised); var devControlsCheckbox = $('toggle-dev-on'); devControlsCheckbox.checked = profileInfo.inDeveloperMode; devControlsCheckbox.disabled = supervised || developerModeControlledByPolicy; // This is necessary e.g. if developer mode is now disabled by policy // but extension developer tools were visible. this.updateDevControlsVisibility_(false); this.updateDevToggleControlledIndicator_(developerModeControlledByPolicy); $('load-unpacked').disabled = !profileInfo.canLoadUnpacked; var extensionList = $('extension-settings-list'); extensionList.updateExtensionsData( profileInfo.isIncognitoAvailable, profileInfo.appInfoDialogEnabled).then(function() { if (!this.hasLoaded_) { this.hasLoaded_ = true; this.setLoading_(false); } this.onExtensionCountChanged(); }.bind(this)); }, /** * Shows or hides the 'controlled by policy' indicator on the dev-toggle * checkbox. * @param {boolean} devModeControlledByPolicy true if the indicator * should be showing. * @private */ updateDevToggleControlledIndicator_: function(devModeControlledByPolicy) { var controlledIndicator = document.querySelector( '#dev-toggle .controlled-setting-indicator'); if (!(controlledIndicator instanceof cr.ui.ControlledIndicator)) cr.ui.ControlledIndicator.decorate(controlledIndicator); // We control the visibility of the ControlledIndicator by setting or // removing the 'controlled-by' attribute (see controlled_indicator.css). var isVisible = controlledIndicator.getAttribute('controlled-by'); if (devModeControlledByPolicy && !isVisible) { var controlledBy = 'policy'; controlledIndicator.setAttribute( 'controlled-by', controlledBy); controlledIndicator.setAttribute( 'text' + controlledBy, loadTimeData.getString('extensionControlledSettingPolicy')); } else if (!devModeControlledByPolicy && isVisible) { // This hides the element - see above. controlledIndicator.removeAttribute('controlled-by'); } }, /** * Shows the loading spinner and hides elements that shouldn't be visible * while loading. * @param {boolean} isLoading * @private */ setLoading_: function(isLoading) { document.documentElement.classList.toggle('loading', isLoading); $('loading-spinner').hidden = !isLoading; $('dev-controls').hidden = isLoading; this.updateDevControlsVisibility_(false); // The extension list is already hidden/shown elsewhere and shouldn't be // updated here because it can be hidden if there are no extensions. }, /** * Handles the Pack Extension button. * @param {Event} e Change event. * @private */ handlePackExtension_: function(e) { ExtensionSettings.showOverlay($('pack-extension-overlay')); chrome.send('metricsHandler:recordAction', ['Options_PackExtension']); }, /** * Shows the Extension Commands configuration UI. * @private */ showExtensionCommandsConfigUi_: function() { ExtensionSettings.showOverlay($('extension-commands-overlay')); chrome.send('metricsHandler:recordAction', ['Options_ExtensionCommands']); }, /** * Handles the Configure (Extension) Commands link. * @param {Event} e Change event. * @private */ handleExtensionCommandsConfig_: function(e) { this.showExtensionCommandsConfigUi_(); }, /** * Handles the Update Extension Now button. * @param {Event} e Change event. * @private */ handleUpdateExtensionNow_: function(e) { chrome.developerPrivate.autoUpdate(); chrome.send('metricsHandler:recordAction', ['Options_UpdateExtensions']); }, /** * Updates the visibility of the developer controls based on whether the * [x] Developer mode checkbox is checked. * @param {boolean} animated Whether to animate any updates. * @private */ updateDevControlsVisibility_: function(animated) { var showDevControls = $('toggle-dev-on').checked; $('extension-settings').classList.toggle('dev-mode', showDevControls); var devControls = $('dev-controls'); devControls.classList.toggle('animated', animated); var buttons = devControls.querySelector('.button-container'); Array.prototype.forEach.call(buttons.querySelectorAll('a, button'), function(control) { control.tabIndex = showDevControls ? 0 : -1; }); buttons.setAttribute('aria-hidden', !showDevControls); window.requestAnimationFrame(function() { devControls.style.height = !showDevControls ? '' : buttons.offsetHeight + 'px'; document.dispatchEvent(new Event('devControlsVisibilityUpdated')); }.bind(this)); }, /** @override */ onExtensionCountChanged: function() { /** @const */ var hasExtensions = $('extension-settings-list').getNumExtensions() != 0; $('no-extensions').hidden = hasExtensions; $('extension-list-wrapper').hidden = !hasExtensions; }, }; /** * Returns the current overlay or null if one does not exist. * @return {Element} The overlay element. */ ExtensionSettings.getCurrentOverlay = function() { return document.querySelector('#overlay .page.showing'); }; /** * Sets the given overlay to show. If the overlay is already showing, this is * a no-op; otherwise, hides any currently-showing overlay. * @param {HTMLElement} node The overlay page to show. If null, all overlays * are hidden. */ ExtensionSettings.showOverlay = function(node) { var pageDiv = $('extension-settings'); pageDiv.style.width = node ? window.getComputedStyle(pageDiv).width : ''; document.body.classList.toggle('no-scroll', !!node); var currentlyShowingOverlay = ExtensionSettings.getCurrentOverlay(); if (currentlyShowingOverlay) { if (currentlyShowingOverlay == node) // Already displayed. return; currentlyShowingOverlay.classList.remove('showing'); } if (node) { var lastFocused; var focusOutlineManager = cr.ui.FocusOutlineManager.forDocument(document); if (focusOutlineManager.visible) lastFocused = document.activeElement; $('overlay').addEventListener('cancelOverlay', function f() { if (lastFocused && focusOutlineManager.visible) lastFocused.focus(); $('overlay').removeEventListener('cancelOverlay', f); uber.replaceState({}, ''); }); node.classList.add('showing'); } var pages = document.querySelectorAll('.page'); for (var i = 0; i < pages.length; i++) { pages[i].setAttribute('aria-hidden', node ? 'true' : 'false'); } $('overlay').hidden = !node; if (node) ExtensionSettings.focusOverlay(); // If drag-drop for external Extension installation is available, enable // drag-drop when there is any overlay showing other than the usual overlay // shown when drag-drop is started. var settings = ExtensionSettings.getInstance(); if (settings.dragWrapper_) { assert(settings.dragWrapperHandler_).dragEnabled = !node || node == $('drop-target-overlay'); } uber.invokeMethodOnParent(node ? 'beginInterceptingEvents' : 'stopInterceptingEvents'); }; ExtensionSettings.focusOverlay = function() { var currentlyShowingOverlay = ExtensionSettings.getCurrentOverlay(); assert(currentlyShowingOverlay); if (cr.ui.FocusOutlineManager.forDocument(document).visible) cr.ui.setInitialFocus(currentlyShowingOverlay); if (!currentlyShowingOverlay.contains(document.activeElement)) { // Make sure focus isn't stuck behind the overlay. document.activeElement.blur(); } }; /** * Utility function to find the width of various UI strings and synchronize * the width of relevant spans. This is crucial for making sure the * Enable/Enabled checkboxes align, as well as the Developer Mode checkbox. */ function measureCheckboxStrings() { var trashWidth = 30; var measuringDiv = $('font-measuring-div'); measuringDiv.textContent = loadTimeData.getString('extensionSettingsEnabled'); measuringDiv.className = 'enabled-text'; var pxWidth = measuringDiv.clientWidth + trashWidth; measuringDiv.textContent = loadTimeData.getString('extensionSettingsEnable'); measuringDiv.className = 'enable-text'; pxWidth = Math.max(measuringDiv.clientWidth + trashWidth, pxWidth); measuringDiv.textContent = loadTimeData.getString('extensionSettingsDeveloperMode'); measuringDiv.className = ''; pxWidth = Math.max(measuringDiv.clientWidth, pxWidth); var style = document.createElement('style'); style.type = 'text/css'; style.textContent = '.enable-checkbox-text {' + ' min-width: ' + (pxWidth - trashWidth) + 'px;' + '}' + '#dev-toggle span {' + ' min-width: ' + pxWidth + 'px;' + '}'; document.querySelector('head').appendChild(style); }; // Export return { ExtensionSettings: ExtensionSettings }; }); window.addEventListener('load', function(e) { extensions.ExtensionSettings.getInstance().initialize(); }); // Copyright 2016 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. cr.define('extensions', function() { 'use strict'; /** * @param {boolean} dragEnabled * @param {!EventTarget} target * @constructor * @implements cr.ui.DragWrapperDelegate */ function DragAndDropHandler(dragEnabled, target) { this.dragEnabled = dragEnabled; /** @private {!EventTarget} */ this.eventTarget_ = target; } // TODO(devlin): Un-chrome.send-ify this implementation. DragAndDropHandler.prototype = { /** @type {boolean} */ dragEnabled: false, /** @override */ shouldAcceptDrag: function(e) { // External Extension installation can be disabled globally, e.g. while a // different overlay is already showing. if (!this.dragEnabled) return false; // We can't access filenames during the 'dragenter' event, so we have to // wait until 'drop' to decide whether to do something with the file or // not. // See: http://www.w3.org/TR/2011/WD-html5-20110113/dnd.html#concept-dnd-p return !!e.dataTransfer.types && e.dataTransfer.types.indexOf('Files') > -1; }, /** @override */ doDragEnter: function() { chrome.send('startDrag'); this.eventTarget_.dispatchEvent( new CustomEvent('extension-drag-started')); }, /** @override */ doDragLeave: function() { this.fireDragEnded_(); chrome.send('stopDrag'); }, /** @override */ doDragOver: function(e) { e.preventDefault(); }, /** @override */ doDrop: function(e) { this.fireDragEnded_(); if (e.dataTransfer.files.length != 1) return; var toSend = ''; // Files lack a check if they're a directory, but we can find out through // its item entry. for (var i = 0; i < e.dataTransfer.items.length; ++i) { if (e.dataTransfer.items[i].kind == 'file' && e.dataTransfer.items[i].webkitGetAsEntry().isDirectory) { toSend = 'installDroppedDirectory'; break; } } // Only process files that look like extensions. Other files should // navigate the browser normally. if (!toSend && /\.(crx|user\.js|zip)$/i.test(e.dataTransfer.files[0].name)) { toSend = 'installDroppedFile'; } if (toSend) { e.preventDefault(); chrome.send(toSend); } }, /** @private */ fireDragEnded_: function() { this.eventTarget_.dispatchEvent(new CustomEvent('extension-drag-ended')); } }; return { DragAndDropHandler: DragAndDropHandler, }; }); // Copyright 2016 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. cr.define('extensions', function() { 'use strict'; /** * @enum {number} */ var Key = { Comma: 188, Del: 46, Down: 40, End: 35, Escape: 27, Home: 36, Ins: 45, Left: 37, MediaNextTrack: 176, MediaPlayPause: 179, MediaPrevTrack: 177, MediaStop: 178, PageDown: 34, PageUp: 33, Period: 190, Right: 39, Space: 32, Tab: 9, Up: 38, }; /** * Enum for whether we require modifiers of a keycode. * @enum {number} */ var ModifierPolicy = { NOT_ALLOWED: 0, REQUIRED: 1 }; /** * Gets the ModifierPolicy. Currently only "MediaNextTrack", "MediaPrevTrack", * "MediaStop", "MediaPlayPause" are required to be used without any modifier. * @param {number} keyCode * @return {ModifierPolicy} */ function getModifierPolicy(keyCode) { switch (keyCode) { case Key.MediaNextTrack: case Key.MediaPlayPause: case Key.MediaPrevTrack: case Key.MediaStop: return ModifierPolicy.NOT_ALLOWED; default: return ModifierPolicy.REQUIRED; } } /** * Returns whether the keyboard event has a key modifier, which could affect * how it's handled. * @param {!KeyboardEvent} e * @param {boolean} countShiftAsModifier Whether the 'Shift' key should be * counted as modifier. * @return {boolean} True if the event has any modifiers. */ function hasModifier(e, countShiftAsModifier) { return e.ctrlKey || e.altKey || // Meta key is only relevant on Mac and CrOS, where we treat Command // and Search (respectively) as modifiers. (cr.isMac && e.metaKey) || (cr.isChromeOS && e.metaKey) || (countShiftAsModifier && e.shiftKey); } /** * Checks whether the passed in |keyCode| is a valid extension command key. * @param {number} keyCode * @return {boolean} Whether the key is valid. */ function isValidKeyCode(keyCode) { if (keyCode == Key.Escape) return false; for (var k in Key) { if (Key[k] == keyCode) return true; } return (keyCode >= 'A'.charCodeAt(0) && keyCode <= 'Z'.charCodeAt(0)) || (keyCode >= '0'.charCodeAt(0) && keyCode <= '9'.charCodeAt(0)); } /** * Converts a keystroke event to string form, ignoring invalid extension * commands. * @param {!KeyboardEvent} e * @return {string} The keystroke as a string. */ function keystrokeToString(e) { var output = []; // TODO(devlin): Should this be i18n'd? if (cr.isMac && e.metaKey) output.push('Command'); if (cr.isChromeOS && e.metaKey) output.push('Search'); if (e.ctrlKey) output.push('Ctrl'); if (!e.ctrlKey && e.altKey) output.push('Alt'); if (e.shiftKey) output.push('Shift'); var keyCode = e.keyCode; if (isValidKeyCode(keyCode)) { if ((keyCode >= 'A'.charCodeAt(0) && keyCode <= 'Z'.charCodeAt(0)) || (keyCode >= '0'.charCodeAt(0) && keyCode <= '9'.charCodeAt(0))) { output.push(String.fromCharCode(keyCode)); } else { switch (keyCode) { case Key.Comma: output.push('Comma'); break; case Key.Del: output.push('Delete'); break; case Key.Down: output.push('Down'); break; case Key.End: output.push('End'); break; case Key.Home: output.push('Home'); break; case Key.Ins: output.push('Insert'); break; case Key.Left: output.push('Left'); break; case Key.MediaNextTrack: output.push('MediaNextTrack'); break; case Key.MediaPlayPause: output.push('MediaPlayPause'); break; case Key.MediaPrevTrack: output.push('MediaPrevTrack'); break; case Key.MediaStop: output.push('MediaStop'); break; case Key.PageDown: output.push('PageDown'); break; case Key.PageUp: output.push('PageUp'); break; case Key.Period: output.push('Period'); break; case Key.Right: output.push('Right'); break; case Key.Space: output.push('Space'); break; case Key.Tab: output.push('Tab'); break; case Key.Up: output.push('Up'); break; } } } return output.join('+'); } /** * Returns true if the event has valid modifiers. * @param {!KeyboardEvent} e The keyboard event to consider. * @return {boolean} True if the event is valid. */ function hasValidModifiers(e) { switch (getModifierPolicy(e.keyCode)) { case ModifierPolicy.REQUIRED: return hasModifier(e, false); case ModifierPolicy.NOT_ALLOWED: return !hasModifier(e, true); } assertNotReached(); } return { isValidKeyCode: isValidKeyCode, keystrokeToString: keystrokeToString, hasValidModifiers: hasValidModifiers, Key: Key, }; }); // Copyright 2016 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. (function() { 'use strict'; var manager = /** @type {extensions.Manager} */( document.querySelector('extensions-manager')); manager.readyPromiseResolver.promise.then(function() { extensions.Service.getInstance().managerReady(manager); }); })(); // Copyright 2016 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. cr.exportPath('extensions'); /** * The different types of animations this helper supports. * @enum {number} */ extensions.Animation = { HERO: 0, FADE_IN: 1, FADE_OUT: 2, SCALE_DOWN: 3, }; cr.define('extensions', function() { 'use strict'; /** * A helper object for setting entry/exit animations. Polymer's support of * this is pretty limited, since it doesn't allow for things like specifying * hero properties or nodes. * @param {!HTMLElement} element The parent element to set the animations on. * This will be used as the page in to/fromPage. * @param {?HTMLElement} node The node to use for scaling and fading * animations. * @constructor */ function AnimationHelper(element, node) { this.element_ = element; this.node_ = node; element.animationConfig = {}; } AnimationHelper.prototype = { /** * Set the entry animation for the element. * @param {extensions.Animation} animation */ setEntryAnimation: function(animation) { var config; switch (animation) { case extensions.Animation.HERO: config = {name: 'hero-animation', id: 'hero', toPage: this.element_}; break; case extensions.Animation.FADE_IN: assert(this.node_); config = {name: 'fade-in-animation', node: this.node_}; break; default: assertNotReached(); } this.element_.animationConfig.entry = [config]; }, /** * Set the exit animation for the element. * @param {extensions.Animation} animation */ setExitAnimation: function(animation) { var config; switch (animation) { case extensions.Animation.HERO: config = {name: 'hero-animation', id: 'hero', fromPage: this.element_}; break; case extensions.Animation.FADE_OUT: assert(this.node_); config = {name: 'fade-out-animation', node: this.node_}; break; case extensions.Animation.SCALE_DOWN: assert(this.node_); config = { name: 'scale-down-animation', node: this.node_, transformOrigin: '50% 50%', axis: 'y', }; break; default: assertNotReached(); } this.element_.animationConfig.exit = [config]; }, }; return {AnimationHelper: AnimationHelper}; }); // Copyright 2016 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. cr.define('extensions', function() { 'use strict'; var CodeSection = Polymer({ is: 'extensions-code-section', properties: { /** * The code this object is displaying. * @type {?chrome.developerPrivate.RequestFileSourceResponse} */ code: { type: Object, // We initialize to null so that Polymer sees it as defined and calls // isMainHidden_(). value: null, }, /** * The string to display if no code is set. * @type {string} */ noCodeError: String, }, /** * Computes the content of the line numbers span, which basically just * contains 1\n2\n3\n... for the number of lines. * @return {string} * @private */ computeLineNumbersContent_: function() { if (!this.code) return ''; var lines = [this.code.beforeHighlight, this.code.highlight, this.code.afterHighlight].join('').match(/\n/g); var lineCount = lines ? lines.length : 0; var textContent = ''; for (var i = 1; i <= lineCount; ++i) textContent += i + '\n'; return textContent; }, /** * @return {boolean} * @private */ isMainHidden_: function() { return !this.code; }, }); return {CodeSection: CodeSection}; }); // Copyright 2016 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. cr.define('extensions', function() { 'use strict'; var DetailView = Polymer({ is: 'extensions-detail-view', behaviors: [Polymer.NeonAnimatableBehavior], properties: { /** * The underlying ExtensionInfo for the details being displayed. * @type {chrome.developerPrivate.ExtensionInfo} */ data: Object, /** @type {!extensions.ItemDelegate} */ delegate: Object, }, ready: function() { this.sharedElements = {hero: this.$.main}; /** @type {!extensions.AnimationHelper} */ this.animationHelper = new extensions.AnimationHelper(this, this.$.main); }, /** @private */ onCloseButtonTap_: function() { this.fire('close'); }, /** * @return {boolean} * @private */ hasDependentExtensions_: function() { return this.data.dependentExtensions.length > 0; }, /** * @return {boolean} * @private */ hasPermissions_: function() { return this.data.permissions.length > 0; }, /** * @return {boolean} * @private */ shouldShowHomepageButton_: function() { // Note: we ignore |data.homePage.specified| - we use an extension's // webstore entry as a homepage if the extension didn't explicitly specify // a homepage. (|url| can still be unset in the case of unpacked // extensions.) return this.data.homePage.url.length > 0; }, /** * @return {boolean} * @private */ shouldShowOptionsButton_: function() { return !!this.data.optionsPage; }, /** * @return {boolean} * @private */ shouldShowOptionsSection_: function() { return this.data.incognitoAccess.isEnabled || this.data.fileAccess.isEnabled || this.data.runOnAllUrls.isEnabled || this.data.errorCollection.isEnabled; }, /** @private */ onOptionsButtonTap_: function() { this.delegate.showItemOptionsPage(this.data.id); }, /** @private */ onAllowIncognitoChange_: function() { this.delegate.setItemAllowedIncognito( this.data.id, this.$$('#allow-incognito').checked); }, /** @private */ onAllowOnFileUrlsChange_: function() { this.delegate.setItemAllowedOnFileUrls( this.data.id, this.$$('#allow-on-file-urls').checked); }, /** @private */ onAllowOnAllSitesChange_: function() { this.delegate.setItemAllowedOnAllSites( this.data.id, this.$$('#allow-on-all-sites').checked); }, /** @private */ onCollectErrorsChange_: function() { this.delegate.setItemCollectsErrors( this.data.id, this.$$('#collect-errors').checked); }, /** * @param {!chrome.developerPrivate.DependentExtension} item * @private */ computeDependentEntry_: function(item) { return loadTimeData.getStringF('itemDependentEntry', item.name, item.id); }, /** @private */ computeSourceString_: function() { return extensions.getItemSourceString( extensions.getItemSource(this.data)); } }); return {DetailView: DetailView}; }); // Copyright 2016 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. (function() { 'use strict'; Polymer({ is: 'extensions-drop-overlay', created: function() { this.hidden = true; if (loadTimeData.getBoolean('offStoreInstallEnabled')) return; var dragTarget = document.documentElement; this.dragWrapperHandler_ = new extensions.DragAndDropHandler(true, dragTarget); dragTarget.addEventListener('extension-drag-started', function() { this.hidden = false; }.bind(this)); dragTarget.addEventListener('extension-drag-ended', function() { this.hidden = true; }.bind(this)); this.dragWrapper_ = new cr.ui.DragWrapper(dragTarget, this.dragWrapperHandler_); }, }); })(); // Copyright 2016 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. /** @typedef {chrome.developerPrivate.ManifestError} */ var ManifestError; /** @typedef {chrome.developerPrivate.RuntimeError} */ var RuntimeError; cr.define('extensions', function() { 'use strict'; /** @interface */ var ErrorPageDelegate = function() {}; ErrorPageDelegate.prototype = { /** * @param {string} extensionId * @param {!Array=} opt_errorIds * @param {chrome.developerPrivate.ErrorType=} opt_type */ deleteErrors: assertNotReached, /** * @param {chrome.developerPrivate.RequestFileSourceProperties} args * @return {!Promise} */ requestFileSource: assertNotReached, }; var ErrorPage = Polymer({ is: 'extensions-error-page', behaviors: [Polymer.NeonAnimatableBehavior], properties: { /** @type {!chrome.developerPrivate.ExtensionInfo|undefined} */ data: Object, /** @type {!extensions.ErrorPageDelegate|undefined} */ delegate: Object, /** @private {?(ManifestError|RuntimeError)} */ selectedError_: Object, }, observers: [ 'observeDataChanges_(data)', 'onSelectedErrorChanged_(selectedError_)', ], ready: function() { /** @type {!extensions.AnimationHelper} */ this.animationHelper = new extensions.AnimationHelper(this, this.$.main); this.animationHelper.setEntryAnimation(extensions.Animation.FADE_IN); this.animationHelper.setExitAnimation(extensions.Animation.SCALE_DOWN); this.sharedElements = {hero: this.$.main}; }, /** * Watches for changes to |data| in order to fetch the corresponding * file source. * @private */ observeDataChanges_: function() { assert(this.data); var e = this.data.manifestErrors[0] || this.data.runtimeErrors[0]; if (e) this.selectedError_ = e; }, /** * @return {!Array} * @private */ calculateShownItems_: function() { return this.data.manifestErrors.concat(this.data.runtimeErrors); }, /** @private */ onCloseButtonTap_: function() { this.fire('close'); }, /** * @param {!ManifestError|!RuntimeError} error * @return {string} * @private */ computeErrorIconClass_: function(error) { if (error.type == chrome.developerPrivate.ErrorType.RUNTIME) { switch (error.severity) { case chrome.developerPrivate.ErrorLevel.LOG: return 'icon-severity-info'; case chrome.developerPrivate.ErrorLevel.WARN: return 'icon-severity-warning'; case chrome.developerPrivate.ErrorLevel.ERROR: return 'icon-severity-fatal'; } assertNotReached(); } assert(error.type == chrome.developerPrivate.ErrorType.MANIFEST); return 'icon-severity-warning'; }, /** * @param {!Event} event * @private */ onDeleteErrorTap_: function(event) { // TODO(devlin): It would be cleaner if we could cast this to a // PolymerDomRepeatEvent-type thing, but that doesn't exist yet. var e = /** @type {!{model:Object}} */(event); this.delegate.deleteErrors(this.data.id, [e.model.item.id]); }, /** * Fetches the source for the selected error and populates the code section. * @private */ onSelectedErrorChanged_: function() { var error = this.selectedError_; var args = { extensionId: error.extensionId, message: error.message, }; switch (error.type) { case chrome.developerPrivate.ErrorType.MANIFEST: args.pathSuffix = error.source; args.manifestKey = error.manifestKey; args.manifestSpecific = error.manifestSpecific; break; case chrome.developerPrivate.ErrorType.RUNTIME: // slice(1) because pathname starts with a /. args.pathSuffix = new URL(error.source).pathname.slice(1); args.lineNumber = error.stackTrace && error.stackTrace[0] ? error.stackTrace[0].lineNumber : 0; break; } this.delegate.requestFileSource(args).then(function(code) { this.$['code-section'].code = code; }.bind(this)); }, /** * Computes the class name for the error item depending on whether its * the currently selected error. * @param {!RuntimeError|!ManifestError} selectedError * @param {!RuntimeError|!ManifestError} error * @return {string} * @private */ computeErrorClass_: function(selectedError, error) { return selectedError == error ? 'error-item selected' : 'error-item'; }, /** * Causes the given error to become the currently-selected error. * @param {!RuntimeError|!ManifestError} error * @private */ selectError_: function(error) { this.selectedError_ = error; }, /** * @param {!{model: !{item: (!RuntimeError|!ManifestError)}}} e * @private */ onErrorItemTap_: function(e) { this.selectError_(e.model.item); }, /** * @param {!{model: !{item: (!RuntimeError|!ManifestError)}}} e * @private */ onErrorItemKeydown_: function(e) { if (e.key == ' ' || e.key == 'Enter') this.selectError_(e.model.item); }, }); return { ErrorPage: ErrorPage, ErrorPageDelegate: ErrorPageDelegate, }; }); // Copyright 2016 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. cr.define('extensions', function() { 'use strict'; // The UI to display and manage keyboard shortcuts set for extension commands. var KeyboardShortcuts = Polymer({ is: 'extensions-keyboard-shortcuts', behaviors: [Polymer.NeonAnimatableBehavior], properties: { /** @type {Array} */ items: Array, }, ready: function() { /** @type {!extensions.AnimationHelper} */ this.animationHelper = new extensions.AnimationHelper(this, this.$.main); this.animationHelper.setEntryAnimation(extensions.Animation.FADE_IN); this.animationHelper.setExitAnimation(extensions.Animation.SCALE_DOWN); this.sharedElements = {hero: this.$.main}; }, /** * @return {!Array} * @private */ calculateShownItems_: function() { return this.items.filter(function(item) { return item.commands.length > 0; }); }, /** * A polymer bug doesn't allow for databinding of a string property as a * boolean, but it is correctly interpreted from a function. * Bug: https://github.com/Polymer/polymer/issues/3669 * @param {string} keybinding * @return {boolean} * @private */ hasKeybinding_: function(keybinding) { return !!keybinding; }, /** * Returns the scope index in the dropdown menu for the command's scope. * @param {chrome.developerPrivate.Command} command * @return {number} * @private */ computeSelectedScope_: function(command) { // These numbers match the indexes in the dropdown menu in the html. switch (command.scope) { case chrome.developerPrivate.CommandScope.CHROME: return 0; case chrome.developerPrivate.CommandScope.GLOBAL: return 1; } assertNotReached(); }, /** @private */ onCloseButtonClick_: function() { this.fire('close'); }, }); return {KeyboardShortcuts: KeyboardShortcuts}; }); // Copyright 2015 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. /** * The different pages that can be shown at a time. * Note: This must remain in sync with the order in manager.html! * @enum {string} */ var Page = { ITEM_LIST: '0', DETAIL_VIEW: '1', KEYBOARD_SHORTCUTS: '2', ERROR_PAGE: '3', }; cr.define('extensions', function() { 'use strict'; /** * Compares two extensions to determine which should come first in the list. * @param {chrome.developerPrivate.ExtensionInfo} a * @param {chrome.developerPrivate.ExtensionInfo} b * @return {number} */ var compareExtensions = function(a, b) { function compare(x, y) { return x < y ? -1 : (x > y ? 1 : 0); } function compareLocation(x, y) { if (x.location == y.location) return 0; if (x.location == chrome.developerPrivate.Location.UNPACKED) return -1; if (y.location == chrome.developerPrivate.Location.UNPACKED) return 1; return 0; } return compareLocation(a, b) || compare(a.name.toLowerCase(), b.name.toLowerCase()) || compare(a.id, b.id); }; var Manager = Polymer({ is: 'extensions-manager', behaviors: [I18nBehavior], properties: { /** @type {extensions.Sidebar} */ sidebar: Object, /** @type {extensions.ItemDelegate} */ itemDelegate: Object, inDevMode: { type: Boolean, value: false, }, filter: { type: String, value: '', }, /** * The item currently displayed in the error subpage. We use a separate * item for different pages (rather than a single subpageItem_ property) * so that hidden subpages don't update when an item updates. That is, we * don't want the details view subpage to update when the item shown in * the errors page updates, and vice versa. * @private {!chrome.developerPrivate.ExtensionInfo|undefined} */ errorPageItem_: Object, /** * The item currently displayed in the details view subpage. See also * errorPageItem_. * @private {!chrome.developerPrivate.ExtensionInfo|undefined} */ detailViewItem_: Object, /** @type {!Array} */ extensions: { type: Array, value: function() { return []; }, }, /** @type {!Array} */ apps: { type: Array, value: function() { return []; }, }, }, listeners: { 'items-list.extension-item-show-details': 'onShouldShowItemDetails_', 'items-list.extension-item-show-errors': 'onShouldShowItemErrors_', }, created: function() { this.readyPromiseResolver = new PromiseResolver(); }, ready: function() { /** @type {extensions.Sidebar} */ this.sidebar = /** @type {extensions.Sidebar} */(this.$$('extensions-sidebar')); this.listHelper_ = new ListHelper(this); this.sidebar.setListDelegate(this.listHelper_); this.readyPromiseResolver.resolve(); }, get keyboardShortcuts() { return this.$['keyboard-shortcuts']; }, get packDialog() { return this.$['pack-dialog']; }, get optionsDialog() { return this.$['options-dialog']; }, get errorPage() { return this.$['error-page']; }, /** * Shows the details view for a given item. * @param {!chrome.developerPrivate.ExtensionInfo} data */ showItemDetails: function(data) { this.$['items-list'].willShowItemSubpage(data.id); this.detailViewItem_ = data; this.changePage(Page.DETAIL_VIEW); }, /** * @param {!CustomEvent} event * @private */ onFilterChanged_: function(event) { this.filter = /** @type {string} */ (event.detail); }, /** * @param {chrome.developerPrivate.ExtensionType} type The type of item. * @return {string} The ID of the list that the item belongs in. * @private */ getListId_: function(type) { var listId; var ExtensionType = chrome.developerPrivate.ExtensionType; switch (type) { case ExtensionType.HOSTED_APP: case ExtensionType.LEGACY_PACKAGED_APP: case ExtensionType.PLATFORM_APP: listId = 'apps'; break; case ExtensionType.EXTENSION: case ExtensionType.SHARED_MODULE: listId = 'extensions'; break; case ExtensionType.THEME: assertNotReached( 'Don\'t send themes to the chrome://extensions page'); break; } assert(listId); return listId; }, /** * @param {string} listId The list to look for the item in. * @param {string} itemId The id of the item to look for. * @return {number} The index of the item in the list, or -1 if not found. * @private */ getIndexInList_: function(listId, itemId) { return this[listId].findIndex(function(item) { return item.id == itemId; }); }, /** * @return {boolean} Whether the list should be visible. * @private */ computeListHidden_: function() { return this.$['items-list'].items.length == 0; }, /** * Creates and adds a new extensions-item element to the list, inserting it * into its sorted position in the relevant section. * @param {!chrome.developerPrivate.ExtensionInfo} item The extension * the new element is representing. */ addItem: function(item) { var listId = this.getListId_(item.type); // We should never try and add an existing item. assert(this.getIndexInList_(listId, item.id) == -1); var insertBeforeChild = this[listId].findIndex(function(listEl) { return compareExtensions(listEl, item) > 0; }); if (insertBeforeChild == -1) insertBeforeChild = this[listId].length; this.splice(listId, insertBeforeChild, 0, item); }, /** * @param {!chrome.developerPrivate.ExtensionInfo} item The data for the * item to update. */ updateItem: function(item) { var listId = this.getListId_(item.type); var index = this.getIndexInList_(listId, item.id); // We should never try and update a non-existent item. assert(index >= 0); this.set([listId, index], item); // Update the subpage if it is open and displaying the item. If it's not // open, we don't update the data even if it's displaying that item. We'll // set the item correctly before opening the page. It's a little weird // that the DOM will have stale data, but there's no point in causing the // extra work. if (this.detailViewItem_ && this.detailViewItem_.id == item.id && this.$.pages.selected == Page.DETAIL_VIEW) { this.detailViewItem_ = item; } else if (this.errorPageItem_ && this.errorPageItem_.id == item.id && this.$.pages.selected == Page.ERROR_PAGE) { this.errorPageItem_ = item; } }, /** * @param {!chrome.developerPrivate.ExtensionInfo} item The data for the * item to remove. */ removeItem: function(item) { var listId = this.getListId_(item.type); var index = this.getIndexInList_(listId, item.id); // We should never try and remove a non-existent item. assert(index >= 0); this.splice(listId, index, 1); }, /** * @param {Page} page * @return {!(extensions.KeyboardShortcuts | * extensions.DetailView | * extensions.ItemList)} * @private */ getPage_: function(page) { switch (page) { case Page.ITEM_LIST: return this.$['items-list']; case Page.DETAIL_VIEW: return this.$['details-view']; case Page.KEYBOARD_SHORTCUTS: return this.$['keyboard-shortcuts']; case Page.ERROR_PAGE: return this.$['error-page']; } assertNotReached(); }, /** * Changes the active page selection. * @param {Page} toPage */ changePage: function(toPage) { var fromPage = this.$.pages.selected; if (fromPage == toPage) return; var entry; var exit; if (fromPage == Page.ITEM_LIST && (toPage == Page.DETAIL_VIEW || toPage == Page.ERROR_PAGE)) { entry = extensions.Animation.HERO; exit = extensions.Animation.HERO; } else if (toPage == Page.ITEM_LIST) { entry = extensions.Animation.FADE_IN; exit = extensions.Animation.SCALE_DOWN; } else { assert(toPage == Page.DETAIL_VIEW || toPage == Page.KEYBOARD_SHORTCUTS); entry = extensions.Animation.FADE_IN; exit = extensions.Animation.FADE_OUT; } this.getPage_(fromPage).animationHelper.setExitAnimation(exit); this.getPage_(toPage).animationHelper.setEntryAnimation(entry); this.$.pages.selected = toPage; }, /** * Handles the event for the user clicking on a details button. * @param {!CustomEvent} e * @private */ onShouldShowItemDetails_: function(e) { this.showItemDetails(e.detail.data); }, /** * Handles the event for the user clicking on the errors button. * @param {!CustomEvent} e * @private */ onShouldShowItemErrors_: function(e) { var data = e.detail.data; this.$['items-list'].willShowItemSubpage(data.id); this.errorPageItem_ = data; this.changePage(Page.ERROR_PAGE); }, /** @private */ onDetailsViewClose_: function() { // Note: we don't reset detailViewItem_ here because doing so just causes // extra work for the data-bound details view. this.changePage(Page.ITEM_LIST); }, /** @private */ onErrorPageClose_: function() { // Note: we don't reset errorPageItem_ here because doing so just causes // extra work for the data-bound error page. this.changePage(Page.ITEM_LIST); } }); /** * @param {extensions.Manager} manager * @constructor * @implements {extensions.SidebarListDelegate} */ function ListHelper(manager) { this.manager_ = manager; } ListHelper.prototype = { /** @override */ showType: function(type) { var items; switch (type) { case extensions.ShowingType.EXTENSIONS: items = this.manager_.extensions; break; case extensions.ShowingType.APPS: items = this.manager_.apps; break; } this.manager_.$/* hack */ ['items-list'].set('items', assert(items)); this.manager_.changePage(Page.ITEM_LIST); }, /** @override */ showKeyboardShortcuts: function() { this.manager_.changePage(Page.KEYBOARD_SHORTCUTS); }, /** @override */ showPackDialog: function() { this.manager_.packDialog.show(); } }; return {Manager: Manager}; }); // Copyright 2015 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. cr.define('extensions', function() { /** @interface */ var ItemDelegate = function() {}; ItemDelegate.prototype = { /** @param {string} id */ deleteItem: assertNotReached, /** * @param {string} id * @param {boolean} isEnabled */ setItemEnabled: assertNotReached, /** * @param {string} id * @param {boolean} isAllowedIncognito */ setItemAllowedIncognito: assertNotReached, /** * @param {string} id * @param {boolean} isAllowedOnFileUrls */ setItemAllowedOnFileUrls: assertNotReached, /** * @param {string} id * @param {boolean} isAllowedOnAllSites */ setItemAllowedOnAllSites: assertNotReached, /** * @param {string} id * @param {boolean} collectsErrors */ setItemCollectsErrors: assertNotReached, /** * @param {string} id * @param {chrome.developerPrivate.ExtensionView} view */ inspectItemView: assertNotReached, /** @param {string} id */ repairItem: assertNotReached, /** @param {string} id */ showItemOptionsPage: assertNotReached, }; var Item = Polymer({ is: 'extensions-item', behaviors: [I18nBehavior], properties: { // The item's delegate, or null. delegate: { type: Object, }, // Whether or not dev mode is enabled. inDevMode: { type: Boolean, value: false, }, // The underlying ExtensionInfo itself. Public for use in declarative // bindings. /** @type {chrome.developerPrivate.ExtensionInfo} */ data: { type: Object, }, // Whether or not the expanded view of the item is shown. /** @private */ showingDetails_: { type: Boolean, value: false, }, }, observers: [ 'observeIdVisibility_(inDevMode, showingDetails_, data.id)', ], /** @private */ observeIdVisibility_: function(inDevMode, showingDetails, id) { Polymer.dom.flush(); var idElement = this.$$('#extension-id'); if (idElement) { assert(this.data); idElement.innerHTML = this.i18n('itemId', this.data.id); } this.fire('extension-item-size-changed', {item: this.data}); }, /** * @return {boolean} * @private */ computeErrorsHidden_: function() { return !this.data.manifestErrors.length && !this.data.runtimeErrors.length; }, /** @private */ onRemoveTap_: function() { this.delegate.deleteItem(this.data.id); }, /** @private */ onEnableChange_: function() { this.delegate.setItemEnabled(this.data.id, this.$['enable-toggle'].checked); }, /** @private */ onErrorsTap_: function() { this.fire('extension-item-show-errors', {data: this.data}); }, /** @private */ onDetailsTap_: function() { this.fire('extension-item-show-details', {data: this.data}); }, /** * @param {!{model: !{item: !chrome.developerPrivate.ExtensionView}}} e * @private */ onInspectTap_: function(e) { this.delegate.inspectItemView(this.data.id, e.model.item); }, /** @private */ onRepairTap_: function() { this.delegate.repairItem(this.data.id); }, /** * Returns true if the extension is enabled, including terminated * extensions. * @return {boolean} * @private */ isEnabled_: function() { switch (this.data.state) { case chrome.developerPrivate.ExtensionState.ENABLED: case chrome.developerPrivate.ExtensionState.TERMINATED: return true; case chrome.developerPrivate.ExtensionState.DISABLED: return false; } assertNotReached(); // FileNotFound. }, /** @private */ computeClasses_: function() { return this.isEnabled_() ? 'enabled' : 'disabled'; }, /** * @return {string} * @private */ computeSourceIndicatorIcon_: function() { switch (extensions.getItemSource(this.data)) { case SourceType.POLICY: return 'communication:business'; case SourceType.SIDELOADED: return 'input'; case SourceType.UNPACKED: return 'extensions-icons:unpacked'; case SourceType.WEBSTORE: return ''; } assertNotReached(); }, /** * @return {string} * @private */ computeSourceIndicatorText_: function() { var sourceType = extensions.getItemSource(this.data); return sourceType == SourceType.WEBSTORE ? '' : extensions.getItemSourceString(sourceType); }, /** * @param {chrome.developerPrivate.ExtensionView} view * @private */ computeInspectLabel_: function(view) { // Trim the "chrome-extension:///". var url = new URL(view.url); var label = view.url; if (url.protocol == 'chrome-extension:') label = url.pathname.substring(1); if (label == '_generated_background_page.html') label = this.i18n('viewBackgroundPage'); // Add any qualifiers. label += (view.incognito ? ' ' + this.i18n('viewIncognito') : '') + (view.renderProcessId == -1 ? ' ' + this.i18n('viewInactive') : '') + (view.isIframe ? ' ' + this.i18n('viewIframe') : ''); return label; }, /** * @return {boolean} * @private */ hasWarnings_: function() { return this.data.disableReasons.corruptInstall || this.data.disableReasons.suspiciousInstall || !!this.data.blacklistText; }, /** * @return {string} * @private */ computeWarningsClasses_: function() { return this.data.blacklistText ? 'severe' : 'mild'; }, }); return { Item: Item, ItemDelegate: ItemDelegate, }; }); // Copyright 2015 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. cr.define('extensions', function() { var ItemList = Polymer({ is: 'extensions-item-list', behaviors: [ Polymer.NeonAnimatableBehavior, Polymer.IronResizableBehavior ], properties: { /** @type {Array} */ items: Array, /** @type {extensions.ItemDelegate} */ delegate: Object, inDevMode: { type: Boolean, value: false, }, filter: String, }, listeners: { 'list.extension-item-size-changed': 'itemSizeChanged_', }, ready: function() { /** @type {extensions.AnimationHelper} */ this.animationHelper = new extensions.AnimationHelper(this, this.$.list); this.animationHelper.setEntryAnimation(extensions.Animation.FADE_IN); this.animationHelper.setExitAnimation(extensions.Animation.HERO); }, /** * Called when a subpage for a given item is about to be shown. * @param {string} id */ willShowItemSubpage: function(id) { this.sharedElements = {hero: this.$$('#' + id)}; }, /** * Updates the size for a given item. * @param {CustomEvent} e * @private * @suppress {checkTypes} Closure doesn't know $.list is an IronList. */ itemSizeChanged_: function(e) { this.$.list.updateSizeForItem(e.detail.item); }, /** * Called right before an item enters the detailed view. * @param {CustomEvent} e * @private */ showItemDetails_: function(e) { this.sharedElements = {hero: e.detail.element}; }, /** * Computes the list of items to be shown. * @param {Object} changeRecord The changeRecord for |items|. * @param {string} filter The updated filter string. * @return {Array} * @private */ computeShownItems_: function(changeRecord, filter) { return this.items.filter(function(item) { return item.name.toLowerCase().includes(this.filter.toLowerCase()); }, this); }, }); return { ItemList: ItemList, }; }); // Copyright 2016 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. // Closure compiler won't let this be declared inside cr.define(). /** @enum {string} */ var SourceType = { WEBSTORE: 'webstore', POLICY: 'policy', SIDELOADED: 'sideloaded', UNPACKED: 'unpacked', }; cr.define('extensions', function() { /** * @param {chrome.developerPrivate.ExtensionInfo} item * @return {SourceType} */ function getItemSource(item) { if (item.controlledInfo && item.controlledInfo.type == chrome.developerPrivate.ControllerType.POLICY) { return SourceType.POLICY; } if (item.location == chrome.developerPrivate.Location.THIRD_PARTY) return SourceType.SIDELOADED; if (item.location == chrome.developerPrivate.Location.UNPACKED) return SourceType.UNPACKED; return SourceType.WEBSTORE; } /** * @param {SourceType} source * @return {string} */ function getItemSourceString(source) { switch (source) { case SourceType.POLICY: return loadTimeData.getString('itemSourcePolicy'); case SourceType.SIDELOADED: return loadTimeData.getString('itemSourceSideloaded'); case SourceType.UNPACKED: return loadTimeData.getString('itemSourceUnpacked'); case SourceType.WEBSTORE: return loadTimeData.getString('itemSourceWebstore'); } assertNotReached(); } return {getItemSource: getItemSource, getItemSourceString: getItemSourceString}; }); // Copyright 2016 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. cr.define('extensions', function() { 'use strict'; var MAX_HEIGHT = 600; var MAX_WIDTH = 600; var MIN_HEIGHT = 300; var MIN_WIDTH = 300; var HEADER_PADDING = 40; var OptionsDialog = Polymer({ is: 'extensions-options-dialog', properties: { /** @private {Object} */ extensionOptions_: Object, /** @private {chrome.developerPrivate.ExtensionInfo} */ data_: Object, }, /** @param {chrome.developerPrivate.ExtensionInfo} data */ show: function(data) { this.data_ = data; if (!this.extensionOptions_) this.extensionOptions_ = document.createElement('ExtensionOptions'); this.extensionOptions_.extension = this.data_.id; this.extensionOptions_.onclose = this.close.bind(this); var bounded = function(min, max, val) { return Math.min(Math.max(min, val), max); }; var onSizeChanged = function(e) { var minHeaderWidth = this.$['icon-and-name-wrapper'].offsetWidth + this.$['close-button'].offsetWidth + HEADER_PADDING; var minWidth = Math.max(minHeaderWidth, MIN_WIDTH); this.$.main.style.height = bounded(MIN_HEIGHT, MAX_HEIGHT, e.height) + 'px'; this.$.main.style.width = bounded(minWidth, MAX_WIDTH, e.width) + 'px'; }.bind(this); this.extensionOptions_.onpreferredsizechanged = onSizeChanged; this.$.main.appendChild(this.extensionOptions_); this.$$('dialog').showModal(); onSizeChanged({height: 0, width: 0}); }, close: function() { this.$$('dialog').close(); }, }); return {OptionsDialog: OptionsDialog}; }); // Copyright 2016 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. cr.define('extensions', function() { 'use strict'; /** @interface */ function PackDialogDelegate() {} PackDialogDelegate.prototype = { /** * Opens a file browser for the user to select the root directory. * @return {Promise} A promise that is resolved with the path the * user selected. */ choosePackRootDirectory: assertNotReached, /** * Opens a file browser for the user to select the private key file. * @return {Promise} A promise that is resolved with the path the * user selected. */ choosePrivateKeyPath: assertNotReached, /** * Packs the extension into a .crx. * @param {string} rootPath * @param {string} keyPath */ packExtension: assertNotReached, }; var PackDialog = Polymer({ is: 'extensions-pack-dialog', properties: { /** @type {extensions.PackDialogDelegate} */ delegate: Object, /** @private */ packDirectory_: String, /** @private */ keyFile_: String, }, show: function() { this.$$('dialog').showModal(); }, close: function() { this.$$('dialog').close(); }, /** @private */ onRootBrowse_: function() { this.delegate.choosePackRootDirectory().then(function(path) { if (path) this.set('packDirectory_', path); }.bind(this)); }, /** @private */ onKeyBrowse_: function() { this.delegate.choosePrivateKeyPath().then(function(path) { if (path) this.set('keyFile_', path); }.bind(this)); }, /** @private */ onConfirmTap_: function() { this.delegate.packExtension(this.packDirectory_, this.keyFile_); this.close(); }, }); return {PackDialog: PackDialog, PackDialogDelegate: PackDialogDelegate}; }); // Copyright 2015 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. cr.define('extensions', function() { 'use strict'; /** * @constructor * @implements {extensions.ItemDelegate} * @implements {extensions.SidebarDelegate} * @implements {extensions.PackDialogDelegate} * @implements {extensions.ErrorPageDelegate} */ function Service() {} Service.prototype = { /** @private {boolean} */ isDeleting_: false, /** @param {extensions.Manager} manager */ managerReady: function(manager) { /** @private {extensions.Manager} */ this.manager_ = manager; this.manager_.sidebar.setDelegate(this); this.manager_.set('itemDelegate', this); this.manager_.packDialog.set('delegate', this); this.manager_.errorPage.delegate = this; var keyboardShortcuts = this.manager_.keyboardShortcuts; keyboardShortcuts.addEventListener( 'shortcut-updated', this.onExtensionCommandUpdated_.bind(this)); keyboardShortcuts.addEventListener( 'shortcut-capture-started', this.onShortcutCaptureChanged_.bind(this, true)); keyboardShortcuts.addEventListener( 'shortcut-capture-ended', this.onShortcutCaptureChanged_.bind(this, false)); chrome.developerPrivate.onProfileStateChanged.addListener( this.onProfileStateChanged_.bind(this)); chrome.developerPrivate.onItemStateChanged.addListener( this.onItemStateChanged_.bind(this)); chrome.developerPrivate.getExtensionsInfo( {includeDisabled: true, includeTerminated: true}, function(extensions) { /** @private {Array} */ this.extensions_ = extensions; for (let extension of extensions) this.manager_.addItem(extension); var id = new URLSearchParams(location.search).get('id'); if (id) { var data = this.extensions_.find(function(e) { return e.id == id; }); if (data) this.manager_.showItemDetails(data); } }.bind(this)); chrome.developerPrivate.getProfileConfiguration( this.onProfileStateChanged_.bind(this)); }, /** * @param {chrome.developerPrivate.ProfileInfo} profileInfo * @private */ onProfileStateChanged_: function(profileInfo) { this.manager_.set('inDevMode', profileInfo.inDeveloperMode); }, /** * @param {chrome.developerPrivate.EventData} eventData * @private */ onItemStateChanged_: function(eventData) { var currentIndex = this.extensions_.findIndex(function(extension) { return extension.id == eventData.item_id; }); var EventType = chrome.developerPrivate.EventType; switch (eventData.event_type) { case EventType.VIEW_REGISTERED: case EventType.VIEW_UNREGISTERED: case EventType.INSTALLED: case EventType.LOADED: case EventType.UNLOADED: case EventType.ERROR_ADDED: case EventType.ERRORS_REMOVED: case EventType.PREFS_CHANGED: // |extensionInfo| can be undefined in the case of an extension // being unloaded right before uninstallation. There's nothing to do // here. if (!eventData.extensionInfo) break; if (currentIndex >= 0) { this.extensions_[currentIndex] = eventData.extensionInfo; this.manager_.updateItem(eventData.extensionInfo); } else { this.extensions_.push(eventData.extensionInfo); this.manager_.addItem(eventData.extensionInfo); } break; case EventType.UNINSTALLED: this.manager_.removeItem(this.extensions_[currentIndex]); this.extensions_.splice(currentIndex, 1); break; default: assertNotReached(); } }, /** * Opens a file browser dialog for the user to select a file (or directory). * @param {chrome.developerPrivate.SelectType} selectType * @param {chrome.developerPrivate.FileType} fileType * @return {Promise} The promise to be resolved with the selected * path. */ chooseFilePath_: function(selectType, fileType) { return new Promise(function(resolve, reject) { chrome.developerPrivate.choosePath( selectType, fileType, function(path) { if (chrome.runtime.lastError && chrome.runtime.lastError != 'File selection was canceled.') { reject(chrome.runtime.lastError); } else { resolve(path || ''); } }); }); }, /** * Updates an extension command. * @param {!CustomEvent} e * @private */ onExtensionCommandUpdated_: function(e) { chrome.developerPrivate.updateExtensionCommand({ extensionId: e.detail.item, commandName: e.detail.commandName, keybinding: e.detail.keybinding, }); }, /** * Called when shortcut capturing changes in order to suspend or re-enable * global shortcut handling. This is important so that the shortcuts aren't * processed normally as the user types them. * TODO(devlin): From very brief experimentation, it looks like preventing * the default handling on the event also does this. Investigate more in the * future. * @param {boolean} isCapturing * @param {!CustomEvent} e * @private */ onShortcutCaptureChanged_: function(isCapturing, e) { chrome.developerPrivate.setShortcutHandlingSuspended(isCapturing); }, /** @override */ deleteItem: function(id) { if (this.isDeleting_) return; this.isDeleting_ = true; chrome.management.uninstall(id, {showConfirmDialog: true}, function() { // The "last error" was almost certainly the user canceling the dialog. // Do nothing. We only check it so we don't get noisy logs. /** @suppress {suspiciousCode} */ chrome.runtime.lastError; this.isDeleting_ = false; }.bind(this)); }, /** @override */ setItemEnabled: function(id, isEnabled) { chrome.management.setEnabled(id, isEnabled); }, /** @override */ setItemAllowedIncognito: function(id, isAllowedIncognito) { chrome.developerPrivate.updateExtensionConfiguration({ extensionId: id, incognitoAccess: isAllowedIncognito, }); }, /** @override */ setItemAllowedOnFileUrls: function(id, isAllowedOnFileUrls) { chrome.developerPrivate.updateExtensionConfiguration({ extensionId: id, fileAccess: isAllowedOnFileUrls, }); }, /** @override */ setItemAllowedOnAllSites: function(id, isAllowedOnAllSites) { chrome.developerPrivate.updateExtensionConfiguration({ extensionId: id, runOnAllUrls: isAllowedOnAllSites, }); }, /** @override */ setItemCollectsErrors: function(id, collectsErrors) { chrome.developerPrivate.updateExtensionConfiguration({ extensionId: id, errorCollection: collectsErrors, }); }, /** @override */ inspectItemView: function(id, view) { chrome.developerPrivate.openDevTools({ extensionId: id, renderProcessId: view.renderProcessId, renderViewId: view.renderViewId, incognito: view.incognito, }); }, /** @override */ repairItem: function(id) { chrome.developerPrivate.repairExtension(id); }, /** @override */ showItemOptionsPage: function(id) { var extension = this.extensions_.find(function(extension) { return extension.id == id; }); assert(extension && extension.optionsPage); if (extension.optionsPage.openInTab) chrome.developerPrivate.showOptions(id); else this.manager_.optionsDialog.show(extension); }, /** @override */ setProfileInDevMode: function(inDevMode) { chrome.developerPrivate.updateProfileConfiguration( {inDeveloperMode: inDevMode}); }, /** @override */ loadUnpacked: function() { chrome.developerPrivate.loadUnpacked({failQuietly: true}); }, /** @override */ choosePackRootDirectory: function() { return this.chooseFilePath_( chrome.developerPrivate.SelectType.FOLDER, chrome.developerPrivate.FileType.LOAD); }, /** @override */ choosePrivateKeyPath: function() { return this.chooseFilePath_( chrome.developerPrivate.SelectType.FILE, chrome.developerPrivate.FileType.PEM); }, /** @override */ packExtension: function(rootPath, keyPath) { chrome.developerPrivate.packDirectory(rootPath, keyPath); }, /** @override */ updateAllExtensions: function() { chrome.developerPrivate.autoUpdate(); }, /** @override */ deleteErrors: function(extensionId, errorIds, type) { chrome.developerPrivate.deleteExtensionErrors({ extensionId: extensionId, errorIds: errorIds, type: type, }); }, /** @override */ requestFileSource: function(args) { return new Promise(function(resolve, reject) { chrome.developerPrivate.requestFileSource(args, function(code) { resolve(code); }); }); }, }; cr.addSingletonGetter(Service); return {Service: Service}; }); // Copyright 2016 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. cr.define('extensions', function() { 'use strict'; // The UI to display and manage keyboard shortcuts set for extension commands. var ShortcutInput = Polymer({ is: 'extensions-shortcut-input', behaviors: [I18nBehavior], properties: { item: { type: String, value: '', }, commandName: { type: String, value: '', }, shortcut: { type: String, value: '', }, /** @private */ capturing_: { type: Boolean, value: false, }, /** @private */ pendingShortcut_: { type: String, value: '', }, }, ready: function() { var node = this.$['input']; node.addEventListener('mouseup', this.startCapture_.bind(this)); node.addEventListener('blur', this.endCapture_.bind(this)); node.addEventListener('focus', this.startCapture_.bind(this)); node.addEventListener('keydown', this.onKeyDown_.bind(this)); node.addEventListener('keyup', this.onKeyUp_.bind(this)); }, /** @private */ startCapture_: function() { if (this.capturing_) return; this.capturing_ = true; this.fire('shortcut-capture-started'); }, /** @private */ endCapture_: function() { if (!this.capturing_) return; this.pendingShortcut_ = ''; this.capturing_ = false; this.$['input'].blur(); this.fire('shortcut-capture-ended'); }, /** * @param {!KeyboardEvent} e * @private */ onKeyDown_: function(e) { if (e.keyCode == extensions.Key.Escape) { if (!this.capturing_) { // If we're not currently capturing, allow escape to propagate. return; } // Otherwise, escape cancels capturing. this.endCapture_(); e.preventDefault(); e.stopPropagation(); return; } if (e.keyCode == extensions.Key.Tab) { // Allow tab propagation for keyboard navigation. return; } if (!this.capturing_) this.startCapture_(); this.handleKey_(e); }, /** * @param {!KeyboardEvent} e * @private */ onKeyUp_: function(e) { if (e.keyCode == extensions.Key.Escape || e.keyCode == extensions.Key.Tab) return; this.handleKey_(e); }, /** * @param {!KeyboardEvent} e * @private */ handleKey_: function(e) { // While capturing, we prevent all events from bubbling, to prevent // shortcuts lacking the right modifier (F3 for example) from activating // and ending capture prematurely. e.preventDefault(); e.stopPropagation(); // We don't allow both Ctrl and Alt in the same keybinding. // TODO(devlin): This really should go in extensions.hasValidModifiers, // but that requires updating the existing page as well. if ((e.ctrlKey && e.altKey) || !extensions.hasValidModifiers(e)) { this.pendingShortcut_ = 'invalid'; return; } this.pendingShortcut_ = extensions.keystrokeToString(e); if (extensions.isValidKeyCode(e.keyCode)) { this.commitPending_(); this.endCapture_(); } }, /** @private */ commitPending_: function() { this.shortcut = this.pendingShortcut_; this.fire('shortcut-updated', {keybinding: this.shortcut, item: this.item, commandName: this.commandName}); }, /** * @return {string} The text to be displayed in the shortcut field. * @private */ computeText_: function() { if (this.capturing_) return this.pendingShortcut_ || this.i18n('shortcutTypeAShortcut'); return this.shortcut || this.i18n('shortcutNotSet'); }, /** * @return {boolean} Whether the clear button is hidden. * @private */ computeClearHidden_: function() { // We don't want to show the clear button if the input is currently // capturing a new shortcut or if there is no shortcut to clear. return this.capturing_ || !this.shortcut; }, /** @private */ onClearTap_: function() { if (this.shortcut) { this.pendingShortcut_ = ''; this.commitPending_(); } }, }); return {ShortcutInput: ShortcutInput}; }); // Copyright 2015 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. cr.exportPath('extensions'); // Declare this here to make closure compiler happy, and us sad. /** @enum {number} */ extensions.ShowingType = { EXTENSIONS: 0, APPS: 1, }; cr.define('extensions', function() { /** @interface */ var SidebarDelegate = function() {}; SidebarDelegate.prototype = { /** * Toggles whether or not the profile is in developer mode. * @param {boolean} inDevMode */ setProfileInDevMode: assertNotReached, /** Opens the dialog to load unpacked extensions. */ loadUnpacked: assertNotReached, /** Updates all extensions. */ updateAllExtensions: assertNotReached, }; /** @interface */ var SidebarListDelegate = function() {}; SidebarListDelegate.prototype = { /** * Shows the given type of item. * @param {extensions.ShowingType} type */ showType: assertNotReached, /** Shows the keyboard shortcuts page. */ showKeyboardShortcuts: assertNotReached, /** Shows the pack extension dialog. */ showPackDialog: assertNotReached, }; var Sidebar = Polymer({ is: 'extensions-sidebar', behaviors: [I18nBehavior], properties: { inDevMode: { type: Boolean, value: false, }, }, /** @param {extensions.SidebarDelegate} delegate */ setDelegate: function(delegate) { /** @private {extensions.SidebarDelegate} */ this.delegate_ = delegate; }, /** @param {extensions.SidebarListDelegate} listDelegate */ setListDelegate: function(listDelegate) { /** @private {extensions.SidebarListDelegate} */ this.listDelegate_ = listDelegate; }, /** @private */ onExtensionsTap_: function() { this.listDelegate_.showType(extensions.ShowingType.EXTENSIONS); }, /** @private */ onAppsTap_: function() { this.listDelegate_.showType(extensions.ShowingType.APPS); }, /** @private */ onDevModeChange_: function() { this.delegate_.setProfileInDevMode( this.$['developer-mode-checkbox'].checked); }, /** @private */ onLoadUnpackedTap_: function() { this.delegate_.loadUnpacked(); }, /** @private */ onPackTap_: function() { this.listDelegate_.showPackDialog(); }, /** @private */ onUpdateNowTap_: function() { this.delegate_.updateAllExtensions(); }, /** @private */ onKeyboardShortcutsTap_: function() { this.listDelegate_.showKeyboardShortcuts(); }, }); return { Sidebar: Sidebar, SidebarDelegate: SidebarDelegate, SidebarListDelegate: SidebarListDelegate, }; }); { // chrome-extension://gfdkimpbcpahaombhbimeihdjnejgicl/ "key": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDMZElzFX2J1g1nRQ/8S3rg/1CjFyDltWOxQg+9M8aVgNVxbutEWFQz+oQzIP9BB67mJifULgiv12ToFKsae4NpEUR8sPZjiKDIHumc6pUdixOm8SJ5Rs16SMR6+VYxFUjlVW+5CA3IILptmNBxgpfyqoK0qRpBDIhGk1KDEZ4zqQIDAQAB", "name": "Feedback", "version": "1.0", "manifest_version": 2, "incognito" : "split", "description": "User feedback extension", "icons": { "32": "images/icon32.png", "64": "images/icon64.png" }, "permissions": [ "feedbackPrivate", "chrome://resources/" ], "app": { "background": { "scripts": ["js/event_handler.js"] }, "content_security_policy": "default-src 'none'; script-src 'self' blob: filesystem: chrome://resources; style-src 'unsafe-inline' blob: chrome: file: filesystem: data: *; img-src * blob: chrome: file: filesystem: data:; media-src 'self' blob: filesystem:" }, "display_in_launcher": false, "display_in_new_tab_page": false } // Copyright 2016 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. var Feedback = {}; /** * API invoked by the browser MdFeedbackWebUIMessageHandler to communicate * with this UI. */ Feedback.UI = class { /** * Populates the feedback form with data. * * @param {{email: (string|undefined), * url: (string|undefined)}} data * Parameters in data: * email - user's email, if available. * url - url of the tab the user was on before triggering feedback. */ static setData(data) { $('container').email = data['email']; $('container').url = data['url']; } }; /** API invoked by this UI to communicate with the browser WebUI message * handler. */ Feedback.BrowserApi = class { /** * Requests data to initialize the WebUI with. * The data will be returned via Feedback.UI.setData. */ static requestData() { chrome.send('requestData'); } }; window.addEventListener('DOMContentLoaded', function() { Feedback.BrowserApi.requestData(); }); // Copyright 2016 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. // This Polymer element is used as a container for all the feedback // elements. Based on a number of factors, it determines which elements // to show and what will be submitted to the feedback servers. Polymer({ is: 'feedback-container', properties: { /** * The user's email, if available. * @type {string|undefined} */ email: { type: String, }, /** * The URL of the page the user was on before sending feedback. * @type {string|undefined} */ url: { type: String, }, }, ready: function() { // Retrieves the feedback privacy note text, if it exists. On non-official // branded builds, the string is not defined. this.$.privacyNote.innerHTML = loadTimeData.valueExists('privacyNote') ? loadTimeData.getString('privacyNote') : ''; }, }); VYoF~ׯ-&0TqICQŠ;4W^.ݡwiӔwvo.&p*ƪۜ3V- U𾦼.ZCC{282ʕW6EHKA r!thR4,F H β6a"oW), ՇW| I@ AMY.`2)Ҹ+U9vpi%q0K2w^.Gr"H=xsSb¯Z7=Z9zXZ Q ."p6=hkle$X_$K!פ \KAkRzW*6K6l,=(+ `lSi!9SR `j>$| 6~~ߓp>J.!NL{^YCrk&+ ?|02F9U̼wE3six*'͟Q+--?)c^s#AQ>!9@ALke(0%)_$yPA|_'VK&-++/Sֲ1k͗\g-|)1oV?WURB&2fkyt<6qzc[)Gw <-g8_|Ma%T|8x2/&B̭(u0z!if&oeRY0[ɣk' ל -xrVTўu ?sm#b9Q|X=#ˣzm"ӓbƣrui"גv&eH 閻 \eQב|n#_( _qU^ ]O0+5!I l41.&Mav'UǮl%N Ij><>Ԫ|,dIwpŴjxek&K! h4H j0B({]5j| +2b"JZn뗫o7Ps1N&\ L KnJцS v>)?1},#. XY+NN el.@/ʣ)gYr-k%mdoGK/5D/sUM֏H_ V+ ]"ĨZk2h,Mڭqc)'x$NQ da:eMDCJBZU z(=V% F_ll.y~HwۻĮ֤A̕@2-h F޹OsD~Pu/ye{Kwr~ӱ!CH=:DVw|T!]2Š~*.QʪI θ\rˉpLzpQ_Mn Y`+qcE:ľK):tug þ)$ ߫CGlDݠ{}(ʜ/ZsVRwS'αyR0IEV`&fd;% ?3br5qRWn8}WZ)[/l.fѤ-c6(]7!)%I̓cQ3Ù9g.TK#硫޾LNFd5M)(EAȒafyerSidLΙxl i$A[*&%3.Q& 4U,pt9MLD3*ᄴ7Y8\&hRHGԇYgdG=4qtR(k;[g}r ׼'=;<:D&*TÅ1qM+;E%b*+i_n3rM ȃ/pQvEYBU|w(H҄g<>߮z.܋^!.$֦(*OzNefuxҊdaҠݺq5r@):Л6R`;h>hEݳ{/>Q60T'< P aJE"5k r͖ QUIapUZזTIjn% IhPkC4u0ҐOgTJ]}ּjm# ?HwRș198 $ ,YvO@9@[8e8c9~GA mQ(be o.D0ly*'() EcQu~tsuv8r|0ءmQVv r"9 ;ٟ @$h%Vl5ݛ7oeaоh#U ,ؕE0kt3E귡ƯnbVn{62G5]IK;zDmԳy-PX_ؤH_΀Psny> Nnv+,}^t[?968v1mrZ[4my]݆qCKnW (v8Hv7ўA6WɆ\wK;ܐѾ -ꎞ8:yao=NXpE7v;] @U// Copyright (c) 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. // // Copyright 2014 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. cr.define('uber', function() { var PageManager = cr.ui.pageManager.PageManager; /** * A PageManager observer that updates the uber page. * @constructor * @implements {cr.ui.pageManager.PageManager.Observer} */ function PageManagerObserver() {} PageManagerObserver.prototype = { __proto__: PageManager.Observer.prototype, /** * Informs the uber page when a top-level overlay is opened or closed. * @param {cr.ui.pageManager.Page} page The page that is being shown or was * hidden. * @override */ onPageVisibilityChanged: function(page) { if (PageManager.isTopLevelOverlay(page)) { if (page.visible) uber.invokeMethodOnParent('beginInterceptingEvents'); else uber.invokeMethodOnParent('stopInterceptingEvents'); } }, /** * Uses uber to set the title. * @param {string} title The title to set. * @override */ updateTitle: function(title) { uber.setTitle(title); }, /** * Pushes the current page onto the history stack, replacing the current * entry if appropriate. * @param {string} path The path of the page to push onto the stack. * @param {boolean} replace If true, allow no history events to be created. * @override */ updateHistory: function(path, replace) { var historyFunction = replace ? uber.replaceState : uber.pushState; historyFunction({}, path); }, }; // Export return { PageManagerObserver: PageManagerObserver }; }); // // Copyright (c) 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. /** * @fileoverview A collection of utility methods for UberPage and its contained * pages. */ cr.define('uber', function() { /** * Fixed position header elements on the page to be shifted by handleScroll. * @type {NodeList} */ var headerElements; /** * This should be called by uber content pages when DOM content has loaded. */ function onContentFrameLoaded() { headerElements = document.getElementsByTagName('header'); document.addEventListener('scroll', handleScroll); document.addEventListener('mousedown', handleMouseDownInFrame, true); invokeMethodOnParent('ready'); // Prevent the navigation from being stuck in a disabled state when a // content page is reloaded while an overlay is visible (crbug.com/246939). invokeMethodOnParent('stopInterceptingEvents'); // Trigger the scroll handler to tell the navigation if our page started // with some scroll (happens when you use tab restore). handleScroll(); window.addEventListener('message', handleWindowMessage); } /** * Handles scroll events on the document. This adjusts the position of all * headers and updates the parent frame when the page is scrolled. */ function handleScroll() { var scrollLeft = scrollLeftForDocument(document); var offset = scrollLeft * -1; for (var i = 0; i < headerElements.length; i++) { // As a workaround for http://crbug.com/231830, set the transform to // 'none' rather than 0px. headerElements[i].style.webkitTransform = offset ? 'translateX(' + offset + 'px)' : 'none'; } invokeMethodOnParent('adjustToScroll', scrollLeft); } /** * Tells the parent to focus the current frame if the mouse goes down in the * current frame (and it doesn't already have focus). * @param {Event} e A mousedown event. */ function handleMouseDownInFrame(e) { if (!e.isSynthetic && !document.hasFocus()) window.focus(); } /** * Handles 'message' events on window. * @param {Event} e The message event. */ function handleWindowMessage(e) { e = /** @type {!MessageEvent} */(e); if (e.data.method === 'frameSelected') { handleFrameSelected(); } else if (e.data.method === 'mouseWheel') { handleMouseWheel( /** @type {{deltaX: number, deltaY: number}} */(e.data.params)); } else if (e.data.method === 'mouseDown') { handleMouseDown(); } else if (e.data.method === 'popState') { handlePopState(e.data.params.state, e.data.params.path); } } /** * This is called when a user selects this frame via the navigation bar * frame (and is triggered via postMessage() from the uber page). */ function handleFrameSelected() { setScrollTopForDocument(document, 0); } /** * Called when a user mouse wheels (or trackpad scrolls) over the nav frame. * The wheel event is forwarded here and we scroll the body. * There's no way to figure out the actual scroll amount for a given delta. * It differs for every platform and even initWebKitWheelEvent takes a * pixel amount instead of a wheel delta. So we just choose something * reasonable and hope no one notices the difference. * @param {{deltaX: number, deltaY: number}} params A structure that holds * wheel deltas in X and Y. */ function handleMouseWheel(params) { window.scrollBy(-params.deltaX * 49 / 120, -params.deltaY * 49 / 120); } /** * Fire a synthetic mousedown on the body to dismiss transient things like * bubbles or menus that listen for mouse presses outside of their UI. We * dispatch a fake mousedown rather than a 'mousepressedinnavframe' so that * settings/history/extensions don't need to know about their embedder. */ function handleMouseDown() { var mouseEvent = new MouseEvent('mousedown'); mouseEvent.isSynthetic = true; document.dispatchEvent(mouseEvent); } /** * Called when the parent window restores some state saved by uber.pushState * or uber.replaceState. Simulates a popstate event. * @param {PopStateEvent} state A state object for replaceState and pushState. * @param {string} path The path the page navigated to. * @suppress {checkTypes} */ function handlePopState(state, path) { window.history.replaceState(state, '', path); window.dispatchEvent(new PopStateEvent('popstate', {state: state})); } /** * @return {boolean} Whether this frame has a parent. */ function hasParent() { return window != window.parent; } /** * Invokes a method on the parent window (UberPage). This is a convenience * method for API calls into the uber page. * @param {string} method The name of the method to invoke. * @param {?=} opt_params Optional property bag of parameters to pass to the * invoked method. */ function invokeMethodOnParent(method, opt_params) { if (!hasParent()) return; invokeMethodOnWindow(window.parent, method, opt_params, 'chrome://chrome'); } /** * Invokes a method on the target window. * @param {string} method The name of the method to invoke. * @param {?=} opt_params Optional property bag of parameters to pass to the * invoked method. * @param {string=} opt_url The origin of the target window. */ function invokeMethodOnWindow(targetWindow, method, opt_params, opt_url) { var data = {method: method, params: opt_params}; targetWindow.postMessage(data, opt_url ? opt_url : '*'); } /** * Updates the page's history state. If the page is embedded in a child, * forward the information to the parent for it to manage history for us. This * is a replacement of history.replaceState and history.pushState. * @param {Object} state A state object for replaceState and pushState. * @param {string} path The path the page navigated to. * @param {boolean} replace If true, navigate with replacement. */ function updateHistory(state, path, replace) { var historyFunction = replace ? window.history.replaceState : window.history.pushState; if (hasParent()) { // If there's a parent, always replaceState. The parent will do the actual // pushState. historyFunction = window.history.replaceState; invokeMethodOnParent('updateHistory', { state: state, path: path, replace: replace}); } historyFunction.call(window.history, state, '', '/' + path); } /** * Sets the current title for the page. If the page is embedded in a child, * forward the information to the parent. This is a replacement for setting * document.title. * @param {string} title The new title for the page. */ function setTitle(title) { document.title = title; invokeMethodOnParent('setTitle', {title: title}); } /** * Pushes new history state for the page. If the page is embedded in a child, * forward the information to the parent; when embedded, all history entries * are attached to the parent. This is a replacement of history.pushState. * @param {Object} state A state object for replaceState and pushState. * @param {string} path The path the page navigated to. */ function pushState(state, path) { updateHistory(state, path, false); } /** * Replaces the page's history state. If the page is embedded in a child, * forward the information to the parent; when embedded, all history entries * are attached to the parent. This is a replacement of history.replaceState. * @param {Object} state A state object for replaceState and pushState. * @param {string} path The path the page navigated to. */ function replaceState(state, path) { updateHistory(state, path, true); } return { invokeMethodOnParent: invokeMethodOnParent, invokeMethodOnWindow: invokeMethodOnWindow, onContentFrameLoaded: onContentFrameLoaded, pushState: pushState, replaceState: replaceState, setTitle: setTitle, }; }); (function() { var HelpPage = help.HelpPage; var PageManager = cr.ui.pageManager.PageManager; /** * DOMContentLoaded handler, sets up the page. */ function load() { PageManager.register(HelpPage.getInstance()); if (help.ChannelChangePage) { PageManager.registerOverlay(help.ChannelChangePage.getInstance(), HelpPage.getInstance()); } PageManager.addObserver(new uber.PageManagerObserver()); PageManager.initialize(HelpPage.getInstance()); uber.onContentFrameLoaded(); var pageName = PageManager.getPageNameFromPath(); // Still update history so that chrome://help/nonexistant redirects // appropriately to chrome://help/. If the URL matches, updateHistory // will avoid adding the extra state. var updateHistory = true; PageManager.showPageByName(pageName, updateHistory, {replaceState: true}); } document.addEventListener('DOMContentLoaded', load); /** * Listener for the |beforeunload| event. */ window.onbeforeunload = function() { PageManager.willClose(); }; /** * Listener for the |popstate| event. * @param {Event} e The |popstate| event. */ window.onpopstate = function(e) { var pageName = PageManager.getPageNameFromPath(); PageManager.setState(pageName, location.hash, e.state); }; })(); // Copyright 2014 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. cr.define('help', function() { var Page = cr.ui.pageManager.Page; var PageManager = cr.ui.pageManager.PageManager; /** * Encapsulated handling of the About page. Called 'help' internally to avoid * confusion with generic AboutUI (about:version, about:sandbox, etc.). */ function HelpPage() { var id = loadTimeData.valueExists('aboutOverlayTabTitle') ? 'aboutOverlayTabTitle' : 'aboutTitle'; Page.call(this, 'help', loadTimeData.getString(id), 'help-page'); } cr.addSingletonGetter(HelpPage); HelpPage.prototype = { __proto__: Page.prototype, /** * List of the channel names. Should be ordered in increasing level of * stability. * @private */ channelList_: ['dev-channel', 'beta-channel', 'stable-channel'], /** * Name of the channel the device is currently on. * @private */ currentChannel_: null, /** * Name of the channel the device is supposed to be on. * @private */ targetChannel_: null, /** * Last status received from the version updater. * @private */ status_: null, /** * Last message received from the version updater. * @private */ message_: null, /** * True if user is allowed to change channels, false otherwise. * @private */ canChangeChannel_: false, /** * True if we have never checked for available updates. * @private */ haveNeverCheckedForUpdates_: true, /** * Last EndofLife status received from the version updater. * @private */ eolStatus_: null, /** * Last EndofLife message received from the version updater. * @private */ eolMessage_: null, /** @override */ initializePage: function() { Page.prototype.initializePage.call(this); $('product-license').innerHTML = loadTimeData.getString('productLicense'); if (cr.isChromeOS) { $('product-os-license').innerHTML = loadTimeData.getString('productOsLicense'); $('eol-learnMore').innerHTML = loadTimeData.getString('eolLearnMore'); } var productTOS = $('product-tos'); if (productTOS) productTOS.innerHTML = loadTimeData.getString('productTOS'); $('get-help').onclick = function() { chrome.send('openHelpPage'); }; // this.maybeSetOnClick_($('more-info-expander'), this.toggleMoreInfo_.bind(this)); this.maybeSetOnClick_($('promote'), function() { chrome.send('promoteUpdater'); }); this.maybeSetOnClick_($('relaunch'), function() { chrome.send('relaunchNow'); }); if (cr.isChromeOS) { this.maybeSetOnClick_($('relaunch-and-powerwash'), function() { chrome.send('relaunchAndPowerwash'); }); this.channelTable_ = { 'stable-channel': { 'name': loadTimeData.getString('stable'), 'label': loadTimeData.getString('currentChannelStable'), }, 'beta-channel': { 'name': loadTimeData.getString('beta'), 'label': loadTimeData.getString('currentChannelBeta') }, 'dev-channel': { 'name': loadTimeData.getString('dev'), 'label': loadTimeData.getString('currentChannelDev') } }; } this.maybeSetOnClick_($('about-done'), function() { // Event listener for the close button when shown as an overlay. PageManager.closeOverlay(); }); var self = this; var channelChanger = $('channel-changer'); if (channelChanger) { channelChanger.onchange = function(event) { self.setChannel_(event.target.value, false); }; } if (cr.isChromeOS) { // Add event listener for the check for and apply updates button. this.maybeSetOnClick_($('request-update'), function() { self.setUpdateStatus_('checking'); $('request-update').disabled = true; chrome.send('requestUpdate'); }); $('change-channel').onclick = function() { PageManager.showPageByName('channel-change-page', false); }; var channelChangeDisallowedError = document.createElement('div'); channelChangeDisallowedError.className = 'channel-change-error-bubble'; var channelChangeDisallowedIcon = document.createElement('div'); channelChangeDisallowedIcon.className = 'help-page-icon channel-change-error-icon'; channelChangeDisallowedError.appendChild(channelChangeDisallowedIcon); var channelChangeDisallowedText = document.createElement('div'); channelChangeDisallowedText.className = 'channel-change-error-text'; channelChangeDisallowedText.textContent = loadTimeData.getString('channelChangeDisallowedMessage'); channelChangeDisallowedError.appendChild(channelChangeDisallowedText); $('channel-change-disallowed-icon').onclick = function() { PageManager.showBubble(channelChangeDisallowedError, $('channel-change-disallowed-icon'), $('help-container'), cr.ui.ArrowLocation.TOP_END); }; // Unhide the regulatory label if/when the image loads. $('regulatory-label').onload = function() { $('regulatory-label-container').hidden = false; }; $('controlled-feature-icon').onclick = function(e) { var content = /** @type {HTMLElement} */( document.createElement('div')); content.textContent = loadTimeData.getString('updateDisabledByPolicy'); var bubble = new cr.ui.AutoCloseBubble; bubble.id = 'controlled-feature-bubble'; bubble.anchorNode = $('controlled-feature-icon'); bubble.domSibling = $('controlled-feature-icon'); bubble.arrowLocation = cr.ui.ArrowLocation.TOP_END; bubble.content = content; bubble.show(); }; } var logo = $('product-logo'); logo.onclick = function(e) { logo.animate({ transform: ['none', 'rotate(-10turn)'], }, /** @type {!KeyframeEffectOptions} */({ duration: 500, easing: 'cubic-bezier(1, 0, 0, 1)', })); }; // Attempt to update. chrome.send('onPageLoaded'); }, /** @override */ didClosePage: function() { this.setMoreInfoVisible_(false); }, /** * Sets the visible state of the 'More Info' section. * @param {boolean} visible Whether the section should be visible. * @private */ setMoreInfoVisible_: function(visible) { var moreInfo = $('more-info-container'); if (!moreInfo || visible == moreInfo.classList.contains('visible')) return; moreInfo.classList.toggle('visible', visible); moreInfo.style.height = visible ? moreInfo.scrollHeight + 'px' : ''; moreInfo.addEventListener('webkitTransitionEnd', function(event) { $('more-info-expander').textContent = visible ? loadTimeData.getString('hideMoreInfo') : loadTimeData.getString('showMoreInfo'); }); }, /** * Toggles the visible state of the 'More Info' section. * @private */ toggleMoreInfo_: function() { var moreInfo = $('more-info-container'); this.setMoreInfoVisible_(!moreInfo.classList.contains('visible')); }, /** * Assigns |method| to the onclick property of |el| if |el| exists. * @param {HTMLElement} el The element on which to set the click handler. * @param {Function} method The click handler. * @private */ maybeSetOnClick_: function(el, method) { if (el) el.onclick = method; }, /** * @param {string} state The state of the update. * private */ setUpdateImage_: function(state) { $('update-status-icon').className = 'help-page-icon ' + state; }, /** * @return {boolean} True, if new channel switcher UI is used, * false otherwise. * @private */ isNewChannelSwitcherUI_: function() { return !loadTimeData.valueExists('disableNewChannelSwitcherUI'); }, /** * @return {boolean} True if target and current channels are not null and * not equal. * @private */ channelsDiffer_: function() { var current = this.currentChannel_; var target = this.targetChannel_; return (current != null && target != null && current != target); }, /** * @return {boolean} True if target channel is more stable than the current * one, and false otherwise. * @private */ targetChannelIsMoreStable_: function() { var current = this.currentChannel_; var target = this.targetChannel_; if (current == null || target == null) return false; var currentIndex = this.channelList_.indexOf(current); var targetIndex = this.channelList_.indexOf(target); if (currentIndex < 0 || targetIndex < 0) return false; return currentIndex < targetIndex; }, /** * @param {string} status The status of the update. * @param {string} message Failure message to display. * @private */ setUpdateStatus_: function(status, message) { var oldStatus = this.status_; this.status_ = status; if (oldStatus != status && oldStatus == 'disabled_by_admin') { // If the auto update policy was recently re-enabled, then we'll // re-enable the 'request-update' button. this.haveNeverCheckedForUpdates_ = true; } if (status == 'checking') this.haveNeverCheckedForUpdates_ = false; this.message_ = message; this.updateUI_(); }, /** * @param {string} eolStatus: The EndofLife status of the device. * @param {string} eolMessage: The EndofLife message to display. * @private */ updateEolMessage_: function(eolStatus, eolMessage) { this.eolStatus_ = eolStatus; this.eolMessage_ = eolMessage; this.updateUI_(); }, /** * Updates UI elements on the page according to current state. * @private */ updateUI_: function() { var status = this.status_; var message = this.message_; var channel = this.targetChannel_; var eolStatus = this.eolStatus_; var eolMessage = this.eolMessage_; if (this.channelList_.indexOf(channel) >= 0) { $('current-channel').textContent = loadTimeData.getStringF( 'currentChannel', this.channelTable_[channel].label); this.updateChannelChangePageContainerVisibility_(); if (cr.isChromeOS) $('dev-channel-disclaimer').hidden = (channel != 'dev-channel'); } if (status == null) return; if (cr.isMac && $('update-status-message') && $('update-status-message').hidden) { // Chrome has reached the end of the line on this system. The // update-obsolete-system message is displayed. No other auto-update // status should be displayed. return; } if (status == 'checking') { this.setUpdateImage_('working'); $('update-status-message').innerHTML = loadTimeData.getString('updateCheckStarted'); } else if (status == 'updating') { this.setUpdateImage_('working'); if (this.channelsDiffer_()) { $('update-status-message').innerHTML = loadTimeData.getStringF('updatingChannelSwitch', this.channelTable_[channel].label); } else { $('update-status-message').innerHTML = loadTimeData.getStringF('updating'); } } else if (status == 'nearly_updated') { this.setUpdateImage_('up-to-date'); if (this.channelsDiffer_()) { $('update-status-message').innerHTML = loadTimeData.getString('successfulChannelSwitch'); } else { $('update-status-message').innerHTML = loadTimeData.getString('updateAlmostDone'); } } else if (status == 'updated') { this.setUpdateImage_('up-to-date'); $('update-status-message').innerHTML = loadTimeData.getString('upToDate'); } else if (status == 'failed') { this.setUpdateImage_('failed'); $('update-status-message').innerHTML = message; } else if (status == 'disabled_by_admin') { // This is the general behavior for non-chromeos. this.setUpdateImage_('disabled-by-admin'); $('update-status-message').innerHTML = message; } // Show EndofLife Strings if applicable if (eolStatus == 'device_supported') { $('eol-status-container').hidden = true; } else if (eolStatus == 'device_endoflife') { $('eol-message').innerHTML = eolMessage; $('eol-status-container').hidden = false; } if (cr.isChromeOS) { $('change-channel').disabled = !this.canChangeChannel_ || status == 'nearly_updated'; $('channel-change-disallowed-icon').hidden = this.canChangeChannel_; } // Following invariant must be established at the end of this function: // { ~$('relaunch_and_powerwash').hidden -> $('relaunch').hidden } var relaunchAndPowerwashHidden = true; if ($('relaunch-and-powerwash')) { // It's allowed to do powerwash only for customer devices, // when user explicitly decides to update to a more stable // channel. relaunchAndPowerwashHidden = !this.targetChannelIsMoreStable_() || status != 'nearly_updated'; $('relaunch-and-powerwash').hidden = relaunchAndPowerwashHidden; } if (cr.isChromeOS) { // Re-enable the update button if we are in a stale 'updated' status or // update has failed, and disable it if there's an update in progress or // updates are disabled by policy. // In addition, Update button will be disabled when device is in eol // status $('request-update').disabled = !((this.haveNeverCheckedForUpdates_ && status == 'updated') || status == 'failed') || (eolStatus == 'device_endoflife'); // If updates are disabled by policy, unhide the // controlled-feature-icon. $('controlled-feature-icon').hidden = (status != 'disabled_by_admin'); // If updates are no longer disabled by policy and the tooltip bubble // is present, we hide it. if (status != 'disabled_by_admin' && $('controlled-feature-bubble')) $('controlled-feature-bubble').hide(); } var container = $('update-status-container'); if (container) { container.hidden = status == 'disabled'; $('relaunch').hidden = (status != 'nearly_updated') || !relaunchAndPowerwashHidden; if (cr.isChromeOS) { // Assume the "updated" status is stale if we haven't checked yet. if (status == 'updated' && this.haveNeverCheckedForUpdates_ || status == 'disabled_by_admin' || eolStatus == 'device_endoflife') { container.hidden = true; } // Hide the request update button if auto-updating is disabled or // a relaunch button is showing. $('request-update').hidden = status == 'disabled' || !$('relaunch').hidden || !relaunchAndPowerwashHidden; } if (!cr.isMac) $('update-percentage').hidden = status != 'updating'; } }, /** * @param {number} progress The percent completion. * @private */ setProgress_: function(progress) { $('update-percentage').innerHTML = progress + '%'; }, /** * @param {string} message The allowed connection types message. * @private */ setAllowedConnectionTypesMsg_: function(message) { $('allowed-connection-types-message').innerText = message; }, /** * @param {boolean} visible Whether to show the message. * @private */ showAllowedConnectionTypesMsg_: function(visible) { $('allowed-connection-types-message').hidden = !visible; }, /** * @param {string} state The promote state to set. * @private */ setPromotionState_: function(state) { if (state == 'hidden') { $('promote').hidden = true; } else if (state == 'enabled') { $('promote').disabled = false; $('promote').hidden = false; } else if (state == 'disabled') { $('promote').disabled = true; $('promote').hidden = false; } }, /** * @param {boolean} obsolete Whether the system is obsolete. * @private */ setObsoleteSystem_: function(obsolete) { if ($('update-obsolete-system-container')) { $('update-obsolete-system-container').hidden = !obsolete; } }, /** * @param {boolean} endOfTheLine Whether the train has rolled into * the station. * @private */ setObsoleteSystemEndOfTheLine_: function(endOfTheLine) { if ($('update-obsolete-system-container') && !$('update-obsolete-system-container').hidden && $('update-status-message')) { $('update-status-message').hidden = endOfTheLine; if (endOfTheLine) { this.setUpdateImage_('failed'); } } }, /** * @param {string} version Version of Chrome OS. * @private */ setOSVersion_: function(version) { if (!cr.isChromeOS) console.error('OS version unsupported on non-CrOS'); $('os-version').parentNode.hidden = (version == ''); $('os-version').textContent = version; }, /** * @param {string} version Version of ARC. * @private */ setARCVersion_: function(version) { if (!cr.isChromeOS) console.error('ARC version unsupported on non-CrOS'); $('arc-version').parentNode.hidden = (version == ''); $('arc-version').textContent = version; }, /** * @param {string} firmware Firmware on Chrome OS. * @private */ setOSFirmware_: function(firmware) { if (!cr.isChromeOS) console.error('OS firmware unsupported on non-CrOS'); $('firmware').parentNode.hidden = (firmware == ''); $('firmware').textContent = firmware; }, /** * Updates page UI according to device owhership policy. * @param {boolean} isEnterpriseManaged True if the device is * enterprise managed. * @private */ updateIsEnterpriseManaged_: function(isEnterpriseManaged) { help.ChannelChangePage.updateIsEnterpriseManaged(isEnterpriseManaged); this.updateUI_(); }, /** * Updates name of the current channel, i.e. the name of the * channel the device is currently on. * @param {string} channel The name of the current channel. * @private */ updateCurrentChannel_: function(channel) { if (this.channelList_.indexOf(channel) < 0) return; this.currentChannel_ = channel; help.ChannelChangePage.updateCurrentChannel(channel); this.updateUI_(); }, /** * Updates name of the target channel, i.e. the name of the * channel the device is supposed to be. * @param {string} channel The name of the target channel. * @private */ updateTargetChannel_: function(channel) { if (this.channelList_.indexOf(channel) < 0) return; this.targetChannel_ = channel; help.ChannelChangePage.updateTargetChannel(channel); this.updateUI_(); }, /** * @param {boolean} enabled True if the release channel can be enabled. * @private */ updateEnableReleaseChannel_: function(enabled) { this.updateChannelChangerContainerVisibility_(enabled); this.canChangeChannel_ = enabled; this.updateUI_(); }, /** * Sets the device target channel. * @param {string} channel The name of the target channel. * @param {boolean} isPowerwashAllowed True iff powerwash is allowed. * @private */ setChannel_: function(channel, isPowerwashAllowed) { chrome.send('setChannel', [channel, isPowerwashAllowed]); $('channel-change-confirmation').hidden = false; $('channel-change-confirmation').textContent = loadTimeData.getStringF( 'channel-changed', this.channelTable_[channel].name); this.updateTargetChannel_(channel); }, /** * Sets the value of the "Build Date" field of the "More Info" section. * @param {string} buildDate The date of the build. * @private */ setBuildDate_: function(buildDate) { $('build-date-container').classList.remove('empty'); $('build-date').textContent = buildDate; }, /** * Updates channel-change-page-container visibility according to * internal state. * @private */ updateChannelChangePageContainerVisibility_: function() { if (!this.isNewChannelSwitcherUI_()) { $('channel-change-page-container').hidden = true; return; } $('channel-change-page-container').hidden = !help.ChannelChangePage.isPageReady(); }, /** * Updates channel-changer dropdown visibility if |visible| is * true and new channel switcher UI is disallowed. * @param {boolean} visible True if channel-changer should be * displayed, false otherwise. * @private */ updateChannelChangerContainerVisibility_: function(visible) { if (this.isNewChannelSwitcherUI_()) { $('channel-changer').hidden = true; return; } $('channel-changer').hidden = !visible; }, /** * Sets the regulatory label's source. * @param {string} path The path to use for the image. * @private */ setRegulatoryLabelPath_: function(path) { $('regulatory-label').src = path; }, /** * Sets the regulatory label's alt text. * @param {string} text The text to use for the image. * @private */ setRegulatoryLabelText_: function(text) { $('regulatory-label').alt = text; }, }; HelpPage.setUpdateStatus = function(status, message) { HelpPage.getInstance().setUpdateStatus_(status, message); }; HelpPage.setProgress = function(progress) { HelpPage.getInstance().setProgress_(progress); }; HelpPage.setAndShowAllowedConnectionTypesMsg = function(message) { HelpPage.getInstance().setAllowedConnectionTypesMsg_(message); HelpPage.getInstance().showAllowedConnectionTypesMsg_(true); }; HelpPage.showAllowedConnectionTypesMsg = function(visible) { HelpPage.getInstance().showAllowedConnectionTypesMsg_(visible); }; HelpPage.setPromotionState = function(state) { HelpPage.getInstance().setPromotionState_(state); }; HelpPage.setObsoleteSystem = function(obsolete) { HelpPage.getInstance().setObsoleteSystem_(obsolete); }; HelpPage.setObsoleteSystemEndOfTheLine = function(endOfTheLine) { HelpPage.getInstance().setObsoleteSystemEndOfTheLine_(endOfTheLine); }; HelpPage.setOSVersion = function(version) { HelpPage.getInstance().setOSVersion_(version); }; HelpPage.setARCVersion = function(version) { HelpPage.getInstance().setARCVersion_(version); }; HelpPage.setOSFirmware = function(firmware) { HelpPage.getInstance().setOSFirmware_(firmware); }; HelpPage.updateIsEnterpriseManaged = function(isEnterpriseManaged) { if (!cr.isChromeOS) return; HelpPage.getInstance().updateIsEnterpriseManaged_(isEnterpriseManaged); }; HelpPage.updateCurrentChannel = function(channel) { if (!cr.isChromeOS) return; HelpPage.getInstance().updateCurrentChannel_(channel); }; HelpPage.updateTargetChannel = function(channel) { if (!cr.isChromeOS) return; HelpPage.getInstance().updateTargetChannel_(channel); }; HelpPage.updateEnableReleaseChannel = function(enabled) { HelpPage.getInstance().updateEnableReleaseChannel_(enabled); }; HelpPage.setChannel = function(channel, isPowerwashAllowed) { HelpPage.getInstance().setChannel_(channel, isPowerwashAllowed); }; HelpPage.setBuildDate = function(buildDate) { HelpPage.getInstance().setBuildDate_(buildDate); }; HelpPage.setRegulatoryLabelPath = function(path) { assert(cr.isChromeOS); HelpPage.getInstance().setRegulatoryLabelPath_(path); }; HelpPage.setRegulatoryLabelText = function(text) { assert(cr.isChromeOS); HelpPage.getInstance().setRegulatoryLabelText_(text); }; HelpPage.updateEolMessage = function(eolStatus, eolMessage) { assert(cr.isChromeOS); HelpPage.getInstance().updateEolMessage_(eolStatus, eolMessage); }; // Export return { HelpPage: HelpPage }; }); // Copyright 2013 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. cr.define('help', function() { var Page = cr.ui.pageManager.Page; var PageManager = cr.ui.pageManager.PageManager; /** * Encapsulated handling of the channel change overlay. */ function ChannelChangePage() { Page.call(this, 'channel-change-page', '', 'channel-change-page'); } cr.addSingletonGetter(ChannelChangePage); ChannelChangePage.prototype = { __proto__: Page.prototype, /** * Name of the channel the device is currently on. * @private */ currentChannel_: null, /** * Name of the channel the device is supposed to be on. * @private */ targetChannel_: null, /** * True iff the device is enterprise-managed. * @private */ isEnterpriseManaged_: undefined, /** * List of the channels names, from the least stable to the most stable. * @private */ channelList_: ['dev-channel', 'beta-channel', 'stable-channel'], /** * List of the possible ui states. * @private */ uiClassTable_: ['selected-channel-requires-powerwash', 'selected-channel-requires-delayed-update', 'selected-channel-good', 'selected-channel-unstable'], /** override */ initializePage: function() { Page.prototype.initializePage.call(this); $('channel-change-page-cancel-button').onclick = PageManager.closeOverlay.bind(PageManager); var self = this; var options = this.getAllChannelOptions_(); for (var i = 0; i < options.length; i++) { var option = options[i]; option.onclick = function() { self.updateUI_(this.value); }; } $('channel-change-page-powerwash-button').onclick = function() { self.setChannel_(self.getSelectedOption_(), true); PageManager.closeOverlay(); }; $('channel-change-page-change-button').onclick = function() { self.setChannel_(self.getSelectedOption_(), false); PageManager.closeOverlay(); }; }, /** @override */ didShowPage: function() { if (this.targetChannel_ != null) this.selectOption_(this.targetChannel_); else if (this.currentChannel_ != null) this.selectOption_(this.currentChannel_); var options = this.getAllChannelOptions_(); for (var i = 0; i < options.length; i++) { var option = options[i]; if (option.checked) option.focus(); } }, /** * Returns the list of all radio buttons responsible for channel selection. * @return {NodeList} Array of radio buttons * @private */ getAllChannelOptions_: function() { return this.pageDiv.querySelectorAll('input[type="radio"]'); }, /** * Returns value of the selected option. * @return {?string} Selected channel name or null, if neither * option is selected. * @private */ getSelectedOption_: function() { var options = this.getAllChannelOptions_(); for (var i = 0; i < options.length; i++) { var option = options[i]; if (option.checked) return option.value; } return null; }, /** * Selects option for a given channel. * @param {string} channel Name of channel option that should be selected. * @private */ selectOption_: function(channel) { var options = this.getAllChannelOptions_(); for (var i = 0; i < options.length; i++) { var option = options[i]; if (option.value == channel) { option.checked = true; } } this.updateUI_(channel); }, /** * Updates UI according to selected channel. * @param {string} selectedChannel Selected channel * @private */ updateUI_: function(selectedChannel) { var currentStability = this.channelList_.indexOf(this.currentChannel_); var newStability = this.channelList_.indexOf(selectedChannel); var newOverlayClass = null; if (selectedChannel == this.currentChannel_) { if (this.currentChannel_ != this.targetChannel_) { // Allow user to switch back to the current channel. newOverlayClass = 'selected-channel-good'; } } else if (selectedChannel != this.targetChannel_) { // Selected channel isn't equal to the current and target channel. if (newStability > currentStability) { // More stable channel is selected. For customer devices // notify user about powerwash. if (this.isEnterpriseManaged_) newOverlayClass = 'selected-channel-requires-delayed-update'; else newOverlayClass = 'selected-channel-requires-powerwash'; } else if (selectedChannel == 'dev-channel') { // Warn user about unstable channel. newOverlayClass = 'selected-channel-unstable'; } else { // Switching to the less stable channel. newOverlayClass = 'selected-channel-good'; } } // Switch to the new UI state. for (var i = 0; i < this.uiClassTable_.length; i++) this.pageDiv.classList.remove(this.uiClassTable_[i]); if (newOverlayClass) this.pageDiv.classList.add(newOverlayClass); }, /** * Sets the device target channel. * @param {string} channel The name of the target channel * @param {boolean} isPowerwashAllowed True iff powerwash is allowed * @private */ setChannel_: function(channel, isPowerwashAllowed) { this.targetChannel_ = channel; this.updateUI_(channel); help.HelpPage.setChannel(channel, isPowerwashAllowed); }, /** * Updates page UI according to device owhership policy. * @param {boolean} isEnterpriseManaged True if the device is * enterprise managed * @private */ updateIsEnterpriseManaged_: function(isEnterpriseManaged) { this.isEnterpriseManaged_ = isEnterpriseManaged; }, /** * Updates name of the current channel, i.e. the name of the * channel the device is currently on. * @param {string} channel The name of the current channel * @private */ updateCurrentChannel_: function(channel) { if (this.channelList_.indexOf(channel) < 0) return; this.currentChannel_ = channel; this.selectOption_(channel); }, /** * Updates name of the target channel, i.e. the name of the * channel the device is supposed to be in case of a pending * channel change. * @param {string} channel The name of the target channel * @private */ updateTargetChannel_: function(channel) { if (this.channelList_.indexOf(channel) < 0) return; this.targetChannel_ = channel; }, /** * @return {boolean} True if the page is ready and can be * displayed, false otherwise * @private */ isPageReady_: function() { if (typeof this.isEnterpriseManaged_ == 'undefined') return false; if (!this.currentChannel_ || !this.targetChannel_) return false; return true; }, }; ChannelChangePage.updateIsEnterpriseManaged = function(isEnterpriseManaged) { ChannelChangePage.getInstance().updateIsEnterpriseManaged_( isEnterpriseManaged); }; ChannelChangePage.updateCurrentChannel = function(channel) { ChannelChangePage.getInstance().updateCurrentChannel_(channel); }; ChannelChangePage.updateTargetChannel = function(channel) { ChannelChangePage.getInstance().updateTargetChannel_(channel); }; ChannelChangePage.isPageReady = function() { return ChannelChangePage.getInstance().isPageReady_(); }; // Export return { ChannelChangePage: ChannelChangePage }; }); }g:+8}kՌ {*J$*4o,JLbPz )Qn}j]nJ"dMq2o ?0,٭hI1@Sñ0E[P'XIaKSh"x۟jTv@St۟8*<SKϱWT =Yqq) . 1 >趪K_ȱc(*2(RZK_041TM 6UѰ`iX"&; VlU&ba%LCVmP-X&6&愶6b-w<؋ {F@z}âؿ0˰;C XA?Scְu1zA7*V}b A\]QV_1S;OtOA?^1Su#XN5l[3+?H3Sd- b M{<]`,F^BI'Z\.f tO^`~v@#oo`Ȣ&P%P F-,p0q 5M#n @tvc~(e!a;^QAFJ" T eJ6E4@j,1Eo6'*&8$۟Ve(5h;ꀮjK {kDkc/`gq3*EoqNL)+{(KYzedDvC5I׺4kX 0=Q1>#*@}\ v/, eZJ*G>& Ițy@45U ]P+p"C Y>2HX#AIR$L!DzŌ`;P/"SԄ#BE &D+f2ShmAւ"CQw_reu u'%WTtu 暪4ZU]$![`| ޮ/O$|D+Pi{U:}4v JTgltD2Px }U0h1@vUM@?0V<͔\D\-OFqOh*yW\DˬfrV%_E߲@my4_I'JcL_D@ <TۥSuAӸb873l7 =N~ח%Up "fT5ip!\TF&dPAYXL~>p#Ҍ򊱠64<{ 1RôFмUeWXxzgE WzfoH3_z8i#a!WmVh吣ۇVm{eX!wQ8@t%F0Jbjb >#uPf.fJYupe?A_X/UC8@ !S68](sEV%ri6:NVJyЎ%N>xJTz N(6`&+VB](AթAL6Tk4p:9ZĮ5Z < A^qu=0xt}@?a_v`_ҷ ޾cmЏ֨G Tu!l{SlрG|4gwdwUS1*#z7َݺiqtȦ!~e`eTZۼOO5 mOF~4Q;kkHprABBT$|$ -Q]U1`p0]tHEFHDqTX?"=Nr wI,G D ,0(OnDwoӯ;}I3PO=GZz#2okǥ3J HJ(4[\tB[LjgO^Nuǝg8Bu\Q5ny-'T$n]QR'@@I~5=r\[O cRvD;X_w*ssQjɵo &ߟ nkJjD}nNoWg땦`VQbW}Qn}Wsɏ`Ϸn6`ac\1xoufM3ǐXEw|ctA'\{7.}\n"tN9=ITKȓovV?`| p&wAA(z70߆ Q l04L!`G?3|_>?f+<\*F,rupĠ@+i¾ ؉'|ߨp_&3_s< .}Βo)(1"n Ws?XcLG˖iu]\ܺϿ[9X>S>IС89PcLҀsǻ!#*@.t '2B?P{1$"qEvRTUkpDf ~TZ+{8FOUAt {6 BS9Gh8&z띤#QM"XYg;p|݅QJ$7f/ϧ)y- Lt4!ʑ ^WmD%$ >_mUs+qcvy <4m~]tI07+'╅$w|t/(ݕvr9"dsҝyo}&9.df|&O\5L\ "@uʼk{$uC<3ܾyMd >!fMQR5L/b26q#IĺNrgL{^l)` !Uk4 =TOz>:F&́0l0k24H+xb4CD𧅺J/+! (#b @ӽ+zE0Bb#k|2:MPω'u$Ğ˝Ѐ;Pq`[sv2YYQEzEy .zb(A9LGJ;m&oFyvyF03 H p YNa#a=?؅CC*IM#D8`q#Eq,,`V"(p5>]LH40Se l\oc~w;57{R^,wpU;a_˨rR,X#/<^Ϳ{Lu.^٦f6<ܣ;;yHLe8ADg@gPdE \Jbb*$A1cy`>vqtS} `}Bp?ņ(Jҡ.@ى0T1Zng K!3V2?"JGP u|ߐЁF(X5b!,+(/ʾ]?ЯA,qE@5MMzvUoL/_-I.#s׆.!A}㊉%e"ٮ|[T7M\NZ"x H`p?X$hxUeVՠqN,u׀8J^Zx~%^al?JƝ$3Q 5y2.~+Utpbw|N @{:fo 93rFMCW+QA6 >;y~( I1o:E/u?H*<*Gs8Yu8%iR ?y=MpnOD@]%\>R;vw3чYoGd _1A;~fآo<tZFKЃ;_̻OvD(Dw@Y ȍro@Ka^֩[UZ-*me )|i6hvtzI 7t>ӮR (rdMMq3i.e/bhK6˒ GdڪU/IރB_,Mue{{@=],{8jT4!i w(O[}ߡ|9&7Q憲}XXOrsf*GhR4x\D!{3^Vn8\|ky[ U B6v)1(O(fR~mpRVEBFt8.´Qx3T].PRyg~rNK|?3rjiY2 aVqWbaG$eJQ-V’m%3UQg1nCrF_l'$U UPۍ3+[rIj%auZ*P)h]Ĝh+Ưz=^[:׷}u!8f} /fu{[nԞhAWe5 E|#.?F޶ +ܐ[Mmӳ^qlf.Ly p2!HZ^,gݭEk6La\mˎ纺W:1mVwmKFe6c0fq<퍷,Ld:ב\ 9xqקsVcjE̔F2w^>Ҷ[LEE65S꫺<|qn51RS[u63/an*5HXZQ)c7 A)\o5-ZfsbEsy"-C[qIƹ.W 9N4|JMeUڊmlRVͦZJkdGTP}o]f̍m jp2;iM[3cu<¸9`7NV]k$J_G;\t3ݣPlvk^+K>q$eu~[]oMdx5?iW/pm[7zaͪ;Þ?4hڴoW=Wge&bt]W4c\[]+Xk2Z[dwV̀<;PJf`cHfJeXhU%} P 2921SdxX^(I{Jk/ )9u~V$ְjnW{5g !W\fcqf+u#c_"E_ͅLMv`l]ǀ+ǕU9 /ĺ?(ǩ篷c[vvUmq98Xh`K{-KBrB6P;V%HY֪vujN!X1^2Ő~ި]CNtaEFJ3+൴Ng4꛵ۛ/|#ߜHyV4KzK9rZa+.lW ;:e.X|͗"V+esVΰFy_:Y[/m;,,"9Ytz孿Vٴf{3c7Z \=S(9^N}**ŞDf>S m86[-6c;og6^ijhNHLeg3ʴ(U]J3F e3ϨGCw5;Ld,8a?daFmfC^wzѥnge~il 3FvCs v+@ȓ.*E/51nU*Zs9>Egiݷ ^np*6ndEU]V0ꕼnw+S޶Ilf0hlCo݌D:x{wP=20]3¼Zӥ߉v ;_ bEˏpZrt3N%7ٍ7=M!>1*r4Yd(͝s ?)̫RkbI q:Nnm_ q z1ʗVv٦=ۊouje-CZlT|QscƽTď`^#+v^scp4:v~-q_g?rНssP }6t+A8YeoPsf(jdC#tp{s|Ș6NWjo$m4AXJQJͤPEJo43}tҐFdWY yFq\ę%Otj](5s;4s$Yr;ѨR?Ǯ[$p*(#I52dkP]󬠒/d 9w-W7z yo3rV\hftgme{vn5l]^~EL Ҩ#{7vrvеJPۡکVYmk4Qqy_U ݽlE-cywK=-cU WQ~Em ㆺ?zCrHL2GKLخV!يj)e c\QԭfqnH;^D{:,fle㙸Vp"zCj:2 璷 ˓.L7!\BI%Px8e2N@ךTk5ce\si叧ۆ;%n^3|Mmu 8F^fkϐ%UD"q k:6,⦓k9617'1 qY |ӞA=CNʁ&فvEE*f|2UVֈ19ijͭ(fw:C~m>]Ez̯NFk7fI3SRe:aX`WLSq(/OB%mx=—J0u7h}y[*M&<JQDLy6 ^Fxjc1x7`%P7 DŽ9)i3yq_Ln'Y-أ).;&J=Cx?̣*wvMYǶS$"e &.l288!&/ SjtEr:m.ݫ(EaA.ԁcp4,Np.p,5>%`2Q2G k{hO(RH?*Fi0w~۳ n-4g?XLgЉXt/S.*$"pT|yaeF؟3̝dV*Kx;{odU=ʓ2ECzobIe1NA;K0bM kUn<ӆ^jn p+]l QBgʬ79=BGȇ! Z6dXľkBCf qawHJjJ<˃{'SV +PۓzBۼNvأn+ O0LwOGE%:*J u蠿QQQQ#Yx޵;ZTWQDH#zpeirnߐ -=m{@Fg;s6G!XwR<*љ  O'u^?H >eAoCP$CH$Bⅈ#1d7GrW\OxRdĮڰHًƞ)àOoF6J\闧;d ~{]6NssFTz0oRUC?SwNL!lkD >k͗MUi18I(Ã礪o!ފo1ڤv뉣JYxx{E}47O2q7~SDHQˣ-+EP=Q`)q pDGh~CR bZZR.p6]3=7V>y @^V>|OMTOU9 _n )תa`%{cY|:ļ_ _ _{Ƈ |eҝKo>؃~21Q181md*XI_@.`0]~w8uKكjoHUdK',H3 ^|b+|Q| קߑK ?^RţQ/q `PRع8+~;7~ ֩u7zYhowP%yhmI@HODMďAoJ7tOٯOӯuexj'*w')&f8C2n^.8}p"}:b✷7%x? 5{q g]&yYhP}M0J?]@5OxܖרR)fDhN7Vej99l1Zljx榄غ:W6-W%sN9|OWtCFɝ!3Ut Er{I;zgAENpnYg ]#h' v*FsdLs ^#=%évzWtR?"5պU Ċ]ּ8F\cJD5,?, =k'*7#-}(&K,_UѷK,_i&T}Ծ5wu'yA;:%}Iw7MTdyU4ISqIՇ.I2Mfy;˓Y1K趼ʖ8+Q#MU9Iɋg?=K,Ow{ꫝOC$G0lEU" e1+<' X'oFy*̓ U -&vvu:ɧka2]4~$G=aa)h_eM2`Ij&:/B&jR??=. iM&>?py^Cnnj4=_◧夤 e{ `ه:IrZj߫۞|U8e}(.3ښ)\Lj|H%F2[ۙލ:獇fҀ ,?M#Gt\7kof+XlEsh٬tJiweK7Hج?7 ~zP=5'7U}Dqr77 _Y@9`WVeK ?Z|Lϻ}JoDzPth6yd_ݸ[wΫ3%?vC#8v7C=S86^vSH ^D@ $Sp~>T,o·gg71wIR~iќݖ'_~<07ތD0;7\eRJߎ '"6ǣ v19 i@|7f@/ '_}wsj'y ?KKrxxr@Ik_FU͚k@:Wy>kw.&lm?)Wsx /u 1% 8^a\i:W:Tg,|FY#7: Cbx(hfpt"i @W&"긽F&17#M q|&B̚h^d['=᪁d5(@1o=1&{`a+UiU6`̒fdlN)> oyz Hh|~?cN-R@ `lE ,lV49pe$㫪MDPq7U~U )\< h&7p E+ثZj6ix0"<3Ր;/H'tmW _{?S+)Ic Hh aA)YF#|7 ;2#ΐ~eV˦0 [ 1ঀ!\._ !'E ׄnI0"5v/05]po`eud#T>g챇c! ~4\GHm$ e{n9øL~#2@ ,FM(='H8_2Ռm$ yͥgCnx$F*KXOŀ \Eli};A=Ղp%m|%nNּs[G'oKy']!fxBEw {)FU5˳.!<5hWAO*}{v?8?y@#?=vPxl\͛r// AnrMN2K N%,U1K^l%)_<œ7?=;Zvg>gIM #uSyrS!Nᒖn)ۉ( \L7D  xHݵ$gwfK2֏Wњ0ct//~l,y+vuTᭀbз_ Mj6cq5#4]>4%qZΊ2>o!^%0}F'|5yV 1)+`\`7w Dx0D:5z&4yGEs|Аu':b{q.pT\0?)71.ћ <3ۓ0?z~yGO?8=i?lk qUaL*w'ys o  ސzQ)ؾami#uw^âyIt'(wv/q9TIZn4o-%tfg@.?]@MgG@x)SM- 7[Qs_;}t1Ucz'BE'4wO HC\;ds(feFBnC}ix"Hᜁu:Cؑ#p(/B)3tڠ#>C|S>)͘ {g' ~yMx!fXO6'1)>2)=weKGB9 -K>/}h0qܔGal|&|q(W|oeRakK1ub 'Bsxs֑_Qx<@X[q$!5ুB4 Y8՘R)v4/`jKEի m1f,yJ TȞfc$Ε37h; O$FTXYN6Qdh :Cd͗53:d}M#_HiZ;8~BǣpZh8k ˋ<79W C<7*]ʚbvi x&sq$g9J^sңy"\` Sr .k7w͂B 7R,C)nstjn>{π_ Y&WܕaW[l/<+aʻ +?M3xzE7W0kO3$4L2n5?nڥdcM}\ۛ3UNTNW=)Ě=3^*I5aCXc,?.C_Kx28H!zcy{g5p.[}d@sb5 ݒFk]35MoNQ &#dʿ91?لxh@pWOI9B(9g@1#8d2 |a `0D/ 2e6!^BA!=j CbbN4'ywoAװAW藺|g-w@UH-Ws`լ@.2+0) k =lj;8O:OfQ\vݹ'9~"J aʉxыj*M2C<uF~4XeTȺ;w>)(vT7u038o޺\ꖩSѫC/^'O|4 S}$R05P죯?,L E]1 S \MM.g cRGy+PYy{CO07yf jR1KHj@Aa}%s/#?_l3n6Eo:ma#A|FR1WĢ`v,WC G_mDl|IyXnb5[U?Yr;ON&9pF  ?H!p4e~Z.ɐ4, Y[y t_tІY~d3NoID~,YA3\ȖӜP2.'B%k1jGY 4,Wϫ5`SoDvm?e5 +zdJ88uѰ:W'K;1Z&j}*gj"i){"vs {) wtڨy1 _h4iU>!]֛RƗT%MAbjM1޵ ~| j QȣšE-FD0( &}zNc{ Ǯ;F'GVs[ GZ) sا><B6[O3M [5ձ [g`gi8sgp==ڋVB>;uZn]nXy& B*ְhFMz߷EdTrТ8DE+Q0*Us# t0904 o1zC$ }^| hw}$Mg+q*EE 4}O#[$+Q3+5{{p V#ʭtYU|oeؽiVyͣw#>=2EF#:tMȋt{IWk[0GO5 Cp/Mse*.*x6Y;5,='[eH ʺا/Ԑq_`tl5 J KZlH׳, A͒9 \Jл$+'O9'Egd4GV~Ոqݸ!vlm3/|kF,X)o 4K@ 6 * EA~&q2? xˈg8ƽCQg"B3J͗͹6Pә{S ,yUzUj'ۃ-Ťe_%XVN\"\ ci9->"v]Sv-hJ㊉:-8fo^]fb0J<3sMĬH(e Ғt[U ݕIa7%F}LP7AeIG݈`eN$o _av3 US| [)ȵ~}W R9n~£T/p(8,ٱm 4pj1I.5:2S%-yC;d׶ h!J(ܺoƓ0dVX v dd$Eߕ@1_2 t RFmyh fyѐHy]h71KhbOՓej'pj֋w#RQtPO<zw-.;si:(Oro :!rLˮC@$\Q AGL*{6oZ~1C&:Scb"a28 g^['XVZ94  r^a(?X e@/1оo=XYwfu^;?jOȳc咜gVSsrlzP5^r牋`(7?NʧmczP,_GOnj{Zp%9o 3/m\SGTy2?ӌ,Ԇ AE_5;!|&HꝬǹW2KHzeȊ|1yH r92XV!c1MBsSMrCw7136:W6`[Ҳ18]'nxv9a%X.19:󝦋-XY !_KG׶ !4'S*#6SRj#/fm+l)CM@L|6-CJ׺JL7KD\Af6ہ]3SnC ã&Zܵ,/R `\n]~B~ Z.RT*IQhK`ܩ]^NV3L sHCi;~Irf_* ̝t#2brYrVmpT/9;p5/I n1#WTS{H  *teޑ|vHެh\5e#ӞsJ^҂wA}B#2F`|Î" 84s1>KQu7_VOD;ұ2r"O|U)uK m mv5f:Us;p ] * k8VAaU ´CVz`"I33rq}2ޱrʺC${^b]7zLs 6:sQ9YDDTisp|O0vpn;Ͷ#RoK^7cfo$-Lg~XgV^!]"1;W4y!"&?ZG8%5JjB/Ѽ$|9NxX+NƢ%ExlDhytW,@*Gq?|Bt wgy/]vdEX1z _ :>)Rrރ/Mt$xm#uKL[өUxX#=hK%͂xٙύcJ.jME4=Pl( 'I"=rֱ.G^Tē~mdEW17LZH%%a$V+`342!O'{n֌G~ }Lr_̹'K8^F2vZ-N6fB߹=CrR L:Oj\ErzکQen`vbQ`Zt+ꆬۤ":#5+YA-+a!?R5 t2X53 *(+q w8FI7[d)BQl䮑[INE,v(&T_&uH> #eG%YӶV"TiEi嗫|F?()]OXjȺ<7PsV77&*H P[gűLѷr/ t@7}%YNBb6&\lf Ln4oL8jz;<_ܫc+T-ADjO۱7lsl 1RtlOpsjM'w!;7(}s}cnyz'%\a5: Yj35T@!jc&Y1$65ս%kOXۥ`1|Mgb7,x#ID!pX#$9snF dʠFRx*"^y [QvƁe5>N64KoVrEZ]Wl)\,/ELR7'}60&{Md^ӞQk|o87ƦT~'ɳ M #۾xkzA &@$ kRǔٯ+ 73d!gc*Mknn LCb [#l8|tfG&s7B87kLOt5; Y`q>[A]7~>OlpKjr35Fq(p+3,2>!4Nt(zV{'!Jx;Nn~r'&Bk,g3t6>U[ I|5(9&ً &C Prc Qj16ًD1ȶ.\E gӸTxu[;R!:é{xs";ARg`W׷kMtU8Ӻo'DOjne'/MbdauE #Šɯ`}V wI;A¿]mkzOiL˱qm4 ).HŅ/:2F c=Q!3JŞ|j+~n&Ύ uuk΢DZ`7MLa5 CZ%->,{vfXaFؚ6ߊ ۷ GeMVRj7Y12D\c4~hDd-YMWnOm(8ZqrgȫN]>a컢DNh뺱Ss/%6M,btuGqzXOT WLxX$,TT87孱IU񥯒yv-;!e%JLqθ BzWJsOLc=I;9c%\`]k;e(/Zbه'Il*E$oib.|UI%nl!~݈ip˃,3uti=wv\nrGϖjGҭ4n+1b }8^*w Lej[*k i/y W?xmuói J}p}߄ &r Z–2!ɏįٮQnM4a=]]&u,`9ɧr 3kke'qvVM}CG?>"xQ=.j)ys*4X'*=Qs_TU6s+-Q dufntR埉}Q?LڠcwNVV˝ M7L8/OvcRqϬU|QųP '9mw$;5Vŋogw+MNa8Yx a/N!8<&z,:zѰ&4iSÂg$T"g.P8inMl7$RRHJ]Q䗣8Tj/C^g4o[ t\Ev#fS:iӅS:OnӹGLv&i&'/9M'j@ J _^ns"ÉWlVTЍy?Lo ;H[b_Q:h"qgf~ߨf6%GF#2Pd4vkTl^KC f 'kR 3iESzVPo_FʐwԨDPklz/I0+ >_~@M C$;ɬ۠h _Z=[+>p 49쁫% Ė"]h&g/:![mdʰC3L Uৢ*MGX nݓ[*hҗCACB3hWpW]շNF}0H(7t~/7,<.^E,9&'Hx +gꦾ3 朲ΪӦ@D4+Qq¯-P ;͖ݳ^< mO$|: Xiws߸ڮth/AˏY&#Ζ]! ߑ I񡘬0i LOčPq`R꣑lŅ2PJG5 QʽY0. u#w;ډ 5? j[˽̻!\GD6?WJܿP3 uGdT*ZZDc ! Nt6Cݠ&6;5\V hxnдc&'u4%朣3&ybdŊ,bE)􋚢OLvS#ލT+R 8׀+Aʺ]/dٟ"-b2<2.yLyZk e}0SgBz& >q^I`O̫>*FMyA<<G|#a$gT1 &$y͆KB&<e)&6~߰3{WgDnLw6 x1ԮACb9$HQO .qUN I,]mar~K,\|{MRL0+ywH|sN)} bwRS٘ٶLS~n$`2ziJ}0n_늣t֏H}1 ;k'µ'rsR1@q9K/Pm%V {)h[p%,s C)/B.pP#8TOmE8(~J odU&%W*(\5VTS+jhԣlV[KZP榼ժ]:}b~Mqʕ“z&.^S}ԋ*Xm=n1P5)9+0(rv8,2;ΤJp\:~8F "QU'nUS3(_!qZ-<?:pplP㨽{hJ8 ?j3RW%#MĔ6x]gVEK1f^{P+OVu(:f3 9L?xAi^p!tGI5`",>2|O&7:x`s]&V*㑥4 _zҝu}BS[oxa@yQaj[1wt lݣ^5ݾ:B.k0wkQiNMIfo#Rl㶠 _~ܫ͞WتػV65{몙W/f#GX3>8flѠrYP̿)$)2ѸUNOަ;DyخDhjЉi\^ Z QH+ ?~G?HnkuOiE @$ݒ>˛f4q~ӊ|[P\]]Xw[5bx,g$n04v ڎ0A?&FL/NgTctBҿp37 %յY5&ךi4(*{ jU G"E#k3pz&dnKm!bNp9]L4 +aͻ~ިHm04K%eٍskd2L+jj?1RF`Sby-QK0kkPyAa5eW`^;j4*Vаv:jGd5zH%0UKZ& 4}{v?yG]eɾ4ؙpbs$ݢ-ce7'=94{by8/C)ј9 GU4S-"/쒴r'\y2!>.h%wۉ DI,?|4d~6E H{U\?!!׉z8]}פ1_S!U=8Qf[8NaB2$D gBեW8)͸Kkǝ- H&Qk>3"צ(茤( ݐQkuAԪeT)֒ 4x8﹙γGwYqS5SG|-a=ih&qlm|A܄a:4^ˍfj^lm)eS< Ӱ;| Ecq̈<֙;eϟ7๨]{ OQjzډ)' c0ޫk^ 5XlZ-,C}Co1eo0Nzfi>ϒ' $>ۈEtXvYbgwW tK때޸eat2fIwQ/D.1.$,kˀTf|N˪>? pe~1FڴaQ$@'Iu8s] O'a_]V0틸 K)9}qjcv3/`"($ #CTM]Q o M`n\Vf6jmAHgw2<4n.54  ) F]r\Ї.Yo6 &-V8;l^{6{@7]b)8\TڨL]h 5YObމ zQXuCCE)<ĂH+e7!Ս wou~(ۆI꿸仄1SXj9 *׵mO^ooDUȴ][j"я*Szў_+1SRW ٺ$Lb?%1]w`yv;EabV,oK s+ƠzdPA$##9R(Ks?Lka2Lr,CH&JM1Iߒ~.vTe{ \-|~!N4KxfN)2pb)ɺ2,Rtd:lg2K&)8՘}r+0fb)1b WPeH9xI(dp&G %@hyv2jlsbBLc1_7j զ~z4o_=XCB]l B^8H)NS %=e %!^φX},+}ؚv-6M X,YRV"-ā&y]W$)\/&i+ ٬X 4j^ :Rehu |oAMp_7B޻?|{|џx[>y-`K?"9cNơ4|9g·6W9gG6/x"%ͳƙ-s,pQX|9w3r-M Nl&œg7@'n'=qpl1[C|CȆcIb^GnjXZƭ^x؁IQ5s.iaRΤ+,X,(i{${1ȍYu`ee%JkJk|a>v/OS% (i6O<ؾLlg:E4RIcq)-D alP"6u,p;8H?P+0֍ Bn@IVm0ѺaxV7)AO3u O!Ǘdn7n e;G2 1ߣMޫQղ2 2;tzvVyi! cͳ%f5k,ɲL ցsn5+`* J7ehI'=Ty+0$R7q +' wAo5˸t2ρuG Xt8EP19F$1?4E\u%.$̂(=D\%<4bFXߴY(__v6yi T)vڔa{~c;7Ȏ{#&c%@62N/prI8`3"gưv da2||D!^CX<'*آD dlq5fAHzy%%(_^\a5lS"'dAVTiA 8YQR.d'&Jf!> j%i^@k5ͥ{wZ#1wL/k:Y]`trT3d*JڛY$ЎD,ћ3~=DG^1uR¤ZwB  cMLYh B} J۷@۲uޟ'٨OI\Ϋ1[f(J!%4]bU)*EgMIj9eVP5ʇ џ9y<'SBDOkܕ5B' T~w(Jcutam~Anɐr}PV#K.;4O"-/bÞyn8:vDv |\ÿpEʄg:]|qW Mf5?UlvR"u۵y+^_,̝ܿIJ]:ߪ(.\JQrKR Ͷ7d2GJ́2G޷kDY*;A+ߜ~H+^UC:.8)򯔎)f˝ezrW溋.} >φ`6D4?`4iغ`w2^ErVj&cXS$T5ǃ_'TNC..4ؚowT\.(qR. 77m8wr&e ^틈|H@"ndO&SyJG8]syqBLʄ&}9 ARlrj'[ee65?DCo+|A6M3K;6e3+Rԋ0A,]y)0nx7u|PŕKojà>G]e}f׼ZA3 K(YVݜz~ʬjrqVѺ٭F4u$?4IvP,}*ePg,mQjjZcP~ȩR)YJ^i~b(asm]@m+em"IKnMc|7{[cT4 I\L}?TݱE>;1zHꨗ&yLj%& M#} y|{v&tcxlܮrM@!#J#z 2PB ~kEf<\V[%K@qZydP'WRZF/=w4=& lVD96fo@Xm ejS%䁼`Bnbg<(d4H*tq}e7,%.WsF _/ )`3yB}|*/Dx^)K&a{ɲ;7[po9g#/B9|eiwGX-کf^)tI_#딅U(6c (o会f9 ԝ6ދp`*p?6icNx' tp“ܞs8sn eVx2yv"-"-[L kiŻUE) :2[JJ\/#Dxc@kOTEQب""W9$P]/8-_{ ,jɏ`/(ϯqr^I*Thk  krZ;QlNܧuw&g[]ѿ8v[bk]o$:?8$X_(O]GAHɥQ`*stĭYD0aKgaP@q+'dRd GZ511*3|5tD2k6u0UHO캻CEa /8W1C%L6^}-DmA:{#->F5]Uy ZKE1 ƅaq"Žû6elv;Cu x劈T]16ixafJyhg:UdF҆R׭1䤔g.#9О-Ek.9˶s֬{S`3"%ʋM4';^auP9^x7-9.e='v$f]TQewTW3;ߗq)JlNF`r>'-'fh56>I}__'.hlOgd/#(k5sj ׹uv̼W;,md}E.۰1ƞu[t ɮT߇í|8C~jm3h0aD]/SN+YOOfOi|x @,=}=v"o{׵/;Ex|84:QEcdd h8/܂1tϓ虷U tv"coa~vlNFb0̰ D>с xc?#fʺF4 FaFJ葻V~=`5E=N`UVLq!UW<~ʨV}gˤ N兺!SUN= t~ On)b!,J6)| t/G7(QNiE4NtF/- n\ Wˑ!8֦nNeOE7 -J[zpH~ol'@G81jYg~ [~`Vpgp⤇Yu _2vN `UbWtD)&N&=6H2\1 èHJck$(CؑT` -o%jDL vR|]폜PDB4UF®k7s~Qq8"5§;kflNY&-d1Q:MNl{bkC\ 8+}ǫh,rg0AZ?8ʸmĝ7gNoO>]agonή.&G7o'W7_]K7>nU#LĴ n,Ňӱ(sFIUDut<:w1>9'gdG{\ҧGgK~29Ngwbʽ]^NnƧGnQsy'~|v'><tv3>9_@ɻxO7=󏰓 zȪ82^uNKX-_ ad*ԝJK# H9IR!VT<|LDRffNQjizRϪ|Hλk:U* Dy2 cIj7w"x74\3"^wd,T_J?Y۱ទ I"Y[T&0Gx3l$$y9t߂Kuts̳22eDR훙QS}"hd^4Ǫw,a-04}ܟ%"|QDt4g~-R0@*!$$% At8n XrkG@C. 5)UHr ℓqgF]̛+Oo OhR8Ǟ-t[)|菠f遆{}YhߌH;۾}߿8d7n|؊Aw*BQ(yR$lZD(KiyٕD)nR|WLAh rB1 "Ygh2cћ&VOQ*5%|uyW?A-'WK 2R Ҙl2= ph9`ZF=0:]ɡ%Y CmaRLebSQ`U8=J0)D\Cy ɨfROyQ5?/ޛS"̂ 4Օ^`aG :4?u?C#ѡ݀H,ë(Q @94%yvMegT\PX'']!vsгY6+lxU([P?/}EF=#a~Ӭ7Dgpp,N;;f_W2zcv}X(k-^ #1AC+m*@]"ֵfeݛ`Vk8CZr٘0ϖ@1y4 J[ 9Mq`x$A}EZX0U C@-Ka,ؚ^)(tHup~ƄA.F1<$  ^6" ow\t2+*c94UEI2 3q0s$hȾ|rjp]pƀa(eN@LVq9ԜOWlr$>*8 3qvQ=Y0:R{aNLl0KYm?<T淺) R hUNռtN83^+~g{@΢T^C\O=fֲYf'J~t3ȓ.UVQ ^bx( [l|8V(˸P" |TnnM-Ar ͸OnBGEEg&b[AԱ_WW/F@>/nCnaò1Iq\as iib #K:TۼzXTnawTҷMa'E:u~uŞ6szNkrnR^rUc)/2ErL:0q<,-0 Al 6\)dptQ@U RA~[+|u{ i:Nx]y=| 3."G\*7p4B1QMl Gl [yzKqax1>/do<8 aCe! Q1-EB-VS5[FNT+4>TM2_)17q$m եH󉉁 YVqbQȸvCtisMƳOY6 ۀE zЭTq-}pmX[uGvN a%XjK2)B?Fu#vy如U-"VF2AUcS[(.ᓌ˟?e돉*X,nƏeS/CQ|=t$EXI;6yd)*D x7Air@uQO @ y"ԏjCz;틞4 S٩m9>-,؅Lmṋc.EkRGEK+"[+Q74tr}:m)":?<K{!\"Au>dx"䝌b91.rTcjo/0}p!ip5')\Hn^fzX' zsu̅fB-E+T>1)LVIr~( 65 fXl.[8_1s|0Vp"0I5c \a Htz`nd>^B|E_Ѿ3pm 4L:2[bhi9u|׹yv t|egn3'S~ȟTӥ+9*S̖U,mO`]_f Ck.sTXHvLe)/cU 6׵gj!tݦL{ { 1qhLL3Q c-WaE`촂v0^uZz;ͅ*J%a~@w[̕aO' mוivo 8oj-vmU@kv=pA~\ kb4^S"%J4]p\+ 1<!ԳZs*%Q%ms7HrQwi2 Zd!u.q+sľt-FPvw5-yVkb=X~m1R{A|Lݧ˻D79 Q(+[m1tm{"fekFS0LMTo"D$LM*l!DMI[0&t ?q~ӫzitwvlN$5M^S&Y;0 g.&Kh[ 0w3jrW}PXۮOZ<{ڦk>*}تZaܽC)e[Opvt{W 8NՆ˭) h$,w)x2vuF^SuG4<%q]&2ץ̼d{p{Ľ&A0O\l,Mjަ^V/6F wA-Ѱ5ϸ.U/EdS&^"IԳ(/`첕i}, B0j+ttt%% 6vjeE64p2Z=O+*GCǰLBk;=29` JCD[Z4^Hݲ^=5،.'W|Q-Q]j4hyRzvr+#\1|j9p[^wjvd6i}a(;{gG8:pmv=n9A;P7pAa8Ett#1){04Pn%`.s 3wuT9aD bC!l3o־(tV;)ݾP`G"+*r HC''敩sF!nzDgZ"kwOL G3 ծ9lP `t95'Y_:Su2z~O!9g"_SQHbVŨ]ℇ]' s @zR DWM={ :g ]+f}e5UAy~('toEOeDTk1dZt B2 hbb u+X 9xB9 // Copyright 2016 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. Polymer({ is: 'bookmarks-app', properties: { selectedId: String, /** @type {BookmarkTreeNode} */ rootNode: Object, searchTerm: String, /** @type {Array} */ displayedList: Array, }, /** @override */ attached: function() { /** @type {BookmarksStore} */ (this.$$('bookmarks-store')) .initializeStore(); }, }); $i18n{title} // Copyright 2016 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. Polymer({ is: 'bookmarks-folder-node', properties: { /** @type {BookmarkTreeNode} */ item: Object, isSelected: { type: Boolean, value: false, reflectToAttribute: true, }, }, /** * @private * @return {string} */ getFolderIcon_: function() { return this.isSelected ? 'bookmarks:folder-open' : 'cr:folder'; }, /** * @private * @return {string} */ getArrowIcon_: function() { return this.item.isOpen ? 'cr:arrow-drop-up' : 'cr:arrow-drop-down'; }, /** @private */ selectFolder_: function() { this.fire('selected-folder-changed', this.item.id); }, /** * Occurs when the drop down arrow is tapped. * @private */ toggleFolder_: function() { this.fire('folder-open-changed', { id: this.item.id, open: !this.item.isOpen, }); }, /** * @private * @return {boolean} */ hasChildFolder_: function() { for (var i = 0; i < this.item.children.length; i++) { if (!this.item.children[i].url) return true; } return false; }, }); // Copyright 2016 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. Polymer({ is: 'bookmarks-item', properties: { /** @type {BookmarkTreeNode} */ item: { type: Object, observer: 'onItemChanged_', }, isFolder_: Boolean, }, observers: [ 'updateFavicon_(item.url)', ], /** * @param {Event} e * @private */ onMenuButtonOpenTap_: function(e) { this.fire('open-item-menu', { target: e.target, item: this.item }); }, /** @private */ onItemChanged_: function() { this.isFolder_ = !(this.item.url); }, /** @private */ updateFavicon_: function(url) { this.$.icon.style.backgroundImage = cr.icon.getFavicon(url); }, }); // Copyright 2016 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. Polymer({ is: 'bookmarks-list', properties: { /** @type {BookmarkTreeNode} */ menuItem_: Object, /** @type {Array} */ displayedList: Array, }, listeners: { 'open-item-menu': 'onOpenItemMenu_', }, /** * @param {Event} e * @private */ onOpenItemMenu_: function(e) { this.menuItem_ = e.detail.item; var menu = /** @type {!CrActionMenuElement} */ ( this.$.dropdown); menu.showAt(/** @type {!Element} */ (e.detail.target)); }, // TODO(jiaxi): change these dummy click event handlers later. /** @private */ onEditTap_: function() { this.closeDropdownMenu_(); if (this.menuItem_.url) this.$.editBookmark.showModal(); }, /** @private */ onCopyURLTap_: function() { var idList = [this.menuItem_.id]; chrome.bookmarkManagerPrivate.copy(idList, function() { // TODO(jiaxi): Add toast later. }); this.closeDropdownMenu_(); }, /** @private */ onDeleteTap_: function() { if (this.menuItem_.url) { chrome.bookmarks.remove(this.menuItem_.id, function() { // TODO(jiaxi): Add toast later. }.bind(this)); } else { chrome.bookmarks.removeTree(this.menuItem_.id, function() { // TODO(jiaxi): Add toast later. }.bind(this)); } this.closeDropdownMenu_(); }, /** @private */ onSaveEditTap_: function() { chrome.bookmarks.update(this.menuItem_.id, { 'title': this.menuItem_.title, 'url': this.menuItem_.url, }); this.$.editBookmark.close(); }, /** @private */ onCancelEditTap_: function() { this.$.editBookmark.cancel(); }, /** @private */ closeDropdownMenu_: function() { var menu = /** @type {!CrActionMenuElement} */ ( this.$.dropdown); menu.close(); }, /** @private */ isListEmpty_: function() { return this.displayedList.length == 0; } }); // Copyright 2016 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. Polymer({ is: 'bookmarks-sidebar', properties: { rootFolders: Array, }, }); // Copyright 2016 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. var BookmarksStore = Polymer({ is: 'bookmarks-store', properties: { /** @type {BookmarkTreeNode} */ rootNode: { type: Object, notify: true, }, /** @type {?string} */ selectedId: { type: String, observer: 'updateSelectedDisplay_', notify: true, }, searchTerm: { type: String, observer: 'updateSearchDisplay_', notify: true, }, /** * This updates to either the result of a search or the contents of the * selected folder. * @type {Array} */ displayedList: { type: Array, notify: true, readOnly: true, }, idToNodeMap_: Object, }, /** @private {Object} */ documentListeners_: null, /** @override */ attached: function() { this.documentListeners_ = { 'selected-folder-changed': this.onSelectedFolderChanged_.bind(this), 'folder-open-changed': this.onFolderOpenChanged_.bind(this), 'search-term-changed': this.onSearchTermChanged_.bind(this), }; for (var event in this.documentListeners_) document.addEventListener(event, this.documentListeners_[event]); }, /** @override */ detached: function() { for (var event in this.documentListeners_) document.removeEventListener(event, this.documentListeners_[event]); }, /** * Initializes the store with data from the bookmarks API. * Called by app on attached. */ initializeStore: function() { chrome.bookmarks.getTree(function(results) { this.setupStore_(results[0]); }.bind(this)); // Attach bookmarks API listeners. chrome.bookmarks.onRemoved.addListener(this.onBookmarkRemoved_.bind(this)); chrome.bookmarks.onChanged.addListener(this.onBookmarkChanged_.bind(this)); }, //////////////////////////////////////////////////////////////////////////////// // bookmarks-store, private: /** * @param {BookmarkTreeNode} rootNode * @private */ setupStore_: function(rootNode) { this.rootNode = rootNode; this.idToNodeMap_ = {}; this.rootNode.path = 'rootNode'; BookmarksStore.generatePaths(rootNode, 0); BookmarksStore.initNodes(this.rootNode, this.idToNodeMap_); this.fire('selected-folder-changed', this.rootNode.children[0].id); }, /** @private */ deselectFolders_: function() { this.unlinkPaths('displayedList'); this.set(this.idToNodeMap_[this.selectedId].path + '.isSelected', false); this.selectedId = null; }, /** * @param {BookmarkTreeNode} folder * @private * @return {boolean} */ isAncestorOfSelected_: function(folder) { if (!this.selectedId) return false; var selectedNode = this.idToNodeMap_[this.selectedId]; return selectedNode.path.startsWith(folder.path); }, /** @private */ updateSearchDisplay_: function() { if (this.searchTerm == '') { this.fire('selected-folder-changed', this.rootNode.children[0].id); } else { chrome.bookmarks.search(this.searchTerm, function(results) { if (this.selectedId) this.deselectFolders_(); this._setDisplayedList(results); }.bind(this)); } }, /** @private */ updateSelectedDisplay_: function() { // Don't change to the selected display if ID was cleared. if (!this.selectedId) return; var selectedNode = this.idToNodeMap_[this.selectedId]; this.linkPaths('displayedList', selectedNode.path + '.children'); this._setDisplayedList(selectedNode.children); }, /** * Remove all descendants of a given node from the map. * @param {string} id * @private */ removeDescendantsFromMap_: function(id) { var node = this.idToNodeMap_[id]; if (!node) return; if (node.children) { for (var i = 0; i < node.children.length; i++) this.removeDescendantsFromMap_(node.children[i].id); } delete this.idToNodeMap_[id]; }, //////////////////////////////////////////////////////////////////////////////// // bookmarks-store, bookmarks API event listeners: /** * Callback for when a bookmark node is removed. * If a folder is selected or is an ancestor of a selected folder, the parent * of the removed folder will be selected. * @param {string} id The id of the removed bookmark node. * @param {!{index: number, * parentId: string, * node: BookmarkTreeNode}} removeInfo */ onBookmarkRemoved_: function(id, removeInfo) { if (this.isAncestorOfSelected_(this.idToNodeMap_[id])) this.fire('selected-folder-changed', removeInfo.parentId); var parentNode = this.idToNodeMap_[removeInfo.parentId]; this.splice(parentNode.path + '.children', removeInfo.index, 1); this.removeDescendantsFromMap_(id); BookmarksStore.generatePaths(parentNode, removeInfo.index); // Regenerate the search list if its displayed. if (this.searchTerm) this.updateSearchDisplay_(); }, /** * Called when the title of a bookmark changes. * @param {string} id The id of changed bookmark node. * @param {!Object} changeInfo */ onBookmarkChanged_: function(id, changeInfo) { if (changeInfo.title) this.set(this.idToNodeMap_[id].path + '.title', changeInfo.title); if (changeInfo.url) this.set(this.idToNodeMap_[id].path + '.url', changeInfo.url); if (this.searchTerm) this.updateSearchDisplay_(); }, //////////////////////////////////////////////////////////////////////////////// // bookmarks-store, bookmarks app event listeners: /** * @param {Event} e * @private */ onSearchTermChanged_: function(e) { this.searchTerm = /** @type {string} */ (e.detail); }, /** * Selects the folder specified by the event and deselects the previously * selected folder. * @param {CustomEvent} e * @private */ onSelectedFolderChanged_: function(e) { if (this.searchTerm) this.searchTerm = ''; // Deselect the old folder if defined. if (this.selectedId) this.set(this.idToNodeMap_[this.selectedId].path + '.isSelected', false); var selectedId = /** @type {string} */ (e.detail); var newFolder = this.idToNodeMap_[selectedId]; this.set(newFolder.path + '.isSelected', true); this.selectedId = selectedId; }, /** * Handles events that open and close folders. * @param {CustomEvent} e * @private */ onFolderOpenChanged_: function(e) { var folder = this.idToNodeMap_[e.detail.id]; this.set(folder.path + '.isOpen', e.detail.open); if (!folder.isOpen && this.isAncestorOfSelected_(folder)) this.fire('selected-folder-changed', folder.id); }, }); //////////////////////////////////////////////////////////////////////////////// // bookmarks-store, static methods: /** * Stores the path from the store to a node inside the node. * @param {BookmarkTreeNode} bookmarkNode * @param {number} startIndex */ BookmarksStore.generatePaths = function(bookmarkNode, startIndex) { if (!bookmarkNode.children) return; for (var i = startIndex; i < bookmarkNode.children.length; i++) { bookmarkNode.children[i].path = bookmarkNode.path + '.children.#' + i; BookmarksStore.generatePaths(bookmarkNode.children[i], 0); } }; /** * Initializes the nodes in the bookmarks tree as follows: * - Populates |idToNodeMap_| with a mapping of all node ids to their * corresponding BookmarkTreeNode. * - Sets all the nodes to not selected and open by default. * @param {BookmarkTreeNode} bookmarkNode * @param {Object=} idToNodeMap */ BookmarksStore.initNodes = function(bookmarkNode, idToNodeMap) { if (idToNodeMap) idToNodeMap[bookmarkNode.id] = bookmarkNode; if (bookmarkNode.url) return; bookmarkNode.isSelected = false; bookmarkNode.isOpen = true; for (var i = 0; i < bookmarkNode.children.length; i++) BookmarksStore.initNodes(bookmarkNode.children[i], idToNodeMap); }; // Copyright 2016 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. Polymer({ is: 'bookmarks-toolbar', properties: { searchTerm: { type: String, observer: 'onSearchTermChanged_', }, }, /** @return {CrToolbarSearchFieldElement} */ get searchField() { return /** @type {CrToolbarElement} */ (this.$$('cr-toolbar')) .getSearchField(); }, /** * @param {Event} e * @private */ onMenuButtonOpenTap_: function(e) { var menu = /** @type {!CrActionMenuElement} */ (this.$.dropdown); menu.showAt(/** @type {!Element} */ (e.target)); }, /** @private */ onBulkEditTap_: function() { this.closeDropdownMenu_(); }, /** @private */ onSortTap_: function() { this.closeDropdownMenu_(); }, /** @private */ onAddBookmarkTap_: function() { this.closeDropdownMenu_(); }, /** @private */ onAddImportTap_: function() { this.closeDropdownMenu_(); }, /** @private */ onAddExportTap_: function() { this.closeDropdownMenu_(); }, /** @private */ closeDropdownMenu_: function() { var menu = /** @type {!CrActionMenuElement} */ (this.$.dropdown); menu.close(); }, /** * @param {Event} e * @private */ onSearchChanged_: function(e) { var searchTerm = /** @type {string} */ (e.detail); this.fire('search-term-changed', searchTerm); }, /** @private */ onSearchTermChanged_: function() { if (!this.searchTerm) this.searchField.setValue(''); }, }); // Copyright 2016 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. // Globals: /** @const */ var RESULTS_PER_PAGE = 150; /** * Amount of time between pageviews that we consider a 'break' in browsing, * measured in milliseconds. * @const */ var BROWSING_GAP_TIME = 15 * 60 * 1000; /** * The largest bucket value for UMA histogram, based on entry ID. All entries * with IDs greater than this will be included in this bucket. * @const */ var UMA_MAX_BUCKET_VALUE = 1000; /** * The largest bucket value for a UMA histogram that is a subset of above. * @const */ var UMA_MAX_SUBSET_BUCKET_VALUE = 100; /** * Histogram buckets for UMA tracking of which view is being shown to the user. * Keep this in sync with the HistoryPageView enum in histograms.xml. * This enum is append-only. * @enum {number} */ var HistoryPageViewHistogram = { HISTORY: 0, GROUPED_WEEK: 1, GROUPED_MONTH: 2, SYNCED_TABS: 3, SIGNIN_PROMO: 4, END: 5, // Should always be last. }; /** * @const */ var SYNCED_TABS_HISTOGRAM_NAME = 'HistoryPage.OtherDevicesMenu'; /** * Histogram buckets for UMA tracking of synced tabs. * @const */ var SyncedTabsHistogram = { INITIALIZED: 0, SHOW_MENU_DEPRECATED: 1, LINK_CLICKED: 2, LINK_RIGHT_CLICKED: 3, SESSION_NAME_RIGHT_CLICKED_DEPRECATED: 4, SHOW_SESSION_MENU: 5, COLLAPSE_SESSION: 6, EXPAND_SESSION: 7, OPEN_ALL: 8, HAS_FOREIGN_DATA: 9, HIDE_FOR_NOW: 10, LIMIT: 11 // Should always be the last one. }; /** * @enum {number} */ var HistoryRange = { ALL_TIME: 0, WEEK: 1, MONTH: 2, }; Un0}߯0)HȦ*V"Ɠԗ\-{fR<m !.E0!*y,/Pf0M=¹5..VIGy$I5wEAb*A*8VI8!2h;Wf%bTSk-^gZKKp!yO+WdZUlgDή{Z42y6:,;lx9,\Vڿ[+&Y_Il2޶ yqKe֍Dn޵DJ6z!56JQ_Uk-#ߠI0j\,ɶb>[np0!%kՆ ~͕"ꑸ_n p%kJ`}}47Q줪y޼mV=T _2~N!j>Ø00E/ETq $R{r~vc~A1i2[nO!+#d̇<"j͍*C$~9uS k @]Q&aKӴE.hvlK[8N͗FVt)o|[o{ς7He8wC$5'GCgd x=0NЕw*.bh;C=oٞ#5eo j\daD¨dO7`6// Copyright 2015 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. // Send the history query immediately. This allows the query to process during // the initial page startup. chrome.send('queryHistory', ['', 0, 0, 0, RESULTS_PER_PAGE]); chrome.send('getForeignSessions'); /** @type {Promise} */ var upgradePromise = null; /** @type {boolean} */ var resultsRendered = false; /** * @return {!Promise} Resolves once the history-app has been fully upgraded. */ function waitForAppUpgrade() { if (!upgradePromise) { upgradePromise = new Promise(function(resolve, reject) { if (window.Polymer && Polymer.isInstance && Polymer.isInstance($('history-app'))) { resolve(); } else { $('bundle').addEventListener('load', resolve); } }); } return upgradePromise; } // Chrome Callbacks------------------------------------------------------------- /** * Our history system calls this function with results from searches. * @param {HistoryQuery} info An object containing information about the query. * @param {!Array} results A list of results. */ function historyResult(info, results) { waitForAppUpgrade().then(function() { var app = /** @type {HistoryAppElement} */ ($('history-app')); app.historyResult(info, results); document.body.classList.remove('loading'); if (!resultsRendered) { resultsRendered = true; app.onFirstRender(); } }); } /** * Called by the history backend after receiving results and after discovering * the existence of other forms of browsing history. * @param {boolean} hasSyncedResults Whether there are synced results. * @param {boolean} includeOtherFormsOfBrowsingHistory Whether to include * a sentence about the existence of other forms of browsing history. */ function showNotification( hasSyncedResults, includeOtherFormsOfBrowsingHistory) { waitForAppUpgrade().then(function() { var app = /** @type {HistoryAppElement} */ ($('history-app')); app.showSidebarFooter = includeOtherFormsOfBrowsingHistory; app.hasSyncedResults = hasSyncedResults; }); } /** * Receives the synced history data. An empty list means that either there are * no foreign sessions, or tab sync is disabled for this profile. * * @param {!Array} sessionList Array of objects describing the * sessions from other devices. */ function setForeignSessions(sessionList) { waitForAppUpgrade().then(function() { /** @type {HistoryAppElement} */ ($('history-app')) .setForeignSessions(sessionList); }); } /** * Called when the history is deleted by someone else. */ function historyDeleted() { waitForAppUpgrade().then(function() { /** @type {HistoryAppElement} */ ($('history-app')).historyDeleted(); }); } /** * Called by the history backend after user's sign in state changes. * @param {boolean} isUserSignedIn Whether user is signed in or not now. */ function updateSignInState(isUserSignedIn) { waitForAppUpgrade().then(function() { if ($('history-app')) { /** @type {HistoryAppElement} */ ($('history-app')) .updateSignInState(isUserSignedIn); } }); } ExifII*Ducky9Adobed      !!!!!!!!!! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!> !1QqRA23Ss4aBTd"br5“D#C$%1Qa2A!"q# ? Ro #uNr_M0<4H:[M FifrRZC?w9{~>sw9{~>sw9{~>sw9{~>sw9{~>sw9{~>sw9{~>sw9{~>sw9{~>sw9{~>sw9{~>sw9{~>sw9{~>sw9{~>sw9{~>s̍muvr+uy:<ÓѨw;7TP(͋ܲfVB7F#v2DMv]܌E,תQJ9w.ՠ,=5^JW&h5|GLk!eȩK-+:YgKTm UWPUN4r]qxC%T2]qxE:MrXŴC;(_mas}Mb-o69& 6ܜQPU-S};bVu17ht%I517h$ݢtSvRMLn*7AI517h$ݢtSvRMLn*7AI517h$ݢtSvRX1>+uy:<ÓѨw;7TP\ Eifd\*7ELWy.tHo%=r{GiKh&U TSWRgx-\pH:%n'GXrz5GrFQXR[@M q@P q@P q@P q@PlJy?]u(2j@/Y ya@>f&% m@[}j9=#4¥ڴEn|*q)٫=L !Zp3m!q jxia~;WWAqEaH agڳrPa<&9R[g6'X6'X6'X6'X6'X6'X6'XJNIDTcczhFڪR6B<$b7^Vm0٫J-JN&LL4Er-`9St @*P9w.ՠ+uW0/SoN]g1 Ӏi TPĭKOFB\zS+ @Yi/ g*Rh[UIXjSxDjl #.B˫P2*l ,:hZw KCIuG*br?W_1Vk!R< r䶋JQG%JP S+ZV)DrRpޕj2ZIO; jM7f3֘!k-.-^L^+b#m6<$1fWY6[i %Tc}Vty'Qw!_?=]n -Ju.31+즛v"­ڴa6-9 E'HEhdY2r(#Gn/? rbbлea1ϚU->_@e?-5;'%7@%MЃ[BYUzRoOx<~0v[q} sxvjS=ik-/mYlΊ&lCJ3{? ЪZUiRLe"6.)3|1Zg$Dw-;?wWyܷd'`^,7@8c*4r6(%Ix~/+uQ1IZ9@!V5ޔrRD$U+{p /&; jM7f3֘&S"3Xۆ>obB՘֢jl]SO+y G9x g/na>ղbžlļy]z{ZN!'5?r)鯋X Dxkך__/foGi5} vsTxx~/-DWbڠ(Lˬ5g1p\f#YulԴ%qxaRZ U~8?zEIJU٣~uN&')b7enb4bsHd9wM?G6̤EJa=dnWyOCWLe rb5n*.vM.ez܊"cdI~V.]T .@=*[?ö?//޳Ceè[TT4) hL+*Ҩ@62j3-R\P-˜rk΅ աBP7%1Un -]NUP.4Y~7姼d~f?TVh_N9e;5tO穞Ri[*4cfQsrҪb|5Ȋ !͋i??|ae$Yjn]uTZ?|(i [NK)*IT޻'uOK盡, F:X+M(XSp˭ŗ_;OUs!9UR6WpdboLThW&g4~^Qq\S+gv%%|H{Xu^j+a-2R V-@E4-2EVQ)HRvUDt ]Ziv \Z{GiKh&U TSWDzL@zCjw$YE^sk+HvC{~JmLI[@{UB5l=*޵whDfQe'<6(2ڻF?b1ҩ;ck>$=^[:} ŵ@K m/AlV#Q1x%cE@3lE9["9V%p--5h1KGZ RoOx<~0v[q} sxvjS=iU)[Hy3AF4U}Դ&|ܗv]ͰWL!͋i??|ae$NJfdln#3ݏ ]2~ܪY8ϡ*~<YHS+gv%%|H{Xu^jT6Ćj*@"D^.~7姼d~f?TVh_N9e;5tO穞5զ82þ+~ULl]SO+s }3)'N_; wZ'Jc^vWvD5= sڀJ'VKKyn&:3`*F뽖5 56/b7TZ},m\ 6ˇeiqxaRZ U~8?zr1!}lL؊{UrS鬯DQ^.sbr~_?ØmI=_WЋ> Sي/Th3P5[7 KQ=;sh9]R3wު%>_;/ c6I1oSdUac3!= ԣ1Ui//޳Bw6cC1mPx)|3o; jM7f3֘ O /Ռ6d9wM?G6̤.ԫEhǧ\z%TLRZ0QgrpZisH9hkK@dzω_)[:} ŵ@b@@mqxaRZ V9 ʍDWRa9r)٫tIw*2c3arݻ4%`˖٤+\n$6X2vi&1J-۳I1V nݚImevLcl.[fcdrݻ4%`˖٤+\n$6X2vi&1JC˽ejc)dk;["(gp3K徥)=徥)=徥)=徥)=徥)=徥)=徥)=徥)=徥)=De{Q-opzӎ5s&,F=' uU \zDG_S'--U#̽$}%t%%ܣ| 7V]J\.^t.v ?bƤՔya+[ )6\!l_f)#׀7x~RXu^j5KJi[bf^09w.ՠ(TVۀ5mVۀ5mVۀ5mVۀ5mVۀ5mVۀ5mVۀ5mVۀ5mVۀ5mVۀ5mVۀ5mVۀ5mVۀf@.Umr͢)sZD|.z&4G:m9UxJ+ 3\߇%P6YjY[QD9ņk\> h/6µMU"W6K7Cllj dzω_)[:} ŵ@nSEn^#}xkj"P^%4l jD %(mQ. CjqJTK*Pڢ\AR 6TD %(mQ. CjqJTK*Pڢ\AR 6TD %(mQ. CjqJTK*Pڢ\AR 6TD %(mQ. CjqJTK*Pڢ\AR zFZ8tǫ8nj0˓U%O>]sxrfz.M_JN+9UƮ"&ɴٗ'Pm"2 .:`c[! 5R\=ׇ.vo)jǢt킸xuU&wӆ68l =EsNpo4\um<ƷsCrjwQ{DNy:=GS 9E;VJ"+%jN5uȧr CSd]r)܂P殹AY(sW\w 9E;VJ"+%jN5uȧr CSd]r)܂P殹AY(sW\w 9E;VJ"+%jN5uȧr CSd]r)܂P殹AY(sW\w 9E;VJ"+%jN5uȧr CSdE86mfeJ7*@T|-X]TW⽋7d4͓6DY$m5R˛ؼ' h5o >0˓U%3~(:=Qh w qrGˮ{\RՏEIXpL.g0xmz2A1ʦٙEvLc0/M1U [@1c}a\.@TQUuEGȌ/&hop淪\r|#Xާ>淩'59qc{8Nkzr|#Xާ>淩,o5^wEWYM1mSƦpFĽ6Jwq,UoWoTI7w6I7w6I7w6I7w6I7w6I7w6K Jo/.1ͱ=i{mWX;;G⎺3uրo)@T|-X]TW3: U 0u118"ܶ oy0;jf٦whc[! 5R\ƣN\`\.o˸Ƽ x$˝_F{ybϩN;7Z.ɺ!T[IXA0c]4LT[MQdG;UUuVNsFnUbԚ ;;G⎺3uրo)@T|-X]TW5: 6}Gs[x5N9ewu776DLL3켜Wd-+§|,'Bҽ<&6дGO t-+ol J{'Bҽ<&6дGO t-+ol J{'Bҽ<&6дGO t-+ol J{'Bҽ<&6дGO t-+ol J{'Bҽ<&6дGO t-+ol J{'Bҽ<&6дGO t-+olI,bٛcm- 4B5ꏗ\^ټWұӶ cGZA@TM ݰMSqN(U3bqwyEq<{hrpVOS{ kw>:|a&KAI_2{ |gu۫ħZ@*i-ƅkP.9syKV=U&cl2-9LnF l@ES -7_7V z]7TN w=u5o >0˓U% ~Ǥ=]E zO ^N%8ԲSNan4.SZ=>uu˝Z躩5}+;` 3 swܐ9LH&w$n;7yɝrgr@93 swܐ9LH&w$n;7yɝɝ դŷRs@]sxrfz.M_JN+6]]M1-SE4Eŀ5QMqeQh*MqH5"QU9`LcVY<[3`o/8Y^ ^?H/fTi}A1|t0.MT sd>u{hd\4ѳ٤=૭/mSK 4B5ꏗ\^ټWұӶ b&f"1^1@)Wh\_q/(Oض0Lb3tC_G7QGyyWt7QGyyWt7QGyyWt7QGyyWt7QGyyWt7QGyyWt7QGyyWt7QGyyWt7QGyyWt7QGyyWt7QGyyWt7QGyyWt7QGyyWt7QGyyWt7QGyyWt7v46nkriLqj1u{DULWL8f&&bq@{OsLen3O@cw\^QMq0 s1pF9'@GdOS{ kw>:|a&KAI_2{ |׽l2.`{hـl}U֗tq)֥ w qrGˮ{\RՏEIXp ?lno+ַSM0˓U% ~Ǥ=]k60M=l6i>{*Kuy:8kRM;{}йMj=ׇ.vo)jǢt킸qi<ƷsCrj..몙1Lv:Z.&lLY=<m"&o)*xmF%wBPwZ5oLaaIr:8Zky̘kURl¥_-^NյWLWL?14R=5eò n|sF9&A\.,ָ1OEV,kw>:|a&KAI_2{ |׽l2.`fl1(^E8)ӼǓܶ4έwwו]͑ƌo}gX[8"] ']ivmՎtq)֥ w qrGˮ{\RՏEIXpӷyoC2Ir[[:+)fH h5o >0˓U% ~Ǥ=]k60DŵMEY{VqoVX2{^mm`%-lFUgq=uݤO u`Jq5d=h\z}Q×;7cvIiSӶ 6 E[ 7wESM ?{09Zfآd#:|a&KAI_2{ |׽l2.`D[8!F L}ȏū^mm:k2;=wF%}U֗[SK 4B5.j>fn"mS&Ֆ[WN,vlYgbVqCz577v(gMMYYF,"Ӛ b &upr[%XΕ:kY 1;0;GS3*ƗIѫq^U1oV&qD] ~Cn׆%u5WSRxū14)ڶ :]r;Z산 qom2L]:Wqiۜuqwu \[@1c}a\.@ c='|/^Ѱȹ W]Vf\Ͷ~5UӃ6G/&kզb4` wzS[?pK=૭/V ħZ@*i-ƅkP_]%WS;G /Z$M=lENq)ݵ\>at?݈sca:}bj8Uϻgter]͊ޏK(ٶPz{ 'NVFpra:^8띫!\9'.;z0]S*-/vF38&jSBtzzMq_'?DU7%=]eWυ 1yB6+)fu{hd\EUSD[T_|[:*^WNڛ1DjĸyO]n/87i.X'GMjYo)AgSUQ)Q?w5D7%Q.ۚ5]u)'˛Wk]}]1LDU1}Wٶ?u1)_poHCt:f?=\&zAz3Ke =N{W ސnL=竄oH7OSi~l4Oy6[ _p-د{ ^P~k;}ҟ ocn-^NյJ)Sӿ^٣눙{Zp: h5o >0˓U% ~Ǥ=]k60nF3f"Ʋsru\T_oUhZ@;=wF%}U֗[SK 4B53jW)w,2&w9a~"],?*.c@lO]gXg&'%QWy}'?ÙV@k/b|(sqyC]8 J|*Yz&;;VX,F 4*$DF/h|K4@o[@1c}a\.@ c='|/^Ѱȹ O-Z,*s@gq=uݤZeSNTce6k h鷶brx] ͹MN tO MN tO WYƈ-YJ-35kI./9UTJRWۘGZ_?Gs-ҨMO1Fmq5y/.i݊RMj^{PpYMCT{+qj M wv51d~q7U[hS +9<F(4\um<ƷsCrj$ۘs'˭{F"&bfȇ]TϻN zFfoFVc,k@ǞT#vŤ~ (si2%NL8y/^Hh۱9t1O渍ƃ6icOhHЯyQyLM3wzʧs:@Y{sCmgu5WSRxū14)ڶǜuFsgn**i-Us6'H-hT[]Ubِ]4\um<ƷsCrj$ۘs'˭{F"#TűY{U+g51\$kȵ`]*zqyIttbJ_Eg9h4t-^Wꋚ6?c?mا /otYHzn_2B0˓U% ~Ǥ=]k60EUE15U!b"W{Vjc8NIea0yO]n/87i.ZOW>FN"^"^ھ}QWe<M8c\qk0k0ƌQz&qDO)l+ ̗7Byyu{hd\U/ӊ1w+_4ie?2gq=uݤ:wi?_/yq:LzN tmUҴUfc=v'^,k4kTu]S8&% 0^E&LN~sezmeWυn/(kG>_OK7@ЧjXL_%Qffpd6Ss{V*' wYNXcA = 13ov*c-pRLcEV,kw>:|a&KAI_2{ |׽l2.` :E8ӽ ~}HWc7ǞT#vŤ~ (si2% &UFOa<+qi1Nxqsf*ib[pxW_^Dlq{s՚鵗^>9wSPe>,NJZBk `k-QlN@VA{Jd7 =;kTo(s׼ Bs2 F"vpdSM=DlE =#EZx/jp8pu4\um<ƷsCrj$ۘs'˭{F"1.隧j[YJk4Pq˃ut[b(^3^qn];شүY}<8&DU4jv9z]*0;.'7chfX,j_?U㵥޺TU. !ɫ鵗^>9wSPe>,NJZBk `qњAZh: .:`c[! 5R\zO_ֽas9֜N/}ߍ!U3^qn];شүY}<8&D:=\M?E5MQ$2fv.'oƬȴog Q}yʼ-&}%us֟.ԤY{sCmgu5WSRxū14)ڶ zD[q\u-`:5q`ƋX1|t0.MT sd>u{hd\HSŎڮmen+k5SqǞT#vŤ~ (si2% Wq&I&gGNsLK?5Tb\6e/? u4Zs՟İόMد{ ^P~k;}ҟ ocn-^Nյ5Dc888Fjsz9QTnƧxKm s@q6^ȳP8+MUs՟̰q Y{sCmgu5WSRxū14)ڶUm75ۋ't9@nA1,m6` t g\cEf; 7ρeɪv?nc̞_.m U{ >JF_UǞT#vŤ~ (si2% /b%sTNk//w1?qSz7{'u>w1?qSz7{'u>w1?qUaq^yMUTl&{hgl,b#bSPe>,NJZBk `5_cjf g&#uA9b$Z9 l_՘;/kw>:|a&KAI_2{ |׽l2.`(^WǮjp2tu(R@;=wF%ӽI*}PݣӉdK5UuL㿗?7Y'"l DTmWgP~7s6+cu5WSRxū14)ڶU@jlcF,mNL@@/wd5o >0˓U% ~Ǥ=]k60"-ݙjde:meIkgq=uݤ:wi?_/yq:Lz*z/c5UNR4l,2]k:Lj2y-mHdnzu5WSRxū14)ڶ]}&f:طF@$ r{AF0@yA|c[! 5R\zO_׽DE8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8fU(")ł\j.M!CO1}I1}q_tu9GW哊_z:,WNbd⿤sގ''7GSu~Y8?:I /_N+On1}q_tu9GW哊_z:,WNbd⿤sގ''7GSu~Y8?:I /_N+On1}q_tu9GW哊_z:,WNbd⿤/h-':X:uIiŤ~ yC <8&D@=Nvo)jF+,RjF LST≗%SWnQK6.o˩EqDӻ7cDUMߗ۸ue>_OK7@ЧjX o;IEm e3eLFMF @1lf 1w=_1|t0.MT C_7q8*l[fdJ-tF5_+Yn|~8no㚯 |sU~ỡjOW7t7WS>9?y_p5_+N~)wC|~8no㚯 |sU~ỡjOW7t7WS>9?y_p5_+N~)wC|~8no㚯 |sU~ỡjOW7t7WS>9?y_p5_+N~)wC|~8no㚯 |sU~ỡjOW7t7WS>9?y_p5_+N~)wC|~8no㚯 |sU~ỡjOW7t7WS>9?y_p5_+N~)wC|~8no㚯 |sU~ỡjOW7t7WS>9?y_p5_+N~)wC|5iZW^Wwj뻮c\[3LctL~MG/@Ԙ..=5W.vo)jǢ֑V!M03,V7F+< FѶ`e-Yjמ9,ey/#(Ѯ̧k..m۔ 7n|T]9ɺU:4x31#7wVרnWsSE[M\|*~4w Ky!^7_o+U=h(=^r4}gMzUUU֊Rr*h1YnL$u5WSRxū14)ڶ1lLgl8v&s0Yp Հ:L80,$dY<ƷsCrjkD8z;ڪnm)fpGm.vo)jǢz@U;V։T]v?`4UF{V;2 5~俟7ժR iz"zLtq^+ȣb{1 -+^FyueŲ(屳6EMT+}yo.ƝU :|a&KW\RՏDNIi83 t ]HGpg\uƱ! 5Sijl¥[WbhSmuo NpkYY0a0l"-7pldlm@' ]ո2v_ZEk;f&bgkPwy[T(top(top(top(top(top(top(top(top(top(top(top(top(top(top(top(top(top(top(top(top(top(top(top(top(top(top(top(top(top(top(top(top(top(top(top(top(top(top(top(top(top(top(top(top(top(top(top(top(top(top(top(top(top(top(top(top(top(top(top(top(top(top(top(V\U4_TW.<;}`\1|t06MT[Z@u5WSRxū14)ڶ o"g1,0͸#pǰ.0&`F ̂6@`YA{V{ owo @{]AS_)hGERjV:v\lpc[! 5R\jl¥[WbhSmuV-S 1=`0[ 6FE8wA@2- qX ct ]ո6 owo @{]AS_)hGERjV:v\lpc[! 5R\jl¥[WbhSmukl 4͛@#t,q 8MF #&- A8DyA|/_H?ߋX?wҦRft킸qiYԴvyto'-1N>4f_fmJ+ʷ~dxϤgqI߫ϱ>Wc}'~>cO_},3?{~Y;g~v8ϤgqI߫ϱ>Wc}'~>cO_},3?{~Y;g~v8ϤgqI߫ϱ>Wc}'~>cO_},3?{~Y;g~v8ϤgqI߫ϱ>Wc}'~>cO_},3?{~Y;g~v8ϤgqI߫ϱ>Wc}'~>cO_},3?{~Y;g~v8ϤgqI߫ϱ>Wc}'~>cO_},3?{~Y;g~v8ϤgqI߫ϱ>Wc}'~>cO_},3?{~Y;g~v8ϤgqI߫ϱ>Wc}'~>cO_},3?{~Y;n֯:woƪjlgU!e7u^Y4ـ⸋fG3X3ș zGqΖ^0~jk/?FVcשڶ{G8ю:lcsLbf"&ṕ8]׀t3`58A0[GmNmX~]9s8k88љ}(+*I߫>Wc}'~>cO_},3?{~Y;g~v8ϤgqI߫ϱ>Wc}'~>cO_},3?{~Y;g~v8ϤgqG"Q4qx\smi̯&]J%muSmuf1],p )0d-j=Wូ -نΠ0V ru1dU \kʧ DUNPV$cusz7h}_zn8[lck7QTUX ئ&{iPE hSeǧ> "g r= =1́7FP"g(zg/\=6n> "=p9#sgސ93H#ɝ32Hd f0O3ffoЯ(&يg,fޙrrr4n^:fޞ:nޞ:fޞ:nޫN*4y^tn^'Lѧzx=7FO3GO#+J=3G|;Lѱrrr:foo<Nzx7/zx?/4n^tn^4i}t<tzx7GO'h6'GO3FO7GO3GO7FO7FO3GVO3FO3Fޞ:fm}hܽhkԞ:fޞ:noo<t<t-xv*t 7/zx7/zx7/z:nޞ:nޞGL{pcLѹ{Lѱqr4|?l}<؞OMѹ{Lׇ,Y<قv4l\}x7/zx3G؞:fޞ:fޞ#Lѹ{Mѹ{Mѹ{Mѹ{<>tn^:e +W&fg<Ǥ]Y?zd&mpOR@&ثzAiwYgzAJoH02'.:ܻt?G;t뎮>n\bøNn::u[w-:}WpqtNq[½2Ӄe@ML[`q} ` O(2#cu@&&qemDAgc˰dskdq䳪 @1d͖dߴ(ؐcv9vxZ{8`#(''x v O(&qdF@2Om0#'d d?Whm2@h&s cs2s 4} =`NL]2H[ñlIq:TJ8$FpIɖS~>}ۍ%{(fFh4vٚdŋgrG)skȣ.֭s9G5q~6[R;?I:\nf7C& oH1{v]6N^fN:K\7pwiosvC^ ZgݶOY]_/ge/Cʫ~CZ4u1h)Wa^ں\^w?oV~(ʆ6,o+h|8!W82g 6M^md;'+-]q~Yv~?e!'GK?>͜|'{7=ylS~n}U'κ&':'LzɋGϨh_6G -4ֶ@lx&pIKOCz6PKZ|s&Nmn*+lSpuS6r~*7e|'NbӋdCfjJcSK eBf˦ڨ\g0mk nsUV[cֆXFTզzskku46ĸB}[Y}Ut$5M=fm@p{&_Ls;֘{v_)Sh6jDY2$g3M ` QĢ^ UfO!XųG9ylO0`))h Dj?(IiV3255@QkNun9x1M'?: Ϡ|'^aQmrqmarUwb*;su 5oR0~WaQ. ޙThB4  auh>W{ݡ1:nss->qz(X<ޝ j^gWCgEqvc/?\Mv7y߸:yHJn W ڻ{MLDa6;"v9(M e2|p?h"{.Op}?!sx2~;!%A$< !<ˡPs,ŠR,1hMڶ&7({:)MuJQs 4)NML72oCZS`w T5 ]Œ}?̛>NOՐ"z ge@kglA+X9|s<'p1 _%7.h]_LJ]4ZqʝwOׁu` Hͫ{l=u *{K~*7X*Jk/7>B>nM1Bu>ۡ4ZӍ8X*1Tp S0:oZUQ+Y_FEB3uIuMY)2 @-/s33T âw/}:3ן4ߤ,5u]*]c{ nY @* ,׸S^OS_@r1%`2F?tƍ>(Li &`$5c,tXÚQXPZAsH6x}Jk_oܐvh e:@F0`]c D&`Heޫ]DJ*2 اH/B 'l(l?@.HߊhN 4$33N? (ڄ!T烔iLTkd֥?n~w⺳$y(̋K2Kw m2u[Y |I>W!3uK^hXCAԴ LEn#r;O?B?Luݹ;^:qL˵oiᢁϩc7@m.`Tp/i`;߃^|=Jz Z0QC?q}pCip2ƥGEj$nLuMk_VbsnSs $ *b]4ġ)Eۑ= 98FBo@9( pvwz܀c3nmoRSF4Fa1cvgޮpB Y"8 »OB cH^,^À}hRp9`3#&1~CScV% (Z/_qmR, 4y; zE".J'lUQq>EA`9A   qiQ 4ȪdAkU4%蜠5 b3a-*C.%s MSXp"f}F#h)1a܋S4;0 ľ`==4pZ ` ̰w{)wEr9 ,?E4̜ f#Uyp.` p_BZ\\To'k ݂^bChHh_LtD"rG8˅9Koȅ;:PШ͒1[ RgBYÎ_qrn8k7XMy<{ȼKs0qZN=rl,ty˪.YVSܭI]}~vFْW=2nIp%]g$7d6WlWn34jb9]jK?"NsǓL0aA Tx<~ݐܙ_ uRZ)7e4XFQ87div`|e+TYOɩ\E sFj-a.oA<{}s#^6c@cp4M8p\" VjnA:]ps?fi[Yijy91^тl`_~ID4h.-]. ϐi7ds`țg&h'Ihf*RH+xif ?l&4H2d22Nh#M4V I0u \H3]pn`4do%in`Ȗ|_i$K 2# * LI/f LQGH.Loe6ɋ ul0Cd@tWɅGdabd/EQ,PL̋x#5p7.FҘrnbRIfCb R;̳C1%}В&,4]F_yT1%* %(*)t LW(^ȌQ[I4y̽ 3ū_F&p-2sbe`0^F&f? 91p upaR,8+"l%ъB(0ELh]Na%\^FhB#rgCBC6OҔJyv4 ЏXW"< Bx+S>cL95=X9OXN&~ FXs+7hY8UV Ѳe\7 pRgDA(@q| {.P\^A1Vy-6q%Vs]t4? U`D{1"/ l.&x"2ds{Yw gsC6ʐ%^b[2"S[t+c~Ҷ+-6؂x+Vtf <2d -,7d er845d 4"1e %D[TD!-ޔ!g*LMb ʔ߉mź" Vȫ@yJܘ"ѵZ^$J|F|%K텑<*zϤana`2&~.VInlbeud4ͣ$ʕJuQ@|B{P| ?PL)ԷCE|.&tQfJaF;b>eC-B) Xv(yZ-0>q7(WyZyo'g|[X- Wj孢<4dyB"7dBؐcSLyQ䜃'HS2 TAdkP?&ݡZoE D/Z\adMWV.8uYM09X'=+u'!qzaQEK#(R}W]vo`& ra+̨nAi[RK@tl%"4.᷐h{,[IKSg,_4!tA)r u|3b]we\%Ndm*CǂRE׆PUH'Őh(x~G Ljǐ=ܪW91d/*0{ޑ /8|`X\Sw /ށ饙GI]1RוVU Ed6 E5gB +#٪ҡ^ XQt,\ %PJJQ(AC DI?Xa?*7(F]7Ye[ϒ ,= }.[Bk)֓2a- )>DO atA:~""h88~- ȶ8ߌedubh1f|0НƮ<w泽Pr2q^=;?ZgT6G*~[,!6҅I: D5u_o8_/~6w __HsGV1E{F~4< RU`u.ܺj3-ImtY80oϻmaqӊURap,^P/;&. ޖٴ xP4])v iL'y 3:Td5(ͪܕe/[ E{~':ےfXviW3ĈH ;d& xZmTz?h?5;ޏ4j=/_ȡ9_wBN#@wW zѺea8t2I2j0|gcr/.w2wcISo;@+8wQTìB5(Z0-~ީrֈ6(qV;_` ܅ Igtw3'ލtFKg40vðb n qG^;Q;@w2p~;@"ptc|/1D2IE0Y>YV-'F jӞ:0/ ;sGc\i/,d/ /oXWij~*-|UF+=Uk as@N^gF%=ieA{ ^۹= c @-l4 M üϧ؝@,&/Wԫ׽,Ru¶OlMGGbn+mq[|V>n+mq[|V>n+mq[|V>n+f[Y}tLט'dMCm^p̫|%s!A]}:}Dbaih~<塴igLϪ]mNdUI6š$śl >z)71jCG{zǏmcN;Dk=" W6Ma(-ED#@n6Jh`$]  㡀+hz#QS3]aDAA9 ̨`Ǯӿ{jRpShZ6446Wf~,̿s-Y[e4]atmUkJ7hd 2kH1#\|ϩMI)5ZIɖdc芐cOؼ`<2$Yg0oHo0Dw)rYn$g6U2b>2-<:)1j3J*9OޝX%0|8oO&s$krL*#6>M,mdk|eW|wY]W;Sq 3K 4 3ҡ 91AB=֬H5c Z¬`ƨsh+~(CG+q*rJM#JȀᑱ e,l39I`"tD'\8i0oݥjf38. > OYOl7Ŧ ْۢ{-S~6=*+N fYQ'`jZYϞ]afu{ ޡD<ڽ(ࠐ7}YtK_,B|/e,7u-|r6 WsxH- UP 3s{(FZ.e{rC+ 6ȳY~ĩ2YQ+~H 8y47&i..,? 2w9 lGG60Wnfg^7V.atisFWՉ!z:dēBfE?$_7mum> DN]AJ*8Td!&Husdg&GgT#q>P,kewpO'P+:M { fV&jd D氻ܘIۻh&ƧO\(ʬ{%ƭ5F]^US^9uGÞ;ah7c\RMhhyn7b?F,5}!>bOdhc%=bIʢMqR#xɄ" Һz`>ȢD'8xmK.Ȍ1y &kCo?~7j*S,K}sshպ\: VrG錠ujh%Nw 쪰EhFr;#cI|\f.Ok?-P~dbI Ff4m p]-I $XTTe^a-\a,6S TNg:i :;rlu .}, ?x q}$͹,c%뿮I}ON.< |²T4X/oRp/"1;',L0Gkpl S]#ˉJ)˦[L33>2L/A $Pȋy#+HV2O+jkrHV!$dzw9MCto0<|D>`AsBE_VWu&dN?4~'Ɵ 9-9fn8'm'~q i:x7GRF[VEv4ckӢw9pG:b1[?NȁΙ?KڏrxN 9mZ*~q X7񫹁Z6.m Q\m^< W=R-Q^56 ~cCs@'OX|]Ҿ' ',ሴlvTҽl \:"+~f(ғɆ" C)QImSab].#:2eaBlR] GCֳC^% n+]q#mQ%7ԇ&=%كVV('|XRH̤3j;' zdž-8(Utzͫ|ìh24HuK{ W>|C9 L`Ǖ#ۼѽ3ϓ''NwDYPKjFt7fX;m3ᴂ4f"mn`uaz${loYџZ1-%ɳ[WdG#oDbZ+ SwqN>ۿL?|tՆn9^X+,3Р>b^8:QNTqU#5-e'ilLcIL宱iC 8#& gGk55ptq8<&1hBOxۏ"KcTMRPxJW=H\1üdUgL-=L'=zN؎ysQ&gעޓq3zm'puxj'ړ&F yx2&ݰBAt/{3bYCG[IrHUl7GVӟEGIU>׾Fd]/K>tAvٽtJ hZfGrkwٲQ`G6Tnx622e0f^+][Umvfwf>yzfљHc5*i5pcA&WOdlFt5pGIMpMbq^*y_{_q {p~̿"3ԦlKg=׌z~OS=p-l[+@a=8s?ƏL|MjXj2mݳfzEtI۞gYyf7=@{ݛכq ~t;6&9tġJ bˏO#aPʆ!F=I:kE%K\U7p.j˜ y[cx"ݥnIw )Uxd阙5H)~aW#M^{lǧfz舋⩲OD q6CS35㾃ti so dMw:RUZ0n3}t]ˏIdv'  l]螱%*_)}+zU`spŽ5Gib'4;U;y"依rF[rB%lBKLмeÒA)rgKmm[@k\! F! ьYɩubiW?cU=}Edmdkr cͧOӂO[fy[n'&/+ߧ ?BRbc0"FMm6;^Mmq}g2/O陽7+"cٙw~;PEv񡇺F͎bX(bOr) i!͸pcO}W`T8-`2 ܝ_.wE@ς|Xaw/IX #z9tᑖeCy#Q?,ʦ̀Do.W2.i &`@-qKkVc]ҟmN˸$ .>f{+Q72g|c)}W<:T\oؼq }`bˊF5($i;??w?_ӖeY3HNH?#)vPq%U2n>l0g.YϿ_f]ɜO37wWHh`x8nDOu}Ye6(> (rTtv&.~LA>=)5nvW'T`:h2E'19/?s6iY#pݩ[݉0oo{R,1 1we=Re y-wynP~0*?z>+Sϥ4QvI% gv]RRZRުS6s]DrS7.= \_{|@m=4/DcίY3 Aј,X6ZzSq6Ht [yAaZH_I[n/_f_NJ?> z&'jdZ>2 n5tuu8,ﰗ> =Cxs![C$0'Jqk1=[x=UN͉%OEsm"!c1D':4袻ޝFt4Ftd":mhr׿+ՉdQ^d&SA pI]I]8ON&#ѝ7 ;F<#8''Sٍm+7G2OrEװ:c` nyl 76xxxxx_nmvvh3e&ml[d5_ˆ%`~3ׂnhBg&tw ՘Dk!xrCmG ՌƏ^t=>^ϙ]k~[+ )$XBHH|Az>72\sYXu߀u 5Av~'@UxDSFmLZTůb[LvECp})z؀~%=opEo %K<N):~k7[\ڹnu.-y%&TVvoV@  K/EQ/奶-#Jurr 뚆ʝ.=-E:y W|SvIBzBWmvo*>:v'fdꭌ|!JȾAM>ޫ~K˴.,[ p+ˊߛ![Po9'Ur(+ 0{͕<ʸ>qaʻk2#&#wY]6dwI*#Ct*> #~WL,H"DI-f*bBH4Z/U@VZh;^ x,o3_K%G?C2J:eO79HJځF݃??σN+a2҃!Fyy\=`(((((((((@$U#lu< X(Mu̖Uqb=xB]ꂦa9xgS,ز~Θ iU٠')ŝn=I^0-WPGHmL$mإd 3=k GVn5~GTK $"k9a*Hh\=Nh$] @!<^y$2 Uq``Ds 2\Us_ z /BbQ3wFb9|đYYGfNBY6U Wꭵ~5>ezuVx`Ħ}[D:Oz!.=Miݟp id#@(A,82H P\pF̘v(^eLFPk̏rD.j0~{bS0G#Y`TifY!>>9x~]%2:\nf7 ;80 )f`[CjM6*[9WDs^:lJBv$iE;H'^~ӛU!3O=ztaCʶ|cMff } "b M?uP:S4J d]XZlZ3ڲkk=`b[IrKS?Usk6ŰMDiknw9)fN 3P>RGR L鏫̻mm[Ig/<LzTݯz &zqЎDU8XDN;Զ<)5u/4Yd4Ը[8Bժ!ysJ3 c40fiQg ;咇CH8Di &pi&QRX 84k4PX<ʡ :UI=wV4r$gL@8uwU[D@h)0 玢>,,|X7.OMM2>򟌢53(/ TkWe9}?R `'yGS waQss'q(W}#7Ti{9Qcʓ&.w?[)P|kM("*Hԕ~UHpPN$7aa~h-%AQ5M% | yNCp'dQ֐ $eYZRsm`=0EJ3MϜL B{5p u垸(IJ@5VmhHxގx^ Ť;r  ^XT {8rgn6_3jz y][$gٓzU<*O>͋.2wܽk[I }aTMIecfǷ眗)j,T0f@}㒗K ޝyyڨ^=W[\;,.Uu}{Momw2YBڼlrO}lʵ|mqY5kM}=kz\EOZeYNʵI5*PmqY,FtDHtVM!\{shq~=-zjò'aý9oh>f\cO]uTŌxrÁe﬚;K-ltQ/nge~wQ.\\ϧkfˬef]}aY.q']fF2]f v B@D`YR䛧|󹘯]LbcCkyM,i9< Sw54,h _f|X\vUճŰ>CR>;;>+&f6<ջgA NoRN/9a8AzWw݂<:P6O@aYN3PJ4qrX \㪙%L0e1(;b\T;$͟5t2L&r B=pst1`z_nLmO wwpv(ro 13?a&ŨT(!U4Up \#GlR*٤l7hrRwP;U/kZ}H.D4A]!8@\v:&'69VZyRXU˲ͫ٢I=ąYNGJȴr4Hͥ%B1UӋIGXE<k<T& O3؀P쪄͘لG,Si= > ?N%^Ht[x;zt}ٟ#xlZCWf[/Aom\MkZCbŭսe*Bss%DA; C@ Vscz=d)W8?|H{:{ Z95XQB7-zPvGRMY66Sʦ]6) ̛_q^\Pwj2C*?f,wYDG2όU>Է?3KGtO"ҏeT{YT/2[‚hq:E m3Ǔޢl}. \%gb*5 o[ E7Իng*_l-|Dx^ JxDeW.i . uL%;A JhͣzxdP6 %J֮ݼq)ؿ xzLգ߯ Wp>bJpY|++'P*eN*l픓5 ˔L'ʉ\X\)u9j;K=+.RCۆGyRͨJf3v.tEeRi*%#lyPzR$QlBJvIw*/4ap]hsgL[gA3Da#(E,mv-@5HtiEꪂ zj {r͊9q]US6RR#4{7X2Of_L~ZMs/z*o줾ˡnd"T+h|(aO>/5A.c^a!/PR؛TTR7<X&vl ڮi{R/<~+LvSuvV Q̶͖3a,bުr23M }`0 !2>Z6QW;4#¨K$:%8>7wtH$Ʉ`?Ā*/s&w!3q!j%KrIOv/꽦3p^nHUͫjZPyn9K>`bQ!@k6:Us-Ty ZoosEce o 7f˞f,T/E0a3gP:A=yrJ~j/ضxP lBLUp Z+ #.\6⸺*EGN$Ah vl+3o-};SW ar?/Y]Gb٨<q+SC5QEhh$ mxoތ=SM.I1Y$}5Z'I> I;ኛ t"9͑Ig?}E@:s} SxM#I7L'L4`]d~;W/ޙ[]IMOmm#Jx 5s!-%'-O>:2u<]S4$XTʗ4:_7J23A)0oiqQ݄M1@xQS;]DeV~)@W[7?->25'odғn6bf'm^!.0h6(ZW[\ *IT߁${XWe/`g$bi30|ΒIqVN J>zzU;dֆ>KMb%(ٟJdLЊi@0,_ d6B7ٍ+Qĵt (I Sulԁ%90edBK&Q1yZ§$D0 <,Ē(IfqLpzp꣒Vl(ѮI"G*;uKZKUj[vS=WP]7٨7EYG`[F_1W^&+,Vfp= X0ޒ2h ROm'["%NuEӫ[,w?[.iWovvyIn2PC@22\|5p1,u{L*l@]@wڮ֖%zזc]6:M=Ϸh(q:-irMzфB!bҴ!՚ȥf|Y!2p-(hĚ. I2\7:AثQkmM$^ƐAcWe.i42N^&MN|c{UM@o@ɵrĎnzM{"C/t'"2Dp;KtkH(E^!{NSc\_1wk6~@U+q+Cޤi&[Ժy {78 C.━tޑ&A=04MNAq1,T=+o|Lޕ_>_v;_ABVQyt;;QS :3n83g'8wb ,]ye鯛Dl4,ܝǙp㲷|욢x%͉vv+]c3E Z(n9<d|Y5j+oF,I:`0m>YL[A`^oo5jX5ux,}cpp W3s}odr< ؍@$ &B6[7[ Ot* M7ldeA `ӷ;U,a2 KRldPU'yAD+]]!v?[- UĄ@|JhaL]RNWy}Y`ډ!Esy\Oȫ1Q-7>!p\آP::ꍮ-pv{)$dL@AӁ!z֜VdSlw3"qiejFbYK }8. B=GGd׋"\ׅF MHm3ݦeqmrGA96 x]XYθB@0mz 7)9Z|6'Y#{b1V,tIvg3WvFأ[f\FfE$ٙ2Vuhq8)ZqM#JlP l% LEZ& D 6U#II&m؞Wu2kQRs^̳;^oMM ·}=reBDNyfLnmZl]1(~PgZW to>6+j\,PZ3p^'l?CH_qe ITÛ߂&±x"J9FL) ]jjJ &-ToJ\JwS\QM#Bi^x@!bXmꀄ''}Ƥx[6ĵ*edMpE>}gUnVXqE]@'x+hd[zCuVӼumL|x1e(̅EL SIitRv-cu%3vNúOelAr2QJ:ǁ_(,o9yB.A2{v7_QRW3!\GP~S[|յϩ˝@9~iU_֣5(?a[m) 氼@S1`qOl-9QPy=FNpDc"ΩrֶHGv0'Xb 3J0LD G'KsM0W~n3@^Q&`}EOlq_r2 ‹N}. knp TmeSLe"yl r"ol:5Uhh1k;ҎBHd!m^Rŋˌ38ܧHz Qeu"+6vȕېXO3ySjL-N?xq< AXX-VBj[e/h.:U0 .bJ+ae%(ۭe} g>j-y"/#yGH!&gL/_~|s<<{hXJ2'q 3ұ=JOT'ƺ Y1C\mcpr]uFגGw K_7esퟸ+@~$V)0;,^]hvˆuHNeﲚ-TpkR9IoT}*pwu[ymNYX[ﴠB'{t/"F1uaW?}  jw8PwgnUȳ $ȼ$c@;t˄ϸD ?-RN{j5/\i[fM^|zXfmal l*Asx&0c@7,hu8zo;{rK,n N e bO/9UJv55:LXm1U yXɿ^+)~95f7w2=U1耝Nk ioN\ A$1 ֠q8AF!#(A+J#{:>:5t]B1D.#,ri5e(֝ ]4g $Inb:| H6\mɕWU۸8}YՈ_]= jֹ2 -g(}ML6-KS/34 輦'2+;%7(j<3/T_;ZĽ巴{1! C_JCL$_kx΃W]q柀11#(KFn- ,|"/A8M;{wr@Y@MixF}=4,?\W臧/hjC3$ [l{ ~}}%bwo>focC!#aE=̟lu ɉMOwb>dd笘;jŬ jdwZ (cƘw+4cQ6[0_^Q|s`GJɃb6 ۖ8(*pq'@8uNM,2MLiK@-Ж?Wnw 2joK5ӄ[qt+B@h:?nlgcci@mo? 9<%P0RSe[?W}k)}(1ll(8Tp|A(r2I(S‡86uT&<-4NMMFspMI=[QYVӉqR^"ncBʆb7 þz\VC:5UNn1hK 4[Fn!(B8ʗm>6IfuD]Vav-G/ЙfJx-w4Qnhxu&^75+d

!$G,lx1%)c[$2Ol`VVí>vZ%[׋t@e󢽦w`>mgΞ2m ^pYǶMΞl϶wzUrk儌?ͲX?9i`{sE)}*G!u"Jy5nɌbݕ8q1'ܵYG o},9ISzU,j"}0J [>o uX#B]>Mʁ{%8%T;p䢐{uq`0 2aᏩ_BB BSICe!nx[oTv氚1`J-Px}rNTzO&_4ₜRrI*>~ 莞l#sw.nZ[lWz%쎁O$(29J)40rV*~)0i3WOzr:=aBł̷9Kǵ nGBNwjogDC"br=7a멐yv]s8 El<?$m~;%tJK3(t)4X5Qq{+.H]ꁵ*7P |s22Crl{Mv|'ca[GcȜ5/cᅹU M’QVeAcB1}HIp8~BSC5orޠ>4A>o}>,KzfP_`d(N] v^l/ aԤӮ,gDzDEeݷHVG qzTCBJ6r^`e*~Ej-P;(TF%ռJ*1p5Q)WeWr )9E N5ɼǧQWLZEP yVbk|,s)A=dзŮgGџU}ve'tlV)4@ n[Sr6!mJSѮ m \yGgÀ7!!Q?2H  /9(X ,vRs_:6%Yi@E'UiKlo#KKK+zle^GZW8g'.$Yѱ1AB=2\sסi9zT`枋HAѝx2o)üz%ěWm,Acݘ`)ɗX= ()ht.De8c`3 |Ϥ[{bMvtS@jeul{GUva:3z1Dmw>D=]Tj`īU%-DǵTHe"FR@a5M0ɨzJC[]]WF0#+M/z^̑?(:0h@H'h۱ݑM[1n?O- ،:[AׂGv?/>?lm T79pۅa9L.HT' _֬bsR4 v#uNCg eBfi_wPR8 Jq,J=.!)ՕUh㪘 n!XrT\^=V̋qu("!TOR5mB@߯~m08U!a֫jڱ+:J`o7$PkCR$ȋJuUoDU`elw 0Bqt8:OyBNZ1TyHbldny;-{?1uJ!fe*F%L@ڨf.ye-F£'!h/.ƞwf\U[}THg!-PCۆ ͓ۙF;sŽٔ|ٟݾ|^ ?Moooi6hjmGeBnˇNd=uCi{)ˆ5A|[|i =>CcKͫ5 ZH5mo'G%`teiEQ\UWpgzl{u( 1c 2KYv5/w_ϳ5x,.eW/ ]y1Q'6 U`zmG2M5.\jײDo=rsf>pzxYՈsJ(tg9q74_:]wZM30oм츲c,>>N8@PDxLB Acp/2Zߪa;=*i{ޠHZ''z 誂uӟ[ԬrZGvVxE/dRg[qNQJ SL~WD/9%&)a`IײB:I H3M}xBfNKиnccd2'u u8/~ bŕsDO\T(l-r政EJ H7˛1 a-.Dr/8/sn+xDc="l2Ej$崹k{=oHOVި8^9%NwfcqY4&ھ*Ye ^os )ce %bp0+RXHZ=/#%sj}eq V8~iC]u |W9kR:6~R-ˌnIș9Q׌Ɗ_Eb`/(Iѫc>t `EnL\'\0R\Emfԝ$Sy3jM,bQH%kJUzbd}@͐rjkIIJ9=O^J0)?k0*4xvד8W;-X7_ (gr`wS}Zilx؀& ?bw| KuA74UXwV[_~Y,VƇ]T045ߟӐ x;~3<_uihk"R}Nlw;kKl?ŭOzc^Ղ#G[X?S0isW]ЧU}WvSӑu7. [}eW wVӹ<'0t[j rim$3QU!AZ!XxtYaANB'pgmoml=D~ι,׸uW ^N{mFa3Ɣ?p8w?|Z|. D=zAoaΓCg }1ϵtgmtZǛ@^߿ys66W?7ý˗']-Kzue^8cǻ/޽ y&/.qFdM?q jD'=D ߻-E0oyi)fPTNijߟ>Lb+b͊rn1VN;4$7SPe/NuQ7UA]A4y=QXb,h?hʕ*Dm¹Kl>gIcXsKWt).M <~̆ܵry\`R?(uSf{hT\Ēxts5_\9$Upq![koߟa10O|ɐeot3u *{9U/$'7EjC7rwV?Y@ȣ 5ZE1G4"XZo뼾 ~twߥ# x #Eqh> #b,gP#$좉 `3VPME 0j+CnG<ٔtWӟ-B2ُe#93$_[`$߼^0̍嶸%NݯގO5mŠٌ]P&\KwnGIcҐDhH1 Ho!ƢCq&K=[|ZP6Wp8ǠrREQKH}$VA JH0ڱ,G%(5Nf'(8&Co DS>bx+u!^_OzⱩ1VSXi߃5ɠ{[s|,fA,,/\ىOA2v¹Yhf4].KBB܍lq~'ڸ'%2Ӽad`ɑgM<$aA &gd#ͩ^VJZ=1G$B'eUzɺ#}d_!Xbbp0"gdP[5`NY*~m`wK;Ñ{MqX.7w._+0aYAIjU7^^ gcϊW(~1ȫ:d" lQ܃F$1{{K=OI@_x@. E b>|gC@ lel`qe٘0BG ?]uTj n QYJ0Lhj2}n" 5wn<բ#\[MYp]GlAe#KAD]c*lIG{Nir:ސ^݅50{P_m]201vyU$VKϓ¥ht+1]P#˼Ire 7ω%K2bdVƞk~N;^T L"z>02sJ@@uvвT|-WzQڗ N(CJ.3 `:pD`*U }W#=$Ѝ;N:âi):|M *u^Lh"HmJ&߉$UyBmSe],5X(1,vFr4ȩ)͡lci; +UK4bE9 &G^;r@::F&S'0|~rN=R2?yd1{:g `C yuR}GB\TL4j_!S W%?A1H_Zu0{ s[gܰ:cPO#?!/| 9܉":U}l"s7Pox{ FۤKU<Joئp\| {!}n?= ۆy@@oMugbe99U%#jOVŮ^]'f :VP>ZCT#e E[Bym&}P2eI _o%+%IOsmu@w2y=k~Op҉< ҔTa=d^o%CРr:^#a=Fsb|:- ]beF)|ڱgk:UY+tkkzD~@ %"<ci8BMdj^Nd5@ R$e+inaLzxL6BXmlʧFo"/+0f'oGP&]fR|n)r*z%GˡG-Ic۠>V"!d=084g _ʠ3xb2mRRm[<hl2^QBG<hp>#DԒ(StlXN!Dy-z'R#fӣ 0_5fiR1B`1(FbGHc4G2hmQ7y.P: >ԱL,YI 5\*v@=pH2E)7U=;G\)fywZw }A]۩ 6 IDVkLzt~Kg*tؔc†KU!8sOݧٺ:E^LLŽGNn(Ij{kz&4޸䟜bBKdJJh)6w5^Q>j!fbG:= `g1flnRjjj+ #+4C-!{ mVuSnrEi &BG!%N# s,1c4Cc<kXjZO=?ȝTêl=9QkA5Fe;GR,vDv4GҡMNT fp9&qUs\icΦPd|mI7X.Փq$-{/bڏDNc}"=t6c:gvvMV47hŠZ;-;x׽+үk}?o09äM*]oP@R6{|**:}/ȤOڛNѭgg[;1:k/I;l?2*ׁ)A=wOwǃW<^ݙ{y4bo6:p/wRR\%?S(XǺ.TE_X~y@48*_'Kq$Y~*A(b}a V;n= m*km R]P7d pʏT 鎣j-p=86]yoi[8j97%*wP;pvϝ?6C.^qb!*G]FK[jF纜= gUI@ת3+$c#)Q[y`"vs:' /ַ09Z[i̕c[>{ `OR$x7ua67D_}E|GծD/: F-s[+˽ՄrO:ݶ N$(R?a |r|XIS'|oB @zx}q1Qcn 2ąsWR5% t)hgy/qf$rOW[ﴙre{4}ǀ;КkO }ӈb2,J>k%6s 4@;N1;s!i cV:f:{Ab3tpLU<*Qv $F CMl9i n!_`WKVxN/R)Ľ9!4E/_nog]{f>鉰˜=FJ L7ΙYDBG+UVy߈PW ⡑ j+GM.)ano2K?73a7Zv g}Ĩrb.:u.qKJ28ZrsDFu٪1}96$;*U2>sHo#f,irG*qnK%0`R?osߗyۣŏM8t_3uR5'/_Ò4?y8ߔyD16..h1DǴ~z^1#ۃ;#gɰV<5{Z_W;~}~~eFU9:p>T@A40tɐG9.΅|ji#u8aTiZchN :gFtgk="2w<崜:j,M:bnR:cabE)u=3 #gᨘ TE+#!iA諿B/L`1O(4<je-Kkxcn_G;g897CI12 &Y=Z:r`\C"V#(]i???У+953vWi^HX xtaߛ'}@VrKUnr@K Ad9ب)o}X%Yͻ C@;>yf(8y}Wiw{V)FR(FUDzrXIiM0`QîuN{w◼-ĴF׊InUv;A@$}wˤ[P%S FT,sk{5/.D {' T C] .LG'5))U7VngVz1.tK}aJq[{dVSqPN"\u {$g빊/_O_P>pPm^͙'&Wq-).'޸l: _߻n߯+؁:7Uɦ75S\£1# :U)8UlJN,[&b6]QX`(m4Ըz~6Jf0FB$VTUWRK?ap"JY2₶0Xĝ }{|[?dЛ Fa67+K/N.9NSMIXaZ5JmGirD?\D] LlEdDf/~֫$҇jљxVt1= d>'QTCZ+)z2c4YZ`4ꏕJF*]=x$q*$Q75X+kf:_zb5bW Pc*GZ^Yϰ|lD/p);㴑Uu,n~FEJVBj| * & e@z\Whz+MMIZIߩ7lpʇZ]ݏ~]_V~ՕKJ}IR˭:tPFlKjOAd,akB?BlDϿ@ķz&`zLUяy9xEMfTm1_D+^Y '[QDmg ݇K99w)%Zu)rpU\G᭢F; | QI`,8 V9*3z zYRD'̣5(4"O`le馀.Ct\k;L~%x6dIOUARHf3Ima_Q<' LKtw[|0[Bw:Ͻ vOA/I7LB!v>{np̟7ʃTWgM(:6=v^߸7A Tg.tr}3G/c -ӑM8趑P1܄Ѡ-;:pP.&K\#zOؚ)U:-@*"#PQ h)R,"oym!ׂWQlP^ 6mr;>nXx (-Fv4 S6ƾ4tU ͢VKQDiA,:oӲ@.b6~:1F9wsbNBn]|KHle[mRT+6 e=_MD*i EېYVsVT=/+; NDv#NӢiR9}:V|$ >rbsj\/[vV_²ص M ~oe/R.I9" :6pm(uݡlv@8 Ph8 7j:oZ!g|*#eSr(c5mla6>HE׵L{ܾ-VF BD@%nJZ+}%[̴pj[KԹ%%2CN=/k%xC(#T́SrGpspk@3\dgfueE _Nvt`:SC9Я1x>,rհOPh(YbS~ 7ucçzvnzv`d>&sV/(cn2VT]ڿ`;7z|(6_{v$caެ9$z ^Y3R1DQ1u5/`RJaz6C?OH(1i+,+^|St#K6h_cb=F;3ܔ= Dpr/A=E;Q$"i*VAU:5d/4ݗ;|"><.kkFr3c4c'=|6ûNjEɤ;Uⴛa1'2%&zavkc#βJigY:E`.}ف.]BM=\6&w+(toܶc#f@ E#'"]G[SAaRܦ<6UO\ɍ:׵VR@ ÒDoyS7v5= %QW{ib _TzGe|p -V`:0=R3T/%IЍYtkd6n 8QfR&hߴAj]}nctBQYAHvPNxz8Z7CUX"{ezaF82FrL39*2Ij_蕡TU|s#h,uRA2BiejC(,͘Xq\K-$[D I5CR&6Y[|EE}պ7tK)nJΫ-:֏藳xb۷)ugHpJ(j[zeoo7 Xst*>BH"+1xEw\,r -+>$rJOk3JJg-W إo?Y9@W#z{ȪMeAM"p Q~!R"h (sIx!1o`.&f]'nmxV!h>Nh|v FOPG%3ul#Y Wg 'm37)O1zS`A^5991+%v]1xVע 6)զ`7>?*akq!%FW 1ꍐ-|hѥhYud^;ؾEQk]a%yý$1+pm¬mqUڒaWҁN!v_O+/nxܛhU,"Y9ioXRm S-g(c⠍s5ڀ.+BTO%>v,rĆhYV6V*S,=>X1(6#JtMo!>D*FyDSՃޫt]m ClKhe:MEjkI3 SWܴmO1.;=!m۪Эd3>yʛ&2HG)~t0>^h&=(m}& %?kE7F=-o Iegy7wߔ%Q9Ѭfta{gCQjˬ7RL U@Ȼ 5w.ƺp@TueyW߽"dl!K*v&g`ysq_{zS27{X]iVϚ=WpVodD+Ť>+&"}P[u/39vUx1ݧ[[er3$uuф j_;YM~"GrxRڀ`E GUX?79vHj*:ǨPΓ'Y?$2qx9P5!RNDžEY}#%u/omR%,Fh76c '> >3F>d92T{̔/dKjfs1-ohRW,\JxÅxbTO6  `ڻtU³4,GMwxUZKTZen%Dw\9}xUCPL pICJp(ӯz2^A<{?.*14:²r q"Hfv,eBe_OX Fu æ,)osOVS."~7+ttYE3 (.!ph n֪AG|6/OU9A9ټБ'óI1#%,9 ~2H9\GA1d~dZ\T |c9'yлe+p}n9?n7WmaȈ$}5+؁ AMuR7ֿs;C[!/Β\rrQK8/5gah4- D3*pŚawUDv0Fr6l7g0<3`O3 c |b.lyx5ئff~4SŞ.AgU#ۥ;Ht)7S\Yv&1-'@>@j;BLܹQp7w$QfJcY#h5+ZP2B|/d DUf0rqot([/|Yw6b>ˮRxsH˝]8wZc^.w_/['P4ODlѩWet,Ygk׉"Ѧ?Ffb4p_v&UN\KV&n*n8[ vY@ٟgeX蒝l(BE&ɤ#3'|5Z#P;(5kQܺ^9ݳy Y2ZT19 <"li|^[~f"ŗ3DCGGXRfnȫ"kђ2c48#h9ޔP4Ri6.z1ہ3=eR3_  wRMt~7C ٲTԡ{ף[MPSvnӪ2,qv6Yj8)4ÌgGunEUsuu>xtN;x/K`.5L'40`+7!v8`8 G2ZH5YoV.gA#0D xo^QcJ %U%b3 e]݂ o^oB7`)wK&_ #㹗Gu)pEM>s@?˪b:AgwuƦ XC|x ' 8b`-qQo9u>3߇эzbPY / h+PNy W+kFSE!jClK C?)?؛#a$tc SePv4}ϐ9#,*cssD-c+cd>f:l {5Ӽ ` 7=tݻCBy4j#xS,Lg]X%=Ŧi~hKbHx/ wIR3 0 P2tf |ELs>;N7K3ly5?NB:XCÇc4q7L2zQ֦ ~/s9EX!&,ݭhL[^c | vf}{;XցQ|S sBP23CV˅4^Vn?4&e+ka@/(:e3wt@ ,&c6›#ܙ 7Yf^&Jď.}όuV{~0y &㭗}ݺEH΋bɐCF3<>?& 7ki\<,A6 E̶.MCTôҿf|:SѨo9z,J;qopV0#'÷]Pź8ސR٭ݧDk"Ёw莛a$su!z!4ޕGkΛ]-umG}[V>)8 xT#`2#M` 䈮ALz3oRlٌuu * I7HYNwlڐCfɆn's0~ :5FK_sjGC m`+-@*b~RY9EhZBxsCaKݟGbL4c܍Z^HP87G#L M5o*x1~>;;is-Iޱm^`s! n7f|ZR=p^99;-I Nl7F|/<;:9^~女zzgΙi}WvT*s݊u7׽GI5?ONlf('V̰Cά^?ZA=ks۶+Pt1eIW=763}MNgHHbCG:w/Ri .b28[08"'M"1:_=|QO \PS>,iA~*hTrl@^))(-{()Y$2dhLY hJ@ʇ7PK’,r59<\N\$?x/(_RZRhI Yi/ǟ5", _.ȋ8aa@9 >dd3DFV0e[O2'aF8N$”I^Yف^^~å ,6IːD1~Wpڻ8;agqrKIuY狽8_<^ÀyDy+F{0F@ y/Z] -(SގnaDuR#䬤0QaI',$E_L9+&d+us4ELmX+4< φ%[ZP$,ǽ=N9I\g:$8fpO=;,QAAY ~lwțBMad^MQXA+6d P&TQIz2y+e/'d',OXdՅZqcſC2Tc.$ 8<Ҫ'xaZ£ӕN+6f%OP)}p4MKXnl *(N]$9[a>pށ.ARn7q>? kGá5+C T=2dE|y>)eX̓,Y NtN=J}]ѰD_ $F] UW/ݫ":>67i+*4'Ke \I?}b9ĻnsMkN!fi?WKuJa YD!Xj>M72ʘ*Tiӓ{?{WW,K[ o)ޅdUK}F"ZkZ< i@E ؚtyO> yWA u%{aI Jj?V}ʱH[MkehL(NM %+HPE#{jf96s)2\|>O^*MԆ/+ =ᬠ寢-T}ː&n0+V2V7X# *.-jXfx ?}s?:Shk՝|,yؿzfWÇ| G h(#adw;pǀ'L7_TSëЎ…i*CIJ A)u#rSȫ?FQ5\/+m#jh+{ڎ⟮Qts9HHR>5wL,#YSDC8mQS<=&=&=&=&=&w$m5UP LUzJE5jh(;JUw2Lp^!W?K9ًw(J|\͚ qH" eV]pY7 @/4|ORxRe4/x7ܔ<$쒂X{MU}|_L2 $SmW>y tIur*D5TI])E*uT2-4ޫpf~R7 i+u.;1J^"nꐅX&jg-W/*ޚu_,<#f#KaG_'hCAC#jT2 뵬›}yI\p)`U myZ.VXؘ%~kI׉a9O ,WatA{lLdQЙ$h.R-}8eT2-ZʼMc@܈2T%$\g֛ YdVS6^u2M 7$DYv37aw'"!ŔyVX쮪ŧ{aG,R=EGѣ/([%:.H,Ä*92msbC0MԊǟ6 cy,2=? q:nfcŽ.]("}K ԸZs9j0p~u¯л]UiZ''섎Cz:Aulvmj#7 VEVbXϏTD:.Lj*W ,$aG7e֛vḪ :a`MoGe")~B}4SvJsIW\TUr4.2;6Hݫ^BT즾(;& Kuzs(*#v ijzאL9+AݖKwc*K3sOsNFm=r_W{?CS)~qM Xow%tv@szZU|'uc?!zZU3ZFx%9 5۞0T3w|H`9F+f^ι]`',8(SȊtY<Z et>GC\C ^@:k4ƭ=')u۾LSk|jm:CӮ݋R6ͤ WݣBfh[7h 7Otg*Uf9^bRVnXꃓ~+(nΒ{41"{c<ͱ/{$́tl0NR$Lk0|.Y6G<pQ}AM}v3q2|Hǰ[kN c퀴L7мKb7JQR!c&3=qF _,A'omG7nw<3NkEl53:qzAQn}e]߈,5A^:5%=1Csn|k2ko\Z>o4`s:^ˊ`8}87< `$NFNzОPmՈ+{Bw*6{Dj|츬7t:7ߨе9[d9z )DZhs.B˪,v^.ro&rηB{k8[I T zb >2B:#nyvgRL/$GU}r9y vVZ9R^j,m]ۖCRoO-%SEZ%F;ޓ\~#BTwĴD"HćrpWǢGEUNr2fu%m(z'qzRdVy-k9ga_$Ϧl7٤ΓΫyՏ,m6eئxEi9/njrϴ<wټ(.er5fTyUL?zhL>qqOkЉ|[ۢY_Fx~ۼx;).]GU~iEqX׫t-!嶬shbSǪ\̢$ʪ"q>Oe1դ`y]|:)r>ϳ-b:3f/y#r_cԷ}$-ƭd1g:HIF’GLOFb^F/ P{g_6tx9ɄE6O55;;!;;z61%JǏg՜gQ6g'FpݾHq?LKE! r~nz=\lc h<PS>S4YlR`)BjM%B>&AQ0ۼ:@>^ ׷.BKʵ#Kٗ(AMuf3s,;"LripL&4kIܝlIYcJg.&\U"[~S5 wd%!8jof?YBU>Z d)VkMsn *1" MYuT"~KLڋd3 D%{R5ݰ}A.a9{pm}[mUy:Lr|.(+`VK25)F@Fe(n]Fpz{rxxd"ᨼˊ@|6Ho9KJ>['lyV!rWw-v^k²yv?!I*z8:AT,2B ΫR戅f*ljhdv1V`*NHA @{'7Ŵo͠RLwh?0tj c=%& "K܀]szȩᒿi C[!aEDoO?oso;Ha€Drv+ eaq;^UVWAac\}2^UG k@x&^ F@K 1D:ïޏ߫NHhQCGfťOސA<#BՉb5Aүh60z>琑(^2hhbu"bc<˝FPkItYr~ |AI1v-oG"F`Kiy`j?bSK?= ranw/ެo)T&ߍ[ѮS|q/Kecz0U4m֤yO1"`2x5 }măwP4G;M})o2*ڃy>l&(&ZLLy~G|i[/\Z,^@j=JqМ@}=I,!Is-Y6}]VXYmb:.@[DW#QĴEu#,1dTNѮ1"MBt>aqp`]^%`| ȔRlD&\XF~uIITi&,2U}NuM+8+7cޞWCc"c𧧊Ə>+=gݠ4Ylsqcxyd1$n0FoP kjK)-!XnY<]L&K\}g!sΫ(i`XtwLM/#kʏ'DF xgS _lKEʨgP^BIwhPX”}?-zQaݬ,0@nr|cb]^(j&ce)eOO{C2XjY2K5~,7pY^ZTog&9,ϴxËHO˖NmC-`UkVGָi8ʨ-*2Fi2qn4[QdrHuM6&aG{9$HMݞfS)~A9 nJRhm:I)Ҷs'r+is3ROEI{ae#7`MJ}8C *nihs5Ԛޥ;`Uq SzzJrwGU7#vunZ ٤% > &Gegr#O@Tzs?BDkWgpͮVkR2ðeiȸ*--B xX#͐ 1~ ։**\.u\c5f)@,z^4fyDƫ:zz_:8;]K-g;뺔ņ.;$h9y8ٶ;;*%Ȭ%-2!t \{{DjGG00Y֪ťR^sρX9@zt>'xQl+](z37 D:ĦGޕl{L1Yli(!n4Hm}K]t770qSVuϬi6G BB+,aXYDǮ(ˮ$/9ӏnh]6ܗkKbː}00pVYj 9| ZC9I ,Wbf@'4<ɢ@`NXZzuOY:`fUGиC+xғx;Y_uS[m:CUzj4jqj׋a:quGU؋Z3Zn-åX()6Ch:2v9y?r >!P糬ʀhupa%_ Q:v-tGo c=h! q|Gg}o/Б"n(O-:YiX?^FȶmNK8Q^P)_ Y蟌WWxJ._z@;C9(nn)sNA>Qd4wϧ_]N (OV,g8STTob5ɫs9T$rrP?L9̃h_pçcuzx|tuqwg;Ni}=y! Zޝ\4jņ.+aZOht2_y$u"&?p cϜ+P#O߾=p~|u~|~~r:뇃Gda<b6}=C֮IZtƖGXc\6Oc&]QZF xnlMn PrQV7*p?#{}iRtp'EͶE;iMט\J?QпvNUzZw*vZ2#r߁ԶqmyrqrՃ}ٶVI4%)™mA?d1q)ZUT~uSH&$$cu\ SXƒƏ8M膟㱾70@V+:%#L$]{[aF蚧4/qa. )hV\|ƚI% IrILN5YEӣZWk\.WjXA7EΞjM~յ̡໙Ϥ$KL0`"5pN5EGG+zR޳c^c9:Qo4+Qp8 h} rNG7?eջ?ḃG. #Ǐ/Wo2vIOk;gӵ֩s5q#3ԍg+6P IO}F~3eaV6@F0nq([o#NK<ξG=s7'GWOϮޟ \zfen?wڧtFr6H7ꍵoirnENpwǕNc^H-HjX԰--Jfu!Ip~Zg1p Pz ʒ1t 6b/#=ŠJ9Krn3"Wg1X+ɸ´k.*X:~) T(/"~|z1.ڍlm#G;*-5bɖL E0Dm~KgMIH)pHS2"$L[wZ|yu!t2Wp}BDwAl@EQw+x f] Ew"~O(21tYXm-4+Wr3 hu^r@ '4yW}_&{G.BqARz7]t޳\&7:2T10' ^LYɬpUJ CL>#pa#eXOw˽Ä﹪@Wq>+ߍ?IȣMYӘivKG ||ǻa7E3wUQK!;Y{5  3 !629S2mįD"c>7'AT\3K2[I>$Hh('b:< θSp~, |WӔ볃ӣQaW&t>qGPWE}vV+q!1@"Or<p.\&RU /+Ju0UgXL'`w4rwy(A{Ȕ 3u:+\ԡ)j0aV3__ @0|gG0ʅ6 &|E:}E&'DfgM`y4VE&ݾ'H -Ҟf62AMո'Q*=ߋN1 ܾr S},,9X5@ak;hd +Ԡ$a@4KS\H:5TIce0ei;zb\YFbDõ_zFEQUki9d~NeZR7b֙ì Wxu!-kw WTOOK'v"]zRUR6ZYѬ'buOU~dGub2 lD3RC**[>5KL(,h87ftMitS|`DSs%j^P[TYxOEԙˋeuL08t%Ex51sGFZhWDMʿwjui ZL IiN$ n*uvH}f s`o-9zu Sg<4ʫ[^w^QuSv1(4\2}!?$0 5aUU P@%o4_J* jbNsD.Fs`ݚe ʟh؀Qw MMYCcRVc`9NhfuZN<je47FjZv3iHբ}Eu &t2YŬ^W$ /~V=:P$D"s̝SBNjI>'tX٫ B3ӗ1㦂 ׮5&OH+V#PhdA_ِ94ba'K*E },"3EL=AVr_"nc4m:#늆)rW%/IFf_xP4}csj͔P`sϔkDK3tqЕVßi&dؽŮu:ch+tl>rD{:I z[y 㮈ڢS{3T ܭ\Bm4Kح^-b~kY>>LS=V6\9,gǽDZʎc"~-,$ UVW5 ʢ{lM/t@(u28+˾[^|^xʪN/#@CC5K^@T`2NnyCl˲f-dٴJB7nPkUmmڨF-rsL&fWR^L쵔Q9{HЧ6dLbg3Le:,-zIC&bcl/)a7uڥL __)_=g[_;xDk_ [0J?-M>'x!Nt.QtwA!OۇL-@gyS[eS~ A+YwN?0j"l ZۣCYNKI:A N!¾ jdw*W4C5[$&{zBich-Maƚ[U`Bu7Mem1 #S@FfT'mVDoySIdN#5Ȉ0u{8t53*~E!7Hq'uE_[a{'z*4zNOm_a5CQL?n\-S;q5cׄc'P@~/RƟۮp?4 ][-ڄ@/- U$Az(2Pڼ!pG~,BYN/Vf/Smy ]e1FjԥAZJ)LJ^0 fn cP[umQ_SWvFU)0FIW~ţaœe(?!1Fm4.ɮ obC' oR~a̕eƥ򗼐 :1Cڣt'W-'!QQ= UNJ.PZ \?7/t6kKݲ&HSUͳpETI~jѢ~/܃b8Eޝ &Sl͢} q;W'SYOzz2?uxJ* ց2v_*^{E 4Tt5ΜSڀs#ћ0S'Fč\$A=()T/X>Mc/_J7*Ɠl/ʳd##'[%COs_`vc" ]V07ВK ]8z#aDM5Sm\1;;AMUQw?o2]5>+??ac⥱w"MSwݬ&j]4%ϕd-H~Klcsuik(w"^s 3hMǷ{*y2[q+& zFS16Ixp4 \*_Q^LOnԴKrtWcŜDcv09NeA|R ͷXf5ENU ^ 7?.5#o ̧zM;/ln3} ;7>/@J'Q^k'[_q_e>-ư`;=W$عgh&hp9c&(3bK %ǥ8Ffy]jlE P,jpNv0>RPUGc+Pv&[XtjNHDX@c+Wqp_LԼTcDCehp<`[L5Loɫsf$uH[wQQ~ 3:7E^ֵiTɰ'WWs" B|`<ӦkdP3@eYj_K8&4JU !l{_mx_97)hF2IJuv)>Z\M֡X僲Q^.wY \AV?pM"xb G8g4[AᔵV( LP&eؐ,sԪH#G(>6,bUEUv<Ǭ%J"$Wis h9kXQ-w] B̛i347{o*"  `"ep%j\+g@}j`[?[RO4K3ˮ者ʗ,)ۋ!ט\ OEB1i]0E}Yhc}R{Ϛ&N|mFՏh"G`R%>J soM!BM\lQfoIz\yR]RGvos|l%SHE ֩P\]^)h)rBG 2uc ҨմS7,ieIG 4NhmV.\S0һު )+V? W;~0i깺<߾BЊVOu=5t[ZxDr8PGÄlkYr et}r@ǩ{C߄73+0>'J|[i"%mMnr/oѧ2^6W EWclqIcG>%m#A(銨ڧ'78*:Ot BG`7G_Mp&AjK5_VQ`NtiUJB2bN۞7-T3+vL\@Ş~3Rm˱ݒlI˱7;_9vGVlHCg Kj^@l+7 Y)K~jrC)V65m8_H>\|dj\;xA,§Vޘy{p~quq᛫''agK)@}w|aON߾=xѮ{^G,N=ޖOG[_Sl|OjǓfQ5AdkϽ4cK\2,r ߐrܸ&[En lݕP>ǥ,:lE~U .K*}0 yٸ7t<Up=_Qn< / B&wQ.𦤎 i kK VOZA4G7P(moǵ1W,.W6֬e}_;D~ &8#E})\ p 4Y:98x_f JZ® %e6bWڈWE  WF .tʥl|sj3_=$|՛=Z!Vzm hs Xy_j瑻ymN+S൒Ð-/n0vNC+XtkX cbJRSsɖ2'6HhHWnL6Np쩅s˒ulnQ-@ .:;x"bO)d. 7 |Y\.0Yg'5o6 t&m6OvH:4q&LR acTݖṾF7vGeV|ũhKD+8K[F+A}6*%FG 0 pexp{}ym%ZؼҌ5p=L}{KC%>|!xU^ pרԵ;xggL6`o/2$hZvkmC!uH2VgNgMlzܤgSDjX$bsnc`f~sQ٠'ڑ{mۏ,ܡ8od\cqCP:Y-y47 6YEi9!Cy/|\&z+RӚ{eй-f(Kw>MëYP#>{Y3ZW䶬m@$w}h} lxKho?Q?G)L(I5 2Kh& 5޶OOKNeQ##NePYJ8fXY'>ѭxט~Oԍߓy~\髂 5b>ɝݔVفa@.h/n1v7.ۅ$c$%}=-#, {pUA3@%1⦔Q_v.׏.Q9&_dXpHI!#0,r4*~ܥ6BZ2x׀f ^ !Zba60^[)b?~w-3E&>yKs(*Α 'roorH`ڨ_.+xEK}LFEpD;hhP k`c+H36>|{нcށ*bT)2\" }\g8v2fOOm'84f])LF%Ϊyg/Kz`@ݠ,%klfSDkʿ yA r"2>zD8S8PSXZ.aݷwUǩ[쪣^-zsuVNlX[q|v+_9BHs&&dɊz\ú2g6XUORPӷ}:lzR%ºw8_|c"JH/-)Ot.ƞuӪ 2 ЖqYZ7%z6ыFg p=w3q]< D,ǣ"jls?"R F4lKP8^N;8/v&ps Z[_Z˸{ZD] _g^; et.nR3i놅/ێ_]L[{K*6*!zU A1u ?#rKws*Ϥby@!'@*qejDxQ d8֭۾cryU{gbWljb ꦘ[ Պ>5'+e8}Uʆwݹy6Vkr|Ay tByy Vhki&~~z/gle wN.Dfuw YiwpybԹt7i nԂ~s*￧e1q+PrvDK1{ݶ ꢸż|l݋[:UV҅ܜЕ1:] ]8fо4 :bj] nVzk!0g9(m&,Vw'!IjHzbCzLěMz )W?3Xn bZλ4W$_(ˡ.łD9濌h#_F[ (zoI;ܢ ;"ҁV0ֽD#MM>FAKj<a,8߸+d/6Kqx#LX=ăFR(6YGΘ5}<$Y Cgr V׭YBkӶmUb[+&op²o~JU W>8{ [OLaa4e&$ &7246NI2:Gan[7 Zkuf\d|1l$^#SX#;s&Z׺sN3q 7 @IW 6EV|7L`ζx%e؝Җߪ\h@;wF(b/n/2Tqk/czUR*ʦ]vjhJБ{8⻳"~*d.{JwLrSHkU9  ~-'Fʬ<ܪVVl}wEIyMQnH%Z>ka/3Zp;yJypP9{Nx(+f<ø_}ZQG1]Y 1.QԭKjHZNFj[l'qB +~zznHI<;ol=%>SU|qLڑEn=r!P .5 TL#D(L$M0zf-m~B|˲T v*QAW/ryu ͮd岆U{`Zr^vդd8yc\Q9 ;-c0;Nbw4r`0'~MCȿ 9E̶OM청=n >)E;?(H3)a?'`EM(ۍb%φ>Mv<;eobcP!iSɍ&<3NSM&\N { // chrome-extension://ahjaciijnoiaklcomgnblndopackapon "key": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDNyyvaNmqNZsjBwes4YNlrsy64asdP710pdMUM27jtvOe2YkXUdvglcC6r2ihlvPg16mjYK+ZmvxchcEu497KUPqBq34jXILabiUuXLrQJlvl3A7QMLatuZlijSx1qXL/5w5/ggF2Tblo9SHSVtlVyhwyyGkT9ckga5erBUbbwkQIDAQAB", "name": "Identity API Scope Approval UI", "version": "1.1", "manifest_version": 2, "description": "Displays scope approval dialog boxes for the Identity API", "permissions": [ "chrome://theme/", "identityPrivate", "resourcesPrivate", "webview" ], "app": { "background": { "scripts": [ "background.js" ] }, "content_security_policy": "default-src 'none'; script-src 'self' blob: filesystem:; style-src 'self' blob: filesystem:; img-src chrome://theme; object-src 'self' blob: filesystem:" }, "display_in_launcher": false, "display_in_new_tab_page": false }

/* Copyright 2013 The Chromium Authors. All rights reserved. * Use of this source code is governed by a BSD-style license that can be * found in the LICENSE file. */ html, body, #contents, #signin-frame { height: 100%; margin: 0; overflow: hidden; padding: 0; width: 100%; } #signin-frame, #spinner-container { background-color: #f5f5f5; bottom: 0; left: 0; position: absolute; right: 0; top: 0; } #spinner-container { -webkit-box-align: center; -webkit-box-pack: center; display: -webkit-box; } #contents:not(.loading) #spinner-container { display: none; } #navigation-button { color: white; position: absolute; top: 0; visibility: hidden; } #navigation-button.enabled { visibility: visible; } // Copyright 2013 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. /** * @fileoverview Inline login UI. */ cr.define('inline.login', function() { 'use strict'; /** * The auth extension host instance. * @type {cr.login.GaiaAuthHost} */ var authExtHost; /** * Whether the auth ready event has been fired, for testing purpose. */ var authReadyFired; /** * Whether the login UI is loaded for signing in primary account. */ var isLoginPrimaryAccount; function onResize(e) { chrome.send('switchToFullTab', [e.detail]); } function onAuthReady(e) { $('contents').classList.toggle('loading', false); authReadyFired = true; if (isLoginPrimaryAccount) chrome.send('metricsHandler:recordAction', ['Signin_SigninPage_Shown']); } function onDropLink(e) { // Navigate to the dropped link. window.location.href = e.detail; } function onNewWindow(e) { window.open(e.detail.targetUrl, '_blank'); e.detail.window.discard(); } function onAuthCompleted(e) { completeLogin(e.detail); } function completeLogin(credentials) { chrome.send('completeLogin', [credentials]); $('contents').classList.toggle('loading', true); } /** * Initialize the UI. */ function initialize() { $('navigation-button').addEventListener('click', navigationButtonClicked); authExtHost = new cr.login.GaiaAuthHost('signin-frame'); authExtHost.addEventListener('dropLink', onDropLink); authExtHost.addEventListener('ready', onAuthReady); authExtHost.addEventListener('newWindow', onNewWindow); authExtHost.addEventListener('resize', onResize); authExtHost.addEventListener('authCompleted', onAuthCompleted); chrome.send('initialize'); } /** * Loads auth extension. * @param {Object} data Parameters for auth extension. */ function loadAuthExtension(data) { // TODO(rogerta): in when using webview, the |completeLogin| argument // is ignored. See addEventListener() call above. authExtHost.load(data.authMode, data, completeLogin); $('contents').classList.toggle('loading', data.authMode != cr.login.GaiaAuthHost.AuthMode.DESKTOP || data.constrained == '1'); isLoginPrimaryAccount = data.isLoginPrimaryAccount; } /** * Closes the inline login dialog. */ function closeDialog() { chrome.send('dialogClose', ['']); } /** * Invoked when failed to get oauth2 refresh token. */ function handleOAuth2TokenFailure() { // TODO(xiyuan): Show an error UI. authExtHost.reload(); $('contents').classList.toggle('loading', true); } /** * Returns the auth host instance, for testing purpose. */ function getAuthExtHost() { return authExtHost; } /** * Returns whether the auth UI is ready, for testing purpose. */ function isAuthReady() { return authReadyFired; } function showBackButton() { $('navigation-button').icon = isRTL() ? 'icons:arrow-forward' : 'icons:arrow-back'; $('navigation-button').setAttribute( 'aria-label', loadTimeData.getString('accessibleBackButtonLabel')); } function showCloseButton() { $('navigation-button').icon = 'icons:close'; $('navigation-button').classList.add('enabled'); $('navigation-button').setAttribute( 'aria-label', loadTimeData.getString('accessibleCloseButtonLabel')); } function navigationButtonClicked() { chrome.send('navigationButtonClicked'); } return { closeDialog: closeDialog, getAuthExtHost: getAuthExtHost, handleOAuth2TokenFailure: handleOAuth2TokenFailure, initialize: initialize, isAuthReady: isAuthReady, loadAuthExtension: loadAuthExtension, navigationButtonClicked: navigationButtonClicked, showBackButton: showBackButton, showCloseButton: showCloseButton }; }); document.addEventListener('DOMContentLoaded', inline.login.initialize); // Copyright 2014 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. // // Copyright 2015 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. // // Copyright 2015 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. /** * @fileoverview * Provides a HTML5 postMessage channel to the injected JS to talk back * to Authenticator. */ 'use strict'; // // Copyright 2013 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. /** * Channel to the background script. */ function Channel() { this.messageCallbacks_ = {}; this.internalRequestCallbacks_ = {}; } /** @const */ Channel.INTERNAL_REQUEST_MESSAGE = 'internal-request-message'; /** @const */ Channel.INTERNAL_REPLY_MESSAGE = 'internal-reply-message'; Channel.prototype = { // Message port to use to communicate with background script. port_: null, // Registered message callbacks. messageCallbacks_: null, // Internal request id to track pending requests. nextInternalRequestId_: 0, // Pending internal request callbacks. internalRequestCallbacks_: null, /** * Initialize the channel with given port for the background script. */ init: function(port) { this.port_ = port; this.port_.onMessage.addListener(this.onMessage_.bind(this)); }, /** * Connects to the background script with the given name. */ connect: function(name) { this.port_ = chrome.runtime.connect({name: name}); this.port_.onMessage.addListener(this.onMessage_.bind(this)); }, /** * Associates a message name with a callback. When a message with the name * is received, the callback will be invoked with the message as its arg. * Note only the last registered callback will be invoked. */ registerMessage: function(name, callback) { this.messageCallbacks_[name] = callback; }, /** * Sends a message to the other side of the channel. */ send: function(msg) { this.port_.postMessage(msg); }, /** * Sends a message to the other side and invokes the callback with * the replied object. Useful for message that expects a returned result. */ sendWithCallback: function(msg, callback) { var requestId = this.nextInternalRequestId_++; this.internalRequestCallbacks_[requestId] = callback; this.send({ name: Channel.INTERNAL_REQUEST_MESSAGE, requestId: requestId, payload: msg }); }, /** * Invokes message callback using given message. * @return {*} The return value of the message callback or null. */ invokeMessageCallbacks_: function(msg) { var name = msg.name; if (this.messageCallbacks_[name]) return this.messageCallbacks_[name](msg); console.error('Error: Unexpected message, name=' + name); return null; }, /** * Invoked when a message is received. */ onMessage_: function(msg) { var name = msg.name; if (name == Channel.INTERNAL_REQUEST_MESSAGE) { var payload = msg.payload; var result = this.invokeMessageCallbacks_(payload); this.send({ name: Channel.INTERNAL_REPLY_MESSAGE, requestId: msg.requestId, result: result }); } else if (name == Channel.INTERNAL_REPLY_MESSAGE) { var callback = this.internalRequestCallbacks_[msg.requestId]; delete this.internalRequestCallbacks_[msg.requestId]; if (callback) callback(msg.result); } else { this.invokeMessageCallbacks_(msg); } } }; /** * Class factory. * @return {Channel} */ Channel.create = function() { return new Channel(); }; var PostMessageChannel = (function() { /** * Allowed origins of the hosting page. * @type {Array} */ var ALLOWED_ORIGINS = [ 'chrome://oobe', 'chrome://chrome-signin' ]; /** @const */ var PORT_MESSAGE = 'post-message-port-message'; /** @const */ var CHANNEL_INIT_MESSAGE = 'post-message-channel-init'; /** @const */ var CHANNEL_CONNECT_MESSAGE = 'post-message-channel-connect'; /** * Whether the script runs in a top level window. */ function isTopLevel() { return window === window.top; } /** * A simple event target. */ function EventTarget() { this.listeners_ = []; } EventTarget.prototype = { /** * Add an event listener. */ addListener: function(listener) { this.listeners_.push(listener); }, /** * Dispatches a given event to all listeners. */ dispatch: function(e) { for (var i = 0; i < this.listeners_.length; ++i) { this.listeners_[i].call(undefined, e); } } }; /** * ChannelManager handles window message events by dispatching them to * PostMessagePorts or forwarding to other windows (up/down the hierarchy). * @constructor */ function ChannelManager() { /** * Window and origin to forward message up the hierarchy. For subframes, * they defaults to window.parent and any origin. For top level window, * this would be set to the hosting webview on CHANNEL_INIT_MESSAGE. */ this.upperWindow = isTopLevel() ? null : window.parent; this.upperOrigin = isTopLevel() ? '' : '*'; /** * Channle Id to port map. * @type {Object} */ this.channels_ = {}; /** * Deferred messages to be posted to |upperWindow|. * @type {Array} */ this.deferredUpperWindowMessages_ = []; /** * Ports that depend on upperWindow and need to be setup when its available. */ this.deferredUpperWindowPorts_ = []; /** * Whether the ChannelManager runs in daemon mode and accepts connections. */ this.isDaemon = false; /** * Fires when ChannelManager is in listening mode and a * CHANNEL_CONNECT_MESSAGE is received. */ this.onConnect = new EventTarget(); window.addEventListener('message', this.onMessage_.bind(this)); } ChannelManager.prototype = { /** * Gets a global unique id to use. * @return {number} */ createChannelId_: function() { return (new Date()).getTime(); }, /** * Posts data to upperWindow. Queue it if upperWindow is not available. */ postToUpperWindow: function(data) { if (this.upperWindow == null) { this.deferredUpperWindowMessages_.push(data); return; } this.upperWindow.postMessage(data, this.upperOrigin); }, /** * Creates a port and register it in |channels_|. * @param {number} channelId * @param {string} channelName * @param {DOMWindow=} opt_targetWindow * @param {string=} opt_targetOrigin */ createPort: function( channelId, channelName, opt_targetWindow, opt_targetOrigin) { var port = new PostMessagePort(channelId, channelName); if (opt_targetWindow) port.setTarget(opt_targetWindow, opt_targetOrigin); this.channels_[channelId] = port; return port; }, /* * Returns a message forward handler for the given proxy port. * @private */ getProxyPortForwardHandler_: function(proxyPort) { return function(msg) { proxyPort.postMessage(msg); }; }, /** * Creates a forwarding porxy port. * @param {number} channelId * @param {string} channelName * @param {!DOMWindow} targetWindow * @param {!string} targetOrigin */ createProxyPort: function( channelId, channelName, targetWindow, targetOrigin) { var port = this.createPort( channelId, channelName, targetWindow, targetOrigin); port.onMessage.addListener(this.getProxyPortForwardHandler_(port)); return port; }, /** * Creates a connecting port to the daemon and request connection. * @param {string} name * @return {PostMessagePort} */ connectToDaemon: function(name) { if (this.isDaemon) { console.error( 'Error: Connecting from the daemon page is not supported.'); return; } var port = this.createPort(this.createChannelId_(), name); if (this.upperWindow) { port.setTarget(this.upperWindow, this.upperOrigin); } else { this.deferredUpperWindowPorts_.push(port); } this.postToUpperWindow({ type: CHANNEL_CONNECT_MESSAGE, channelId: port.channelId, channelName: port.name }); return port; }, /** * Dispatches a 'message' event to port. * @private */ dispatchMessageToPort_: function(e) { var channelId = e.data.channelId; var port = this.channels_[channelId]; if (!port) { console.error('Error: Unable to dispatch message. Unknown channel.'); return; } port.handleWindowMessage(e); }, /** * Window 'message' handler. */ onMessage_: function(e) { if (typeof e.data != 'object' || !e.data.hasOwnProperty('type')) { return; } if (e.data.type === PORT_MESSAGE) { // Dispatch port message to ports if this is the daemon page or // the message is from upperWindow. In case of null upperWindow, // the message is assumed to be forwarded to upperWindow and queued. if (this.isDaemon || (this.upperWindow && e.source === this.upperWindow)) { this.dispatchMessageToPort_(e); } else { this.postToUpperWindow(e.data); } } else if (e.data.type === CHANNEL_CONNECT_MESSAGE) { var channelId = e.data.channelId; var channelName = e.data.channelName; if (this.isDaemon) { var port = this.createPort( channelId, channelName, e.source, e.origin); this.onConnect.dispatch(port); } else { this.createProxyPort(channelId, channelName, e.source, e.origin); this.postToUpperWindow(e.data); } } else if (e.data.type === CHANNEL_INIT_MESSAGE) { if (ALLOWED_ORIGINS.indexOf(e.origin) == -1) return; this.upperWindow = e.source; this.upperOrigin = e.origin; for (var i = 0; i < this.deferredUpperWindowMessages_.length; ++i) { this.upperWindow.postMessage(this.deferredUpperWindowMessages_[i], this.upperOrigin); } this.deferredUpperWindowMessages_ = []; for (var i = 0; i < this.deferredUpperWindowPorts_.length; ++i) { this.deferredUpperWindowPorts_[i].setTarget(this.upperWindow, this.upperOrigin); } this.deferredUpperWindowPorts_ = []; } } }; /** * Singleton instance of ChannelManager. * @type {ChannelManager} */ var channelManager = new ChannelManager(); /** * A HTML5 postMessage based port that provides the same port interface * as the messaging API port. * @param {number} channelId * @param {string} name */ function PostMessagePort(channelId, name) { this.channelId = channelId; this.name = name; this.targetWindow = null; this.targetOrigin = ''; this.deferredMessages_ = []; this.onMessage = new EventTarget(); }; PostMessagePort.prototype = { /** * Sets the target window and origin. * @param {DOMWindow} targetWindow * @param {string} targetOrigin */ setTarget: function(targetWindow, targetOrigin) { this.targetWindow = targetWindow; this.targetOrigin = targetOrigin; for (var i = 0; i < this.deferredMessages_.length; ++i) { this.postMessage(this.deferredMessages_[i]); } this.deferredMessages_ = []; }, postMessage: function(msg) { if (!this.targetWindow) { this.deferredMessages_.push(msg); return; } this.targetWindow.postMessage({ type: PORT_MESSAGE, channelId: this.channelId, payload: msg }, this.targetOrigin); }, handleWindowMessage: function(e) { this.onMessage.dispatch(e.data.payload); } }; /** * A message channel based on PostMessagePort. * @extends {Channel} * @constructor */ function PostMessageChannel() { Channel.apply(this, arguments); }; PostMessageChannel.prototype = { __proto__: Channel.prototype, /** @override */ connect: function(name) { this.port_ = channelManager.connectToDaemon(name); this.port_.onMessage.addListener(this.onMessage_.bind(this)); }, }; /** * Initialize webview content window for postMessage channel. * @param {DOMWindow} webViewContentWindow Content window of the webview. */ PostMessageChannel.init = function(webViewContentWindow) { webViewContentWindow.postMessage({ type: CHANNEL_INIT_MESSAGE }, '*'); }; /** * Run in daemon mode and listen for incoming connections. Note that the * current implementation assumes the daemon runs in the hosting page * at the upper layer of the DOM tree. That is, all connect requests go * up the DOM tree instead of going into sub frames. * @param {function(PostMessagePort)} callback Invoked when a connection is * made. */ PostMessageChannel.runAsDaemon = function(callback) { channelManager.isDaemon = true; var onConnect = function(port) { callback(port); }; channelManager.onConnect.addListener(onConnect); }; return PostMessageChannel; })(); /** @override */ Channel.create = function() { return new PostMessageChannel(); }; /** * @fileoverview Saml support for webview based auth. */ cr.define('cr.login', function() { 'use strict'; /** * The lowest version of the credentials passing API supported. * @type {number} */ var MIN_API_VERSION_VERSION = 1; /** * The highest version of the credentials passing API supported. * @type {number} */ var MAX_API_VERSION_VERSION = 1; /** * The key types supported by the credentials passing API. * @type {Array} Array of strings. */ var API_KEY_TYPES = [ 'KEY_TYPE_PASSWORD_PLAIN', ]; /** @const */ var SAML_HEADER = 'google-accounts-saml'; /** * The script to inject into webview and its sub frames. * @type {string} */ var injectedJs = String.raw` // // Copyright 2015 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. // // Copyright 2015 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. /** * @fileoverview * Provides a HTML5 postMessage channel to the injected JS to talk back * to Authenticator. */ 'use strict'; // // Copyright 2013 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. /** * Channel to the background script. */ function Channel() { this.messageCallbacks_ = {}; this.internalRequestCallbacks_ = {}; } /** @const */ Channel.INTERNAL_REQUEST_MESSAGE = 'internal-request-message'; /** @const */ Channel.INTERNAL_REPLY_MESSAGE = 'internal-reply-message'; Channel.prototype = { // Message port to use to communicate with background script. port_: null, // Registered message callbacks. messageCallbacks_: null, // Internal request id to track pending requests. nextInternalRequestId_: 0, // Pending internal request callbacks. internalRequestCallbacks_: null, /** * Initialize the channel with given port for the background script. */ init: function(port) { this.port_ = port; this.port_.onMessage.addListener(this.onMessage_.bind(this)); }, /** * Connects to the background script with the given name. */ connect: function(name) { this.port_ = chrome.runtime.connect({name: name}); this.port_.onMessage.addListener(this.onMessage_.bind(this)); }, /** * Associates a message name with a callback. When a message with the name * is received, the callback will be invoked with the message as its arg. * Note only the last registered callback will be invoked. */ registerMessage: function(name, callback) { this.messageCallbacks_[name] = callback; }, /** * Sends a message to the other side of the channel. */ send: function(msg) { this.port_.postMessage(msg); }, /** * Sends a message to the other side and invokes the callback with * the replied object. Useful for message that expects a returned result. */ sendWithCallback: function(msg, callback) { var requestId = this.nextInternalRequestId_++; this.internalRequestCallbacks_[requestId] = callback; this.send({ name: Channel.INTERNAL_REQUEST_MESSAGE, requestId: requestId, payload: msg }); }, /** * Invokes message callback using given message. * @return {*} The return value of the message callback or null. */ invokeMessageCallbacks_: function(msg) { var name = msg.name; if (this.messageCallbacks_[name]) return this.messageCallbacks_[name](msg); console.error('Error: Unexpected message, name=' + name); return null; }, /** * Invoked when a message is received. */ onMessage_: function(msg) { var name = msg.name; if (name == Channel.INTERNAL_REQUEST_MESSAGE) { var payload = msg.payload; var result = this.invokeMessageCallbacks_(payload); this.send({ name: Channel.INTERNAL_REPLY_MESSAGE, requestId: msg.requestId, result: result }); } else if (name == Channel.INTERNAL_REPLY_MESSAGE) { var callback = this.internalRequestCallbacks_[msg.requestId]; delete this.internalRequestCallbacks_[msg.requestId]; if (callback) callback(msg.result); } else { this.invokeMessageCallbacks_(msg); } } }; /** * Class factory. * @return {Channel} */ Channel.create = function() { return new Channel(); }; var PostMessageChannel = (function() { /** * Allowed origins of the hosting page. * @type {Array} */ var ALLOWED_ORIGINS = [ 'chrome://oobe', 'chrome://chrome-signin' ]; /** @const */ var PORT_MESSAGE = 'post-message-port-message'; /** @const */ var CHANNEL_INIT_MESSAGE = 'post-message-channel-init'; /** @const */ var CHANNEL_CONNECT_MESSAGE = 'post-message-channel-connect'; /** * Whether the script runs in a top level window. */ function isTopLevel() { return window === window.top; } /** * A simple event target. */ function EventTarget() { this.listeners_ = []; } EventTarget.prototype = { /** * Add an event listener. */ addListener: function(listener) { this.listeners_.push(listener); }, /** * Dispatches a given event to all listeners. */ dispatch: function(e) { for (var i = 0; i < this.listeners_.length; ++i) { this.listeners_[i].call(undefined, e); } } }; /** * ChannelManager handles window message events by dispatching them to * PostMessagePorts or forwarding to other windows (up/down the hierarchy). * @constructor */ function ChannelManager() { /** * Window and origin to forward message up the hierarchy. For subframes, * they defaults to window.parent and any origin. For top level window, * this would be set to the hosting webview on CHANNEL_INIT_MESSAGE. */ this.upperWindow = isTopLevel() ? null : window.parent; this.upperOrigin = isTopLevel() ? '' : '*'; /** * Channle Id to port map. * @type {Object} */ this.channels_ = {}; /** * Deferred messages to be posted to |upperWindow|. * @type {Array} */ this.deferredUpperWindowMessages_ = []; /** * Ports that depend on upperWindow and need to be setup when its available. */ this.deferredUpperWindowPorts_ = []; /** * Whether the ChannelManager runs in daemon mode and accepts connections. */ this.isDaemon = false; /** * Fires when ChannelManager is in listening mode and a * CHANNEL_CONNECT_MESSAGE is received. */ this.onConnect = new EventTarget(); window.addEventListener('message', this.onMessage_.bind(this)); } ChannelManager.prototype = { /** * Gets a global unique id to use. * @return {number} */ createChannelId_: function() { return (new Date()).getTime(); }, /** * Posts data to upperWindow. Queue it if upperWindow is not available. */ postToUpperWindow: function(data) { if (this.upperWindow == null) { this.deferredUpperWindowMessages_.push(data); return; } this.upperWindow.postMessage(data, this.upperOrigin); }, /** * Creates a port and register it in |channels_|. * @param {number} channelId * @param {string} channelName * @param {DOMWindow=} opt_targetWindow * @param {string=} opt_targetOrigin */ createPort: function( channelId, channelName, opt_targetWindow, opt_targetOrigin) { var port = new PostMessagePort(channelId, channelName); if (opt_targetWindow) port.setTarget(opt_targetWindow, opt_targetOrigin); this.channels_[channelId] = port; return port; }, /* * Returns a message forward handler for the given proxy port. * @private */ getProxyPortForwardHandler_: function(proxyPort) { return function(msg) { proxyPort.postMessage(msg); }; }, /** * Creates a forwarding porxy port. * @param {number} channelId * @param {string} channelName * @param {!DOMWindow} targetWindow * @param {!string} targetOrigin */ createProxyPort: function( channelId, channelName, targetWindow, targetOrigin) { var port = this.createPort( channelId, channelName, targetWindow, targetOrigin); port.onMessage.addListener(this.getProxyPortForwardHandler_(port)); return port; }, /** * Creates a connecting port to the daemon and request connection. * @param {string} name * @return {PostMessagePort} */ connectToDaemon: function(name) { if (this.isDaemon) { console.error( 'Error: Connecting from the daemon page is not supported.'); return; } var port = this.createPort(this.createChannelId_(), name); if (this.upperWindow) { port.setTarget(this.upperWindow, this.upperOrigin); } else { this.deferredUpperWindowPorts_.push(port); } this.postToUpperWindow({ type: CHANNEL_CONNECT_MESSAGE, channelId: port.channelId, channelName: port.name }); return port; }, /** * Dispatches a 'message' event to port. * @private */ dispatchMessageToPort_: function(e) { var channelId = e.data.channelId; var port = this.channels_[channelId]; if (!port) { console.error('Error: Unable to dispatch message. Unknown channel.'); return; } port.handleWindowMessage(e); }, /** * Window 'message' handler. */ onMessage_: function(e) { if (typeof e.data != 'object' || !e.data.hasOwnProperty('type')) { return; } if (e.data.type === PORT_MESSAGE) { // Dispatch port message to ports if this is the daemon page or // the message is from upperWindow. In case of null upperWindow, // the message is assumed to be forwarded to upperWindow and queued. if (this.isDaemon || (this.upperWindow && e.source === this.upperWindow)) { this.dispatchMessageToPort_(e); } else { this.postToUpperWindow(e.data); } } else if (e.data.type === CHANNEL_CONNECT_MESSAGE) { var channelId = e.data.channelId; var channelName = e.data.channelName; if (this.isDaemon) { var port = this.createPort( channelId, channelName, e.source, e.origin); this.onConnect.dispatch(port); } else { this.createProxyPort(channelId, channelName, e.source, e.origin); this.postToUpperWindow(e.data); } } else if (e.data.type === CHANNEL_INIT_MESSAGE) { if (ALLOWED_ORIGINS.indexOf(e.origin) == -1) return; this.upperWindow = e.source; this.upperOrigin = e.origin; for (var i = 0; i < this.deferredUpperWindowMessages_.length; ++i) { this.upperWindow.postMessage(this.deferredUpperWindowMessages_[i], this.upperOrigin); } this.deferredUpperWindowMessages_ = []; for (var i = 0; i < this.deferredUpperWindowPorts_.length; ++i) { this.deferredUpperWindowPorts_[i].setTarget(this.upperWindow, this.upperOrigin); } this.deferredUpperWindowPorts_ = []; } } }; /** * Singleton instance of ChannelManager. * @type {ChannelManager} */ var channelManager = new ChannelManager(); /** * A HTML5 postMessage based port that provides the same port interface * as the messaging API port. * @param {number} channelId * @param {string} name */ function PostMessagePort(channelId, name) { this.channelId = channelId; this.name = name; this.targetWindow = null; this.targetOrigin = ''; this.deferredMessages_ = []; this.onMessage = new EventTarget(); }; PostMessagePort.prototype = { /** * Sets the target window and origin. * @param {DOMWindow} targetWindow * @param {string} targetOrigin */ setTarget: function(targetWindow, targetOrigin) { this.targetWindow = targetWindow; this.targetOrigin = targetOrigin; for (var i = 0; i < this.deferredMessages_.length; ++i) { this.postMessage(this.deferredMessages_[i]); } this.deferredMessages_ = []; }, postMessage: function(msg) { if (!this.targetWindow) { this.deferredMessages_.push(msg); return; } this.targetWindow.postMessage({ type: PORT_MESSAGE, channelId: this.channelId, payload: msg }, this.targetOrigin); }, handleWindowMessage: function(e) { this.onMessage.dispatch(e.data.payload); } }; /** * A message channel based on PostMessagePort. * @extends {Channel} * @constructor */ function PostMessageChannel() { Channel.apply(this, arguments); }; PostMessageChannel.prototype = { __proto__: Channel.prototype, /** @override */ connect: function(name) { this.port_ = channelManager.connectToDaemon(name); this.port_.onMessage.addListener(this.onMessage_.bind(this)); }, }; /** * Initialize webview content window for postMessage channel. * @param {DOMWindow} webViewContentWindow Content window of the webview. */ PostMessageChannel.init = function(webViewContentWindow) { webViewContentWindow.postMessage({ type: CHANNEL_INIT_MESSAGE }, '*'); }; /** * Run in daemon mode and listen for incoming connections. Note that the * current implementation assumes the daemon runs in the hosting page * at the upper layer of the DOM tree. That is, all connect requests go * up the DOM tree instead of going into sub frames. * @param {function(PostMessagePort)} callback Invoked when a connection is * made. */ PostMessageChannel.runAsDaemon = function(callback) { channelManager.isDaemon = true; var onConnect = function(port) { callback(port); }; channelManager.onConnect.addListener(onConnect); }; return PostMessageChannel; })(); /** @override */ Channel.create = function() { return new PostMessageChannel(); }; // // Copyright 2013 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. /** * @fileoverview * Script to be injected into SAML provider pages, serving three main purposes: * 1. Signal hosting extension that an external page is loaded so that the * UI around it should be changed accordingly; * 2. Provide an API via which the SAML provider can pass user credentials to * Chrome OS, allowing the password to be used for encrypting user data and * offline login. * 3. Scrape password fields, making the password available to Chrome OS even if * the SAML provider does not support the credential passing API. */ (function() { function APICallForwarder() { } /** * The credential passing API is used by sending messages to the SAML page's * |window| object. This class forwards API calls from the SAML page to a * background script and API responses from the background script to the SAML * page. Communication with the background script occurs via a |Channel|. */ APICallForwarder.prototype = { // Channel to which API calls are forwarded. channel_: null, /** * Initialize the API call forwarder. * @param {!Object} channel Channel to which API calls should be forwarded. */ init: function(channel) { this.channel_ = channel; this.channel_.registerMessage('apiResponse', this.onAPIResponse_.bind(this)); window.addEventListener('message', this.onMessage_.bind(this)); }, onMessage_: function(event) { if (event.source != window || typeof event.data != 'object' || !event.data.hasOwnProperty('type') || event.data.type != 'gaia_saml_api') { return; } // Forward API calls to the background script. this.channel_.send({name: 'apiCall', call: event.data.call}); }, onAPIResponse_: function(msg) { // Forward API responses to the SAML page. window.postMessage({type: 'gaia_saml_api_reply', response: msg.response}, '/'); } }; /** * A class to scrape password from type=password input elements under a given * docRoot and send them back via a Channel. */ function PasswordInputScraper() { } PasswordInputScraper.prototype = { // URL of the page. pageURL_: null, // Channel to send back changed password. channel_: null, // An array to hold password fields. passwordFields_: null, // An array to hold cached password values. passwordValues_: null, // A MutationObserver to watch for dynamic password field creation. passwordFieldsObserver: null, /** * Initialize the scraper with given channel and docRoot. Note that the * scanning for password fields happens inside the function and does not * handle DOM tree changes after the call returns. * @param {!Object} channel The channel to send back password. * @param {!string} pageURL URL of the page. * @param {!HTMLElement} docRoot The root element of the DOM tree that * contains the password fields of interest. */ init: function(channel, pageURL, docRoot) { this.pageURL_ = pageURL; this.channel_ = channel; this.passwordFields_ = []; this.passwordValues_ = []; this.findAndTrackChildren(docRoot); this.passwordFieldsObserver = new MutationObserver(function(mutations) { mutations.forEach(function(mutation) { Array.prototype.forEach.call( mutation.addedNodes, function(addedNode) { if (addedNode.nodeType != Node.ELEMENT_NODE) return; if (addedNode.matches('input[type=password]')) { this.trackPasswordField(addedNode); } else { this.findAndTrackChildren(addedNode); } }.bind(this)); }.bind(this)); }.bind(this)); this.passwordFieldsObserver.observe(docRoot, {subtree: true, childList: true}); }, /** * Find and track password fields that are descendants of the given element. * @param {!HTMLElement} element The parent element to search from. */ findAndTrackChildren: function(element) { Array.prototype.forEach.call( element.querySelectorAll('input[type=password]'), function(field) { this.trackPasswordField(field); }.bind(this)); }, /** * Start tracking value changes of the given password field if it is * not being tracked yet. * @param {!HTMLInputElement} passworField The password field to track. */ trackPasswordField: function(passwordField) { var existing = this.passwordFields_.filter(function(element) { return element === passwordField; }); if (existing.length != 0) return; var index = this.passwordFields_.length; var fieldId = passwordField.id || passwordField.name || ''; passwordField.addEventListener( 'input', this.onPasswordChanged_.bind(this, index, fieldId)); this.passwordFields_.push(passwordField); this.passwordValues_.push(passwordField.value); }, /** * Check if the password field at |index| has changed. If so, sends back * the updated value. */ maybeSendUpdatedPassword: function(index, fieldId) { var newValue = this.passwordFields_[index].value; if (newValue == this.passwordValues_[index]) return; this.passwordValues_[index] = newValue; // Use an invalid char for URL as delimiter to concatenate page url, // password field index and id to construct a unique ID for the password // field. var passwordId = this.pageURL_.split('#')[0].split('?')[0] + '|' + index + '|' + fieldId; this.channel_.send({ name: 'updatePassword', id: passwordId, password: newValue }); }, /** * Handles 'change' event in the scraped password fields. * @param {number} index The index of the password fields in * |passwordFields_|. * @param {string} fieldId The id or name of the password field or blank. */ onPasswordChanged_: function(index, fieldId) { this.maybeSendUpdatedPassword(index, fieldId); } }; function onGetSAMLFlag(channel, isSAMLPage) { if (!isSAMLPage) return; var pageURL = window.location.href; channel.send({name: 'pageLoaded', url: pageURL}); var initPasswordScraper = function() { var passwordScraper = new PasswordInputScraper(); passwordScraper.init(channel, pageURL, document.documentElement); }; if (document.readyState == 'loading') { window.addEventListener('readystatechange', function listener(event) { if (document.readyState == 'loading') return; initPasswordScraper(); window.removeEventListener(event.type, listener, true); }, true); } else { initPasswordScraper(); } } var channel = Channel.create(); channel.connect('injected'); channel.sendWithCallback({name: 'getSAMLFlag'}, onGetSAMLFlag.bind(undefined, channel)); var apiCallForwarder = new APICallForwarder(); apiCallForwarder.init(channel); })(); `; /** * Creates a new URL by striping all query parameters. * @param {string} url The original URL. * @return {string} The new URL with all query parameters stripped. */ function stripParams(url) { return url.substring(0, url.indexOf('?')) || url; } /** * Extract domain name from an URL. * @param {string} url An URL string. * @return {string} The host name of the URL. */ function extractDomain(url) { var a = document.createElement('a'); a.href = url; return a.hostname; } /** * A handler to provide saml support for the given webview that hosts the * auth IdP pages. * @extends {cr.EventTarget} * @param {webview} webview * @constructor */ function SamlHandler(webview) { /** * The webview that serves IdP pages. * @type {webview} */ this.webview_ = webview; /** * Whether a Saml IdP page is display in the webview. * @type {boolean} */ this.isSamlPage_ = false; /** * Pending Saml IdP page flag that is set when a SAML_HEADER is received * and is copied to |isSamlPage_| in loadcommit. * @type {boolean} */ this.pendingIsSamlPage_ = false; /** * The last aborted top level url. It is recorded in loadabort event and * used to skip injection into Chrome's error page in the following * loadcommit event. * @type {string} */ this.abortedTopLevelUrl_ = null; /** * The domain of the Saml IdP. * @type {string} */ this.authDomain = ''; /** * Scraped password stored in an id to password field value map. * @type {Object} * @private */ this.passwordStore_ = {}; /** * Whether Saml API is initialized. * @type {boolean} */ this.apiInitialized_ = false; /** * Saml API version to use. * @type {number} */ this.apiVersion_ = 0; /** * Saml API token received. * @type {string} */ this.apiToken_ = null; /** * Saml API password bytes. * @type {string} */ this.apiPasswordBytes_ = null; /* * Whether to abort the authentication flow and show an error messagen when * content served over an unencrypted connection is detected. * @type {boolean} */ this.blockInsecureContent = false; this.webview_.addEventListener( 'contentload', this.onContentLoad_.bind(this)); this.webview_.addEventListener( 'loadabort', this.onLoadAbort_.bind(this)); this.webview_.addEventListener( 'loadcommit', this.onLoadCommit_.bind(this)); this.webview_.addEventListener( 'permissionrequest', this.onPermissionRequest_.bind(this)); this.webview_.request.onBeforeRequest.addListener( this.onInsecureRequest.bind(this), {urls: ['http://*/*', 'file://*/*', 'ftp://*/*']}, ['blocking']); this.webview_.request.onHeadersReceived.addListener( this.onHeadersReceived_.bind(this), {urls: [''], types: ['main_frame', 'xmlhttprequest']}, ['blocking', 'responseHeaders']); this.webview_.addContentScripts([{ name: 'samlInjected', matches: ['http://*/*', 'https://*/*'], js: { code: injectedJs }, all_frames: true, run_at: 'document_start' }]); PostMessageChannel.runAsDaemon(this.onConnected_.bind(this)); } SamlHandler.prototype = { __proto__: cr.EventTarget.prototype, /** * Whether Saml API is used during auth. * @return {boolean} */ get samlApiUsed() { return !!this.apiPasswordBytes_; }, /** * Returns the Saml API password bytes. * @return {string} */ get apiPasswordBytes() { return this.apiPasswordBytes_; }, /** * Returns the first scraped password if any, or an empty string otherwise. * @return {string} */ get firstScrapedPassword() { var scraped = this.getConsolidatedScrapedPasswords_(); return scraped.length ? scraped[0] : ''; }, /** * Returns the number of scraped passwords. * @return {number} */ get scrapedPasswordCount() { return this.getConsolidatedScrapedPasswords_().length; }, /** * Gets the de-duped scraped passwords. * @return {Array} * @private */ getConsolidatedScrapedPasswords_: function() { var passwords = {}; for (var property in this.passwordStore_) { passwords[this.passwordStore_[property]] = true; } return Object.keys(passwords); }, /** * Resets all auth states */ reset: function() { this.isSamlPage_ = false; this.pendingIsSamlPage_ = false; this.passwordStore_ = {}; this.apiInitialized_ = false; this.apiVersion_ = 0; this.apiToken_ = null; this.apiPasswordBytes_ = null; }, /** * Check whether the given |password| is in the scraped passwords. * @return {boolean} True if the |password| is found. */ verifyConfirmedPassword: function(password) { return this.getConsolidatedScrapedPasswords_().indexOf(password) >= 0; }, /** * Invoked on the webview's contentload event. * @private */ onContentLoad_: function(e) { PostMessageChannel.init(this.webview_.contentWindow); }, /** * Invoked on the webview's loadabort event. * @private */ onLoadAbort_: function(e) { if (e.isTopLevel) this.abortedTopLevelUrl_ = e.url; }, /** * Invoked on the webview's loadcommit event for both main and sub frames. * @private */ onLoadCommit_: function(e) { // Skip this loadcommit if the top level load is just aborted. if (e.isTopLevel && e.url === this.abortedTopLevelUrl_) { this.abortedTopLevelUrl_ = null; return; } // Skip for none http/https url. if (!e.url.startsWith('https://') && !e.url.startsWith('http://')) return; this.isSamlPage_ = this.pendingIsSamlPage_; }, /** * Handler for webRequest.onBeforeRequest, invoked when content served over * an unencrypted connection is detected. Determines whether the request * should be blocked and if so, signals that an error message needs to be * shown. * @param {Object} details * @return {!Object} Decision whether to block the request. */ onInsecureRequest: function(details) { if (!this.blockInsecureContent) return {}; var strippedUrl = stripParams(details.url); this.dispatchEvent(new CustomEvent('insecureContentBlocked', {detail: {url: strippedUrl}})); return {cancel: true}; }, /** * Invoked when headers are received for the main frame. * @private */ onHeadersReceived_: function(details) { var headers = details.responseHeaders; // Check whether GAIA headers indicating the start or end of a SAML // redirect are present. If so, synthesize cookies to mark these points. for (var i = 0; headers && i < headers.length; ++i) { var header = headers[i]; var headerName = header.name.toLowerCase(); if (headerName == SAML_HEADER) { var action = header.value.toLowerCase(); if (action == 'start') { this.pendingIsSamlPage_ = true; // GAIA is redirecting to a SAML IdP. Any cookies contained in the // current |headers| were set by GAIA. Any cookies set in future // requests will be coming from the IdP. Append a cookie to the // current |headers| that marks the point at which the redirect // occurred. headers.push({name: 'Set-Cookie', value: 'google-accounts-saml-start=now'}); return {responseHeaders: headers}; } else if (action == 'end') { this.pendingIsSamlPage_ = false; // The SAML IdP has redirected back to GAIA. Add a cookie that marks // the point at which the redirect occurred occurred. It is // important that this cookie be prepended to the current |headers| // because any cookies contained in the |headers| were already set // by GAIA, not the IdP. Due to limitations in the webRequest API, // it is not trivial to prepend a cookie: // // The webRequest API only allows for deleting and appending // headers. To prepend a cookie (C), three steps are needed: // 1) Delete any headers that set cookies (e.g., A, B). // 2) Append a header which sets the cookie (C). // 3) Append the original headers (A, B). // // Due to a further limitation of the webRequest API, it is not // possible to delete a header in step 1) and append an identical // header in step 3). To work around this, a trailing semicolon is // added to each header before appending it. Trailing semicolons are // ignored by Chrome in cookie headers, causing the modified headers // to actually set the original cookies. var otherHeaders = []; var cookies = [{name: 'Set-Cookie', value: 'google-accounts-saml-end=now'}]; for (var j = 0; j < headers.length; ++j) { if (headers[j].name.toLowerCase().startsWith('set-cookie')) { var header = headers[j]; header.value += ';'; cookies.push(header); } else { otherHeaders.push(headers[j]); } } return {responseHeaders: otherHeaders.concat(cookies)}; } } } return {}; }, /** * Invoked when the injected JS makes a connection. */ onConnected_: function(port) { if (port.targetWindow != this.webview_.contentWindow) return; var channel = Channel.create(); channel.init(port); channel.registerMessage( 'apiCall', this.onAPICall_.bind(this, channel)); channel.registerMessage( 'updatePassword', this.onUpdatePassword_.bind(this, channel)); channel.registerMessage( 'pageLoaded', this.onPageLoaded_.bind(this, channel)); channel.registerMessage( 'getSAMLFlag', this.onGetSAMLFlag_.bind(this, channel)); }, sendInitializationSuccess_: function(channel) { channel.send({name: 'apiResponse', response: { result: 'initialized', version: this.apiVersion_, keyTypes: API_KEY_TYPES }}); }, sendInitializationFailure_: function(channel) { channel.send({ name: 'apiResponse', response: {result: 'initialization_failed'} }); }, /** * Handlers for channel messages. * @param {Channel} channel A channel to send back response. * @param {Object} msg Received message. * @private */ onAPICall_: function(channel, msg) { var call = msg.call; if (call.method == 'initialize') { if (!Number.isInteger(call.requestedVersion) || call.requestedVersion < MIN_API_VERSION_VERSION) { this.sendInitializationFailure_(channel); return; } this.apiVersion_ = Math.min(call.requestedVersion, MAX_API_VERSION_VERSION); this.apiInitialized_ = true; this.sendInitializationSuccess_(channel); return; } if (call.method == 'add') { if (API_KEY_TYPES.indexOf(call.keyType) == -1) { console.error('SamlHandler.onAPICall_: unsupported key type'); return; } // Not setting |email_| and |gaiaId_| because this API call will // eventually be followed by onCompleteLogin_() which does set it. this.apiToken_ = call.token; this.apiPasswordBytes_ = call.passwordBytes; this.dispatchEvent(new CustomEvent('apiPasswordAdded')); } else if (call.method == 'confirm') { if (call.token != this.apiToken_) console.error('SamlHandler.onAPICall_: token mismatch'); } else { console.error('SamlHandler.onAPICall_: unknown message'); } }, onUpdatePassword_: function(channel, msg) { if (this.isSamlPage_) this.passwordStore_[msg.id] = msg.password; }, onPageLoaded_: function(channel, msg) { this.authDomain = extractDomain(msg.url); this.dispatchEvent(new CustomEvent( 'authPageLoaded', {detail: {url: url, isSAMLPage: this.isSamlPage_, domain: this.authDomain}})); }, onPermissionRequest_: function(permissionEvent) { if (permissionEvent.permission === 'media') { // The actual permission check happens in // WebUILoginView::RequestMediaAccessPermission(). this.dispatchEvent(new CustomEvent('videoEnabled')); permissionEvent.request.allow(); } }, onGetSAMLFlag_: function(channel, msg) { return this.isSamlPage_; }, }; return { SamlHandler: SamlHandler }; }); /** * @fileoverview An UI component to authenciate to Chrome. The component hosts * IdP web pages in a webview. A client who is interested in monitoring * authentication events should pass a listener object of type * cr.login.GaiaAuthHost.Listener as defined in this file. After initialization, * call {@code load} to start the authentication flow. */ cr.define('cr.login', function() { 'use strict'; // TODO(rogerta): should use gaia URL from GaiaUrls::gaia_url() instead // of hardcoding the prod URL here. As is, this does not work with staging // environments. var IDP_ORIGIN = 'https://accounts.google.com/'; var IDP_PATH = 'ServiceLogin?skipvpage=true&sarp=1&rm=hide'; var CONTINUE_URL = 'chrome-extension://mfffpogegjflfpflabcdkioaeobkgjik/success.html'; var SIGN_IN_HEADER = 'google-accounts-signin'; var EMBEDDED_FORM_HEADER = 'google-accounts-embedded'; var LOCATION_HEADER = 'location'; var COOKIE_HEADER = 'cookie'; var SET_COOKIE_HEADER = 'set-cookie'; var OAUTH_CODE_COOKIE = 'oauth_code'; var GAPS_COOKIE = 'GAPS'; var SERVICE_ID = 'chromeoslogin'; var EMBEDDED_SETUP_CHROMEOS_ENDPOINT = 'embedded/setup/chromeos'; var SAML_REDIRECTION_PATH = 'samlredirect'; var BLANK_PAGE_URL = 'about:blank'; /** * The source URL parameter for the constrained signin flow. */ var CONSTRAINED_FLOW_SOURCE = 'chrome'; /** * Enum for the authorization mode, must match AuthMode defined in * chrome/browser/ui/webui/inline_login_ui.cc. * @enum {number} */ var AuthMode = { DEFAULT: 0, OFFLINE: 1, DESKTOP: 2 }; /** * Enum for the authorization type. * @enum {number} */ var AuthFlow = { DEFAULT: 0, SAML: 1 }; /** * Supported Authenticator params. * @type {!Array} * @const */ var SUPPORTED_PARAMS = [ 'gaiaId', // Obfuscated GAIA ID to skip the email prompt page // during the re-auth flow. 'gaiaUrl', // Gaia url to use. 'gaiaPath', // Gaia path to use without a leading slash. 'hl', // Language code for the user interface. 'service', // Name of Gaia service. 'continueUrl', // Continue url to use. 'frameUrl', // Initial frame URL to use. If empty defaults to // gaiaUrl. 'constrained', // Whether the extension is loaded in a constrained // window. 'clientId', // Chrome client id. 'useEafe', // Whether to use EAFE. 'needPassword', // Whether the host is interested in getting a password. // If this set to |false|, |confirmPasswordCallback| is // not called before dispatching |authCopleted|. // Default is |true|. 'flow', // One of 'default', 'enterprise', or 'theftprotection'. 'enterpriseDomain', // Domain in which hosting device is (or should be) // enrolled. 'emailDomain', // Value used to prefill domain for email. 'chromeType', // Type of Chrome OS device, e.g. "chromebox". 'clientVersion', // Version of the Chrome build. 'platformVersion', // Version of the OS build. 'releaseChannel', // Installation channel. 'endpointGen', // Current endpoint generation. 'gapsCookie', // GAPS cookie // The email fields allow for the following possibilities: // // 1/ If 'email' is not supplied, then the email text field is blank and the // user must type an email to proceed. // // 2/ If 'email' is supplied, and 'readOnlyEmail' is truthy, then the email // is hardcoded and the user cannot change it. The user is asked for // password. This is useful for re-auth scenarios, where chrome needs the // user to authenticate for a specific account and only that account. // // 3/ If 'email' is supplied, and 'readOnlyEmail' is falsy, gaia will // prefill the email text field using the given email address, but the user // can still change it and then proceed. This is used on desktop when the // user disconnects their profile then reconnects, to encourage them to use // the same account. 'email', 'readOnlyEmail', 'realm', ]; /** * Initializes the authenticator component. * @param {webview|string} webview The webview element or its ID to host IdP * web pages. * @constructor */ function Authenticator(webview) { this.webview_ = typeof webview == 'string' ? $(webview) : webview; assert(this.webview_); this.isLoaded_ = false; this.email_ = null; this.password_ = null; this.gaiaId_ = null, this.sessionIndex_ = null; this.chooseWhatToSync_ = false; this.skipForNow_ = false; this.authFlow = AuthFlow.DEFAULT; this.authDomain = ''; this.videoEnabled = false; this.idpOrigin_ = null; this.continueUrl_ = null; this.continueUrlWithoutParams_ = null; this.initialFrameUrl_ = null; this.reloadUrl_ = null; this.trusted_ = true; this.oauthCode_ = null; this.gapsCookie_ = null; this.gapsCookieSent_ = false; this.newGapsCookie_ = null; this.readyFired_ = false; this.useEafe_ = false; this.clientId_ = null; this.samlHandler_ = new cr.login.SamlHandler(this.webview_); this.confirmPasswordCallback = null; this.noPasswordCallback = null; this.insecureContentBlockedCallback = null; this.samlApiUsedCallback = null; this.missingGaiaInfoCallback = null; this.needPassword = true; this.samlHandler_.addEventListener( 'insecureContentBlocked', this.onInsecureContentBlocked_.bind(this)); this.samlHandler_.addEventListener( 'authPageLoaded', this.onAuthPageLoaded_.bind(this)); this.samlHandler_.addEventListener( 'videoEnabled', this.onVideoEnabled_.bind(this)); this.samlHandler_.addEventListener( 'apiPasswordAdded', this.onSamlApiPasswordAdded_.bind(this)); this.webview_.addEventListener('droplink', this.onDropLink_.bind(this)); this.webview_.addEventListener( 'newwindow', this.onNewWindow_.bind(this)); this.webview_.addEventListener( 'contentload', this.onContentLoad_.bind(this)); this.webview_.addEventListener( 'loadabort', this.onLoadAbort_.bind(this)); this.webview_.addEventListener( 'loadstop', this.onLoadStop_.bind(this)); this.webview_.addEventListener( 'loadcommit', this.onLoadCommit_.bind(this)); this.webview_.request.onCompleted.addListener( this.onRequestCompleted_.bind(this), {urls: [''], types: ['main_frame']}, ['responseHeaders']); this.webview_.request.onHeadersReceived.addListener( this.onHeadersReceived_.bind(this), {urls: [''], types: ['main_frame', 'xmlhttprequest']}, ['responseHeaders']); window.addEventListener( 'message', this.onMessageFromWebview_.bind(this), false); window.addEventListener( 'focus', this.onFocus_.bind(this), false); window.addEventListener( 'popstate', this.onPopState_.bind(this), false); } Authenticator.prototype = Object.create(cr.EventTarget.prototype); /** * Reinitializes authentication parameters so that a failed login attempt * would not result in an infinite loop. */ Authenticator.prototype.resetStates = function() { this.isLoaded_ = false; this.email_ = null; this.gaiaId_ = null; this.password_ = null; this.oauthCode_ = null; this.gapsCookie_ = null; this.gapsCookieSent_ = false; this.newGapsCookie_ = null; this.readyFired_ = false; this.chooseWhatToSync_ = false; this.skipForNow_ = false; this.sessionIndex_ = null; this.trusted_ = true; this.authFlow = AuthFlow.DEFAULT; this.samlHandler_.reset(); this.videoEnabled = false; }; /** * Resets the webview to the blank page. */ Authenticator.prototype.resetWebview = function() { if (this.webview_.src && this.webview_.src != BLANK_PAGE_URL) this.webview_.src = BLANK_PAGE_URL; }; /** * Loads the authenticator component with the given parameters. * @param {AuthMode} authMode Authorization mode. * @param {Object} data Parameters for the authorization flow. */ Authenticator.prototype.load = function(authMode, data) { this.authMode = authMode; this.resetStates(); // gaiaUrl parameter is used for testing. Once defined, it is never changed. this.idpOrigin_ = data.gaiaUrl || IDP_ORIGIN; this.continueUrl_ = data.continueUrl || CONTINUE_URL; this.continueUrlWithoutParams_ = this.continueUrl_.substring(0, this.continueUrl_.indexOf('?')) || this.continueUrl_; this.isConstrainedWindow_ = data.constrained == '1'; this.isNewGaiaFlow = data.isNewGaiaFlow; this.useEafe_ = data.useEafe || false; this.clientId_ = data.clientId; this.gapsCookie_ = data.gapsCookie; this.gapsCookieSent_ = false; this.newGapsCookie_ = null; this.dontResizeNonEmbeddedPages = data.dontResizeNonEmbeddedPages; this.initialFrameUrl_ = this.constructInitialFrameUrl_(data); this.reloadUrl_ = data.frameUrl || this.initialFrameUrl_; // Don't block insecure content for desktop flow because it lands on // http. Otherwise, block insecure content as long as gaia is https. this.samlHandler_.blockInsecureContent = authMode != AuthMode.DESKTOP && this.idpOrigin_.startsWith('https://'); this.needPassword = !('needPassword' in data) || data.needPassword; if (this.isNewGaiaFlow) { this.webview_.contextMenus.onShow.addListener(function(e) { e.preventDefault(); }); if (!this.onBeforeSetHeadersSet_) { this.onBeforeSetHeadersSet_ = true; var filterPrefix = this.idpOrigin_ + EMBEDDED_SETUP_CHROMEOS_ENDPOINT; // This depends on gaiaUrl parameter, that is why it is here. this.webview_.request.onBeforeSendHeaders.addListener( this.onBeforeSendHeaders_.bind(this), {urls: [filterPrefix + '?*', filterPrefix + '/*']}, ['requestHeaders', 'blocking']); } } this.webview_.src = this.reloadUrl_; this.isLoaded_ = true; }; /** * Reloads the authenticator component. */ Authenticator.prototype.reload = function() { this.resetStates(); this.webview_.src = this.reloadUrl_; this.isLoaded_ = true; }; Authenticator.prototype.constructInitialFrameUrl_ = function(data) { if (data.doSamlRedirect) { var url = this.idpOrigin_ + SAML_REDIRECTION_PATH; url = appendParam(url, 'domain', data.enterpriseDomain); url = appendParam(url, 'continue', data.gaiaUrl + 'o/oauth2/programmatic_auth?hl=' + data.hl + '&scope=https%3A%2F%2Fwww.google.com%2Faccounts%2FOAuthLogin&' + 'client_id=' + encodeURIComponent(data.clientId) + '&access_type=offline'); return url; } var path = data.gaiaPath; if (!path && this.isNewGaiaFlow) path = EMBEDDED_SETUP_CHROMEOS_ENDPOINT; if (!path) path = IDP_PATH; var url = this.idpOrigin_ + path; if (this.isNewGaiaFlow) { if (data.chromeType) url = appendParam(url, 'chrometype', data.chromeType); if (data.clientId) url = appendParam(url, 'client_id', data.clientId); if (data.enterpriseDomain) url = appendParam(url, 'manageddomain', data.enterpriseDomain); if (data.clientVersion) url = appendParam(url, 'client_version', data.clientVersion); if (data.platformVersion) url = appendParam(url, 'platform_version', data.platformVersion); if (data.releaseChannel) url = appendParam(url, 'release_channel', data.releaseChannel); if (data.endpointGen) url = appendParam(url, 'endpoint_gen', data.endpointGen); } else { url = appendParam(url, 'continue', this.continueUrl_); url = appendParam(url, 'service', data.service || SERVICE_ID); } if (data.hl) url = appendParam(url, 'hl', data.hl); if (data.gaiaId) url = appendParam(url, 'user_id', data.gaiaId); if (data.email) { if (data.readOnlyEmail) { url = appendParam(url, 'Email', data.email); } else { url = appendParam(url, 'email_hint', data.email); } } if (this.isConstrainedWindow_) url = appendParam(url, 'source', CONSTRAINED_FLOW_SOURCE); if (data.flow) url = appendParam(url, 'flow', data.flow); if (data.emailDomain) url = appendParam(url, 'emaildomain', data.emailDomain); return url; }; /** * Dispatches the 'ready' event if it hasn't been dispatched already for the * current content. * @private */ Authenticator.prototype.fireReadyEvent_ = function() { if (!this.readyFired_) { this.dispatchEvent(new Event('ready')); this.readyFired_ = true; } }; /** * Invoked when a main frame request in the webview has completed. * @private */ Authenticator.prototype.onRequestCompleted_ = function(details) { var currentUrl = details.url; if (!this.isNewGaiaFlow && currentUrl.lastIndexOf(this.continueUrlWithoutParams_, 0) == 0) { if (currentUrl.indexOf('ntp=1') >= 0) this.skipForNow_ = true; this.maybeCompleteAuth_(); return; } if (!currentUrl.startsWith('https')) this.trusted_ = false; if (this.isConstrainedWindow_) { var isEmbeddedPage = false; if (this.idpOrigin_ && currentUrl.lastIndexOf(this.idpOrigin_) == 0) { var headers = details.responseHeaders; for (var i = 0; headers && i < headers.length; ++i) { if (headers[i].name.toLowerCase() == EMBEDDED_FORM_HEADER) { isEmbeddedPage = true; break; } } } // In some cases, non-embedded pages should not be resized. For // example, on desktop when reauthenticating for purposes of unlocking // a profile, resizing would cause a browser window to open in the // system profile, which is not allowed. if (!isEmbeddedPage && !this.dontResizeNonEmbeddedPages) { this.dispatchEvent(new CustomEvent('resize', {detail: currentUrl})); return; } } this.updateHistoryState_(currentUrl); }; /** * Manually updates the history. Invoked upon completion of a webview * navigation. * @param {string} url Request URL. * @private */ Authenticator.prototype.updateHistoryState_ = function(url) { if (history.state && history.state.url != url) history.pushState({url: url}, ''); else history.replaceState({url: url}, ''); }; /** * Invoked when the sign-in page takes focus. * @param {object} e The focus event being triggered. * @private */ Authenticator.prototype.onFocus_ = function(e) { if (this.authMode == AuthMode.DESKTOP) this.webview_.focus(); }; /** * Invoked when the history state is changed. * @param {object} e The popstate event being triggered. * @private */ Authenticator.prototype.onPopState_ = function(e) { var state = e.state; if (state && state.url) this.webview_.src = state.url; }; /** * Invoked when headers are received in the main frame of the webview. It * 1) reads the authenticated user info from a signin header, * 2) signals the start of a saml flow upon receiving a saml header. * @return {!Object} Modified request headers. * @private */ Authenticator.prototype.onHeadersReceived_ = function(details) { var currentUrl = details.url; if (currentUrl.lastIndexOf(this.idpOrigin_, 0) != 0) return; var headers = details.responseHeaders; for (var i = 0; headers && i < headers.length; ++i) { var header = headers[i]; var headerName = header.name.toLowerCase(); if (headerName == SIGN_IN_HEADER) { var headerValues = header.value.toLowerCase().split(','); var signinDetails = {}; headerValues.forEach(function(e) { var pair = e.split('='); signinDetails[pair[0].trim()] = pair[1].trim(); }); // Removes "" around. this.email_ = signinDetails['email'].slice(1, -1); this.gaiaId_ = signinDetails['obfuscatedid'].slice(1, -1); this.sessionIndex_ = signinDetails['sessionindex']; } else if (headerName == LOCATION_HEADER) { // If the "choose what to sync" checkbox was clicked, then the continue // URL will contain a source=3 field. var location = decodeURIComponent(header.value); this.chooseWhatToSync_ = !!location.match(/(\?|&)source=3($|&)/); } else if ( this.isNewGaiaFlow && headerName == SET_COOKIE_HEADER) { var headerValue = header.value; if (headerValue.startsWith(OAUTH_CODE_COOKIE + '=')) { this.oauthCode_ = headerValue.substring(OAUTH_CODE_COOKIE.length + 1).split(';')[0]; } if (headerValue.startsWith(GAPS_COOKIE + '=')) { this.newGapsCookie_ = headerValue.substring(GAPS_COOKIE.length + 1).split(';')[0]; } } } }; /** * This method replaces cookie value in cookie header. * @param@ {string} header_value Original string value of Cookie header. * @param@ {string} cookie_name Name of cookie to be replaced. * @param@ {string} cookie_value New cookie value. * @return {string} New Cookie header value. * @private */ Authenticator.prototype.updateCookieValue_ = function( header_value, cookie_name, cookie_value) { var cookies = header_value.split(/\s*;\s*/); var found = false; for (var i = 0; i < cookies.length; ++i) { if (cookies[i].startsWith(cookie_name + '=')) { found = true; cookies[i] = cookie_name + '=' + cookie_value; break; } } if (!found) { cookies.push(cookie_name + '=' + cookie_value); } return cookies.join('; '); }; /** * Handler for webView.request.onBeforeSendHeaders . * @return {!Object} Modified request headers. * @private */ Authenticator.prototype.onBeforeSendHeaders_ = function(details) { // We should re-send cookie if first request was unsuccessful (i.e. no new // GAPS cookie was received). if (this.isNewGaiaFlow && this.gapsCookie_ && (!this.gapsCookieSent_ || !this.newGapsCookie_)) { var headers = details.requestHeaders; var found = false; var gapsCookie = this.gapsCookie_; for (var i = 0, l = headers.length; i < l; ++i) { if (headers[i].name == COOKIE_HEADER) { headers[i].value = this.updateCookieValue_(headers[i].value, GAPS_COOKIE, gapsCookie); found = true; break; } } if (!found) { details.requestHeaders.push( {name: COOKIE_HEADER, value: GAPS_COOKIE + '=' + gapsCookie}); } this.gapsCookieSent_ = true; } return { requestHeaders: details.requestHeaders }; }; /** * Returns true if given HTML5 message is received from the webview element. * @param {object} e Payload of the received HTML5 message. */ Authenticator.prototype.isGaiaMessage = function(e) { if (!this.isWebviewEvent_(e)) return false; // The event origin does not have a trailing slash. if (e.origin != this.idpOrigin_.substring(0, this.idpOrigin_.length - 1)) { return false; } // EAFE passes back auth code via message. if (this.useEafe_ && typeof e.data == 'object' && e.data.hasOwnProperty('authorizationCode')) { assert(!this.oauthCode_); this.oauthCode_ = e.data.authorizationCode; this.dispatchEvent( new CustomEvent('authCompleted', { detail: { authCodeOnly: true, authCode: this.oauthCode_ } })); return; } // Gaia messages must be an object with 'method' property. if (typeof e.data != 'object' || !e.data.hasOwnProperty('method')) { return false; } return true; }; /** * Invoked when an HTML5 message is received from the webview element. * @param {object} e Payload of the received HTML5 message. * @private */ Authenticator.prototype.onMessageFromWebview_ = function(e) { if (!this.isGaiaMessage(e)) return; var msg = e.data; if (msg.method == 'attemptLogin') { this.email_ = msg.email; if (this.authMode == AuthMode.DESKTOP) this.password_ = msg.password; this.chooseWhatToSync_ = msg.chooseWhatToSync; // We need to dispatch only first event, before user enters password. this.dispatchEvent( new CustomEvent('attemptLogin', {detail: msg.email})); } else if (msg.method == 'dialogShown') { this.dispatchEvent(new Event('dialogShown')); } else if (msg.method == 'dialogHidden') { this.dispatchEvent(new Event('dialogHidden')); } else if (msg.method == 'backButton') { this.dispatchEvent(new CustomEvent('backButton', {detail: msg.show})); } else if (msg.method == 'showView') { this.dispatchEvent(new Event('showView')); } else if (msg.method == 'identifierEntered') { this.dispatchEvent(new CustomEvent( 'identifierEntered', {detail: {accountIdentifier: msg.accountIdentifier}})); } else { console.warn('Unrecognized message from GAIA: ' + msg.method); } }; /** * Invoked by the hosting page to verify the Saml password. */ Authenticator.prototype.verifyConfirmedPassword = function(password) { if (!this.samlHandler_.verifyConfirmedPassword(password)) { // Invoke confirm password callback asynchronously because the // verification was based on messages and caller (GaiaSigninScreen) // does not expect it to be called immediately. // TODO(xiyuan): Change to synchronous call when iframe based code // is removed. var invokeConfirmPassword = (function() { this.confirmPasswordCallback(this.email_, this.samlHandler_.scrapedPasswordCount); }).bind(this); window.setTimeout(invokeConfirmPassword, 0); return; } this.password_ = password; this.onAuthCompleted_(); }; /** * Check Saml flow and start password confirmation flow if needed. Otherwise, * continue with auto completion. * @private */ Authenticator.prototype.maybeCompleteAuth_ = function() { var missingGaiaInfo = !this.email_ || !this.gaiaId_ || !this.sessionIndex_; if (missingGaiaInfo && !this.skipForNow_) { if (this.missingGaiaInfoCallback) this.missingGaiaInfoCallback(); this.webview_.src = this.initialFrameUrl_; return; } if (this.samlHandler_.samlApiUsed) { if (this.samlApiUsedCallback) { this.samlApiUsedCallback(); } this.password_ = this.samlHandler_.apiPasswordBytes; this.onAuthCompleted_(); return; } if (this.samlHandler_.scrapedPasswordCount == 0) { if (this.noPasswordCallback) { this.noPasswordCallback(this.email_); return; } // Fall through to finish the auth flow even if this.needPassword // is true. This is because the flag is used as an intention to get // password when it is available but not a mandatory requirement. console.warn('Authenticator: No password scraped for SAML.'); } else if (this.needPassword) { if (this.samlHandler_.scrapedPasswordCount == 1) { // If we scraped exactly one password, we complete the authentication // right away. this.password_ = this.samlHandler_.firstScrapedPassword; this.onAuthCompleted_(); return; } if (this.confirmPasswordCallback) { // Confirm scraped password. The flow follows in // verifyConfirmedPassword. this.confirmPasswordCallback(this.email_, this.samlHandler_.scrapedPasswordCount); return; } } this.onAuthCompleted_(); }; /** * Invoked to complete the authentication using the password the user enters * manually for non-principals API SAML IdPs that we couldn't scrape their * password input. */ Authenticator.prototype.completeAuthWithManualPassword = function(password) { this.password_ = password; this.onAuthCompleted_(); }; /** * Invoked to process authentication completion. * @private */ Authenticator.prototype.onAuthCompleted_ = function() { assert(this.skipForNow_ || (this.email_ && this.gaiaId_ && this.sessionIndex_)); this.dispatchEvent(new CustomEvent( 'authCompleted', // TODO(rsorokin): get rid of the stub values. { detail: { email: this.email_ || '', gaiaId: this.gaiaId_ || '', password: this.password_ || '', authCode: this.oauthCode_, usingSAML: this.authFlow == AuthFlow.SAML, chooseWhatToSync: this.chooseWhatToSync_, skipForNow: this.skipForNow_, sessionIndex: this.sessionIndex_ || '', trusted: this.trusted_, gapsCookie: this.newGapsCookie_ || this.gapsCookie_ || '', } })); this.resetStates(); }; /** * Invoked when |samlHandler_| fires 'insecureContentBlocked' event. * @private */ Authenticator.prototype.onInsecureContentBlocked_ = function(e) { if (!this.isLoaded_) return; if (this.insecureContentBlockedCallback) this.insecureContentBlockedCallback(e.detail.url); else console.error('Authenticator: Insecure content blocked.'); }; /** * Invoked when |samlHandler_| fires 'authPageLoaded' event. * @private */ Authenticator.prototype.onAuthPageLoaded_ = function(e) { if (!this.isLoaded_) return; if (!e.detail.isSAMLPage) return; this.authDomain = this.samlHandler_.authDomain; this.authFlow = AuthFlow.SAML; this.fireReadyEvent_(); }; /** * Invoked when |samlHandler_| fires 'videoEnabled' event. * @private */ Authenticator.prototype.onVideoEnabled_ = function(e) { this.videoEnabled = true; }; /** * Invoked when |samlHandler_| fires 'apiPasswordAdded' event. * @private */ Authenticator.prototype.onSamlApiPasswordAdded_ = function(e) { // Saml API 'add' password might be received after the 'loadcommit' event. // In such case, maybeCompleteAuth_ should be attempted again if oauth code // is available. if (this.oauthCode_) this.maybeCompleteAuth_(); }; /** * Invoked when a link is dropped on the webview. * @private */ Authenticator.prototype.onDropLink_ = function(e) { this.dispatchEvent(new CustomEvent('dropLink', {detail: e.url})); }; /** * Invoked when the webview attempts to open a new window. * @private */ Authenticator.prototype.onNewWindow_ = function(e) { this.dispatchEvent(new CustomEvent('newWindow', {detail: e})); }; /** * Invoked when a new document is loaded. * @private */ Authenticator.prototype.onContentLoad_ = function(e) { if (this.isConstrainedWindow_) { // Signin content in constrained windows should not zoom. Isolate the // webview from the zooming of other webviews using the 'per-view' zoom // mode, and then set it to 100% zoom. this.webview_.setZoomMode('per-view'); this.webview_.setZoom(1); } // Posts a message to IdP pages to initiate communication. var currentUrl = this.webview_.src; if (currentUrl.lastIndexOf(this.idpOrigin_) == 0) { var msg = { 'method': 'handshake', }; this.webview_.contentWindow.postMessage(msg, currentUrl); this.fireReadyEvent_(); // Focus webview after dispatching event when webview is already visible. this.webview_.focus(); } else if (currentUrl == BLANK_PAGE_URL) { this.fireReadyEvent_(); } }; /** * Invoked when the webview fails loading a page. * @private */ Authenticator.prototype.onLoadAbort_ = function(e) { this.dispatchEvent(new CustomEvent('loadAbort', {detail: {error: e.reason, src: e.url}})); }; /** * Invoked when the webview finishes loading a page. * @private */ Authenticator.prototype.onLoadStop_ = function(e) { // Sends client id to EAFE on every loadstop after a small timeout. This is // needed because EAFE sits behind SSO and initialize asynchrounouly // and we don't know for sure when it is loaded and ready to listen // for message. The postMessage is guarded by EAFE's origin. if (this.useEafe_) { // An arbitrary small timeout for delivering the initial message. var EAFE_INITIAL_MESSAGE_DELAY_IN_MS = 500; window.setTimeout((function() { var msg = { 'clientId': this.clientId_ }; this.webview_.contentWindow.postMessage(msg, this.idpOrigin_); }).bind(this), EAFE_INITIAL_MESSAGE_DELAY_IN_MS); } }; /** * Invoked when the webview navigates withing the current document. * @private */ Authenticator.prototype.onLoadCommit_ = function(e) { if (this.oauthCode_) this.maybeCompleteAuth_(); }; /** * Returns |true| if event |e| was sent from the hosted webview. * @private */ Authenticator.prototype.isWebviewEvent_ = function(e) { // Note: prints error message to console if |contentWindow| is not // defined. // TODO(dzhioev): remove the message. http://crbug.com/469522 var webviewWindow = this.webview_.contentWindow; return !!webviewWindow && webviewWindow === e.source; }; /** * The current auth flow of the hosted auth page. * @type {AuthFlow} */ cr.defineProperty(Authenticator, 'authFlow'); /** * The domain name of the current auth page. * @type {string} */ cr.defineProperty(Authenticator, 'authDomain'); /** * True if the page has requested media access. * @type {boolean} */ cr.defineProperty(Authenticator, 'videoEnabled'); Authenticator.AuthFlow = AuthFlow; Authenticator.AuthMode = AuthMode; Authenticator.SUPPORTED_PARAMS = SUPPORTED_PARAMS; return { // TODO(guohui, xiyuan): Rename GaiaAuthHost to Authenticator once the old // iframe-based flow is deprecated. GaiaAuthHost: Authenticator, Authenticator: Authenticator }; }); /* Copyright (c) 2012 The Chromium Authors. All rights reserved. * Use of this source code is governed by a BSD-style license that can be * found in the LICENSE file. */ * { box-sizing: border-box; } body { color: rgb(48, 57, 66); font-size: 13px; margin: 0; min-width: 47em; } .hidden { display: none !important; } img { flex-shrink: 0; height: 16px; padding-left: 2px; padding-right: 5px; vertical-align: top; width: 23px; } #container { -webkit-flex-direction: row; display: -webkit-flex; } #infobar { background: rgb(255, 212, 0); height: 20px; left: 0; line-height: 20px; position: fixed; right: 0; text-align: center; visibility: hidden; z-index: 1; } #infobar.show { visibility: visible; } #navigation { padding-top: 20px; width: 150px; } #content { -webkit-flex: 1; } #caption { color: rgb(92, 97, 102); font-size: 150%; padding-bottom: 10px; padding-left: 20px; } #serviceworker-internals { visibility: hidden; } .tab-header { -webkit-border-start: 6px solid transparent; padding-left: 15px; } .tab-header.selected { -webkit-border-start-color: rgb(78, 87, 100); } .tab-header > button { background-color: white; border: 0; cursor: pointer; font: inherit; line-height: 17px; margin: 6px 0; padding: 0 2px; } .tab-header:not(.selected) > button { color: #999; } #content > div { padding: 0 20px 65px 0; } #content > div:not(.selected) { display: none; } .content-header { background: linear-gradient(white, white 40%, rgba(255, 255, 255, 0.92)); border-bottom: 1px solid #eee; font-size: 150%; padding: 20px 0 10px 0; z-index: 1; } #devices-help { margin-top: 10px; } .device-header { -webkit-box-align: baseline; -webkit-box-orient: horizontal; display: -webkit-box; margin: 10px 0 0; padding: 2px 0; } .device-name { font-size: 150%; } .device-serial { color: #999; font-size: 80%; margin-left: 6px; } .device-ports { -webkit-box-orient: horizontal; display: -webkit-box; margin-left: 8px; } .port-icon { background-color: rgb(64, 192, 64); border: 0 solid transparent; border-radius: 6px; height: 12px; margin: 2px; width: 12px; } .port-icon.error { background-color: rgb(224, 32, 32); } .port-icon.transient { -webkit-transform: scale(1.2); background-color: orange; } .port-number { height: 16px; margin-right: 5px; } .browser-header { align-items: center; display: flex; flex-flow: row wrap; min-height: 33px; padding-top: 10px; } .browser-header > .browser-name { font-size: 110%; font-weight: bold; } .browser-header > .browser-user { color: #999; margin-left: 6px; } .used-for-port-forwarding { background-image: -webkit-image-set(url(chrome://theme/IDR_INFO) 1x, url(chrome://theme/IDR_INFO@2x) 2x); height: 15px; margin-left: 20px; width: 15px; } .row { padding: 6px 0; position: relative; } .properties-box { display: flex; } .subrow-box { display: inline-block; vertical-align: top; } .subrow { display: flex; flex-flow: row wrap; } .subrow > div { margin-right: 0.5em; } .webview-thumbnail { display: inline-block; flex-shrink: 0; margin-right: 5px; overflow: hidden; position: relative; vertical-align: top; } .screen-rect { background-color: #eee; position: absolute; } .view-rect { background-color: #ccc; min-height: 1px; min-width: 1px; position: absolute; } .view-rect.hidden { background-color: #ddd; } .guest { padding-left: 20px; } .invisible-view { color: rgb(151, 156, 160); } .url { color: #999; } .list { margin-top: 5px; } .action { color: rgb(17, 85, 204); cursor: pointer; margin-right: 15px; } .action:hover { text-decoration: underline; } .browser-header .action { margin-left: 10px; } .list:not(.pages) .subrow { min-height: 19px; } .action.disabled { opacity: 0.5; pointer-events: none; } .open > input { border: 1px solid #aaa; height: 17px; line-height: 17px; margin-left: 20px; padding: 0 2px; } .open > input:focus { -webkit-transition: border-color 200ms; border-color: rgb(77, 144, 254); outline: none; } .open > button { line-height: 13px; } #device-settings { border-bottom: 1px solid #eee; padding: 5px 0; } .settings-bar { padding: 5px 0 5px 0; } .settings-bar label { display: inline-block; width: 35ex; } .node-frontend-action { margin: 6px 4px; } dialog.config::backdrop { background-color: rgba(255, 255, 255, 0.75); } dialog.config { background: white; border: 0; border-radius: 3px; box-shadow: 0 4px 23px 5px rgba(0, 0, 0, 0.2), 0 2px 6px rgba(0,0,0,0.15); color: #333; padding: 17px 17px 12px; position: relative; } #port-forwarding-enable { vertical-align: middle; } .close-button { background-image: url(chrome://theme/IDR_CLOSE_DIALOG); height: 14px; width: 14px; } .close-button:active { background-image: url(chrome://theme/IDR_CLOSE_DIALOG_P); } .close-button:hover { background-image: url(chrome://theme/IDR_CLOSE_DIALOG_H); } dialog.config > .close-button { position: absolute; right: 7px; top: 7px; } dialog.config > .title { font-size: 130%; } dialog.config > .list { border: 1px solid #eee; height: 180px; margin-bottom: 10px; margin-top: 10px; overflow-x: hidden; } .config-list-row { -webkit-flex-direction: row; display: -webkit-flex; } .config-list-row:hover { background-color: #eee; } .config-list-row.selected, .config-list-row.selected:hover { background-color: #ccc; } .config-list-row input { border: 1px solid transparent; line-height: 20px; margin: 4px; min-width: 0; padding: 0 3px; } .config-list-row.fresh:not(.selected) input { border-color: #eee; } .config-list-row input.port { width: 4em; } .config-list-row input.location { -webkit-flex: 1; width: 100%; } .config-list-row:not(.empty) input.invalid { background-color: rgb(255, 200, 200); } .config-list-row .close-button { margin: 8px 8px; } .config-list-row.fresh .close-button, .config-list-row:not(.selected):not(:hover) .close-button:not(:hover) { background-image: none; pointer-events: none; } .config-list-row:not(.selected) .close-button:not(:hover) { opacity: 0.5; } dialog.config > .message { margin-bottom: 12px; width: 20em; } .config-buttons { align-items: center; display: flex; } dialog.port-forwarding .target-discovery { display: none; } dialog.target-discovery .port-forwarding { display: none; } .config-buttons > label { flex-grow: 1 } @media (max-width: 47em) { #navigation, #content { overflow: visible; } } @media (min-width: 47em) { #container { max-height: 100vh; } #navigation, #content { max-height: 100vh; overflow: auto; } } Inspect with Chrome Developer Tools
Port forwarding is active. Closing this page terminates it.
Devices
Open dedicated DevTools for Node
Pages
Extensions
Apps
Shared workers
Service workers
Other
Port forwarding settings
Target discovery settings
Define the listening port on your device that maps to a port accessible from your development machine. Learn more
Specify hosts and ports of the target discovery servers.
// Copyright (c) 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. var MIN_VERSION_TAB_CLOSE = 25; var MIN_VERSION_TARGET_ID = 26; var MIN_VERSION_NEW_TAB = 29; var MIN_VERSION_TAB_ACTIVATE = 30; var WEBRTC_SERIAL = 'WEBRTC'; var queryParamsObject = {}; var browserInspector; var browserInspectorTitle; (function() { var queryParams = window.location.search; if (!queryParams) return; var params = queryParams.substring(1).split('&'); for (var i = 0; i < params.length; ++i) { var pair = params[i].split('='); queryParamsObject[pair[0]] = pair[1]; } if ('trace' in queryParamsObject || 'tracing' in queryParamsObject) { browserInspector = 'chrome://tracing'; browserInspectorTitle = 'trace'; } else { browserInspector = queryParamsObject['browser-inspector']; browserInspectorTitle = 'inspect'; } })(); function sendCommand(command, args) { chrome.send(command, Array.prototype.slice.call(arguments, 1)); } function sendTargetCommand(command, target) { sendCommand(command, target.source, target.id); } function removeChildren(element_id) { var element = $(element_id); element.textContent = ''; } function removeAdditionalChildren(element_id) { var element = $(element_id); var elements = element.querySelectorAll('.row.additional'); for (var i = 0; i != elements.length; i++) element.removeChild(elements[i]); } function removeChildrenExceptAdditional(element_id) { var element = $(element_id); var elements = element.querySelectorAll('.row:not(.additional)'); for (var i = 0; i != elements.length; i++) element.removeChild(elements[i]); } function onload() { var tabContents = document.querySelectorAll('#content > div'); for (var i = 0; i != tabContents.length; i++) { var tabContent = tabContents[i]; var tabName = tabContent.querySelector('.content-header').textContent; var tabHeader = document.createElement('div'); tabHeader.className = 'tab-header'; var button = document.createElement('button'); button.textContent = tabName; tabHeader.appendChild(button); tabHeader.addEventListener('click', selectTab.bind(null, tabContent.id)); $('navigation').appendChild(tabHeader); } onHashChange(); initSettings(); sendCommand('init-ui'); } function onHashChange() { var hash = window.location.hash.slice(1).toLowerCase(); if (!selectTab(hash)) selectTab('devices'); } /** * @param {string} id Tab id. * @return {boolean} True if successful. */ function selectTab(id) { var tabContents = document.querySelectorAll('#content > div'); var tabHeaders = $('navigation').querySelectorAll('.tab-header'); var found = false; for (var i = 0; i != tabContents.length; i++) { var tabContent = tabContents[i]; var tabHeader = tabHeaders[i]; if (tabContent.id == id) { tabContent.classList.add('selected'); tabHeader.classList.add('selected'); found = true; } else { tabContent.classList.remove('selected'); tabHeader.classList.remove('selected'); } } if (!found) return false; window.location.hash = id; return true; } function populateTargets(source, data) { if (source == 'local') populateLocalTargets(data); else if (source == 'remote') populateRemoteTargets(data); else console.error('Unknown source type: ' + source); } function populateAdditionalTargets(data) { removeAdditionalChildren('others-list'); for (var i = 0; i < data.length; i++) addAdditionalTargetsToOthersList(data[i]); } function populateLocalTargets(data) { removeChildren('pages-list'); removeChildren('extensions-list'); removeChildren('apps-list'); removeChildren('workers-list'); removeChildren('service-workers-list'); removeChildrenExceptAdditional('others-list'); for (var i = 0; i < data.length; i++) { if (data[i].type === 'page') addToPagesList(data[i]); else if (data[i].type === 'background_page') addToExtensionsList(data[i]); else if (data[i].type === 'app') addToAppsList(data[i]); else if (data[i].type === 'shared_worker') addToWorkersList(data[i]); else if (data[i].type === 'service_worker') addToServiceWorkersList(data[i]); else addToOthersList(data[i]); } } function showIncognitoWarning() { $('devices-incognito').hidden = false; } function alreadyDisplayed(element, data) { var json = JSON.stringify(data); if (element.cachedJSON == json) return true; element.cachedJSON = json; return false; } function updateBrowserVisibility(browserSection) { var icon = browserSection.querySelector('.used-for-port-forwarding'); browserSection.hidden = !browserSection.querySelector('.open') && !browserSection.querySelector('.row') && !browserInspector && (!icon || icon.hidden); } function updateUsernameVisibility(deviceSection) { var users = new Set(); var browsers = deviceSection.querySelectorAll('.browser'); Array.prototype.forEach.call(browsers, function(browserSection) { if (!browserSection.hidden) { var browserUser = browserSection.querySelector('.browser-user'); if (browserUser) users.add(browserUser.textContent); } }); var hasSingleUser = users.size <= 1; Array.prototype.forEach.call(browsers, function(browserSection) { var browserUser = browserSection.querySelector('.browser-user'); if (browserUser) browserUser.hidden = hasSingleUser; }); } function populateRemoteTargets(devices) { if (!devices) return; if ($('config-dialog').open) { window.holdDevices = devices; return; } function browserCompare(a, b) { if (a.adbBrowserName != b.adbBrowserName) return a.adbBrowserName < b.adbBrowserName; if (a.adbBrowserVersion != b.adbBrowserVersion) return a.adbBrowserVersion < b.adbBrowserVersion; return a.id < b.id; } function insertBrowser(browserList, browser) { for (var sibling = browserList.firstElementChild; sibling; sibling = sibling.nextElementSibling) { if (browserCompare(browser, sibling)) { browserList.insertBefore(browser, sibling); return; } } browserList.appendChild(browser); } var deviceList = $('devices-list'); if (alreadyDisplayed(deviceList, devices)) return; function removeObsolete(validIds, section) { if (validIds.indexOf(section.id) < 0) section.remove(); } var newDeviceIds = devices.map(function(d) { return d.id }); Array.prototype.forEach.call( deviceList.querySelectorAll('.device'), removeObsolete.bind(null, newDeviceIds)); $('devices-help').hidden = !!devices.length; for (var d = 0; d < devices.length; d++) { var device = devices[d]; var deviceSection = $(device.id); if (!deviceSection) { deviceSection = document.createElement('div'); deviceSection.id = device.id; deviceSection.className = 'device'; deviceList.appendChild(deviceSection); var deviceHeader = document.createElement('div'); deviceHeader.className = 'device-header'; deviceSection.appendChild(deviceHeader); var deviceName = document.createElement('div'); deviceName.className = 'device-name'; deviceHeader.appendChild(deviceName); var deviceSerial = document.createElement('div'); deviceSerial.className = 'device-serial'; var serial = device.adbSerial.toUpperCase(); deviceSerial.textContent = '#' + serial; deviceHeader.appendChild(deviceSerial); if (serial === WEBRTC_SERIAL) deviceHeader.classList.add('hidden'); var devicePorts = document.createElement('div'); devicePorts.className = 'device-ports'; deviceHeader.appendChild(devicePorts); var browserList = document.createElement('div'); browserList.className = 'browsers'; deviceSection.appendChild(browserList); var authenticating = document.createElement('div'); authenticating.className = 'device-auth'; deviceSection.appendChild(authenticating); } if (alreadyDisplayed(deviceSection, device)) continue; deviceSection.querySelector('.device-name').textContent = device.adbModel; deviceSection.querySelector('.device-auth').textContent = device.adbConnected ? '' : 'Pending authentication: please accept ' + 'debugging session on the device.'; var browserList = deviceSection.querySelector('.browsers'); var newBrowserIds = device.browsers.map(function(b) { return b.id }); Array.prototype.forEach.call( browserList.querySelectorAll('.browser'), removeObsolete.bind(null, newBrowserIds)); for (var b = 0; b < device.browsers.length; b++) { var browser = device.browsers[b]; var majorChromeVersion = browser.adbBrowserChromeVersion; var pageList; var browserSection = $(browser.id); if (browserSection) { pageList = browserSection.querySelector('.pages'); } else { browserSection = document.createElement('div'); browserSection.id = browser.id; browserSection.className = 'browser'; insertBrowser(browserList, browserSection); var browserHeader = document.createElement('div'); browserHeader.className = 'browser-header'; var browserName = document.createElement('div'); browserName.className = 'browser-name'; browserHeader.appendChild(browserName); browserName.textContent = browser.adbBrowserName; if (browser.adbBrowserVersion) browserName.textContent += ' (' + browser.adbBrowserVersion + ')'; if (browser.adbBrowserUser) { var browserUser = document.createElement('div'); browserUser.className = 'browser-user'; browserUser.textContent = browser.adbBrowserUser; browserHeader.appendChild(browserUser); } browserSection.appendChild(browserHeader); if (majorChromeVersion >= MIN_VERSION_NEW_TAB) { var newPage = document.createElement('div'); newPage.className = 'open'; var newPageUrl = document.createElement('input'); newPageUrl.type = 'text'; newPageUrl.placeholder = 'Open tab with url'; newPage.appendChild(newPageUrl); var openHandler = function(sourceId, browserId, input) { sendCommand( 'open', sourceId, browserId, input.value || 'about:blank'); input.value = ''; }.bind(null, browser.source, browser.id, newPageUrl); newPageUrl.addEventListener('keyup', function(handler, event) { if (event.key == 'Enter' && event.target.value) handler(); }.bind(null, openHandler), true); var newPageButton = document.createElement('button'); newPageButton.textContent = 'Open'; newPage.appendChild(newPageButton); newPageButton.addEventListener('click', openHandler, true); browserHeader.appendChild(newPage); } var portForwardingInfo = document.createElement('div'); portForwardingInfo.className = 'used-for-port-forwarding'; portForwardingInfo.hidden = true; portForwardingInfo.title = 'This browser is used for port ' + 'forwarding. Closing it will drop current connections.'; browserHeader.appendChild(portForwardingInfo); if (browserInspector) { var link = document.createElement('span'); link.classList.add('action'); link.setAttribute('tabindex', 1); link.textContent = browserInspectorTitle; browserHeader.appendChild(link); link.addEventListener( 'click', sendCommand.bind(null, 'inspect-browser', browser.source, browser.id, browserInspector), false); } pageList = document.createElement('div'); pageList.className = 'list pages'; browserSection.appendChild(pageList); } if (!alreadyDisplayed(browserSection, browser)) { pageList.textContent = ''; for (var p = 0; p < browser.pages.length; p++) { var page = browser.pages[p]; // Attached targets have no unique id until Chrome 26. For such // targets it is impossible to activate existing DevTools window. page.hasNoUniqueId = page.attached && majorChromeVersion && majorChromeVersion < MIN_VERSION_TARGET_ID; var row = addTargetToList(page, pageList, ['name', 'url']); if (page['description']) addWebViewDetails(row, page); else addFavicon(row, page); if (majorChromeVersion >= MIN_VERSION_TAB_ACTIVATE) { addActionLink(row, 'focus tab', sendTargetCommand.bind(null, 'activate', page), false); } if (majorChromeVersion) { addActionLink(row, 'reload', sendTargetCommand.bind(null, 'reload', page), page.attached); } if (majorChromeVersion >= MIN_VERSION_TAB_CLOSE) { addActionLink(row, 'close', sendTargetCommand.bind(null, 'close', page), false); } } } updateBrowserVisibility(browserSection); } updateUsernameVisibility(deviceSection); } } function addToPagesList(data) { var row = addTargetToList(data, $('pages-list'), ['name', 'url']); addFavicon(row, data); if (data.guests) addGuestViews(row, data.guests); } function addToExtensionsList(data) { var row = addTargetToList(data, $('extensions-list'), ['name', 'url']); addFavicon(row, data); if (data.guests) addGuestViews(row, data.guests); } function addToAppsList(data) { var row = addTargetToList(data, $('apps-list'), ['name', 'url']); addFavicon(row, data); if (data.guests) addGuestViews(row, data.guests); } function addGuestViews(row, guests) { Array.prototype.forEach.call(guests, function(guest) { var guestRow = addTargetToList(guest, row, ['name', 'url']); guestRow.classList.add('guest'); addFavicon(guestRow, guest); }); } function addToWorkersList(data) { var row = addTargetToList(data, $('workers-list'), ['name', 'description', 'url']); addActionLink(row, 'terminate', sendTargetCommand.bind(null, 'close', data), false); } function addToServiceWorkersList(data) { var row = addTargetToList( data, $('service-workers-list'), ['name', 'description', 'url']); addActionLink(row, 'terminate', sendTargetCommand.bind(null, 'close', data), false); } function addToOthersList(data) { addTargetToList(data, $('others-list'), ['url']); } function addAdditionalTargetsToOthersList(data) { addTargetToList(data, $('others-list'), ['name', 'url']); } function formatValue(data, property) { var value = data[property]; if (property == 'name' && value == '') { value = 'untitled'; } var text = value ? String(value) : ''; if (text.length > 100) text = text.substring(0, 100) + '\u2026'; var div = document.createElement('div'); div.textContent = text; div.className = property; return div; } function addFavicon(row, data) { var favicon = document.createElement('img'); if (data['faviconUrl']) favicon.src = data['faviconUrl']; var propertiesBox = row.querySelector('.properties-box'); propertiesBox.insertBefore(favicon, propertiesBox.firstChild); } function addWebViewDetails(row, data) { var webview; try { webview = JSON.parse(data['description']); } catch (e) { return; } addWebViewDescription(row, webview); if (data.adbScreenWidth && data.adbScreenHeight) addWebViewThumbnail( row, webview, data.adbScreenWidth, data.adbScreenHeight); } function addWebViewDescription(row, webview) { var viewStatus = { visibility: '', position: '', size: '' }; if (!webview.empty) { if (webview.attached && !webview.visible) viewStatus.visibility = 'hidden'; else if (!webview.attached) viewStatus.visibility = 'detached'; viewStatus.size = 'size ' + webview.width + ' \u00d7 ' + webview.height; } else { viewStatus.visibility = 'empty'; } if (webview.attached) { viewStatus.position = 'at (' + webview.screenX + ', ' + webview.screenY + ')'; } var subRow = document.createElement('div'); subRow.className = 'subrow webview'; if (webview.empty || !webview.attached || !webview.visible) subRow.className += ' invisible-view'; if (viewStatus.visibility) subRow.appendChild(formatValue(viewStatus, 'visibility')); if (viewStatus.position) subRow.appendChild(formatValue(viewStatus, 'position')); subRow.appendChild(formatValue(viewStatus, 'size')); var subrowBox = row.querySelector('.subrow-box'); subrowBox.insertBefore(subRow, row.querySelector('.actions')); } function addWebViewThumbnail(row, webview, screenWidth, screenHeight) { var maxScreenRectSize = 50; var screenRectWidth; var screenRectHeight; var aspectRatio = screenWidth / screenHeight; if (aspectRatio < 1) { screenRectWidth = Math.round(maxScreenRectSize * aspectRatio); screenRectHeight = maxScreenRectSize; } else { screenRectWidth = maxScreenRectSize; screenRectHeight = Math.round(maxScreenRectSize / aspectRatio); } var thumbnail = document.createElement('div'); thumbnail.className = 'webview-thumbnail'; var thumbnailWidth = 3 * screenRectWidth; var thumbnailHeight = 60; thumbnail.style.width = thumbnailWidth + 'px'; thumbnail.style.height = thumbnailHeight + 'px'; var screenRect = document.createElement('div'); screenRect.className = 'screen-rect'; screenRect.style.left = screenRectWidth + 'px'; screenRect.style.top = (thumbnailHeight - screenRectHeight) / 2 + 'px'; screenRect.style.width = screenRectWidth + 'px'; screenRect.style.height = screenRectHeight + 'px'; thumbnail.appendChild(screenRect); if (!webview.empty && webview.attached) { var viewRect = document.createElement('div'); viewRect.className = 'view-rect'; if (!webview.visible) viewRect.classList.add('hidden'); function percent(ratio) { return ratio * 100 + '%'; } viewRect.style.left = percent(webview.screenX / screenWidth); viewRect.style.top = percent(webview.screenY / screenHeight); viewRect.style.width = percent(webview.width / screenWidth); viewRect.style.height = percent(webview.height / screenHeight); screenRect.appendChild(viewRect); } var propertiesBox = row.querySelector('.properties-box'); propertiesBox.insertBefore(thumbnail, propertiesBox.firstChild); } function addTargetToList(data, list, properties) { var row = document.createElement('div'); row.className = 'row'; row.targetId = data.id; var propertiesBox = document.createElement('div'); propertiesBox.className = 'properties-box'; row.appendChild(propertiesBox); var subrowBox = document.createElement('div'); subrowBox.className = 'subrow-box'; propertiesBox.appendChild(subrowBox); var subrow = document.createElement('div'); subrow.className = 'subrow'; subrowBox.appendChild(subrow); for (var j = 0; j < properties.length; j++) subrow.appendChild(formatValue(data, properties[j])); var actionBox = document.createElement('div'); actionBox.className = 'actions'; subrowBox.appendChild(actionBox); if (data.isAdditional) { addActionLink(row, 'inspect', sendCommand.bind(null, 'inspect-additional', data.url), false); row.classList.add('additional'); } else if (!data.hasCustomInspectAction) { addActionLink(row, 'inspect', sendTargetCommand.bind(null, 'inspect', data), data.hasNoUniqueId || data.adbAttachedForeign); } list.appendChild(row); return row; } function addActionLink(row, text, handler, opt_disabled) { var link = document.createElement('span'); link.classList.add('action'); link.setAttribute('tabindex', 1); if (opt_disabled) link.classList.add('disabled'); else link.classList.remove('disabled'); link.textContent = text; link.addEventListener('click', handler, true); function handleKey(e) { if (e.key == 'Enter' || e.key == ' ') { e.preventDefault(); handler(); } } link.addEventListener('keydown', handleKey, true); row.querySelector('.actions').appendChild(link); } function initSettings() { checkboxSendsCommand('discover-usb-devices-enable', 'set-discover-usb-devices-enabled'); checkboxSendsCommand('port-forwarding-enable', 'set-port-forwarding-enabled'); checkboxSendsCommand('discover-tcp-devices-enable', 'set-discover-tcp-targets-enabled'); $('port-forwarding-config-open').addEventListener( 'click', openPortForwardingConfig); $('tcp-discovery-config-open').addEventListener( 'click', openTargetsConfig); $('config-dialog-close').addEventListener('click', function() { $('config-dialog').commit(true); }); $('node-frontend').addEventListener( 'click', sendCommand.bind(null, 'open-node-frontend')); } function checkboxHandler(command, event) { sendCommand(command, event.target.checked); } function checkboxSendsCommand(id, command) { $(id).addEventListener('change', checkboxHandler.bind(null, command)); } function handleKey(event) { switch (event.keyCode) { case 13: // Enter var dialog = $('config-dialog'); if (event.target.nodeName == 'INPUT') { var line = event.target.parentNode; if (!line.classList.contains('fresh') || line.classList.contains('empty')) { dialog.commit(true); } else { commitFreshLineIfValid(true /* select new line */); dialog.commit(false); } } else { dialog.commit(true); } break; } } function commitDialog(commitHandler, shouldClose) { var element = $('config-dialog'); if (element.open && shouldClose) { element.onclose = null; element.close(); document.removeEventListener('keyup', handleKey); if (window.holdDevices) { populateRemoteTargets(window.holdDevices); delete window.holdDevices; } } commitFreshLineIfValid(); commitHandler(); } function openConfigDialog(dialogClass, commitHandler, lineFactory, data) { var dialog = $('config-dialog'); if (dialog.open) return; dialog.className = dialogClass; dialog.classList.add('config'); document.addEventListener('keyup', handleKey); dialog.commit = commitDialog.bind(null, commitHandler); dialog.onclose = commitDialog.bind(null, commitHandler, true); $('button-done').onclick = dialog.onclose; var list = $('config-dialog').querySelector('.list'); list.textContent = ''; list.createRow = appendRow.bind(null, list, lineFactory); for (var key in data) list.createRow(key, data[key]); list.createRow(null, null); dialog.showModal(); var defaultFocus = dialog.querySelector('.fresh .preselected'); if (defaultFocus) defaultFocus.focus(); else doneButton.focus(); } function openPortForwardingConfig() { function createPortForwardingConfigLine(port, location) { var line = document.createElement('div'); line.className = 'port-forwarding-pair config-list-row'; var portInput = createConfigField(port, 'port preselected', 'Port', validatePort); line.appendChild(portInput); var locationInput = createConfigField( location, 'location', 'IP address and port', validateLocation); locationInput.classList.add('primary'); line.appendChild(locationInput); return line; } function commitPortForwardingConfig() { var config = {}; filterList(['.port', '.location'], function(port, location) { config[port] = location; }); sendCommand('set-port-forwarding-config', config); } openConfigDialog('port-forwarding', commitPortForwardingConfig, createPortForwardingConfigLine, window.portForwardingConfig); } function openTargetsConfig() { function createTargetDiscoveryConfigLine(index, targetDiscovery) { var line = document.createElement('div'); line.className = 'target-discovery-line config-list-row'; var locationInput = createConfigField( targetDiscovery, 'location preselected', 'IP address and port', validateLocation); locationInput.classList.add('primary'); line.appendChild(locationInput); return line; } function commitTargetDiscoveryConfig() { var entries = []; filterList(['.location'], function(location) { entries.push(location); }); sendCommand('set-tcp-discovery-config', entries); } openConfigDialog('target-discovery', commitTargetDiscoveryConfig, createTargetDiscoveryConfigLine, window.targetDiscoveryConfig); } function filterList(fieldSelectors, callback) { var lines = $('config-dialog').querySelectorAll('.config-list-row'); for (var i = 0; i != lines.length; i++) { var line = lines[i]; var values = []; for (var selector of fieldSelectors) { var input = line.querySelector(selector); var value = input.classList.contains('invalid') ? input.lastValidValue : input.value; if (!value) break; values.push(value); } if (values.length == fieldSelectors.length) callback.apply(null, values); } } function updateCheckbox(id, enabled) { var checkbox = $(id); checkbox.checked = !!enabled; checkbox.disabled = false; } function updateDiscoverUsbDevicesEnabled(enabled) { updateCheckbox('discover-usb-devices-enable', enabled); } function updatePortForwardingEnabled(enabled) { updateCheckbox('port-forwarding-enable', enabled); $('infobar').classList.toggle('show', enabled); $('infobar').scrollIntoView(); } function updatePortForwardingConfig(config) { window.portForwardingConfig = config; $('port-forwarding-config-open').disabled = !config; } function updateTCPDiscoveryEnabled(enabled) { updateCheckbox('discover-tcp-devices-enable', enabled); } function updateTCPDiscoveryConfig(config) { window.targetDiscoveryConfig = config; $('tcp-discovery-config-open').disabled = !config; } function appendRow(list, lineFactory, key, value) { var line = lineFactory(key, value); line.lastElementChild.addEventListener('keydown', function(e) { if (e.key == 'Tab' && !hasKeyModifiers(e) && line.classList.contains('fresh') && !line.classList.contains('empty')) { // Tabbing forward on the fresh line, try create a new empty one. if (commitFreshLineIfValid(true)) e.preventDefault(); } }); var lineDelete = document.createElement('div'); lineDelete.className = 'close-button'; lineDelete.addEventListener('click', function() { var newSelection = line.nextElementSibling || line.previousElementSibling; selectLine(newSelection, true); line.parentNode.removeChild(line); $('config-dialog').commit(false); }); line.appendChild(lineDelete); line.addEventListener( 'click', selectLine.bind(null, line, true)); line.addEventListener( 'focus', selectLine.bind(null, line, true)); checkEmptyLine(line); if (!key && !value) line.classList.add('fresh'); return list.appendChild(line); } function validatePort(input) { var match = input.value.match(/^(\d+)$/); if (!match) return false; var port = parseInt(match[1]); if (port < 1024 || 65535 < port) return false; var inputs = document.querySelectorAll('input.port:not(.invalid)'); for (var i = 0; i != inputs.length; ++i) { if (inputs[i] == input) break; if (parseInt(inputs[i].value) == port) return false; } return true; } function validateLocation(input) { var match = input.value.match(/^([a-zA-Z0-9\.\-_]+):(\d+)$/); if (!match) return false; var port = parseInt(match[2]); return port <= 65535; } function createConfigField(value, className, hint, validate) { var input = document.createElement('input'); input.className = className; input.type = 'text'; input.placeholder = hint; input.value = value || ''; input.lastValidValue = value || ''; function checkInput() { if (validate(input)) input.classList.remove('invalid'); else input.classList.add('invalid'); if (input.parentNode) checkEmptyLine(input.parentNode); } checkInput(); input.addEventListener('keyup', checkInput); input.addEventListener('focus', function() { selectLine(input.parentNode); }); input.addEventListener('blur', function() { if (validate(input)) input.lastValidValue = input.value; }); return input; } function checkEmptyLine(line) { var inputs = line.querySelectorAll('input'); var empty = true; for (var i = 0; i != inputs.length; i++) { if (inputs[i].value != '') empty = false; } if (empty) line.classList.add('empty'); else line.classList.remove('empty'); } function selectLine(line, opt_focusInput) { if (line.classList.contains('selected')) return; var selected = line.parentElement && line.parentElement.querySelector('.selected'); if (selected) selected.classList.remove('selected'); line.classList.add('selected'); if (opt_focusInput) { var el = line.querySelector('.preselected'); if (el) { line.firstChild.select(); line.firstChild.focus(); } } } function commitFreshLineIfValid(opt_selectNew) { var line = $('config-dialog').querySelector('.config-list-row.fresh'); if (line.querySelector('.invalid')) return false; line.classList.remove('fresh'); var freshLine = line.parentElement.createRow(); if (opt_selectNew) freshLine.querySelector('.preselected').focus(); return true; } function populatePortStatus(devicesStatusMap) { for (var deviceId in devicesStatusMap) { if (!devicesStatusMap.hasOwnProperty(deviceId)) continue; var deviceStatus = devicesStatusMap[deviceId]; var deviceStatusMap = deviceStatus.ports; var deviceSection = $(deviceId); if (!deviceSection) continue; var devicePorts = deviceSection.querySelector('.device-ports'); if (alreadyDisplayed(devicePorts, deviceStatus)) continue; devicePorts.textContent = ''; for (var port in deviceStatusMap) { if (!deviceStatusMap.hasOwnProperty(port)) continue; var status = deviceStatusMap[port]; var portIcon = document.createElement('div'); portIcon.className = 'port-icon'; // status === 0 is the default (connected) state. if (status === -1 || status === -2) portIcon.classList.add('transient'); else if (status < 0) portIcon.classList.add('error'); devicePorts.appendChild(portIcon); var portNumber = document.createElement('div'); portNumber.className = 'port-number'; portNumber.textContent = ':' + port; devicePorts.appendChild(portNumber); } function updatePortForwardingInfo(browserSection) { var icon = browserSection.querySelector('.used-for-port-forwarding'); if (icon) icon.hidden = (browserSection.id !== deviceStatus.browserId); updateBrowserVisibility(browserSection); } Array.prototype.forEach.call( deviceSection.querySelectorAll('.browser'), updatePortForwardingInfo); updateUsernameVisibility(deviceSection); } function clearBrowserPorts(browserSection) { var icon = browserSection.querySelector('.used-for-port-forwarding'); if (icon) icon.hidden = true; updateBrowserVisibility(browserSection); } function clearPorts(deviceSection) { if (deviceSection.id in devicesStatusMap) return; var devicePorts = deviceSection.querySelector('.device-ports'); devicePorts.textContent = ''; delete devicePorts.cachedJSON; Array.prototype.forEach.call( deviceSection.querySelectorAll('.browser'), clearBrowserPorts); } Array.prototype.forEach.call( document.querySelectorAll('.device'), clearPorts); } document.addEventListener('DOMContentLoaded', onload); window.addEventListener('hashchange', onHashChange); /* Copyright 2012 The Chromium Authors. All rights reserved. * Use of this source code is governed by a BSD-style license that can be * found in the LICENSE file. */ body { font-size: 12px; margin: 10px; min-width: 47em; padding-bottom: 65px; } img { float: left; height: 16px; padding-right: 5px; width: 16px; } .section { background-color: rgb(235, 239, 249); border-top: 1px solid rgb(181, 199, 222); font-weight: bold; margin: 10px 0 0; padding: 2px 2px; } .row { border-bottom: 1px solid #a0a0a0; padding: 5px; } .url { color: #a0a0a0; } .debug { margin: 1px; } .debug span+span { padding-left: 5px; } .timestamp { color: blue; } Instant preferences

Instant preferences



Instant Event Log



// Copyright 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. // Redefine '$' here rather than including 'cr.js', since this is // the only function needed. This allows this file to be loaded // in a browser directly for layout and some testing purposes. var $ = function(id) { return document.getElementById(id); }; /** * WebUI for configuring instant.* preference values used by * Chrome's instant search system. */ var instantConfig = (function() { 'use strict'; /** List of fields used to dynamically build form. **/ var FIELDS = [ { key: 'instant_ui.zero_suggest_url_prefix', label: 'Prefix URL for the experimental Instant ZeroSuggest provider', type: 'string', size: 40, units: '', default: '' }, ]; /** * Returns a DOM element of the given type and class name. */ function createElementWithClass(elementType, className) { var element = document.createElement(elementType); element.className = className; return element; } /** * Dynamically builds web-form based on FIELDS list. * @return {string} The form's HTML. */ function buildForm() { var buf = []; for (var i = 0; i < FIELDS.length; i++) { var field = FIELDS[i]; var row = createElementWithClass('div', 'row'); row.id = ''; var label = createElementWithClass('label', 'row-label'); label.setAttribute('for', field.key); label.textContent = field.label; row.appendChild(label); var input = createElementWithClass('input', 'row-input'); input.type = field.type; input.id = field.key; input.title = "Default Value: " + field.default; if (field.size) input.size = field.size; input.min = field.min || 0; if (field.max) input.max = field.max; if (field.step) input.step = field.step; row.appendChild(input); var units = createElementWithClass('div', 'row-units'); if (field.units) units.innerHTML = field.units; row.appendChild(units); $('instant-form').appendChild(row); } } /** * Initialize the form by adding 'onChange' listeners to all fields. */ function initForm() { for (var i = 0; i < FIELDS.length; i++) { var field = FIELDS[i]; $(field.key).onchange = (function(key) { setPreferenceValue(key); }).bind(null, field.key); } } /** * Request a preference setting's value. * This method is asynchronous; the result is provided by a call to * getPreferenceValueResult. * @param {string} prefName The name of the preference value being requested. */ function getPreferenceValue(prefName) { chrome.send('getPreferenceValue', [prefName]); } /** * Handle callback from call to getPreferenceValue. * @param {string} prefName The name of the requested preference value. * @param {value} value The current value associated with prefName. */ function getPreferenceValueResult(prefName, value) { if ($(prefName).type == 'checkbox') $(prefName).checked = value; else $(prefName).value = value; } /** * Set a preference setting's value stored in the element with prefName. * @param {string} prefName The name of the preference value being set. */ function setPreferenceValue(prefName) { var value; if ($(prefName).type == 'checkbox') value = $(prefName).checked; else if ($(prefName).type == 'number') value = parseFloat($(prefName).value); else value = $(prefName).value; chrome.send('setPreferenceValue', [prefName, value]); } /** * Saves data back into Chrome preferences. */ function onSave() { for (var i = 0; i < FIELDS.length; i++) { var field = FIELDS[i]; setPreferenceValue(field.key); } return false; } /** * Request debug info. * The method is asynchronous, results being provided via getDebugInfoResult. */ function getDebugInfo() { chrome.send('getDebugInfo'); } /** * Handles callback from getDebugInfo. * @param {Object} info The debug info. */ function getDebugInfoResult(info) { for (var i = 0; i < info.entries.length; ++i) { var entry = info.entries[i]; var row = createElementWithClass('p', 'debug'); row.appendChild(createElementWithClass('span', 'timestamp')).textContent = entry.time; row.appendChild(document.createElement('span')).textContent = entry.text; $('instant-debug-info').appendChild(row); } } /** * Resets list of debug events. */ function clearDebugInfo() { $('instant-debug-info').innerHTML = ''; chrome.send('clearDebugInfo'); } function loadForm() { for (var i = 0; i < FIELDS.length; i++) getPreferenceValue(FIELDS[i].key); } /** * Build and initialize the configuration form. */ function initialize() { buildForm(); loadForm(); initForm(); getDebugInfo(); $('save-button').onclick = onSave.bind(this); $('clear-button').onclick = clearDebugInfo.bind(this); } return { initialize: initialize, getDebugInfoResult: getDebugInfoResult, getPreferenceValueResult: getPreferenceValueResult }; })(); document.addEventListener('DOMContentLoaded', instantConfig.initialize); { "background": { "scripts": [ "tts_extension.js" ], "persistent": false }, "description": "Component extension providing speech via the Google network text-to-speech service.", "key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA8GSbNUMGygqQTNDMFGIjZNcwXsHLzkNkHjWbuY37PbNdSDZ4VqlVjzbWqODSe+MjELdv5Keb51IdytnoGYXBMyqKmWpUrg+RnKvQ5ibWr4MW9pyIceOIdp9GrzC1WZGgTmZismYR3AjaIpufZ7xDdQQv+XrghPWCkdVqLN+qZDA1HU+DURznkMICiDDSH2sU0egm9UbWfS218bZqzKeQDiC3OnTPlaxcbJtKUuupIm5knjze3Wo9Ae9poTDMzKgchg0VlFCv3uqox+wlD8sjXBoyBCCK9HpImdVAF1a7jpdgiUHpPeV/26oYzM9/grltwNR3bzECQgSpyXp0eyoegwIDAQAB", "manifest_version": 2, "name": "Google Network Speech", "permissions": [ "systemPrivate", "ttsEngine", "https://www.google.com/" ], "tts_engine": { "voices": [ { "event_types": [ "start", "end", "error" ], "gender": "female", "lang": "de-DE", "voice_name": "Google Deutsch", "remote": true }, { "event_types": [ "start", "end", "error" ], "gender": "female", "lang": "en-US", "voice_name": "Google US English", "remote": true }, { "event_types": [ "start", "end", "error" ], "gender": "female", "lang": "en-GB", "voice_name": "Google UK English Female", "remote": true }, { "event_types": [ "start", "end", "error" ], "gender": "male", "lang": "en-GB", "voice_name": "Google UK English Male", "remote": true }, { "event_types": [ "start", "end", "error" ], "gender": "female", "lang": "es-ES", "voice_name": "Google español", "remote": true }, { "event_types": [ "start", "end", "error" ], "gender": "female", "lang": "es-US", "voice_name": "Google español de Estados Unidos", "remote": true }, { "event_types": [ "start", "end", "error" ], "gender": "female", "lang": "fr-FR", "voice_name": "Google français", "remote": true }, { "event_types": [ "start", "end", "error" ], "gender": "female", "lang": "hi-IN", "voice_name": "Google हिन्दी", "remote": true }, { "event_types": [ "start", "end", "error" ], "gender": "female", "lang": "id-ID", "voice_name": "Google Bahasa Indonesia", "remote": true }, { "event_types": [ "start", "end", "error" ], "gender": "female", "lang": "it-IT", "voice_name": "Google italiano", "remote": true }, { "event_types": [ "start", "end", "error" ], "gender": "female", "lang": "ja-JP", "voice_name": "Google 日本語", "remote": true }, { "event_types": [ "start", "end", "error" ], "gender": "female", "lang": "ko-KR", "voice_name": "Google 한국의", "remote": true }, { "event_types": [ "start", "end", "error" ], "gender": "female", "lang": "nl-NL", "voice_name": "Google Nederlands", "remote": true }, { "event_types": [ "start", "end", "error" ], "gender": "female", "lang": "pl-PL", "voice_name": "Google polski", "remote": true }, { "event_types": [ "start", "end", "error" ], "gender": "female", "lang": "pt-BR", "voice_name": "Google português do Brasil", "remote": true }, { "event_types": [ "start", "end", "error" ], "gender": "female", "lang": "ru-RU", "voice_name": "Google русский", "remote": true }, { "event_types": [ "start", "end", "error" ], "gender": "female", "lang": "zh-CN", "voice_name": "Google 普通话(中国大陆)", "remote": true }, { "event_types": [ "start", "end", "error" ], "gender": "female", "lang": "zh-HK", "voice_name": "Google 粤語(香港)", "remote": true }, { "event_types": [ "start", "end", "error" ], "gender": "female", "lang": "zh-TW", "voice_name": "Google 國語(臺灣)", "remote": true } ] }, "version": "1.0" } WYS8~ϯcH-BfRN,p,$smIcr8ԗZ Ci6l ~߲Ds -"R'}(#L [)Z|oǟ`sCp3ىf}p;)l(M8N"8+fo Xjcc,\{!˰ vp#k6oXnCpN\hԘSPte)\ :Te}>m3QTɬ8s/0Ǟ9 ײEW25n_K1/bVLƊJ ,'}w_]Z- 0vZ0ehAèFM\! >ğiE8EzJ \br2c<])_er+- yfpB4$(Ra@Ei!e:7]NPf"2(|R ǒR=bk[g iwX A)<ÅҖv`YgC0zg/J]t#/6pIӖ.l8_^,-MM uy4fI}뺦0 NtLQ-_2 p: } G.Uۃ9Q}\l1玿>' Nj9ʢ"VFOM~1FR5DoWZSV@k?#tD<@t`Y'w^ń6ah Z $V7ݧ_Aʾ)%SѓOEf}yǁMLf/\ľ╣aFSILuVrIC/7Xmo6_qJNS%-6`mIҮ(J:GLeQ#F;-qܭa^yrUf!Ld82ƲAe3MyNЀFӨAfҀQN"+u)8<>2v#2LXHD12HUE uGǯώa$sz!Ӄ8?*4DERaE,gD\J,X/덪"RD"KTeL8@fcd$ 69ٜޑsP3l`"m&baɫgHT%ynpXD2ZBhdD .^qK)ǡt/cl,1GI^j3FėKcwi%V*G ,419 cN,DQ2A'd;d9:JҎQS!>Qqu Y3>xC)eQs6 ϼ ~=J?ETdBt}&8YPgܙ#.rYuEXgS@3΅f k$͢˟CN`yq kiKxnbu̠e2MI|gp/glB(pWtqOu&_AMaBFʸR4,~Gu# pdIk㲿+ CٔK.;w3ɐZn >g" #IΠK'o0kT&jLQdsoa`}SpR(Ʌ1Řk?\?C =)yU?onAN]?͙B}(Ks< 49S-iVt_7:l)5˄& 4f{|ǰ[^˪HOYG JJn5t M5]3F }8`+|Di&M78q)ͥWS FhltͩLz_kX9C F{zW\fH˶ɶ\AW kb_ȳl#S'4gn3$XE}W>]TL0U\cRutq^wMwl,/*xR悧eiJ:Uދ7#!&X2: g s!ټ<шpƭ^jƦ2 Zï@jb$hzKW@`,\#JF)wI">THO+s% $2
$2
$1
$1
$2
$2
$2
$3
/* Copyright 2013 The Chromium Authors. All rights reserved. * Use of this source code is governed by a BSD-style license that can be * found in the LICENSE file. */ body { margin: 21px 10px 24px 10px; } h1 { margin: 0 0 13px 0; } h2 { margin: 23px 0 0 0; } header { border-bottom: 1px solid #eee; max-width: 718px; } .device { background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAAAAADFHGIkAAAAO0lEQVR4AWP4jwNAJBajIoRENioiLJGNCqASi7MxwWK8OggblUXYKMpdlUM9o7KpbxTJEUUwzjEAAQkAYJOdbPA0kn4AAAAASUVORK5CYII=) no-repeat; margin: 23px 0; max-width: 695px; overflow: hidden; } html[dir='rtl'] .device { background-position: right top; } .device .device-info { -webkit-padding-start: 40px; float: left; } .printer { background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAQAAABKfvVzAAAAsklEQVR4Xr3UvQ2DMBSF0StKpmCBLMAsVIyBWCCKBEpIjDH5KVJlSuIiusL2syMadDo/fY39ZCwb7RzgLosHHY6CLh70OAn6eHCWBQEdZEGAHA3efzTIGWQtDC5RA5RlspYBZgwJY1mXNUbMDKBxTdBFVVTQ0OvglqDwspQcaAFnDDBB/Uz+lTozBoaBWUDSjLdETiDMnBfmZnobvH7xcAhXer0f1sdjz+LBU7Z5W/f9BL71COOFW+QSiQAAAABJRU5ErkJggg==) no-repeat; } html[dir='rtl'] .device .device-info { float: right; } .device button { float: right; } html[dir='rtl'] .device button { float: left; } .subline, .device-subline { color: #999; margin: 5px 0; } h3.device-name { margin: 0; } .register-page { padding: 15px; width: 600px; } .register-page .button-list { padding-top: 15px; text-align: right; } html[dir='rtl'] .register-page .button-list { text-align: left; } .controls { border-bottom: 1px solid #eee; max-width: 711px; } html[dir='rtl'] .controls { padding: 13px 4px 7px 3px; } .controls .subline { -webkit-margin-start: 10px; } .login-promo { padding-bottom: 5px; padding-top: 5px; } .inline-login-promo { display: inline; } .inline-spinner { position: relative; top: 3px; } .cloud-print-message { margin: 23px 0; } section { margin-bottom: 23px; } .dialog-contents { padding-left: 17px; } #back-link { background: -webkit-image-set( url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAMCAYAAABfnvydAAAAOklEQVR4AWP4//8/Bo6b/q0DiKcDMSMuyf9QHIFTEmYCXkmQOF5JEMYnSVgBUVYQ5Uh8iggHFMGgBgBIGQYQz0k6FwAAAABJRU5ErkJggg==) 1x, url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAYCAMAAADEfo0+AAAAD1BMVEX///9el/Zel/Zel/Zel/YLJ0C2AAAABHRSTlMAiJcBFbIA6QAAAC1JREFUeAFjQAKMTMwMKHwWFogIgg8UQeEjBBB8ZrrzMQQGlwhGEGIGMkY0AACupwIderhj7AAAAABJRU5ErkJggg==) 2x) no-repeat; margin-bottom: 25px; margin-top: 6px; padding-left: 23px; } html[dir='rtl'] #back-link { transform: scaleX(-1); } html[dir='rtl'] #back-link span { display: inline-block; transform: scaleX(-1); } // Copyright 2013 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. /** * Javascript for local_discovery.html, served from chrome://devices/ * This is used to show discoverable devices near the user as well as * cloud devices registered to them. * * The object defined in this javascript file listens for callbacks from the * C++ code saying that a new device is available as well as manages the UI for * registering a device on the local network. */ cr.define('local_discovery', function() { 'use strict'; // Histogram buckets for UMA tracking. /** @const */ var DEVICES_PAGE_EVENTS = { OPENED: 0, LOG_IN_STARTED_FROM_REGISTER_PROMO: 1, LOG_IN_STARTED_FROM_DEVICE_LIST_PROMO: 2, ADD_PRINTER_CLICKED: 3, REGISTER_CLICKED: 4, REGISTER_CONFIRMED: 5, REGISTER_SUCCESS: 6, REGISTER_CANCEL: 7, REGISTER_FAILURE: 8, MANAGE_CLICKED: 9, REGISTER_CANCEL_ON_PRINTER: 10, REGISTER_TIMEOUT: 11, LOG_IN_STARTED_FROM_REGISTER_OVERLAY_PROMO: 12, MAX_EVENT: 13, }; /** * Map of service names to corresponding service objects. * @type {Object} */ var devices = {}; /** * Whether or not the user is currently logged in. * @type bool */ var isUserLoggedIn = true; /** * Whether or not the user is supervised or off the record. * @type bool */ var isUserSupervisedOrOffTheRecord = false; /** * Whether or not the path-based dialog has been shown. * @type bool */ var dialogFromPathHasBeenShown = false; /** * Focus manager for page. */ var focusManager = null; /** * Object that represents a device in the device list. * @param {Object} info Information about the device. * @constructor */ function Device(info, registerEnabled) { this.info = info; this.domElement = null; this.registerButton = null; this.registerEnabled = registerEnabled; } Device.prototype = { /** * Update the device. * @param {Object} info New information about the device. */ updateDevice: function(info) { this.info = info; this.renderDevice(); }, /** * Delete the device. */ removeDevice: function() { this.deviceContainer().removeChild(this.domElement); }, /** * Render the device to the device list. */ renderDevice: function() { if (this.domElement) { clearElement(this.domElement); } else { this.domElement = document.createElement('div'); this.deviceContainer().appendChild(this.domElement); } this.registerButton = fillDeviceDescription( this.domElement, this.info.display_name, this.info.description, this.info.type, loadTimeData.getString('serviceRegister'), this.showRegister.bind(this, this.info.type)); this.setRegisterEnabled(this.registerEnabled); }, /** * Return the correct container for the device. * @param {boolean} is_mine Whether or not the device is in the 'Registered' * section. */ deviceContainer: function() { return $('register-device-list'); }, /** * Register the device. */ register: function() { recordUmaEvent(DEVICES_PAGE_EVENTS.REGISTER_CONFIRMED); chrome.send('registerDevice', [this.info.service_name]); setRegisterPage(isPrinter(this.info.type) ? 'register-printer-page-adding1' : 'register-device-page-adding1'); }, /** * Show registrtation UI for device. */ showRegister: function() { recordUmaEvent(DEVICES_PAGE_EVENTS.REGISTER_CLICKED); $('register-message').textContent = loadTimeData.getStringF( isPrinter(this.info.type) ? 'registerPrinterConfirmMessage' : 'registerDeviceConfirmMessage', this.info.display_name); $('register-continue-button').onclick = this.register.bind(this); showRegisterOverlay(); }, /** * Set registration button enabled/disabled */ setRegisterEnabled: function(isEnabled) { this.registerEnabled = isEnabled; if (this.registerButton) { this.registerButton.disabled = !isEnabled; } } }; /** * Manages focus for local devices page. * @constructor * @extends {cr.ui.FocusManager} */ function LocalDiscoveryFocusManager() { cr.ui.FocusManager.call(this); this.focusParent_ = document.body; } LocalDiscoveryFocusManager.prototype = { __proto__: cr.ui.FocusManager.prototype, /** @override */ getFocusParent: function() { return document.querySelector('#overlay .showing') || $('main-page'); } }; /** * Returns a textual representation of the number of printers on the network. * @return {string} Number of printers on the network as localized string. */ function generateNumberPrintersAvailableText(numberPrinters) { if (numberPrinters == 0) { return loadTimeData.getString('printersOnNetworkZero'); } else if (numberPrinters == 1) { return loadTimeData.getString('printersOnNetworkOne'); } else { return loadTimeData.getStringF('printersOnNetworkMultiple', numberPrinters); } } /** * Fill device element with the description of a device. * @param {HTMLElement} device_dom_element Element to be filled. * @param {string} name Name of device. * @param {string} description Description of device. * @param {string} type Type of device. * @param {string} button_text Text to appear on button. * @param {function()?} button_action Action for button. * @return {HTMLElement} The button (for enabling/disabling/rebinding) */ function fillDeviceDescription(device_dom_element, name, description, type, button_text, button_action) { device_dom_element.classList.add('device'); if (isPrinter(type)) device_dom_element.classList.add('printer'); var deviceInfo = document.createElement('div'); deviceInfo.className = 'device-info'; device_dom_element.appendChild(deviceInfo); var deviceName = document.createElement('h3'); deviceName.className = 'device-name'; deviceName.textContent = name; deviceInfo.appendChild(deviceName); var deviceDescription = document.createElement('div'); deviceDescription.className = 'device-subline'; deviceDescription.textContent = description; deviceInfo.appendChild(deviceDescription); if (button_action) { var button = document.createElement('button'); button.textContent = button_text; button.addEventListener('click', button_action); device_dom_element.appendChild(button); } return button; } /** * Show the register overlay. */ function showRegisterOverlay() { recordUmaEvent(DEVICES_PAGE_EVENTS.ADD_PRINTER_CLICKED); var registerOverlay = $('register-overlay'); registerOverlay.classList.add('showing'); registerOverlay.focus(); $('overlay').hidden = false; setRegisterPage('register-page-confirm'); } /** * Hide the register overlay. */ function hideRegisterOverlay() { $('register-overlay').classList.remove('showing'); $('overlay').hidden = true; } /** * Clear a DOM element of all children. * @param {HTMLElement} element DOM element to clear. */ function clearElement(element) { while (element.firstChild) { element.removeChild(element.firstChild); } } /** * Announce that a registration failed. */ function onRegistrationFailed() { $('error-message').textContent = loadTimeData.getString('addingErrorMessage'); setRegisterPage('register-page-error'); recordUmaEvent(DEVICES_PAGE_EVENTS.REGISTER_FAILURE); } /** * Announce that a registration has been canceled on the printer. */ function onRegistrationCanceledPrinter() { $('error-message').textContent = loadTimeData.getString('addingCanceledMessage'); setRegisterPage('register-page-error'); recordUmaEvent(DEVICES_PAGE_EVENTS.REGISTER_CANCEL_ON_PRINTER); } /** * Announce that a registration has timed out. */ function onRegistrationTimeout() { $('error-message').textContent = loadTimeData.getString('addingTimeoutMessage'); setRegisterPage('register-page-error'); recordUmaEvent(DEVICES_PAGE_EVENTS.REGISTER_TIMEOUT); } /** * Update UI to reflect that registration has been confirmed on the printer. */ function onRegistrationConfirmedOnPrinter() { setRegisterPage('register-printer-page-adding2'); } /** * Shows UI to confirm security code. * @param {string} code The security code to confirm. */ function onRegistrationConfirmDeviceCode(code) { setRegisterPage('register-device-page-adding2'); $('register-device-page-code').textContent = code; } /** * Update device unregistered device list, and update related strings to * reflect the number of devices available to register. * @param {string} name Name of the device. * @param {string} info Additional info of the device or null if the device * has been removed. */ function onUnregisteredDeviceUpdate(name, info) { if (info) { if (devices.hasOwnProperty(name)) { devices[name].updateDevice(info); } else { devices[name] = new Device(info, isUserLoggedIn); devices[name].renderDevice(); } if (name == getOverlayIDFromPath() && !dialogFromPathHasBeenShown) { dialogFromPathHasBeenShown = true; devices[name].showRegister(); } } else { if (devices.hasOwnProperty(name)) { devices[name].removeDevice(); delete devices[name]; } } updateUIToReflectState(); } /** * Create the DOM for a cloud device described by the device section. * @param {Array} devices_list List of devices. */ function createCloudDeviceDOM(device) { var devicesDomElement = document.createElement('div'); var description; if (device.description == '') { if (isPrinter(device.type)) description = loadTimeData.getString('noDescriptionPrinter'); else description = loadTimeData.getString('noDescriptionDevice'); } else { description = device.description; } fillDeviceDescription(devicesDomElement, device.display_name, description, device.type, loadTimeData.getString('manageDevice'), isPrinter(device.type) ? manageCloudDevice.bind(null, device.id) : null); return devicesDomElement; } /** * Handle a list of cloud devices available to the user globally. * @param {Array} devices_list List of devices. */ function onCloudDeviceListAvailable(devices_list) { var devicesListLength = devices_list.length; var devicesContainer = $('cloud-devices'); clearElement(devicesContainer); $('cloud-devices-loading').hidden = true; for (var i = 0; i < devicesListLength; i++) { devicesContainer.appendChild(createCloudDeviceDOM(devices_list[i])); } } /** * Handle the case where the list of cloud devices is not available. */ function onCloudDeviceListUnavailable() { if (isUserLoggedIn) { $('cloud-devices-loading').hidden = true; $('cloud-devices-unavailable').hidden = false; } } /** * Handle the case where the cache for local devices has been flushed.. */ function onDeviceCacheFlushed() { for (var deviceName in devices) { devices[deviceName].removeDevice(); delete devices[deviceName]; } updateUIToReflectState(); } /** * Update UI strings to reflect the number of local devices. */ function updateUIToReflectState() { var numberPrinters = $('register-device-list').children.length; if (numberPrinters == 0) { $('no-printers-message').hidden = false; $('register-login-promo').hidden = true; } else { $('no-printers-message').hidden = true; $('register-login-promo').hidden = isUserLoggedIn || isUserSupervisedOrOffTheRecord; } if (!($('register-login-promo').hidden) || !($('cloud-devices-login-promo').hidden) || !($('register-overlay-login-promo').hidden)) { chrome.send( 'metricsHandler:recordAction', ['Signin_Impression_FromDevicesPage']); } } /** * Announce that a registration succeeeded. */ function onRegistrationSuccess(device_data) { hideRegisterOverlay(); if (device_data.service_name == getOverlayIDFromPath()) { window.close(); } var deviceDOM = createCloudDeviceDOM(device_data); $('cloud-devices').insertBefore(deviceDOM, $('cloud-devices').firstChild); recordUmaEvent(DEVICES_PAGE_EVENTS.REGISTER_SUCCESS); } /** * Update visibility status for page. */ function updateVisibility() { chrome.send('isVisible', [!document.hidden]); } /** * Set the page that the register wizard is on. * @param {string} page_id ID string for page. */ function setRegisterPage(page_id) { var pages = $('register-overlay').querySelectorAll('.register-page'); var pagesLength = pages.length; for (var i = 0; i < pagesLength; i++) { pages[i].hidden = true; } $(page_id).hidden = false; } /** * Request the device list. */ function requestDeviceList() { if (isUserLoggedIn) { clearElement($('cloud-devices')); $('cloud-devices-loading').hidden = false; $('cloud-devices-unavailable').hidden = true; chrome.send('requestDeviceList'); } } /** * Go to management page for a cloud device. * @param {string} device_id ID of device. */ function manageCloudDevice(device_id) { recordUmaEvent(DEVICES_PAGE_EVENTS.MANAGE_CLICKED); chrome.send('openCloudPrintURL', [device_id]); } /** * Record an event in the UMA histogram. * @param {number} eventId The id of the event to be recorded. * @private */ function recordUmaEvent(eventId) { chrome.send('metricsHandler:recordInHistogram', ['LocalDiscovery.DevicesPage', eventId, DEVICES_PAGE_EVENTS.MAX_EVENT]); } /** * Cancel the registration. */ function cancelRegistration() { hideRegisterOverlay(); chrome.send('cancelRegistration'); recordUmaEvent(DEVICES_PAGE_EVENTS.REGISTER_CANCEL); } /** * Confirms device code. */ function confirmCode() { chrome.send('confirmCode'); setRegisterPage('register-device-page-adding1'); } /** * Retry loading the devices from Google Cloud Print. */ function retryLoadCloudDevices() { requestDeviceList(); } /** * User is not logged in. */ function setUserLoggedIn(userLoggedIn, userSupervisedOrOffTheRecord) { isUserLoggedIn = userLoggedIn; isUserSupervisedOrOffTheRecord = userSupervisedOrOffTheRecord; $('cloud-devices-login-promo').hidden = isUserLoggedIn || isUserSupervisedOrOffTheRecord; $('register-overlay-login-promo').hidden = isUserLoggedIn || isUserSupervisedOrOffTheRecord; $('register-continue-button').disabled = !isUserLoggedIn || isUserSupervisedOrOffTheRecord; $('my-devices-container').hidden = userSupervisedOrOffTheRecord; if (isUserSupervisedOrOffTheRecord) { $('cloud-print-connector-section').hidden = true; } if (isUserLoggedIn && !isUserSupervisedOrOffTheRecord) { requestDeviceList(); $('register-login-promo').hidden = true; } else { $('cloud-devices-loading').hidden = true; $('cloud-devices-unavailable').hidden = true; clearElement($('cloud-devices')); hideRegisterOverlay(); } updateUIToReflectState(); for (var device in devices) { devices[device].setRegisterEnabled(isUserLoggedIn); } } function openSignInPage() { chrome.send('showSyncUI'); } function registerLoginButtonClicked() { recordUmaEvent(DEVICES_PAGE_EVENTS.LOG_IN_STARTED_FROM_REGISTER_PROMO); openSignInPage(); } function registerOverlayLoginButtonClicked() { recordUmaEvent( DEVICES_PAGE_EVENTS.LOG_IN_STARTED_FROM_REGISTER_OVERLAY_PROMO); openSignInPage(); } function cloudDevicesLoginButtonClicked() { recordUmaEvent(DEVICES_PAGE_EVENTS.LOG_IN_STARTED_FROM_DEVICE_LIST_PROMO); openSignInPage(); } /** * Set the Cloud Print proxy UI to enabled, disabled, or processing. * @private */ function setupCloudPrintConnectorSection(disabled, label, allowed) { if (!cr.isChromeOS) { $('cloudPrintConnectorLabel').textContent = label; if (disabled || !allowed) { $('cloudPrintConnectorSetupButton').textContent = loadTimeData.getString('cloudPrintConnectorDisabledButton'); } else { $('cloudPrintConnectorSetupButton').textContent = loadTimeData.getString('cloudPrintConnectorEnabledButton'); } $('cloudPrintConnectorSetupButton').disabled = !allowed; if (disabled) { $('cloudPrintConnectorSetupButton').onclick = function(event) { // Disable the button, set its text to the intermediate state. $('cloudPrintConnectorSetupButton').textContent = loadTimeData.getString('cloudPrintConnectorEnablingButton'); $('cloudPrintConnectorSetupButton').disabled = true; chrome.send('showCloudPrintSetupDialog'); }; } else { $('cloudPrintConnectorSetupButton').onclick = function(event) { chrome.send('disableCloudPrintConnector'); requestDeviceList(); }; } } } function getOverlayIDFromPath() { if (document.location.pathname == '/register') { var params = parseQueryParams(document.location); return params['id'] || null; } } /** * Returns true of device is printer. * @param {string} type Type of printer. */ function isPrinter(type) { return type == 'printer'; } document.addEventListener('DOMContentLoaded', function() { cr.ui.overlay.setupOverlay($('overlay')); cr.ui.overlay.globalInitialization(); $('overlay').addEventListener('cancelOverlay', cancelRegistration); [].forEach.call( document.querySelectorAll('.register-cancel'), function(button) { button.addEventListener('click', cancelRegistration); }); [].forEach.call( document.querySelectorAll('.confirm-code'), function(button) { button.addEventListener('click', confirmCode); }); $('register-error-exit').addEventListener('click', cancelRegistration); $('cloud-devices-retry-link').addEventListener('click', retryLoadCloudDevices); $('cloud-devices-login-link').addEventListener( 'click', cloudDevicesLoginButtonClicked); $('register-login-link').addEventListener( 'click', registerLoginButtonClicked); $('register-overlay-login-button').addEventListener( 'click', registerOverlayLoginButtonClicked); if (loadTimeData.valueExists('backButtonURL')) { $('back-link').hidden = false; $('back-link').addEventListener('click', function() { window.location.href = loadTimeData.getString('backButtonURL'); }); } updateVisibility(); document.addEventListener('visibilitychange', updateVisibility, false); focusManager = new LocalDiscoveryFocusManager(); focusManager.initialize(); chrome.send('start'); recordUmaEvent(DEVICES_PAGE_EVENTS.OPENED); }); return { onRegistrationSuccess: onRegistrationSuccess, onRegistrationFailed: onRegistrationFailed, onUnregisteredDeviceUpdate: onUnregisteredDeviceUpdate, onRegistrationConfirmedOnPrinter: onRegistrationConfirmedOnPrinter, onRegistrationConfirmDeviceCode: onRegistrationConfirmDeviceCode, onCloudDeviceListAvailable: onCloudDeviceListAvailable, onCloudDeviceListUnavailable: onCloudDeviceListUnavailable, onDeviceCacheFlushed: onDeviceCacheFlushed, onRegistrationCanceledPrinter: onRegistrationCanceledPrinter, onRegistrationTimeout: onRegistrationTimeout, setUserLoggedIn: setUserLoggedIn, setupCloudPrintConnectorSection: setupCloudPrintConnectorSection, }; }); // Copyright 2013 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. /** * @fileoverview Helpers for validating parameters to chrome-search:// iframes. */ /** * Converts an RGB color number to a hex color string if valid. * @param {number} color A 6-digit hex RGB color code as a number. * @return {?string} A CSS representation of the color or null if invalid. */ function convertToHexColor(color) { // Color must be a number, finite, with no fractional part, in the correct // range for an RGB hex color. if (isFinite(color) && Math.floor(color) == color && color >= 0 && color <= 0xffffff) { var hexColor = color.toString(16); // Pads with initial zeros and # (e.g. for 'ff' yields '#0000ff'). return '#000000'.substr(0, 7 - hexColor.length) + hexColor; } return null; } /** * Validates a RGBA color component. It must be a number between 0 and 255. * @param {number} component An RGBA component. * @return {boolean} True if the component is valid. */ function isValidRBGAComponent(component) { return isFinite(component) && component >= 0 && component <= 255; } /** * Converts an Array of color components into RGBA format "rgba(R,G,B,A)". * @param {Array} rgbaColor Array of rgba color components. * @return {?string} CSS color in RGBA format or null if invalid. */ function convertArrayToRGBAColor(rgbaColor) { // Array must contain 4 valid components. if (rgbaColor instanceof Array && rgbaColor.length === 4 && isValidRBGAComponent(rgbaColor[0]) && isValidRBGAComponent(rgbaColor[1]) && isValidRBGAComponent(rgbaColor[2]) && isValidRBGAComponent(rgbaColor[3])) { return 'rgba(' + rgbaColor[0] + ',' + rgbaColor[1] + ',' + rgbaColor[2] + ',' + rgbaColor[3] / 255 + ')'; } return null; }
/* Copyright 2015 The Chromium Authors. All rights reserved. * Use of this source code is governed by a BSD-style license that can be * found in the LICENSE file. */ /* TODO: Need to discuss with NTP folks before we remove font-family from the * body tag. */ body { background-attachment: fixed !important; cursor: default; font-family: arial, sans-serif; font-size: small; margin: 0; overflow-x: hidden; } #ntp-contents { text-align: -webkit-center; } .non-google-page #ntp-contents { position: absolute; top: calc(50% - 155px); width: 100%; } body.hide-fakebox-logo #logo, body.hide-fakebox-logo #fakebox { visibility: hidden; } body.fakebox-disable #fakebox { border-color: rgb(238, 238, 238); cursor: default; } body.fakebox-disable #fakebox > input { cursor: default; } #logo { background-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNzIgOTIiIHdpZHRoPSIyNzIiIGhlaWdodD0iOTIiPjxwYXRoIGZpbGw9IiNFQTQzMzUiIGQ9Ik0xMTUuNzUgNDcuMThjMCAxMi43Ny05Ljk5IDIyLjE4LTIyLjI1IDIyLjE4cy0yMi4yNS05LjQxLTIyLjI1LTIyLjE4QzcxLjI1IDM0LjMyIDgxLjI0IDI1IDkzLjUgMjVzMjIuMjUgOS4zMiAyMi4yNSAyMi4xOHptLTkuNzQgMGMwLTcuOTgtNS43OS0xMy40NC0xMi41MS0xMy40NFM4MC45OSAzOS4yIDgwLjk5IDQ3LjE4YzAgNy45IDUuNzkgMTMuNDQgMTIuNTEgMTMuNDRzMTIuNTEtNS41NSAxMi41MS0xMy40NHoiLz48cGF0aCBmaWxsPSIjRkJCQzA1IiBkPSJNMTYzLjc1IDQ3LjE4YzAgMTIuNzctOS45OSAyMi4xOC0yMi4yNSAyMi4xOHMtMjIuMjUtOS40MS0yMi4yNS0yMi4xOGMwLTEyLjg1IDkuOTktMjIuMTggMjIuMjUtMjIuMThzMjIuMjUgOS4zMiAyMi4yNSAyMi4xOHptLTkuNzQgMGMwLTcuOTgtNS43OS0xMy40NC0xMi41MS0xMy40NHMtMTIuNTEgNS40Ni0xMi41MSAxMy40NGMwIDcuOSA1Ljc5IDEzLjQ0IDEyLjUxIDEzLjQ0czEyLjUxLTUuNTUgMTIuNTEtMTMuNDR6Ii8+PHBhdGggZmlsbD0iIzQyODVGNCIgZD0iTTIwOS43NSAyNi4zNHYzOS44MmMwIDE2LjM4LTkuNjYgMjMuMDctMjEuMDggMjMuMDctMTAuNzUgMC0xNy4yMi03LjE5LTE5LjY2LTEzLjA3bDguNDgtMy41M2MxLjUxIDMuNjEgNS4yMSA3Ljg3IDExLjE3IDcuODcgNy4zMSAwIDExLjg0LTQuNTEgMTEuODQtMTN2LTMuMTloLS4zNGMtMi4xOCAyLjY5LTYuMzggNS4wNC0xMS42OCA1LjA0LTExLjA5IDAtMjEuMjUtOS42Ni0yMS4yNS0yMi4wOSAwLTEyLjUyIDEwLjE2LTIyLjI2IDIxLjI1LTIyLjI2IDUuMjkgMCA5LjQ5IDIuMzUgMTEuNjggNC45NmguMzR2LTMuNjFoOS4yNXptLTguNTYgMjAuOTJjMC03LjgxLTUuMjEtMTMuNTItMTEuODQtMTMuNTItNi43MiAwLTEyLjM1IDUuNzEtMTIuMzUgMTMuNTIgMCA3LjczIDUuNjMgMTMuMzYgMTIuMzUgMTMuMzYgNi42MyAwIDExLjg0LTUuNjMgMTEuODQtMTMuMzZ6Ii8+PHBhdGggZmlsbD0iIzM0QTg1MyIgZD0iTTIyNSAzdjY1aC05LjVWM2g5LjV6Ii8+PHBhdGggZmlsbD0iI0VBNDMzNSIgZD0iTTI2Mi4wMiA1NC40OGw3LjU2IDUuMDRjLTIuNDQgMy42MS04LjMyIDkuODMtMTguNDggOS44My0xMi42IDAtMjIuMDEtOS43NC0yMi4wMS0yMi4xOCAwLTEzLjE5IDkuNDktMjIuMTggMjAuOTItMjIuMTggMTEuNTEgMCAxNy4xNCA5LjE2IDE4Ljk4IDE0LjExbDEuMDEgMi41Mi0yOS42NSAxMi4yOGMyLjI3IDQuNDUgNS44IDYuNzIgMTAuNzUgNi43MiA0Ljk2IDAgOC40LTIuNDQgMTAuOTItNi4xNHptLTIzLjI3LTcuOThsMTkuODItOC4yM2MtMS4wOS0yLjc3LTQuMzctNC43LTguMjMtNC43LTQuOTUgMC0xMS44NCA0LjM3LTExLjU5IDEyLjkzeiIvPjxwYXRoIGZpbGw9IiM0Mjg1RjQiIGQ9Ik0zNS4yOSA0MS40MVYzMkg2N2MuMzEgMS42NC40NyAzLjU4LjQ3IDUuNjggMCA3LjA2LTEuOTMgMTUuNzktOC4xNSAyMi4wMS02LjA1IDYuMy0xMy43OCA5LjY2LTI0LjAyIDkuNjZDMTYuMzIgNjkuMzUuMzYgNTMuODkuMzYgMzQuOTEuMzYgMTUuOTMgMTYuMzIuNDcgMzUuMy40N2MxMC41IDAgMTcuOTggNC4xMiAyMy42IDkuNDlsLTYuNjQgNi42NGMtNC4wMy0zLjc4LTkuNDktNi43Mi0xNi45Ny02LjcyLTEzLjg2IDAtMjQuNyAxMS4xNy0yNC43IDI1LjAzIDAgMTMuODYgMTAuODQgMjUuMDMgMjQuNyAyNS4wMyA4Ljk5IDAgMTQuMTEtMy42MSAxNy4zOS02Ljg5IDIuNjYtMi42NiA0LjQxLTYuNDYgNS4xLTExLjY1bC0yMi40OS4wMXoiLz48L3N2Zz4=); background-repeat: no-repeat; height: 92px; margin-bottom: 24px; margin-top: 157px; width: 272px; } body.alternate-logo #logo { -webkit-mask-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNzIgOTIiIHdpZHRoPSIyNzIiIGhlaWdodD0iOTIiPjxwYXRoIGZpbGw9IiNFQTQzMzUiIGQ9Ik0xMTUuNzUgNDcuMThjMCAxMi43Ny05Ljk5IDIyLjE4LTIyLjI1IDIyLjE4cy0yMi4yNS05LjQxLTIyLjI1LTIyLjE4QzcxLjI1IDM0LjMyIDgxLjI0IDI1IDkzLjUgMjVzMjIuMjUgOS4zMiAyMi4yNSAyMi4xOHptLTkuNzQgMGMwLTcuOTgtNS43OS0xMy40NC0xMi41MS0xMy40NFM4MC45OSAzOS4yIDgwLjk5IDQ3LjE4YzAgNy45IDUuNzkgMTMuNDQgMTIuNTEgMTMuNDRzMTIuNTEtNS41NSAxMi41MS0xMy40NHoiLz48cGF0aCBmaWxsPSIjRkJCQzA1IiBkPSJNMTYzLjc1IDQ3LjE4YzAgMTIuNzctOS45OSAyMi4xOC0yMi4yNSAyMi4xOHMtMjIuMjUtOS40MS0yMi4yNS0yMi4xOGMwLTEyLjg1IDkuOTktMjIuMTggMjIuMjUtMjIuMThzMjIuMjUgOS4zMiAyMi4yNSAyMi4xOHptLTkuNzQgMGMwLTcuOTgtNS43OS0xMy40NC0xMi41MS0xMy40NHMtMTIuNTEgNS40Ni0xMi41MSAxMy40NGMwIDcuOSA1Ljc5IDEzLjQ0IDEyLjUxIDEzLjQ0czEyLjUxLTUuNTUgMTIuNTEtMTMuNDR6Ii8+PHBhdGggZmlsbD0iIzQyODVGNCIgZD0iTTIwOS43NSAyNi4zNHYzOS44MmMwIDE2LjM4LTkuNjYgMjMuMDctMjEuMDggMjMuMDctMTAuNzUgMC0xNy4yMi03LjE5LTE5LjY2LTEzLjA3bDguNDgtMy41M2MxLjUxIDMuNjEgNS4yMSA3Ljg3IDExLjE3IDcuODcgNy4zMSAwIDExLjg0LTQuNTEgMTEuODQtMTN2LTMuMTloLS4zNGMtMi4xOCAyLjY5LTYuMzggNS4wNC0xMS42OCA1LjA0LTExLjA5IDAtMjEuMjUtOS42Ni0yMS4yNS0yMi4wOSAwLTEyLjUyIDEwLjE2LTIyLjI2IDIxLjI1LTIyLjI2IDUuMjkgMCA5LjQ5IDIuMzUgMTEuNjggNC45NmguMzR2LTMuNjFoOS4yNXptLTguNTYgMjAuOTJjMC03LjgxLTUuMjEtMTMuNTItMTEuODQtMTMuNTItNi43MiAwLTEyLjM1IDUuNzEtMTIuMzUgMTMuNTIgMCA3LjczIDUuNjMgMTMuMzYgMTIuMzUgMTMuMzYgNi42MyAwIDExLjg0LTUuNjMgMTEuODQtMTMuMzZ6Ii8+PHBhdGggZmlsbD0iIzM0QTg1MyIgZD0iTTIyNSAzdjY1aC05LjVWM2g5LjV6Ii8+PHBhdGggZmlsbD0iI0VBNDMzNSIgZD0iTTI2Mi4wMiA1NC40OGw3LjU2IDUuMDRjLTIuNDQgMy42MS04LjMyIDkuODMtMTguNDggOS44My0xMi42IDAtMjIuMDEtOS43NC0yMi4wMS0yMi4xOCAwLTEzLjE5IDkuNDktMjIuMTggMjAuOTItMjIuMTggMTEuNTEgMCAxNy4xNCA5LjE2IDE4Ljk4IDE0LjExbDEuMDEgMi41Mi0yOS42NSAxMi4yOGMyLjI3IDQuNDUgNS44IDYuNzIgMTAuNzUgNi43MiA0Ljk2IDAgOC40LTIuNDQgMTAuOTItNi4xNHptLTIzLjI3LTcuOThsMTkuODItOC4yM2MtMS4wOS0yLjc3LTQuMzctNC43LTguMjMtNC43LTQuOTUgMC0xMS44NCA0LjM3LTExLjU5IDEyLjkzeiIvPjxwYXRoIGZpbGw9IiM0Mjg1RjQiIGQ9Ik0zNS4yOSA0MS40MVYzMkg2N2MuMzEgMS42NC40NyAzLjU4LjQ3IDUuNjggMCA3LjA2LTEuOTMgMTUuNzktOC4xNSAyMi4wMS02LjA1IDYuMy0xMy43OCA5LjY2LTI0LjAyIDkuNjZDMTYuMzIgNjkuMzUuMzYgNTMuODkuMzYgMzQuOTEuMzYgMTUuOTMgMTYuMzIuNDcgMzUuMy40N2MxMC41IDAgMTcuOTggNC4xMiAyMy42IDkuNDlsLTYuNjQgNi42NGMtNC4wMy0zLjc4LTkuNDktNi43Mi0xNi45Ny02LjcyLTEzLjg2IDAtMjQuNyAxMS4xNy0yNC43IDI1LjAzIDAgMTMuODYgMTAuODQgMjUuMDMgMjQuNyAyNS4wMyA4Ljk5IDAgMTQuMTEtMy42MSAxNy4zOS02Ljg5IDIuNjYtMi42NiA0LjQxLTYuNDYgNS4xLTExLjY1bC0yMi40OS4wMXoiLz48L3N2Zz4=); -webkit-mask-repeat: no-repeat; -webkit-mask-size: 100%; background: #eee; } #fakebox { -webkit-transform: translate3d(0, 0, 0); -webkit-transition: -webkit-transform 100ms linear, border-color 100ms linear; background-color: #fff; border: 1px solid rgb(185, 185, 185); border-radius: 1px; border-top-color: rgb(160, 160, 160); cursor: text; font-size: 18px; height: 36px; line-height: 36px; max-width: 672px; position: relative; /* #fakebox width (here and below) should be 2px less than #mv-tiles to account for its border. */ width: 298px; } #fakebox:hover { border: 1px solid rgb(169, 169, 169); border-top-color: rgb(144, 144, 144); } body.fakebox-focused #fakebox { border: 1px solid rgb(77, 144, 254); } #fakebox > input { bottom: 0; box-sizing: border-box; left: 0; margin: 0; opacity: 0; padding-left: 8px; position: absolute; top: 0; width: 100%; } html[dir=rtl] #fakebox > input { padding-left: 0; padding-right: 8px; right: 0; } #fakebox-text { bottom: 0; color: #bbb; font-family: arial, sans-serif; font-size: 16px; left: 9px; margin-top: 1px; overflow: hidden; position: absolute; right: 9px; text-align: initial; text-overflow: ellipsis; top: 0; vertical-align: middle; visibility: inherit; white-space: nowrap; } html[dir=rtl] #fakebox-text { left: auto; right: 9px; } #cursor { background: #333; bottom: 5px; left: 9px; position: absolute; top: 5px; visibility: hidden; width: 1px; } html[dir=rtl] #cursor { left: auto; right: 9px; } @-webkit-keyframes blink { 0% { opacity: 1; } 61.55% { opacity: 0; } } body.fakebox-drag-focused #fakebox-text, body.fakebox-focused #fakebox-text { visibility: hidden; } body.fakebox-drag-focused #cursor { visibility: inherit; } body.fakebox-focused #cursor { -webkit-animation: blink 1.3s step-end infinite; visibility: inherit; } #most-visited { -webkit-user-select: none; margin-top: 64px; text-align: -webkit-center; } .icon-ntp #most-visited { margin-top: calc(100px - 36px); } /* Non-Google pages have no Fakebox, so don't need top margin. */ .non-google-page #most-visited { margin-top: 0; } #mv-tiles { margin: 0; position: relative; text-align: -webkit-auto; } .thumb-ntp #mv-tiles { /* we need a 16px margin and the tiles have 130px height. */ height: calc(2*130px + 16px); line-height: calc(130px + 16px); } .icon-ntp #mv-tiles { background: rgba(255,255,255,0.2); border-radius: 4px; height: calc(2 * 112px); padding: calc(36px - 18px) calc(36px - 18px - 12px); } .icon-ntp.dark #mv-tiles { background: rgba(0,0,0,0.4); } .default-theme.icon-ntp #mv-tiles { background: none; } #mv-notice-x { -webkit-mask-image: -webkit-image-set( url(chrome-search://local-ntp/images/close_3_mask.png) 1x, url(chrome-search://local-ntp/images/close_3_mask.png@2x) 2x); -webkit-mask-position: 3px 3px; -webkit-mask-repeat: no-repeat; -webkit-mask-size: 10px 10px; background-color: rgba(90,90,90,0.7); cursor: pointer; display: inline-block; height: 16px; margin-left: 20px; outline: none; vertical-align: middle; width: 16px; } html[dir=rtl] #mv-notice-x { margin-left: 0; margin-right: 20px; } #mv-notice-x:hover { background-color: rgba(90,90,90,1.0); } #mv-notice-x:active { background-color: rgb(66,133,244); } /* The notification shown when a tile is blacklisted. */ #mv-notice { font-size: 12px; font-weight: bold; opacity: 1; padding: 10px 0; } .icon-ntp #mv-notice { margin-top: 30px; } #mv-notice span { cursor: default; display: inline-block; height: 16px; line-height: 16px; vertical-align: top; } /* Links in the notification. */ #mv-notice-links span { -webkit-margin-start: 6px; color: rgb(17, 85, 204); cursor: pointer; outline: none; padding: 0 4px; } #mv-notice-links span:hover, #mv-notice-links span:focus, #recent-tabs:hover { text-decoration: underline; } .default-theme.dark #mv-msg { color: #fff; } .default-theme.dark #mv-notice-links span { color: #fff; } #mv-notice.mv-notice-delayed-hide { -webkit-transition-delay: 10s; -webkit-transition-property: opacity; opacity: 0; } #mv-notice.mv-notice-hide { display: none; } #attribution { -webkit-user-select: none; bottom: 0; color: #fff; cursor: default; display: inline-block; font-size: 13px; left: auto; position: fixed; right: 8px; text-align: left; z-index: -1; } html[dir=rtl] #attribution, #attribution.left-align-attribution { left: 8px; right: auto; text-align: right; } #recent-tabs { background: #fff; border: 1px solid #c0c0c0; border-radius: 2px; bottom: 0; color: rgb(17, 85, 204); cursor: pointer; font-family: Arial; font-size: 14px; opacity: 0.9; padding: 3px; position: fixed; right: 8px; } html[dir=rtl] #recent-tabs { left: 8px; right: auto; } #mv-single { border: none; height: 100%; width: 100%; } // Copyright 2015 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. /** * @fileoverview The local InstantExtended NTP. */ /** * Controls rendering the new tab page for InstantExtended. * @return {Object} A limited interface for testing the local NTP. */ function LocalNTP() { 'use strict'; /** * Alias for document.getElementById. * @param {string} id The ID of the element to find. * @return {HTMLElement} The found element or null if not found. */ function $(id) { return document.getElementById(id); } /** * Specifications for an NTP design (not comprehensive). * * fakeboxWingSize: Extra distance for fakebox to extend beyond beyond the list * of tiles. * fontFamily: Font family to use for title and thumbnail iframes. * fontSize: Font size to use for the iframes, in px. * mainClass: Class applied to #ntp-contents to control CSS. * numTitleLines: Number of lines to display in titles. * showFavicon: Whether to show favicon. * thumbnailTextColor: The 4-component color that thumbnail iframe may use to * display text message in place of missing thumbnail. * thumbnailFallback: (Optional) A value in THUMBNAIL_FALLBACK to specify the * thumbnail fallback strategy. If unassigned, then the thumbnail.html * iframe would handle the fallback. * tileWidth: The width of each suggestion tile, in px. * tileMargin: Spacing between successive tiles, in px. * titleColor: The 4-component color of title text. * titleColorAgainstDark: The 4-component color of title text against a dark * theme. * titleTextAlign: (Optional) The alignment of title text. If unspecified, the * default value is 'center'. * titleTextFade: (Optional) The number of pixels beyond which title * text begins to fade. This overrides the default ellipsis style. * * @type {{ * fakeboxWingSize: number, * fontFamily: string, * fontSize: number, * mainClass: string, * numTitleLines: number, * showFavicon: boolean, * thumbnailTextColor: string, * thumbnailFallback: string|null|undefined, * tileWidth: number, * tileMargin: number, * titleColor: string, * titleColorAgainstDark: string, * titleTextAlign: string|null|undefined, * titleTextFade: number|null|undefined * }} */ var NTP_DESIGN = { fakeboxWingSize: 0, fontFamily: 'arial, sans-serif', fontSize: 12, mainClass: 'thumb-ntp', numTitleLines: 1, showFavicon: true, thumbnailTextColor: [50, 50, 50, 255], thumbnailFallback: 'dot', // Draw single dot. tileWidth: 154, tileMargin: 16, titleColor: [50, 50, 50, 255], titleColorAgainstDark: [210, 210, 210, 255], titleTextAlign: 'inherit', titleTextFade: 122 - 36 // 112px wide title with 32 pixel fade at end. }; /** * Modifies NTP_DESIGN parameters for icon NTP. */ function modifyNtpDesignForIcons() { NTP_DESIGN.fakeboxWingSize = 132; NTP_DESIGN.mainClass = 'icon-ntp'; NTP_DESIGN.numTitleLines = 2; NTP_DESIGN.showFavicon = false; NTP_DESIGN.thumbnailFallback = null; NTP_DESIGN.tileWidth = 48 + 2 * 18; NTP_DESIGN.tileMargin = 60 - 18 * 2; NTP_DESIGN.titleColor = [120, 120, 120, 255]; NTP_DESIGN.titleColorAgainstDark = [210, 210, 210, 255]; NTP_DESIGN.titleTextAlign = 'center'; delete NTP_DESIGN.titleTextFade; } /** * Enum for classnames. * @enum {string} * @const */ var CLASSES = { ALTERNATE_LOGO: 'alternate-logo', // Shows white logo if required by theme DARK: 'dark', DEFAULT_THEME: 'default-theme', DELAYED_HIDE_NOTIFICATION: 'mv-notice-delayed-hide', FAKEBOX_DISABLE: 'fakebox-disable', // Makes fakebox non-interactive FAKEBOX_FOCUS: 'fakebox-focused', // Applies focus styles to the fakebox // Applies drag focus style to the fakebox FAKEBOX_DRAG_FOCUS: 'fakebox-drag-focused', HIDE_FAKEBOX_AND_LOGO: 'hide-fakebox-logo', HIDE_NOTIFICATION: 'mv-notice-hide', LEFT_ALIGN_ATTRIBUTION: 'left-align-attribution', // Vertically centers the most visited section for a non-Google provided page. NON_GOOGLE_PAGE: 'non-google-page', RTL: 'rtl' // Right-to-left language text. }; /** * Enum for HTML element ids. * @enum {string} * @const */ var IDS = { ATTRIBUTION: 'attribution', ATTRIBUTION_TEXT: 'attribution-text', CUSTOM_THEME_STYLE: 'ct-style', FAKEBOX: 'fakebox', FAKEBOX_INPUT: 'fakebox-input', FAKEBOX_TEXT: 'fakebox-text', LOGO: 'logo', NOTIFICATION: 'mv-notice', NOTIFICATION_CLOSE_BUTTON: 'mv-notice-x', NOTIFICATION_MESSAGE: 'mv-msg', NTP_CONTENTS: 'ntp-contents', RESTORE_ALL_LINK: 'mv-restore', TILES: 'mv-tiles', UNDO_LINK: 'mv-undo' }; /** * Enum for keycodes. * @enum {number} * @const */ var KEYCODE = { ENTER: 13 }; /** * Enum for the state of the NTP when it is disposed. * @enum {number} * @const */ var NTP_DISPOSE_STATE = { NONE: 0, // Preserve the NTP appearance and functionality DISABLE_FAKEBOX: 1, HIDE_FAKEBOX_AND_LOGO: 2 }; /** * The notification displayed when a page is blacklisted. * @type {Element} */ var notification; /** * The container for the theme attribution. * @type {Element} */ var attribution; /** * The "fakebox" - an input field that looks like a regular searchbox. When it * is focused, any text the user types goes directly into the omnibox. * @type {Element} */ var fakebox; /** * The container for NTP elements. * @type {Element} */ var ntpContents; /** * The last blacklisted tile rid if any, which by definition should not be * filler. * @type {?number} */ var lastBlacklistedTile = null; /** * Current number of tiles columns shown based on the window width, including * those that just contain filler. * @type {number} */ var numColumnsShown = 0; /** * The browser embeddedSearch.newTabPage object. * @type {Object} */ var ntpApiHandle; /** * The browser embeddedSearch.searchBox object. * @type {Object} */ var searchboxApiHandle; /** * The state of the NTP when a query is entered into the Omnibox. * @type {NTP_DISPOSE_STATE} */ var omniboxInputBehavior = NTP_DISPOSE_STATE.NONE; /** * The state of the NTP when a query is entered into the Fakebox. * @type {NTP_DISPOSE_STATE} */ var fakeboxInputBehavior = NTP_DISPOSE_STATE.HIDE_FAKEBOX_AND_LOGO; /** @type {number} @const */ var MAX_NUM_TILES_TO_SHOW = 8; /** @type {number} @const */ var MIN_NUM_COLUMNS = 2; /** @type {number} @const */ var MAX_NUM_COLUMNS = 4; /** @type {number} @const */ var NUM_ROWS = 2; /** * Minimum total padding to give to the left and right of the most visited * section. Used to determine how many tiles to show. * @type {number} * @const */ var MIN_TOTAL_HORIZONTAL_PADDING = 200; /** * Heuristic to determine whether a theme should be considered to be dark, so * the colors of various UI elements can be adjusted. * @param {ThemeBackgroundInfo|undefined} info Theme background information. * @return {boolean} Whether the theme is dark. * @private */ function getIsThemeDark(info) { if (!info) return false; // Heuristic: light text implies dark theme. var rgba = info.textColorRgba; var luminance = 0.3 * rgba[0] + 0.59 * rgba[1] + 0.11 * rgba[2]; return luminance >= 128; } /** * Updates the NTP based on the current theme. * @private */ function renderTheme() { var fakeboxText = $(IDS.FAKEBOX_TEXT); if (fakeboxText) { fakeboxText.innerHTML = ''; if (configData.translatedStrings.searchboxPlaceholder) { fakeboxText.textContent = configData.translatedStrings.searchboxPlaceholder; } } var info = ntpApiHandle.themeBackgroundInfo; var isThemeDark = getIsThemeDark(info); ntpContents.classList.toggle(CLASSES.DARK, isThemeDark); if (!info) { return; } var background = [convertToRGBAColor(info.backgroundColorRgba), info.imageUrl, info.imageTiling, info.imageHorizontalAlignment, info.imageVerticalAlignment].join(' ').trim(); document.body.style.background = background; document.body.classList.toggle(CLASSES.ALTERNATE_LOGO, info.alternateLogo); updateThemeAttribution(info.attributionUrl, info.imageHorizontalAlignment); setCustomThemeStyle(info); var themeinfo = {cmd: 'updateTheme'}; if (!info.usingDefaultTheme) { themeinfo.tileBorderColor = convertToRGBAColor(info.sectionBorderColorRgba); themeinfo.tileHoverBorderColor = convertToRGBAColor(info.headerColorRgba); } themeinfo.isThemeDark = isThemeDark; var titleColor = NTP_DESIGN.titleColor; if (!info.usingDefaultTheme && info.textColorRgba) { titleColor = info.textColorRgba; } else if (isThemeDark) { titleColor = NTP_DESIGN.titleColorAgainstDark; } themeinfo.tileTitleColor = convertToRGBAColor(titleColor); $('mv-single').contentWindow.postMessage(themeinfo, '*'); } /** * Updates the NTP based on the current theme, then rerenders all tiles. * @private */ function onThemeChange() { renderTheme(); } /** * Updates the NTP style according to theme. * @param {Object=} opt_themeInfo The information about the theme. If it is * omitted the style will be reverted to the default. * @private */ function setCustomThemeStyle(opt_themeInfo) { var customStyleElement = $(IDS.CUSTOM_THEME_STYLE); var head = document.head; if (opt_themeInfo && !opt_themeInfo.usingDefaultTheme) { ntpContents.classList.remove(CLASSES.DEFAULT_THEME); var themeStyle = '#attribution {' + ' color: ' + convertToRGBAColor(opt_themeInfo.textColorLightRgba) + ';' + '}' + '#mv-msg {' + ' color: ' + convertToRGBAColor(opt_themeInfo.textColorRgba) + ';' + '}' + '#mv-notice-links span {' + ' color: ' + convertToRGBAColor(opt_themeInfo.textColorLightRgba) + ';' + '}' + '#mv-notice-x {' + ' -webkit-filter: drop-shadow(0 0 0 ' + convertToRGBAColor(opt_themeInfo.textColorRgba) + ');' + '}' + '.mv-page-ready .mv-mask {' + ' border: 1px solid ' + convertToRGBAColor(opt_themeInfo.sectionBorderColorRgba) + ';' + '}' + '.mv-page-ready:hover .mv-mask, .mv-page-ready .mv-focused ~ .mv-mask {' + ' border-color: ' + convertToRGBAColor(opt_themeInfo.headerColorRgba) + ';' + '}'; if (customStyleElement) { customStyleElement.textContent = themeStyle; } else { customStyleElement = document.createElement('style'); customStyleElement.type = 'text/css'; customStyleElement.id = IDS.CUSTOM_THEME_STYLE; customStyleElement.textContent = themeStyle; head.appendChild(customStyleElement); } } else { ntpContents.classList.add(CLASSES.DEFAULT_THEME); if (customStyleElement) head.removeChild(customStyleElement); } } /** * Renders the attribution if the URL is present, otherwise hides it. * @param {string} url The URL of the attribution image, if any. * @param {string} themeBackgroundAlignment The alignment of the theme * background image. This is used to compute the attribution's alignment. * @private */ function updateThemeAttribution(url, themeBackgroundAlignment) { if (!url) { setAttributionVisibility_(false); return; } var attributionImage = attribution.querySelector('img'); if (!attributionImage) { attributionImage = new Image(); attribution.appendChild(attributionImage); } attributionImage.style.content = url; // To avoid conflicts, place the attribution on the left for themes that // right align their background images. attribution.classList.toggle(CLASSES.LEFT_ALIGN_ATTRIBUTION, themeBackgroundAlignment == 'right'); setAttributionVisibility_(true); } /** * Sets the visibility of the theme attribution. * @param {boolean} show True to show the attribution. * @private */ function setAttributionVisibility_(show) { if (attribution) { attribution.style.display = show ? '' : 'none'; } } /** * Converts an Array of color components into RRGGBBAA format. * @param {Array} color Array of rgba color components. * @return {string} Color string in RRGGBBAA format. * @private */ function convertToRRGGBBAAColor(color) { return color.map(function(t) { return ('0' + t.toString(16)).slice(-2); // To 2-digit, 0-padded hex. }).join(''); } /** * Converts an Array of color components into RGBA format "rgba(R,G,B,A)". * @param {Array} color Array of rgba color components. * @return {string} CSS color in RGBA format. * @private */ function convertToRGBAColor(color) { return 'rgba(' + color[0] + ',' + color[1] + ',' + color[2] + ',' + color[3] / 255 + ')'; } /** * Called when page data change. */ function onMostVisitedChange() { reloadTiles(); } /** * Fetches new data, creates, and renders tiles. */ function reloadTiles() { var pages = ntpApiHandle.mostVisited; var cmds = []; for (var i = 0; i < Math.min(MAX_NUM_TILES_TO_SHOW, pages.length); ++i) { cmds.push({cmd: 'tile', rid: pages[i].rid}); } cmds.push({cmd: 'show', maxVisible: numColumnsShown * NUM_ROWS}); $('mv-single').contentWindow.postMessage(cmds, '*'); } /** * Shows the blacklist notification and triggers a delay to hide it. */ function showNotification() { notification.classList.remove(CLASSES.HIDE_NOTIFICATION); notification.classList.remove(CLASSES.DELAYED_HIDE_NOTIFICATION); notification.scrollTop; notification.classList.add(CLASSES.DELAYED_HIDE_NOTIFICATION); } /** * Hides the blacklist notification. */ function hideNotification() { notification.classList.add(CLASSES.HIDE_NOTIFICATION); notification.classList.remove(CLASSES.DELAYED_HIDE_NOTIFICATION); } /** * Handles a click on the notification undo link by hiding the notification and * informing Chrome. */ function onUndo() { hideNotification(); if (lastBlacklistedTile != null) { ntpApiHandle.undoMostVisitedDeletion(lastBlacklistedTile); } } /** * Handles a click on the restore all notification link by hiding the * notification and informing Chrome. */ function onRestoreAll() { hideNotification(); ntpApiHandle.undoAllMostVisitedDeletions(); } /** * Recomputes the number of tile columns, and width of various contents based * on the width of the window. * @return {boolean} Whether the number of tile columns has changed. */ function updateContentWidth() { var tileRequiredWidth = NTP_DESIGN.tileWidth + NTP_DESIGN.tileMargin; // If innerWidth is zero, then use the maximum snap size. var maxSnapSize = MAX_NUM_COLUMNS * tileRequiredWidth - NTP_DESIGN.tileMargin + MIN_TOTAL_HORIZONTAL_PADDING; var innerWidth = window.innerWidth || maxSnapSize; // Each tile has left and right margins that sum to NTP_DESIGN.tileMargin. var availableWidth = innerWidth + NTP_DESIGN.tileMargin - NTP_DESIGN.fakeboxWingSize * 2 - MIN_TOTAL_HORIZONTAL_PADDING; var newNumColumns = Math.floor(availableWidth / tileRequiredWidth); if (newNumColumns < MIN_NUM_COLUMNS) newNumColumns = MIN_NUM_COLUMNS; else if (newNumColumns > MAX_NUM_COLUMNS) newNumColumns = MAX_NUM_COLUMNS; if (numColumnsShown === newNumColumns) return false; numColumnsShown = newNumColumns; // We add an extra pixel because rounding errors on different zooms can // make the width shorter than it should be. var tilesContainerWidth = Math.ceil(numColumnsShown * tileRequiredWidth) + 1; $(IDS.TILES).style.width = tilesContainerWidth + 'px'; if (fakebox) { // -2 to account for border. var fakeboxWidth = (tilesContainerWidth - NTP_DESIGN.tileMargin - 2); fakeboxWidth += NTP_DESIGN.fakeboxWingSize * 2; fakebox.style.width = fakeboxWidth + 'px'; } return true; } /** * Resizes elements because the number of tile columns may need to change in * response to resizing. Also shows or hides extra tiles tiles according to the * new width of the page. */ function onResize() { updateContentWidth(); $('mv-single').contentWindow.postMessage( {cmd: 'tilesVisible', maxVisible: numColumnsShown * NUM_ROWS}, '*'); } /** * Handles new input by disposing the NTP, according to where the input was * entered. */ function onInputStart() { if (fakebox && isFakeboxFocused()) { setFakeboxFocus(false); setFakeboxDragFocus(false); disposeNtp(true); } else if (!isFakeboxFocused()) { disposeNtp(false); } } /** * Disposes the NTP, according to where the input was entered. * @param {boolean} wasFakeboxInput True if the input was in the fakebox. */ function disposeNtp(wasFakeboxInput) { var behavior = wasFakeboxInput ? fakeboxInputBehavior : omniboxInputBehavior; if (behavior == NTP_DISPOSE_STATE.DISABLE_FAKEBOX) setFakeboxActive(false); else if (behavior == NTP_DISPOSE_STATE.HIDE_FAKEBOX_AND_LOGO) setFakeboxAndLogoVisibility(false); } /** * Restores the NTP (re-enables the fakebox and unhides the logo.) */ function restoreNtp() { setFakeboxActive(true); setFakeboxAndLogoVisibility(true); } /** * @param {boolean} focus True to focus the fakebox. */ function setFakeboxFocus(focus) { document.body.classList.toggle(CLASSES.FAKEBOX_FOCUS, focus); } /** * @param {boolean} focus True to show a dragging focus to the fakebox. */ function setFakeboxDragFocus(focus) { document.body.classList.toggle(CLASSES.FAKEBOX_DRAG_FOCUS, focus); } /** * @return {boolean} True if the fakebox has focus. */ function isFakeboxFocused() { return document.body.classList.contains(CLASSES.FAKEBOX_FOCUS) || document.body.classList.contains(CLASSES.FAKEBOX_DRAG_FOCUS); } /** * @param {boolean} enable True to enable the fakebox. */ function setFakeboxActive(enable) { document.body.classList.toggle(CLASSES.FAKEBOX_DISABLE, !enable); } /** * @param {!Event} event The click event. * @return {boolean} True if the click occurred in an enabled fakebox. */ function isFakeboxClick(event) { return fakebox.contains(event.target) && !document.body.classList.contains(CLASSES.FAKEBOX_DISABLE); } /** * @param {boolean} show True to show the fakebox and logo. */ function setFakeboxAndLogoVisibility(show) { document.body.classList.toggle(CLASSES.HIDE_FAKEBOX_AND_LOGO, !show); } /** * Shortcut for document.getElementById. * @param {string} id of the element. * @return {HTMLElement} with the id. */ function $(id) { return document.getElementById(id); } /** * Utility function which creates an element with an optional classname and * appends it to the specified parent. * @param {Element} parent The parent to append the new element. * @param {string} name The name of the new element. * @param {string=} opt_class The optional classname of the new element. * @return {Element} The new element. */ function createAndAppendElement(parent, name, opt_class) { var child = document.createElement(name); if (opt_class) child.classList.add(opt_class); parent.appendChild(child); return child; } /** * @param {!Element} element The element to register the handler for. * @param {number} keycode The keycode of the key to register. * @param {!Function} handler The key handler to register. */ function registerKeyHandler(element, keycode, handler) { element.addEventListener('keydown', function(event) { if (event.keyCode == keycode) handler(event); }); } /** * @return {Object} the handle to the embeddedSearch API. */ function getEmbeddedSearchApiHandle() { if (window.cideb) return window.cideb; if (window.chrome && window.chrome.embeddedSearch) return window.chrome.embeddedSearch; return null; } /** * Event handler for the focus changed and blacklist messages on link elements. * Used to toggle visual treatment on the tiles (depending on the message). * @param {Event} event Event received. */ function handlePostMessage(event) { var cmd = event.data.cmd; var args = event.data; if (cmd == 'tileBlacklisted') { showNotification(); lastBlacklistedTile = args.tid; ntpApiHandle.deleteMostVisitedItem(args.tid); } } /** * Prepares the New Tab Page by adding listeners, rendering the current * theme, the most visited pages section, and Google-specific elements for a * Google-provided page. */ function init() { notification = $(IDS.NOTIFICATION); attribution = $(IDS.ATTRIBUTION); ntpContents = $(IDS.NTP_CONTENTS); if (configData.isGooglePage) { var logo = document.createElement('div'); logo.id = IDS.LOGO; logo.title = 'Google'; fakebox = document.createElement('div'); fakebox.id = IDS.FAKEBOX; var fakeboxHtml = []; fakeboxHtml.push('
'); fakeboxHtml.push(''); fakeboxHtml.push('
'); fakebox.innerHTML = fakeboxHtml.join(''); ntpContents.insertBefore(fakebox, ntpContents.firstChild); ntpContents.insertBefore(logo, ntpContents.firstChild); } else { document.body.classList.add(CLASSES.NON_GOOGLE_PAGE); } // Modify design for experimental icon NTP, if specified. if (configData.useIcons) modifyNtpDesignForIcons(); document.querySelector('#ntp-contents').classList.add(NTP_DESIGN.mainClass); // Hide notifications after fade out, so we can't focus on links via keyboard. notification.addEventListener('webkitTransitionEnd', hideNotification); var notificationMessage = $(IDS.NOTIFICATION_MESSAGE); notificationMessage.textContent = configData.translatedStrings.thumbnailRemovedNotification; var undoLink = $(IDS.UNDO_LINK); undoLink.addEventListener('click', onUndo); registerKeyHandler(undoLink, KEYCODE.ENTER, onUndo); undoLink.textContent = configData.translatedStrings.undoThumbnailRemove; var restoreAllLink = $(IDS.RESTORE_ALL_LINK); restoreAllLink.addEventListener('click', onRestoreAll); registerKeyHandler(restoreAllLink, KEYCODE.ENTER, onUndo); restoreAllLink.textContent = configData.translatedStrings.restoreThumbnailsShort; $(IDS.ATTRIBUTION_TEXT).textContent = configData.translatedStrings.attributionIntro; var notificationCloseButton = $(IDS.NOTIFICATION_CLOSE_BUTTON); createAndAppendElement( notificationCloseButton, 'div', CLASSES.BLACKLIST_BUTTON_INNER); notificationCloseButton.addEventListener('click', hideNotification); window.addEventListener('resize', onResize); updateContentWidth(); var topLevelHandle = getEmbeddedSearchApiHandle(); ntpApiHandle = topLevelHandle.newTabPage; ntpApiHandle.onthemechange = onThemeChange; ntpApiHandle.onmostvisitedchange = onMostVisitedChange; ntpApiHandle.oninputstart = onInputStart; ntpApiHandle.oninputcancel = restoreNtp; if (ntpApiHandle.isInputInProgress) onInputStart(); searchboxApiHandle = topLevelHandle.searchBox; if (fakebox) { // Listener for updating the key capture state. document.body.onmousedown = function(event) { if (isFakeboxClick(event)) searchboxApiHandle.startCapturingKeyStrokes(); else if (isFakeboxFocused()) searchboxApiHandle.stopCapturingKeyStrokes(); }; searchboxApiHandle.onkeycapturechange = function() { setFakeboxFocus(searchboxApiHandle.isKeyCaptureEnabled); }; var inputbox = $(IDS.FAKEBOX_INPUT); if (inputbox) { inputbox.onpaste = function(event) { event.preventDefault(); // Send pasted text to Omnibox. var text = event.clipboardData.getData('text/plain'); if (text) searchboxApiHandle.paste(text); }; inputbox.ondrop = function(event) { event.preventDefault(); var text = event.dataTransfer.getData('text/plain'); if (text) { searchboxApiHandle.paste(text); } setFakeboxDragFocus(false); }; inputbox.ondragenter = function() { setFakeboxDragFocus(true); }; inputbox.ondragleave = function() { setFakeboxDragFocus(false); }; } // Update the fakebox style to match the current key capturing state. setFakeboxFocus(searchboxApiHandle.isKeyCaptureEnabled); } if (searchboxApiHandle.rtl) { $(IDS.NOTIFICATION).dir = 'rtl'; // Grabbing the root HTML element. document.documentElement.setAttribute('dir', 'rtl'); // Add class for setting alignments based on language directionality. document.documentElement.classList.add(CLASSES.RTL); } var iframe = document.createElement('iframe'); // Change the order of tabbing the page to start with NTP tiles. iframe.setAttribute('tabindex', '1'); iframe.id = 'mv-single'; var args = []; if (searchboxApiHandle.rtl) args.push('rtl=1'); if (window.configData.useIcons) args.push('icons=1'); if (NTP_DESIGN.numTitleLines > 1) args.push('ntl=' + NTP_DESIGN.numTitleLines); args.push('removeTooltip=' + encodeURIComponent(configData.translatedStrings.removeThumbnailTooltip)); iframe.src = '//most-visited/single.html?' + args.join('&'); $(IDS.TILES).appendChild(iframe); iframe.onload = function() { reloadTiles(); renderTheme(); }; window.addEventListener('message', handlePostMessage); } /** * Binds event listeners. */ function listen() { document.addEventListener('DOMContentLoaded', init); } return { init: init, listen: listen }; } if (!window.localNTPUnitTest) { LocalNTP().listen(); } Local State Debug Page
    Loading Local State file...
  
// Copyright 2014 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. /** * Javascript for local_state.html, served from chrome://local-state/ * This is used to debug the contents of the Local State file. */ cr.define('localState', function() { 'use strict'; /** * Sets the page content to the specified |localState| string, called * from C++. * @param {string} localState the JSON-formatted local state data, * or an error message. */ function setLocalState(localState) { $('content').textContent = localState; } return { setLocalState: setLocalState }; }); // When the page loads, request the JSON local state data from C++. document.addEventListener('DOMContentLoaded', function() { chrome.send('requestJson'); }); /* Copyright 2013 The Chromium Authors. All rights reserved. * Use of this source code is governed by a BSD-style license that can be * found in the LICENSE file. */ body { -webkit-user-select: none; background: none transparent; margin: 0; overflow: hidden; } a { display: block; text-decoration: none; } a:active, a:hover, a:visited { color: inherit; text-decoration: inherit; } /* Copyright 2013 The Chromium Authors. All rights reserved. * Use of this source code is governed by a BSD-style license that can be * found in the LICENSE file. */ html { height: 100%; } body { height: 100%; width: 100%; } a { height: 100%; line-height: 117%; overflow: hidden; text-align: center; /* Can be overridden in JS. */ text-overflow: ellipsis; /* Can be overridden in JS. */ white-space: nowrap; /* Can be overridden in JS. */ } a.multiline { text-overflow: clip; white-space: pre-wrap; word-wrap: break-word; } a:focus { outline: none; /* Remove outline from tabIndex = -1. */ } // Copyright 2013 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. /** * @fileoverview Rendering for iframed most visited titles. */ window.addEventListener('DOMContentLoaded', function() { 'use strict'; fillMostVisited(window.location, function(params, data) { document.body.appendChild( createMostVisitedLink( params, data.url, data.title, data.title, data.direction)); }); }); /* Copyright 2013 The Chromium Authors. All rights reserved. * Use of this source code is governed by a BSD-style license that can be * found in the LICENSE file. */ body { height: 100%; position: absolute; width: 100%; } a { height: 100%; position: relative; width: 100%; } a:focus { outline: none; /* Remove outline from tabIndex = -1. */ } div { bottom: 24px; margin: 0 7px; overflow: hidden; position: absolute; text-align: center; text-overflow: ellipsis; white-space: nowrap; width: 90%; } span.blocker { display: inline-block; height: 100%; position: absolute; width: 100%; } img.thumbnail { height: auto; min-height: 100%; width: 100%; } img.large-icon { -webkit-clip-path: inset(0 0 0 0 round 4px); height: 48px; left: 50%; margin-left: -24px; margin-top: -24px; position: absolute; top: 50%; width: 48px; } // Copyright 2013 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. /** * @fileoverview Rendering for iframed most visited thumbnails. */ window.addEventListener('DOMContentLoaded', function() { 'use strict'; fillMostVisited(document.location, function(params, data) { function displayLink(link) { document.body.appendChild(link); window.parent.postMessage('linkDisplayed', '{{ORIGIN}}'); } function showDomainElement() { var link = createMostVisitedLink( params, data.url, data.title, undefined, data.direction); var domain = document.createElement('div'); domain.textContent = data.domain; link.appendChild(domain); displayLink(link); } // Called on intentionally empty tiles for which the visuals are handled // externally by the page itself. function showEmptyTile() { displayLink(createMostVisitedLink( params, data.url, data.title, undefined, data.direction)); } // Creates and adds an image. function createThumbnail(src, imageClass) { var image = document.createElement('img'); if (imageClass) { image.classList.add(imageClass); } image.onload = function() { var link = createMostVisitedLink( params, data.url, data.title, undefined, data.direction); // Use blocker to prevent context menu from showing image-related items. var blocker = document.createElement('span'); blocker.className = 'blocker'; link.appendChild(blocker); link.appendChild(image); displayLink(link); }; image.onerror = function() { // If no external thumbnail fallback (etfb), and have domain. if (!params.etfb && data.domain) { showDomainElement(); } else { showEmptyTile(); } }; image.src = src; } var useIcons = params['icons'] == '1'; if (data.dummy) { showEmptyTile(); } else if (useIcons && data.largeIconUrl) { createThumbnail(data.largeIconUrl, 'large-icon'); } else if (!useIcons && data.thumbnailUrls && data.thumbnailUrls.length) { createThumbnail(data.thumbnailUrls[0], 'thumbnail'); } else if (data.domain) { showDomainElement(); } else { showEmptyTile(); } }); });
/* Copyright 2015 The Chromium Authors. All rights reserved. * Use of this source code is governed by a BSD-style license that can be * found in the LICENSE file. */ body { -webkit-user-select: none; background: none transparent; color: #323232; margin: 0; overflow: hidden; padding: 0; } a { display: block; } a, a:active, a:hover, a:visited { color: inherit; text-decoration: none; } #most-visited { -webkit-user-select: none; margin: 0; text-align: -webkit-center; } #mv-tiles, .mv-tiles-old { -webkit-user-select: none; font-size: 0; margin: 0; opacity: 0; position: absolute; /* This align correctly for both LTR and RTL */ text-align: -webkit-auto; transition: opacity 1s; } .thumb-ntp #mv-tiles, .thumb-ntp .mv-tiles-old { height: calc(146px + 130px); line-height: 146px; } .icon-ntp #mv-tiles, .icon-ntp .mv-tiles-old { height: calc(2 * 112px); line-height: 112px; width: 100%; } .mv-tile, .mv-empty-tile { box-sizing: border-box; display: inline-block; font-family: arial, sans-serif; font-size: 12px; opacity: 1; outline: 0; overflow: hidden; position: relative; vertical-align: top; white-space: nowrap; } .mv-tile.hidden, .mv-empty-tile.hidden { display: none; } .thumb-ntp .mv-tile, .thumb-ntp .mv-empty-tile { background: rgb(242,242,242); border: 1px solid transparent; border-radius: 2px; height: calc(130px - 2px); line-height: 100%; margin: 0 8px; width: calc(156px - 2px); } .icon-ntp .mv-tile, .icon-ntp .mv-empty-tile { border: none; border-radius: 2px; height: calc(102px + 18px - 12px); margin: 0 12px 4px 12px; width: calc(48px + 2 * 18px); } .mv-tile { -webkit-transition-duration: 200ms; -webkit-transition-property: -webkit-transform, border, box-shadow, margin, opacity, width; cursor: pointer; } .thumb-ntp .mv-tile:focus:not(:hover) { -webkit-filter: brightness(75%); box-shadow: 0 1px 2px 0 rgba(0,0,0,0.1), 0 4px 8px 0 rgba(0,0,0,0.2); } .icon-ntp .mv-tile:focus { background: rgba(0,0,0,0.2); } .icon-ntp.dark .mv-tile:focus { background: rgba(255,255,255,0.2); } .mv-tile.blacklisted { -webkit-transform: scale(0, 0); border: none !important; margin: 0; width: 0; } .thumb-ntp .mv-tile:hover { box-shadow: 0 1px 2px 0 rgba(0,0,0,0.1), 0 4px 8px 0 rgba(0,0,0,0.2); } .mv-tile.mv-blacklist { opacity: 0; } .mv-tile.mv-blacklist { -webkit-transform: scale(0, 0); -webkit-transform-origin: 0 41px; margin-left: 0; margin-right: 0; width: 0; } .mv-title { border: none; overflow: hidden; position: absolute; text-overflow: clip; } .mv-title.multiline { white-space: pre-wrap; word-wrap: break-word; } .thumb-ntp .mv-title { -webkit-mask-image: linear-gradient(to right, #000, #000, 100px, transparent); height: 15px; left: 31px; line-height: 14px; padding: 0; top: 8px; width: calc(156px - 32px - 4px); } html:not([dir=rtl]) .thumb-ntp .mv-title[style*='direction: rtl'] { -webkit-mask-image: linear-gradient(to left, black, black, 100px, transparent); left: auto; right: 8px; text-align: right; } html[dir=rtl] .mv-title { left: 8px; text-align: left; } html[dir=rtl] .thumb-ntp .mv-title[style*='direction: rtl'] { -webkit-mask-image: linear-gradient(to left, black, black, 100px, transparent); right: 31px; text-align: right; } .icon-ntp .mv-title { height: 28px; left: auto; line-height: 117%; right: auto; text-align: center; top: 76px; width: 100%; z-index: 5; } .mv-thumb { border: none; cursor: pointer; display: block; overflow: hidden; position: absolute; } .thumb-ntp .mv-thumb { border-radius: 0; height: 94px; left: 3px; top: 31px; width: 148px; } .mv-thumb img.thumbnail { height: auto; min-height: 100%; width: 100%; } .mv-thumb img.large-icon { -webkit-clip-path: inset(0 0 0 0 round 4px); height: 48px; left: 50%; margin-left: -24px; margin-top: -24px; position: absolute; top: 50%; width: 48px; } .mv-thumb.failed-img, .mv-thumb.large-icon-outer { background-color: #fff; height: 94px; width: 148px; } .icon-ntp .mv-thumb, .icon-ntp .mv-thumb-fallback { background: transparent; height: 48px; left: 50%; margin-left: -24px; top: 18px; width: 48px; } /* We use ::after without content to provide an aditional element on top of the * thumbnail. */ .mv-thumb.failed-img::after { border: 8px solid #f2f2f2; border-radius: 50%; content: ''; display: block; height: 0; margin: 39px 66px; width: 0; } .mv-x { -webkit-transition: opacity 150ms; border: none; cursor: pointer; opacity: 0; position: absolute; } .thumb-ntp .mv-x { background: linear-gradient(to left, rgb(242,242,242) 60%, transparent); height: 30px; right: 0; width: 40px; } .icon-ntp .mv-x { background: none; height: 16px; right: 10px; top: 10px; width: 16px; } /* We use ::after without content to provide the masked X element. The "bottom" * div is actually just the gradient. */ .mv-x::after { -webkit-mask-image: -webkit-image-set( url(chrome-search://local-ntp/images/close_3_mask.png) 1x, url(chrome-search://local-ntp/images/close_3_mask.png@2x) 2x); -webkit-mask-position: 12px 10px; -webkit-mask-repeat: no-repeat; -webkit-mask-size: 10px 10px; background-color: rgba(90,90,90,0.7); content: ''; display: block; height: 32px; position: absolute; right: 0; width: 32px; } .icon-ntp .mv-x::after { -webkit-mask: none; background-color: inherit; background-image: -webkit-image-set( url(chrome-search://local-ntp/images/close_4_button.png) 1x, url(chrome-search://local-ntp/images/close_4_button.png@2x) 2x); height: 16px; width: 16px; } html[dir=rtl] .thumb-ntp .mv-x { background: linear-gradient(to right, rgb(242,242,242) 60%, transparent); left: -1px; right: auto; } html[dir=rtl] .thumb-ntp .mv-x::after { left: -1px; right: auto; } html[dir=rtl] .icon-ntp .mv-x { left: 10px; right: auto; } .thumb-ntp .mv-x:hover::after { background-color: rgb(90,90,90); } .thumb-ntp .mv-x:active::after { background-color: rgb(66,133,244); } .icon-ntp .mv-x:hover::after, .icon-ntp .mv-x:active::after { background-color: inherit; } .mv-tile:hover .mv-x { -webkit-transition-delay: 500ms; opacity: 1; } .icon-ntp .mv-tile:hover .mv-x { -webkit-transition-delay: 800ms; } .mv-x:hover { -webkit-transition: none; } .mv-favicon { background-size: 16px; height: 16px; left: 7px; margin: 0; pointer-events: none; position: absolute; top: 7px; width: 16px; } html[dir=rtl] .mv-favicon { left: auto; right: 7px; } .mv-favicon.failed-favicon { background-image: -webkit-image-set( url(chrome-search://local-ntp/images/ntp_default_favicon.png) 1x, url(chrome-search://local-ntp/images/ntp_default_favicon.png@2x) 2x); background-repeat: no-repeat; background-size: 16px 16px; } .mv-favicon img { height: 100%; width: 100%; } .mv-favicon.failed-favicon img { display: none; } /* Copyright 2015 The Chromium Authors. All rights reserved. * Use of this source code is governed by a BSD-style license that can be * found in the LICENSE file. */ // Single iframe for NTP tiles. (function() { 'use strict'; /** * The different types of events that are logged from the NTP. This enum is * used to transfer information from the NTP JavaScript to the renderer and is * not used as a UMA enum histogram's logged value. * Note: Keep in sync with common/ntp_logging_events.h * @enum {number} * @const */ var LOG_TYPE = { // All NTP Tiles have finished loading (successfully or failing). NTP_ALL_TILES_LOADED: 11, }; /** * The different sources that an NTP tile can have. * Note: Keep in sync with components/ntp_tiles/ntp_tile_source.h * @enum {number} * @const */ var NTPTileSource = { TOP_SITES: 0, SUGGESTIONS_SERVICE: 1, POPULAR: 3, WHITELIST: 4, }; /** * Total number of tiles to show at any time. If the host page doesn't send * enough tiles, we fill them blank. * @const {number} */ var NUMBER_OF_TILES = 8; /** * Whether to use icons instead of thumbnails. * @type {boolean} */ var USE_ICONS = false; /** * Number of lines to display in titles. * @type {number} */ var NUM_TITLE_LINES = 1; /** * The origin of this request. * @const {string} */ var DOMAIN_ORIGIN = '{{ORIGIN}}'; /** * Counter for DOM elements that we are waiting to finish loading. * @type {number} */ var loadedCounter = 1; /** * DOM element containing the tiles we are going to present next. * Works as a double-buffer that is shown when we receive a "show" postMessage. * @type {Element} */ var tiles = null; /** * List of parameters passed by query args. * @type {Object} */ var queryArgs = {}; /** * Log an event on the NTP. * @param {number} eventType Event from LOG_TYPE. */ var logEvent = function(eventType) { chrome.embeddedSearch.newTabPage.logEvent(eventType); }; /** * Log impression of an NTP tile. * @param {number} tileIndex Position of the tile, >= 0 and < NUMBER_OF_TILES. * @param {number} tileSource The source from NTPTileSource. */ function logMostVisitedImpression(tileIndex, tileSource) { chrome.embeddedSearch.newTabPage.logMostVisitedImpression(tileIndex, tileSource); } /** * Log click on an NTP tile. * @param {number} tileIndex Position of the tile, >= 0 and < NUMBER_OF_TILES. * @param {number} tileSource The source from NTPTileSource. */ function logMostVisitedNavigation(tileIndex, tileSource) { chrome.embeddedSearch.newTabPage.logMostVisitedNavigation(tileIndex, tileSource); } /** * Down counts the DOM elements that we are waiting for the page to load. * When we get to 0, we send a message to the parent window. * This is usually used as an EventListener of onload/onerror. */ var countLoad = function() { loadedCounter -= 1; if (loadedCounter <= 0) { showTiles(); logEvent(LOG_TYPE.NTP_ALL_TILES_LOADED); window.parent.postMessage({cmd: 'loaded'}, DOMAIN_ORIGIN); loadedCounter = 1; } }; /** * Handles postMessages coming from the host page to the iframe. * Mostly, it dispatches every command to handleCommand. */ var handlePostMessage = function(event) { if (event.data instanceof Array) { for (var i = 0; i < event.data.length; ++i) { handleCommand(event.data[i]); } } else { handleCommand(event.data); } }; /** * Handles a single command coming from the host page to the iframe. * We try to keep the logic here to a minimum and just dispatch to the relevant * functions. */ var handleCommand = function(data) { var cmd = data.cmd; if (cmd == 'tile') { addTile(data); } else if (cmd == 'show') { countLoad(); hideOverflowTiles(data); } else if (cmd == 'updateTheme') { updateTheme(data); } else if (cmd == 'tilesVisible') { hideOverflowTiles(data); } else { console.error('Unknown command: ' + JSON.stringify(data)); } }; var updateTheme = function(info) { var themeStyle = []; if (info.tileBorderColor) { themeStyle.push('.thumb-ntp .mv-tile {' + 'border: 1px solid ' + info.tileBorderColor + '; }'); } if (info.tileHoverBorderColor) { themeStyle.push('.thumb-ntp .mv-tile:hover {' + 'border-color: ' + info.tileHoverBorderColor + '; }'); } if (info.isThemeDark) { themeStyle.push('.thumb-ntp .mv-tile, .thumb-ntp .mv-empty-tile { ' + 'background: rgb(51,51,51); }'); themeStyle.push('.thumb-ntp .mv-thumb.failed-img { ' + 'background-color: #555; }'); themeStyle.push('.thumb-ntp .mv-thumb.failed-img::after { ' + 'border-color: #333; }'); themeStyle.push('.thumb-ntp .mv-x { ' + 'background: linear-gradient(to left, ' + 'rgb(51,51,51) 60%, transparent); }'); themeStyle.push('html[dir=rtl] .thumb-ntp .mv-x { ' + 'background: linear-gradient(to right, ' + 'rgb(51,51,51) 60%, transparent); }'); themeStyle.push('.thumb-ntp .mv-x::after { ' + 'background-color: rgba(255,255,255,0.7); }'); themeStyle.push('.thumb-ntp .mv-x:hover::after { ' + 'background-color: #fff; }'); themeStyle.push('.thumb-ntp .mv-x:active::after { ' + 'background-color: rgba(255,255,255,0.5); }'); themeStyle.push('.icon-ntp .mv-tile:focus { ' + 'background: rgba(255,255,255,0.2); }'); } if (info.tileTitleColor) { themeStyle.push('body { color: ' + info.tileTitleColor + '; }'); } document.querySelector('#custom-theme').textContent = themeStyle.join('\n'); }; /** * Hides extra tiles that don't fit on screen. */ var hideOverflowTiles = function(data) { var tileAndEmptyTileList = document.querySelectorAll( '#mv-tiles .mv-tile,#mv-tiles .mv-empty-tile'); for (var i = 0; i < tileAndEmptyTileList.length; ++i) { tileAndEmptyTileList[i].classList.toggle('hidden', i >= data.maxVisible); } }; /** * Removes all old instances of #mv-tiles that are pending for deletion. */ var removeAllOldTiles = function() { var parent = document.querySelector('#most-visited'); var oldList = parent.querySelectorAll('.mv-tiles-old'); for (var i = 0; i < oldList.length; ++i) { parent.removeChild(oldList[i]); } }; /** * Called when the host page has finished sending us tile information and * we are ready to show the new tiles and drop the old ones. */ var showTiles = function() { // Store the tiles on the current closure. var cur = tiles; // Create empty tiles until we have NUMBER_OF_TILES. while (cur.childNodes.length < NUMBER_OF_TILES) { addTile({}); } var parent = document.querySelector('#most-visited'); // Only fade in the new tiles if there were tiles before. var fadeIn = false; var old = parent.querySelector('#mv-tiles'); if (old) { fadeIn = true; // Mark old tile DIV for removal after the transition animation is done. old.removeAttribute('id'); old.classList.add('mv-tiles-old'); old.style.opacity = 0.0; cur.addEventListener('webkitTransitionEnd', function(ev) { if (ev.target === cur) { removeAllOldTiles(); } }); } // Add new tileset. cur.id = 'mv-tiles'; parent.appendChild(cur); // getComputedStyle causes the initial style (opacity 0) to be applied, so // that when we then set it to 1, that triggers the CSS transition. if (fadeIn) { window.getComputedStyle(cur).opacity; } cur.style.opacity = 1.0; // Make sure the tiles variable contain the next tileset we may use. tiles = document.createElement('div'); }; /** * Called when the host page wants to add a suggestion tile. * For Most Visited, it grabs the data from Chrome and pass on. * For host page generated it just passes the data. * @param {object} args Data for the tile to be rendered. */ var addTile = function(args) { if (isFinite(args.rid)) { // If a valid number passed in |args.rid|: a local chrome suggestion. var data = chrome.embeddedSearch.newTabPage.getMostVisitedItemData(args.rid); if (!data) return; data.tid = data.rid; if (!data.faviconUrl) { data.faviconUrl = 'chrome-search://favicon/size/16@' + window.devicePixelRatio + 'x/' + data.renderViewId + '/' + data.tid; } tiles.appendChild(renderTile(data)); } else if (args.url) { // If a URL is passed: a server-side suggestion. args.tileSource = NTPTileSource.SUGGESTIONS_SERVICE; // check sanity of the arguments if (/^javascript:/i.test(args.url) || /^javascript:/i.test(args.thumbnailUrl)) return; tiles.appendChild(renderTile(args)); } else { // an empty tile tiles.appendChild(renderTile(null)); } }; /** * Called when the user decided to add a tile to the blacklist. * It sets of the animation for the blacklist and sends the blacklisted id * to the host page. * @param {Element} tile DOM node of the tile we want to remove. */ var blacklistTile = function(tile) { tile.classList.add('blacklisted'); tile.addEventListener('webkitTransitionEnd', function(ev) { if (ev.propertyName != 'width') return; window.parent.postMessage({cmd: 'tileBlacklisted', tid: Number(tile.getAttribute('data-tid'))}, DOMAIN_ORIGIN); }); }; /** * Returns whether the given URL has a known, safe scheme. * @param {string} url URL to check. */ var isSchemeAllowed = function(url) { return url.startsWith('http://') || url.startsWith('https://') || url.startsWith('ftp://') || url.startsWith('file://') || url.startsWith('chrome-extension://'); }; /** * Renders a MostVisited tile to the DOM. * @param {object} data Object containing rid, url, title, favicon, thumbnail. * data is null if you want to construct an empty tile. */ var renderTile = function(data) { var tile = document.createElement('a'); if (data == null) { tile.className = 'mv-empty-tile'; return tile; } // The tile will be appended to tiles. var position = tiles.children.length; logMostVisitedImpression(position, data.tileSource); tile.className = 'mv-tile'; tile.setAttribute('data-tid', data.tid); var html = []; if (!USE_ICONS) { html.push('
'); } html.push('
'); html.push('
'); tile.innerHTML = html.join(''); tile.lastElementChild.title = queryArgs['removeTooltip'] || ''; if (isSchemeAllowed(data.url)) { tile.href = data.url; } tile.setAttribute('aria-label', data.title); tile.title = data.title; tile.addEventListener('click', function(ev) { logMostVisitedNavigation(position, data.tileSource); }); tile.addEventListener('keydown', function(event) { if (event.keyCode == 46 /* DELETE */ || event.keyCode == 8 /* BACKSPACE */) { event.preventDefault(); event.stopPropagation(); blacklistTile(this); } else if (event.keyCode == 13 /* ENTER */ || event.keyCode == 32 /* SPACE */) { event.preventDefault(); this.click(); } else if (event.keyCode >= 37 && event.keyCode <= 40 /* ARROWS */) { // specify the direction of movement var inArrowDirection = function(origin, target) { return (event.keyCode == 37 /* LEFT */ && origin.offsetTop == target.offsetTop && origin.offsetLeft > target.offsetLeft) || (event.keyCode == 38 /* UP */ && origin.offsetTop > target.offsetTop && origin.offsetLeft == target.offsetLeft) || (event.keyCode == 39 /* RIGHT */ && origin.offsetTop == target.offsetTop && origin.offsetLeft < target.offsetLeft) || (event.keyCode == 40 /* DOWN */ && origin.offsetTop < target.offsetTop && origin.offsetLeft == target.offsetLeft); }; var nonEmptyTiles = document.querySelectorAll('#mv-tiles .mv-tile'); var nextTile = null; // Find the closest tile in the appropriate direction. for (var i = 0; i < nonEmptyTiles.length; i++) { if (inArrowDirection(this, nonEmptyTiles[i]) && (!nextTile || inArrowDirection(nonEmptyTiles[i], nextTile))) { nextTile = nonEmptyTiles[i]; } } if (nextTile) { nextTile.focus(); } } }); var title = tile.querySelector('.mv-title'); title.innerText = data.title; title.style.direction = data.direction || 'ltr'; if (NUM_TITLE_LINES > 1) { title.classList.add('multiline'); } if (USE_ICONS) { var thumb = tile.querySelector('.mv-thumb'); if (data.largeIconUrl) { var img = document.createElement('img'); img.title = data.title; img.src = data.largeIconUrl; img.classList.add('large-icon'); loadedCounter += 1; img.addEventListener('load', countLoad); img.addEventListener('load', function(ev) { thumb.classList.add('large-icon-outer'); }); img.addEventListener('error', countLoad); img.addEventListener('error', function(ev) { thumb.classList.add('failed-img'); thumb.removeChild(img); }); thumb.appendChild(img); } else { thumb.classList.add('failed-img'); } } else { // THUMBNAILS // We keep track of the outcome of loading possible thumbnails for this // tile. Possible values: // - null: waiting for load/error // - false: error // - a string: URL that loaded correctly. // This is populated by acceptImage/rejectImage and loadBestImage // decides the best one to load. var results = []; var thumb = tile.querySelector('.mv-thumb'); var img = document.createElement('img'); var loaded = false; var loadBestImage = function() { if (loaded) { return; } for (var i = 0; i < results.length; ++i) { if (results[i] === null) { return; } if (results[i] != false) { img.src = results[i]; loaded = true; return; } } thumb.classList.add('failed-img'); thumb.removeChild(img); countLoad(); }; var acceptImage = function(idx, url) { return function(ev) { results[idx] = url; loadBestImage(); }; }; var rejectImage = function(idx) { return function(ev) { results[idx] = false; loadBestImage(); }; }; img.title = data.title; img.classList.add('thumbnail'); loadedCounter += 1; img.addEventListener('load', countLoad); img.addEventListener('error', countLoad); img.addEventListener('error', function(ev) { thumb.classList.add('failed-img'); thumb.removeChild(img); }); thumb.appendChild(img); if (data.thumbnailUrl) { img.src = data.thumbnailUrl; } else { // Get all thumbnailUrls for the tile. // They are ordered from best one to be used to worst. for (var i = 0; i < data.thumbnailUrls.length; ++i) { results.push(null); } for (var i = 0; i < data.thumbnailUrls.length; ++i) { if (data.thumbnailUrls[i]) { var image = new Image(); image.src = data.thumbnailUrls[i]; image.onload = acceptImage(i, data.thumbnailUrls[i]); image.onerror = rejectImage(i); } else { rejectImage(i)(null); } } } var favicon = tile.querySelector('.mv-favicon'); if (data.faviconUrl) { var fi = document.createElement('img'); fi.src = data.faviconUrl; // Set the title to empty so screen readers won't say the image name. fi.title = ''; loadedCounter += 1; fi.addEventListener('load', countLoad); fi.addEventListener('error', countLoad); fi.addEventListener('error', function(ev) { favicon.classList.add('failed-favicon'); }); favicon.appendChild(fi); } else { favicon.classList.add('failed-favicon'); } } var mvx = tile.querySelector('.mv-x'); mvx.addEventListener('click', function(ev) { removeAllOldTiles(); blacklistTile(tile); ev.preventDefault(); ev.stopPropagation(); }); return tile; }; /** * Do some initialization and parses the query arguments passed to the iframe. */ var init = function() { // Creates a new DOM element to hold the tiles. tiles = document.createElement('div'); // Parse query arguments. var query = window.location.search.substring(1).split('&'); queryArgs = {}; for (var i = 0; i < query.length; ++i) { var val = query[i].split('='); if (val[0] == '') continue; queryArgs[decodeURIComponent(val[0])] = decodeURIComponent(val[1]); } // Apply class for icon NTP, if specified. USE_ICONS = queryArgs['icons'] == '1'; if ('ntl' in queryArgs) { var ntl = parseInt(queryArgs['ntl'], 10); if (isFinite(ntl)) NUM_TITLE_LINES = ntl; } // Duplicating NTP_DESIGN.mainClass. document.querySelector('#most-visited').classList.add( USE_ICONS ? 'icon-ntp' : 'thumb-ntp'); // Enable RTL. if (queryArgs['rtl'] == '1') { var html = document.querySelector('html'); html.dir = 'rtl'; } window.addEventListener('message', handlePostMessage); }; window.addEventListener('DOMContentLoaded', init); })(); // Copyright 2013 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. /** * @fileoverview Utilities for rendering most visited thumbnails and titles. */ // // Copyright 2013 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. /** * @fileoverview Helpers for validating parameters to chrome-search:// iframes. */ /** * Converts an RGB color number to a hex color string if valid. * @param {number} color A 6-digit hex RGB color code as a number. * @return {?string} A CSS representation of the color or null if invalid. */ function convertToHexColor(color) { // Color must be a number, finite, with no fractional part, in the correct // range for an RGB hex color. if (isFinite(color) && Math.floor(color) == color && color >= 0 && color <= 0xffffff) { var hexColor = color.toString(16); // Pads with initial zeros and # (e.g. for 'ff' yields '#0000ff'). return '#000000'.substr(0, 7 - hexColor.length) + hexColor; } return null; } /** * Validates a RGBA color component. It must be a number between 0 and 255. * @param {number} component An RGBA component. * @return {boolean} True if the component is valid. */ function isValidRBGAComponent(component) { return isFinite(component) && component >= 0 && component <= 255; } /** * Converts an Array of color components into RGBA format "rgba(R,G,B,A)". * @param {Array} rgbaColor Array of rgba color components. * @return {?string} CSS color in RGBA format or null if invalid. */ function convertArrayToRGBAColor(rgbaColor) { // Array must contain 4 valid components. if (rgbaColor instanceof Array && rgbaColor.length === 4 && isValidRBGAComponent(rgbaColor[0]) && isValidRBGAComponent(rgbaColor[1]) && isValidRBGAComponent(rgbaColor[2]) && isValidRBGAComponent(rgbaColor[3])) { return 'rgba(' + rgbaColor[0] + ',' + rgbaColor[1] + ',' + rgbaColor[2] + ',' + rgbaColor[3] / 255 + ')'; } return null; } /** * The origin of this request. * @const {string} */ var DOMAIN_ORIGIN = '{{ORIGIN}}'; /** * Parses query parameters from Location. * @param {string} location The URL to generate the CSS url for. * @return {Object} Dictionary containing name value pairs for URL. */ function parseQueryParams(location) { var params = Object.create(null); var query = location.search.substring(1); var vars = query.split('&'); for (var i = 0; i < vars.length; i++) { var pair = vars[i].split('='); var k = decodeURIComponent(pair[0]); if (k in params) { // Duplicate parameters are not allowed to prevent attackers who can // append things to |location| from getting their parameter values to // override legitimate ones. return Object.create(null); } else { params[k] = decodeURIComponent(pair[1]); } } return params; } /** * Creates a new most visited link element. * @param {Object} params URL parameters containing styles for the link. * @param {string} href The destination for the link. * @param {string} title The title for the link. * @param {string|undefined} text The text for the link or none. * @param {string|undefined} direction The text direction. * @return {HTMLAnchorElement} A new link element. */ function createMostVisitedLink(params, href, title, text, direction) { var styles = getMostVisitedStyles(params, !!text); var link = document.createElement('a'); link.style.color = styles.color; link.style.fontSize = styles.fontSize + 'px'; if (styles.fontFamily) link.style.fontFamily = styles.fontFamily; if (styles.textAlign) link.style.textAlign = styles.textAlign; if (styles.textFadePos) { var dir = /^rtl$/i.test(direction) ? 'to left' : 'to right'; // The fading length in pixels is passed by the caller. var mask = 'linear-gradient(' + dir + ', rgba(0,0,0,1), rgba(0,0,0,1) ' + styles.textFadePos + 'px, rgba(0,0,0,0))'; link.style.textOverflow = 'clip'; link.style.webkitMask = mask; } if (styles.numTitleLines && styles.numTitleLines > 1) { link.classList.add('multiline'); } link.href = href; link.title = title; link.target = '_top'; // Include links in the tab order. The tabIndex is necessary for // accessibility. link.tabIndex = '0'; if (text) { // Wrap text with span so ellipsis will appear at the end of multiline. var spanWrap = document.createElement('span'); spanWrap.textContent = text; link.appendChild(spanWrap); } link.addEventListener('focus', function() { window.parent.postMessage('linkFocused', DOMAIN_ORIGIN); }); link.addEventListener('blur', function() { window.parent.postMessage('linkBlurred', DOMAIN_ORIGIN); }); var navigateFunction = function handleNavigation(e) { var isServerSuggestion = 'url' in params; // Ping are only populated for server-side suggestions, never for MV. if (isServerSuggestion && params.ping) { generatePing(DOMAIN_ORIGIN + params.ping); } // Follow normally, so transition type will be LINK. }; link.addEventListener('click', navigateFunction); link.addEventListener('keydown', function(event) { if (event.keyCode == 46 /* DELETE */ || event.keyCode == 8 /* BACKSPACE */) { event.preventDefault(); window.parent.postMessage('tileBlacklisted,' + params.pos, DOMAIN_ORIGIN); } else if (event.keyCode == 13 /* ENTER */ || event.keyCode == 32 /* SPACE */) { // Event target is the tag. Send a click event on it, which will // trigger the 'click' event registered above. event.preventDefault(); event.target.click(); } }); return link; } /** * Returns the color to display string with, depending on whether title is * displayed, the current theme, and URL parameters. * @param {Object} params URL parameters specifying style. * @param {boolean} isTitle if the style is for the Most Visited Title. * @return {string} The color to use, in "rgba(#,#,#,#)" format. */ function getTextColor(params, isTitle) { // 'RRGGBBAA' color format overrides everything. if ('c' in params && params.c.match(/^[0-9A-Fa-f]{8}$/)) { // Extract the 4 pairs of hex digits, map to number, then form rgba(). var t = params.c.match(/(..)(..)(..)(..)/).slice(1).map(function(s) { return parseInt(s, 16); }); return 'rgba(' + t[0] + ',' + t[1] + ',' + t[2] + ',' + t[3] / 255 + ')'; } // For backward compatibility with server-side NTP, look at themes directly // and use param.c for non-title or as fallback. var apiHandle = chrome.embeddedSearch.newTabPage; var themeInfo = apiHandle.themeBackgroundInfo; var c = '#777'; if (isTitle && themeInfo && !themeInfo.usingDefaultTheme) { // Read from theme directly c = convertArrayToRGBAColor(themeInfo.textColorRgba) || c; } else if ('c' in params) { c = convertToHexColor(parseInt(params.c, 16)) || c; } return c; } /** * Decodes most visited styles from URL parameters. * - c: A hexadecimal number interpreted as a hex color code. * - f: font-family. * - fs: font-size as a number in pixels. * - ta: text-align property, as a string. * - tf: text fade starting position, in pixels. * - ntl: number of lines in the title. * @param {Object} params URL parameters specifying style. * @param {boolean} isTitle if the style is for the Most Visited Title. * @return {Object} Styles suitable for CSS interpolation. */ function getMostVisitedStyles(params, isTitle) { var styles = { color: getTextColor(params, isTitle), // Handles 'c' in params. fontFamily: '', fontSize: 11 }; if ('f' in params && /^[-0-9a-zA-Z ,]+$/.test(params.f)) styles.fontFamily = params.f; if ('fs' in params && isFinite(parseInt(params.fs, 10))) styles.fontSize = parseInt(params.fs, 10); if ('ta' in params && /^[-0-9a-zA-Z ,]+$/.test(params.ta)) styles.textAlign = params.ta; if ('tf' in params) { var tf = parseInt(params.tf, 10); if (isFinite(tf)) styles.textFadePos = tf; } if ('ntl' in params) { var ntl = parseInt(params.ntl, 10); if (isFinite(ntl)) styles.numTitleLines = ntl; } return styles; } /** * @param {string} location A location containing URL parameters. * @param {function(Object, Object)} fill A function called with styles and * data to fill. */ function fillMostVisited(location, fill) { var params = parseQueryParams(location); params.rid = parseInt(params.rid, 10); if (!isFinite(params.rid) && !params.url) return; var data; if (params.url) { // Means that the suggestion data comes from the server. Create data object. data = { url: params.url, largeIconUrl: params.liu || '', thumbnailUrl: params.tu || '', title: params.ti || '', direction: params.di || '', domain: params.dom || '' }; } else { var apiHandle = chrome.embeddedSearch.newTabPage; data = apiHandle.getMostVisitedItemData(params.rid); if (!data) return; } if (isFinite(params.dummy) && parseInt(params.dummy, 10)) { data.dummy = true; } if (/^javascript:/i.test(data.url) || /^javascript:/i.test(data.thumbnailUrl)) return; if (data.direction) document.body.dir = data.direction; fill(params, data); } /** * Sends a POST request to ping url. * @param {string} url URL to be pinged. */ function generatePing(url) { if (navigator.sendBeacon) { navigator.sendBeacon(url); } else { // if sendBeacon is not enabled, we fallback for "a ping". var a = document.createElement('a'); a.href = '#'; a.ping = url; a.click(); } } Omnibox Debug Page

Enter omnibox input text:

Input parameters:

Current page context:

Display parameters:

/* Copyright (c) 2012 The Chromium Authors. All rights reserved. * Use of this source code is governed by a BSD-style license that can be * found in the LICENSE file. */ .autocomplete-results-table { margin-bottom: 1.5em; } .autocomplete-results-table th { background-color: #C0C0C0; } .autocomplete-results-table td { background-color: #F0F0F0; } .group-separator { display: block; text-decoration: underline; } .check-mark { color: green; text-align: center; } .x-mark { color: red; text-align: center; } p { margin: 0; } .input-section { margin-bottom: 1em; } .table-header { white-space: nowrap; } .additional-info-property, .additional-info-value { white-space: nowrap; } // Copyright (c) 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. /** * Javascript for omnibox.html, served from chrome://omnibox/ * This is used to debug omnibox ranking. The user enters some text * into a box, submits it, and then sees lots of debug information * from the autocompleter that shows what omnibox would do with that * input. * * The simple object defined in this javascript file listens for * certain events on omnibox.html, sends (when appropriate) the * input text to C++ code to start the omnibox autcomplete controller * working, and listens from callbacks from the C++ code saying that * results are available. When results (possibly intermediate ones) * are available, the Javascript formats them and displays them. */ (function() { /** * Register our event handlers. */ function initialize() { $('omnibox-input-form').addEventListener( 'submit', startOmniboxQuery, false); $('prevent-inline-autocomplete').addEventListener( 'change', startOmniboxQuery); $('prefer-keyword').addEventListener('change', startOmniboxQuery); $('page-classification').addEventListener('change', startOmniboxQuery); $('show-details').addEventListener('change', refresh); $('show-incomplete-results').addEventListener('change', refresh); $('show-all-providers').addEventListener('change', refresh); } /** * @type {OmniboxResultMojo} an array of all autocomplete results we've seen * for this query. We append to this list once for every call to * handleNewAutocompleteResult. See omnibox.mojom for details.. */ var progressiveAutocompleteResults = []; /** * @type {number} the value for cursor position we sent with the most * recent request. We need to remember this in order to display it * in the output; otherwise it's hard or impossible to determine * from screen captures or print-to-PDFs. */ var cursorPositionUsed = -1; /** * Extracts the input text from the text field and sends it to the * C++ portion of chrome to handle. The C++ code will iteratively * call handleNewAutocompleteResult as results come in. */ function startOmniboxQuery(event) { // First, clear the results of past calls (if any). progressiveAutocompleteResults = []; // Then, call chrome with a five-element list: // - first element: the value in the text box // - second element: the location of the cursor in the text box // - third element: the value of prevent-inline-autocomplete // - forth element: the value of prefer-keyword // - fifth element: the value of page-classification cursorPositionUsed = $('input-text').selectionEnd; browserProxy.startOmniboxQuery( $('input-text').value, cursorPositionUsed, $('prevent-inline-autocomplete').checked, $('prefer-keyword').checked, parseInt($('page-classification').value)); // Cancel the submit action. i.e., don't submit the form. (We handle // display the results solely with Javascript.) event.preventDefault(); } /** * Returns a simple object with information about how to display an * autocomplete result data field. * @param {string} header the label for the top of the column/table. * @param {string} urlLabelForHeader the URL that the header should point * to (if non-empty). * @param {string} propertyName the name of the property in the autocomplete * result record that we lookup. * @param {boolean} displayAlways whether the property should be displayed * regardless of whether we're in detailed more. * @param {string} tooltip a description of the property that will be * presented as a tooltip when the mouse is hovered over the column title. * @constructor */ function PresentationInfoRecord(header, url, propertyName, displayAlways, tooltip) { this.header = header; this.urlLabelForHeader = url; this.propertyName = propertyName; this.displayAlways = displayAlways; this.tooltip = tooltip; } /** * A constant that's used to decide what autocomplete result * properties to output in what order. This is an array of * PresentationInfoRecord() objects; for details see that * function. * @type {Array} * @const */ var PROPERTY_OUTPUT_ORDER = [ new PresentationInfoRecord('Provider', '', 'provider_name', true, 'The AutocompleteProvider suggesting this result.'), new PresentationInfoRecord('Type', '', 'type', true, 'The type of the result.'), new PresentationInfoRecord('Relevance', '', 'relevance', true, 'The result score. Higher is more relevant.'), new PresentationInfoRecord('Contents', '', 'contents', true, 'The text that is presented identifying the result.'), new PresentationInfoRecord( 'Can Be Default', '', 'allowed_to_be_default_match', false, 'A green checkmark indicates that the result can be the default ' + 'match (i.e., can be the match that pressing enter in the omnibox ' + 'navigates to).'), new PresentationInfoRecord('Starred', '', 'starred', false, 'A green checkmark indicates that the result has been bookmarked.'), new PresentationInfoRecord('Description', '', 'description', false, 'The page title of the result.'), new PresentationInfoRecord('URL', '', 'destination_url', true, 'The URL for the result.'), new PresentationInfoRecord('Fill Into Edit', '', 'fill_into_edit', false, 'The text shown in the omnibox when the result is selected.'), new PresentationInfoRecord( 'Inline Autocompletion', '', 'inline_autocompletion', false, 'The text shown in the omnibox as a blue highlight selection ' + 'following the cursor, if this match is shown inline.'), new PresentationInfoRecord('Del', '', 'deletable', false, 'A green checkmark indicates that the result can be deleted from ' + 'the visit history.'), new PresentationInfoRecord('Prev', '', 'from_previous', false, ''), new PresentationInfoRecord( 'Tran', 'http://code.google.com/codesearch#OAMlx_jo-ck/src/content/public/' + 'common/page_transition_types.h&exact_package=chromium&l=24', 'transition', false, 'How the user got to the result.'), new PresentationInfoRecord( 'Done', '', 'provider_done', false, 'A green checkmark indicates that the provider is done looking for ' + 'more results.'), new PresentationInfoRecord( 'Associated Keyword', '', 'associated_keyword', false, 'If non-empty, a "press tab to search" hint will be shown and will ' + 'engage this keyword.'), new PresentationInfoRecord( 'Keyword', '', 'keyword', false, 'The keyword of the search engine to be used.'), new PresentationInfoRecord( 'Duplicates', '', 'duplicates', false, 'The number of matches that have been marked as duplicates of this ' + 'match.'), new PresentationInfoRecord( 'Additional Info', '', 'additional_info', false, 'Provider-specific information about the result.') ]; /** * Returns an HTML Element of type table row that contains the * headers we'll use for labeling the columns. If we're in * detailed_mode, we use all the headers. If not, we only use ones * marked displayAlways. */ function createAutocompleteResultTableHeader() { var row = document.createElement('tr'); var inDetailedMode = $('show-details').checked; for (var i = 0; i < PROPERTY_OUTPUT_ORDER.length; i++) { if (inDetailedMode || PROPERTY_OUTPUT_ORDER[i].displayAlways) { var headerCell = document.createElement('th'); if (PROPERTY_OUTPUT_ORDER[i].urlLabelForHeader != '') { // Wrap header text in URL. var linkNode = document.createElement('a'); linkNode.href = PROPERTY_OUTPUT_ORDER[i].urlLabelForHeader; linkNode.textContent = PROPERTY_OUTPUT_ORDER[i].header; headerCell.appendChild(linkNode); } else { // Output header text without a URL. headerCell.textContent = PROPERTY_OUTPUT_ORDER[i].header; headerCell.className = 'table-header'; headerCell.title = PROPERTY_OUTPUT_ORDER[i].tooltip; } row.appendChild(headerCell); } } return row; } /** * @param {AutocompleteMatchMojo} autocompleteSuggestion the particular * autocomplete suggestion we're in the process of displaying. * @param {string} propertyName the particular property of the autocomplete * suggestion that should go in this cell. * @return {HTMLTableCellElement} that contains the value within this * autocompleteSuggestion associated with propertyName. */ function createCellForPropertyAndRemoveProperty(autocompleteSuggestion, propertyName) { var cell = document.createElement('td'); if (propertyName in autocompleteSuggestion) { if (propertyName == 'additional_info') { // |additional_info| embeds a two-column table of provider-specific data // within this cell. |additional_info| is an array of // AutocompleteAdditionalInfo. var additionalInfoTable = document.createElement('table'); for (var i = 0; i < autocompleteSuggestion[propertyName].length; i++) { var additionalInfo = autocompleteSuggestion[propertyName][i]; var additionalInfoRow = document.createElement('tr'); // Set the title (name of property) cell text. var propertyCell = document.createElement('td'); propertyCell.textContent = additionalInfo.key + ':'; propertyCell.className = 'additional-info-property'; additionalInfoRow.appendChild(propertyCell); // Set the value of the property cell text. var valueCell = document.createElement('td'); valueCell.textContent = additionalInfo.value; valueCell.className = 'additional-info-value'; additionalInfoRow.appendChild(valueCell); additionalInfoTable.appendChild(additionalInfoRow); } cell.appendChild(additionalInfoTable); } else if (typeof autocompleteSuggestion[propertyName] == 'boolean') { // If this is a boolean, display a checkmark or an X instead of // the strings true or false. if (autocompleteSuggestion[propertyName]) { cell.className = 'check-mark'; cell.textContent = '✔'; } else { cell.className = 'x-mark'; cell.textContent = '✗'; } } else { var text = String(autocompleteSuggestion[propertyName]); // If it's a URL wrap it in an href. var re = /^(http|https|ftp|chrome|file):\/\//; if (re.test(text)) { var aCell = document.createElement('a'); aCell.textContent = text; aCell.href = text; cell.appendChild(aCell); } else { // All other data types (integer, strings, etc.) display their // normal toString() output. cell.textContent = autocompleteSuggestion[propertyName]; } } } // else: if propertyName is undefined, we leave the cell blank return cell; } /** * Appends some human-readable information about the provided * autocomplete result to the HTML node with id omnibox-debug-text. * The current human-readable form is a few lines about general * autocomplete result statistics followed by a table with one line * for each autocomplete match. The input parameter is an OmniboxResultMojo. */ function addResultToOutput(result) { var output = $('omnibox-debug-text'); var inDetailedMode = $('show-details').checked; var showIncompleteResults = $('show-incomplete-results').checked; var showPerProviderResults = $('show-all-providers').checked; // Always output cursor position. var p = document.createElement('p'); p.textContent = 'cursor position = ' + cursorPositionUsed; output.appendChild(p); // Output the result-level features in detailed mode and in // show incomplete results mode. We do the latter because without // these result-level features, one can't make sense of each // batch of results. if (inDetailedMode || showIncompleteResults) { var p1 = document.createElement('p'); p1.textContent = 'elapsed time = ' + result.time_since_omnibox_started_ms + 'ms'; output.appendChild(p1); var p2 = document.createElement('p'); p2.textContent = 'all providers done = ' + result.done; output.appendChild(p2); var p3 = document.createElement('p'); p3.textContent = 'host = ' + result.host; if ('is_typed_host' in result) { // Only output the is_typed_host information if available. (It may // be missing if the history database lookup failed.) p3.textContent = p3.textContent + ' has is_typed_host = ' + result.is_typed_host; } output.appendChild(p3); } // Combined results go after the lines below. var group = document.createElement('a'); group.className = 'group-separator'; group.textContent = 'Combined results.'; output.appendChild(group); // Add combined/merged result table. var p = document.createElement('p'); p.appendChild(addResultTableToOutput(result.combined_results)); output.appendChild(p); // Move forward only if you want to display per provider results. if (!showPerProviderResults) { return; } // Individual results go after the lines below. var group = document.createElement('a'); group.className = 'group-separator'; group.textContent = 'Results for individual providers.'; output.appendChild(group); // Add the per-provider result tables with labels. We do not append the // combined/merged result table since we already have the per provider // results. for (var i = 0; i < result.results_by_provider.length; i++) { var providerResults = result.results_by_provider[i]; // If we have no results we do not display anything. if (providerResults.results.length == 0) { continue; } var p = document.createElement('p'); p.appendChild(addResultTableToOutput(providerResults.results)); output.appendChild(p); } } /** * @param {Object} result an array of AutocompleteMatchMojos. * @return {HTMLTableCellElement} that is a user-readable HTML * representation of this object. */ function addResultTableToOutput(result) { var inDetailedMode = $('show-details').checked; // Create a table to hold all the autocomplete items. var table = document.createElement('table'); table.className = 'autocomplete-results-table'; table.appendChild(createAutocompleteResultTableHeader()); // Loop over every autocomplete item and add it as a row in the table. for (var i = 0; i < result.length; i++) { var autocompleteSuggestion = result[i]; var row = document.createElement('tr'); // Loop over all the columns/properties and output either them // all (if we're in detailed mode) or only the ones marked displayAlways. // Keep track of which properties we displayed. var displayedProperties = {}; for (var j = 0; j < PROPERTY_OUTPUT_ORDER.length; j++) { if (inDetailedMode || PROPERTY_OUTPUT_ORDER[j].displayAlways) { row.appendChild(createCellForPropertyAndRemoveProperty( autocompleteSuggestion, PROPERTY_OUTPUT_ORDER[j].propertyName)); displayedProperties[PROPERTY_OUTPUT_ORDER[j].propertyName] = true; } } // Now, if we're in detailed mode, add all the properties that // haven't already been output. (We know which properties have // already been output because we delete the property when we output // it. The only way we have properties left at this point if // we're in detailed mode and we're getting back properties // not listed in PROPERTY_OUTPUT_ORDER. Perhaps someone added // something to the C++ code but didn't bother to update this // Javascript? In any case, we want to display them.) if (inDetailedMode) { for (var key in autocompleteSuggestion) { if (!displayedProperties[key] && typeof autocompleteSuggestion[key] != 'function') { var cell = document.createElement('td'); cell.textContent = key + '=' + autocompleteSuggestion[key]; row.appendChild(cell); } } } table.appendChild(row); } return table; } /* Repaints the page based on the contents of the array * progressiveAutocompleteResults, which represents consecutive * autocomplete results. We only display the last (most recent) * entry unless we're asked to display incomplete results. For an * example of the output, play with chrome://omnibox/ */ function refresh() { // Erase whatever is currently being displayed. var output = $('omnibox-debug-text'); output.innerHTML = ''; if (progressiveAutocompleteResults.length > 0) { // if we have results // Display the results. var showIncompleteResults = $('show-incomplete-results').checked; var startIndex = showIncompleteResults ? 0 : progressiveAutocompleteResults.length - 1; for (var i = startIndex; i < progressiveAutocompleteResults.length; i++) { addResultToOutput(progressiveAutocompleteResults[i]); } } } // NOTE: Need to keep a global reference to the |pageImpl| such that it is not // garbage collected, which causes the pipe to close and future calls from C++ // to JS to get dropped. var pageImpl = null; var browserProxy = null; function initializeProxies() { return importModules([ 'mojo/public/js/bindings', 'chrome/browser/ui/webui/omnibox/omnibox.mojom', 'content/public/renderer/frame_interfaces', ]).then(function(modules) { var bindings = modules[0]; var mojom = modules[1]; var frameInterfaces = modules[2]; browserProxy = new mojom.OmniboxPageHandlerPtr( frameInterfaces.getInterface(mojom.OmniboxPageHandler.name)); /** @constructor */ var OmniboxPageImpl = function() { this.binding = new bindings.Binding(mojom.OmniboxPage, this); }; OmniboxPageImpl.prototype = { /** @override */ handleNewAutocompleteResult: function(result) { progressiveAutocompleteResults.push(result); refresh(); }, }; pageImpl = new OmniboxPageImpl(); browserProxy.setClientPage(pageImpl.binding.createInterfacePtrAndBind()); }); } document.addEventListener('DOMContentLoaded', function() { return initializeProxies().then(function() { initialize(); }); }); })(); // Copyright 2014 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. define("chrome/browser/ui/webui/omnibox/omnibox.mojom", [ "mojo/public/js/bindings", "mojo/public/js/codec", "mojo/public/js/core", "mojo/public/js/validator", ], function(bindings, codec, core, validator) { function AutocompleteAdditionalInfo(values) { this.initDefaults_(); this.initFields_(values); } AutocompleteAdditionalInfo.prototype.initDefaults_ = function() { this.key = null; this.value = null; }; AutocompleteAdditionalInfo.prototype.initFields_ = function(fields) { for(var field in fields) { if (this.hasOwnProperty(field)) this[field] = fields[field]; } }; AutocompleteAdditionalInfo.validate = function(messageValidator, offset) { var err; err = messageValidator.validateStructHeader(offset, codec.kStructHeaderSize); if (err !== validator.validationError.NONE) return err; var kVersionSizes = [ {version: 0, numBytes: 24} ]; err = messageValidator.validateStructVersion(offset, kVersionSizes); if (err !== validator.validationError.NONE) return err; // validate AutocompleteAdditionalInfo.key err = messageValidator.validateStringPointer(offset + codec.kStructHeaderSize + 0, false) if (err !== validator.validationError.NONE) return err; // validate AutocompleteAdditionalInfo.value err = messageValidator.validateStringPointer(offset + codec.kStructHeaderSize + 8, false) if (err !== validator.validationError.NONE) return err; return validator.validationError.NONE; }; AutocompleteAdditionalInfo.encodedSize = codec.kStructHeaderSize + 16; AutocompleteAdditionalInfo.decode = function(decoder) { var packed; var val = new AutocompleteAdditionalInfo(); var numberOfBytes = decoder.readUint32(); var version = decoder.readUint32(); val.key = decoder.decodeStruct(codec.String); val.value = decoder.decodeStruct(codec.String); return val; }; AutocompleteAdditionalInfo.encode = function(encoder, val) { var packed; encoder.writeUint32(AutocompleteAdditionalInfo.encodedSize); encoder.writeUint32(0); encoder.encodeStruct(codec.String, val.key); encoder.encodeStruct(codec.String, val.value); }; function AutocompleteMatch(values) { this.initDefaults_(); this.initFields_(values); } AutocompleteMatch.prototype.initDefaults_ = function() { this.provider_name = null; this.provider_done = false; this.deletable = false; this.allowed_to_be_default_match = false; this.starred = false; this.from_previous = false; this.relevance = 0; this.fill_into_edit = null; this.inline_autocompletion = null; this.destination_url = null; this.contents = null; this.description = null; this.transition = 0; this.duplicates = 0; this.type = null; this.associated_keyword = null; this.keyword = null; this.additional_info = null; }; AutocompleteMatch.prototype.initFields_ = function(fields) { for(var field in fields) { if (this.hasOwnProperty(field)) this[field] = fields[field]; } }; AutocompleteMatch.validate = function(messageValidator, offset) { var err; err = messageValidator.validateStructHeader(offset, codec.kStructHeaderSize); if (err !== validator.validationError.NONE) return err; var kVersionSizes = [ {version: 0, numBytes: 104} ]; err = messageValidator.validateStructVersion(offset, kVersionSizes); if (err !== validator.validationError.NONE) return err; // validate AutocompleteMatch.provider_name err = messageValidator.validateStringPointer(offset + codec.kStructHeaderSize + 0, true) if (err !== validator.validationError.NONE) return err; // validate AutocompleteMatch.fill_into_edit err = messageValidator.validateStringPointer(offset + codec.kStructHeaderSize + 16, false) if (err !== validator.validationError.NONE) return err; // validate AutocompleteMatch.inline_autocompletion err = messageValidator.validateStringPointer(offset + codec.kStructHeaderSize + 24, false) if (err !== validator.validationError.NONE) return err; // validate AutocompleteMatch.destination_url err = messageValidator.validateStringPointer(offset + codec.kStructHeaderSize + 32, false) if (err !== validator.validationError.NONE) return err; // validate AutocompleteMatch.contents err = messageValidator.validateStringPointer(offset + codec.kStructHeaderSize + 40, false) if (err !== validator.validationError.NONE) return err; // validate AutocompleteMatch.description err = messageValidator.validateStringPointer(offset + codec.kStructHeaderSize + 48, false) if (err !== validator.validationError.NONE) return err; // validate AutocompleteMatch.type err = messageValidator.validateStringPointer(offset + codec.kStructHeaderSize + 64, false) if (err !== validator.validationError.NONE) return err; // validate AutocompleteMatch.associated_keyword err = messageValidator.validateStringPointer(offset + codec.kStructHeaderSize + 72, true) if (err !== validator.validationError.NONE) return err; // validate AutocompleteMatch.keyword err = messageValidator.validateStringPointer(offset + codec.kStructHeaderSize + 80, false) if (err !== validator.validationError.NONE) return err; // validate AutocompleteMatch.additional_info err = messageValidator.validateArrayPointer(offset + codec.kStructHeaderSize + 88, 8, new codec.PointerTo(AutocompleteAdditionalInfo), false, [0], 0); if (err !== validator.validationError.NONE) return err; return validator.validationError.NONE; }; AutocompleteMatch.encodedSize = codec.kStructHeaderSize + 96; AutocompleteMatch.decode = function(decoder) { var packed; var val = new AutocompleteMatch(); var numberOfBytes = decoder.readUint32(); var version = decoder.readUint32(); val.provider_name = decoder.decodeStruct(codec.NullableString); packed = decoder.readUint8(); val.provider_done = (packed >> 0) & 1 ? true : false; val.deletable = (packed >> 1) & 1 ? true : false; val.allowed_to_be_default_match = (packed >> 2) & 1 ? true : false; val.starred = (packed >> 3) & 1 ? true : false; val.from_previous = (packed >> 4) & 1 ? true : false; decoder.skip(1); decoder.skip(1); decoder.skip(1); val.relevance = decoder.decodeStruct(codec.Int32); val.fill_into_edit = decoder.decodeStruct(codec.String); val.inline_autocompletion = decoder.decodeStruct(codec.String); val.destination_url = decoder.decodeStruct(codec.String); val.contents = decoder.decodeStruct(codec.String); val.description = decoder.decodeStruct(codec.String); val.transition = decoder.decodeStruct(codec.Int32); val.duplicates = decoder.decodeStruct(codec.Int32); val.type = decoder.decodeStruct(codec.String); val.associated_keyword = decoder.decodeStruct(codec.NullableString); val.keyword = decoder.decodeStruct(codec.String); val.additional_info = decoder.decodeArrayPointer(new codec.PointerTo(AutocompleteAdditionalInfo)); return val; }; AutocompleteMatch.encode = function(encoder, val) { var packed; encoder.writeUint32(AutocompleteMatch.encodedSize); encoder.writeUint32(0); encoder.encodeStruct(codec.NullableString, val.provider_name); packed = 0; packed |= (val.provider_done & 1) << 0 packed |= (val.deletable & 1) << 1 packed |= (val.allowed_to_be_default_match & 1) << 2 packed |= (val.starred & 1) << 3 packed |= (val.from_previous & 1) << 4 encoder.writeUint8(packed); encoder.skip(1); encoder.skip(1); encoder.skip(1); encoder.encodeStruct(codec.Int32, val.relevance); encoder.encodeStruct(codec.String, val.fill_into_edit); encoder.encodeStruct(codec.String, val.inline_autocompletion); encoder.encodeStruct(codec.String, val.destination_url); encoder.encodeStruct(codec.String, val.contents); encoder.encodeStruct(codec.String, val.description); encoder.encodeStruct(codec.Int32, val.transition); encoder.encodeStruct(codec.Int32, val.duplicates); encoder.encodeStruct(codec.String, val.type); encoder.encodeStruct(codec.NullableString, val.associated_keyword); encoder.encodeStruct(codec.String, val.keyword); encoder.encodeArrayPointer(new codec.PointerTo(AutocompleteAdditionalInfo), val.additional_info); }; function AutocompleteResultsForProvider(values) { this.initDefaults_(); this.initFields_(values); } AutocompleteResultsForProvider.prototype.initDefaults_ = function() { this.provider_name = null; this.results = null; }; AutocompleteResultsForProvider.prototype.initFields_ = function(fields) { for(var field in fields) { if (this.hasOwnProperty(field)) this[field] = fields[field]; } }; AutocompleteResultsForProvider.validate = function(messageValidator, offset) { var err; err = messageValidator.validateStructHeader(offset, codec.kStructHeaderSize); if (err !== validator.validationError.NONE) return err; var kVersionSizes = [ {version: 0, numBytes: 24} ]; err = messageValidator.validateStructVersion(offset, kVersionSizes); if (err !== validator.validationError.NONE) return err; // validate AutocompleteResultsForProvider.provider_name err = messageValidator.validateStringPointer(offset + codec.kStructHeaderSize + 0, false) if (err !== validator.validationError.NONE) return err; // validate AutocompleteResultsForProvider.results err = messageValidator.validateArrayPointer(offset + codec.kStructHeaderSize + 8, 8, new codec.PointerTo(AutocompleteMatch), false, [0], 0); if (err !== validator.validationError.NONE) return err; return validator.validationError.NONE; }; AutocompleteResultsForProvider.encodedSize = codec.kStructHeaderSize + 16; AutocompleteResultsForProvider.decode = function(decoder) { var packed; var val = new AutocompleteResultsForProvider(); var numberOfBytes = decoder.readUint32(); var version = decoder.readUint32(); val.provider_name = decoder.decodeStruct(codec.String); val.results = decoder.decodeArrayPointer(new codec.PointerTo(AutocompleteMatch)); return val; }; AutocompleteResultsForProvider.encode = function(encoder, val) { var packed; encoder.writeUint32(AutocompleteResultsForProvider.encodedSize); encoder.writeUint32(0); encoder.encodeStruct(codec.String, val.provider_name); encoder.encodeArrayPointer(new codec.PointerTo(AutocompleteMatch), val.results); }; function OmniboxResult(values) { this.initDefaults_(); this.initFields_(values); } OmniboxResult.prototype.initDefaults_ = function() { this.done = false; this.is_typed_host = false; this.time_since_omnibox_started_ms = 0; this.host = null; this.combined_results = null; this.results_by_provider = null; }; OmniboxResult.prototype.initFields_ = function(fields) { for(var field in fields) { if (this.hasOwnProperty(field)) this[field] = fields[field]; } }; OmniboxResult.validate = function(messageValidator, offset) { var err; err = messageValidator.validateStructHeader(offset, codec.kStructHeaderSize); if (err !== validator.validationError.NONE) return err; var kVersionSizes = [ {version: 0, numBytes: 40} ]; err = messageValidator.validateStructVersion(offset, kVersionSizes); if (err !== validator.validationError.NONE) return err; // validate OmniboxResult.host err = messageValidator.validateStringPointer(offset + codec.kStructHeaderSize + 8, false) if (err !== validator.validationError.NONE) return err; // validate OmniboxResult.combined_results err = messageValidator.validateArrayPointer(offset + codec.kStructHeaderSize + 16, 8, new codec.PointerTo(AutocompleteMatch), false, [0], 0); if (err !== validator.validationError.NONE) return err; // validate OmniboxResult.results_by_provider err = messageValidator.validateArrayPointer(offset + codec.kStructHeaderSize + 24, 8, new codec.PointerTo(AutocompleteResultsForProvider), false, [0], 0); if (err !== validator.validationError.NONE) return err; return validator.validationError.NONE; }; OmniboxResult.encodedSize = codec.kStructHeaderSize + 32; OmniboxResult.decode = function(decoder) { var packed; var val = new OmniboxResult(); var numberOfBytes = decoder.readUint32(); var version = decoder.readUint32(); packed = decoder.readUint8(); val.done = (packed >> 0) & 1 ? true : false; val.is_typed_host = (packed >> 1) & 1 ? true : false; decoder.skip(1); decoder.skip(1); decoder.skip(1); val.time_since_omnibox_started_ms = decoder.decodeStruct(codec.Int32); val.host = decoder.decodeStruct(codec.String); val.combined_results = decoder.decodeArrayPointer(new codec.PointerTo(AutocompleteMatch)); val.results_by_provider = decoder.decodeArrayPointer(new codec.PointerTo(AutocompleteResultsForProvider)); return val; }; OmniboxResult.encode = function(encoder, val) { var packed; encoder.writeUint32(OmniboxResult.encodedSize); encoder.writeUint32(0); packed = 0; packed |= (val.done & 1) << 0 packed |= (val.is_typed_host & 1) << 1 encoder.writeUint8(packed); encoder.skip(1); encoder.skip(1); encoder.skip(1); encoder.encodeStruct(codec.Int32, val.time_since_omnibox_started_ms); encoder.encodeStruct(codec.String, val.host); encoder.encodeArrayPointer(new codec.PointerTo(AutocompleteMatch), val.combined_results); encoder.encodeArrayPointer(new codec.PointerTo(AutocompleteResultsForProvider), val.results_by_provider); }; function OmniboxPageHandler_SetClientPage_Params(values) { this.initDefaults_(); this.initFields_(values); } OmniboxPageHandler_SetClientPage_Params.prototype.initDefaults_ = function() { this.page = new OmniboxPagePtr(); }; OmniboxPageHandler_SetClientPage_Params.prototype.initFields_ = function(fields) { for(var field in fields) { if (this.hasOwnProperty(field)) this[field] = fields[field]; } }; OmniboxPageHandler_SetClientPage_Params.validate = function(messageValidator, offset) { var err; err = messageValidator.validateStructHeader(offset, codec.kStructHeaderSize); if (err !== validator.validationError.NONE) return err; var kVersionSizes = [ {version: 0, numBytes: 16} ]; err = messageValidator.validateStructVersion(offset, kVersionSizes); if (err !== validator.validationError.NONE) return err; // validate OmniboxPageHandler_SetClientPage_Params.page err = messageValidator.validateInterface(offset + codec.kStructHeaderSize + 0, false); if (err !== validator.validationError.NONE) return err; return validator.validationError.NONE; }; OmniboxPageHandler_SetClientPage_Params.encodedSize = codec.kStructHeaderSize + 8; OmniboxPageHandler_SetClientPage_Params.decode = function(decoder) { var packed; var val = new OmniboxPageHandler_SetClientPage_Params(); var numberOfBytes = decoder.readUint32(); var version = decoder.readUint32(); val.page = decoder.decodeStruct(new codec.Interface(OmniboxPagePtr)); return val; }; OmniboxPageHandler_SetClientPage_Params.encode = function(encoder, val) { var packed; encoder.writeUint32(OmniboxPageHandler_SetClientPage_Params.encodedSize); encoder.writeUint32(0); encoder.encodeStruct(new codec.Interface(OmniboxPagePtr), val.page); }; function OmniboxPageHandler_StartOmniboxQuery_Params(values) { this.initDefaults_(); this.initFields_(values); } OmniboxPageHandler_StartOmniboxQuery_Params.prototype.initDefaults_ = function() { this.input_string = null; this.cursor_position = 0; this.prevent_inline_autocomplete = false; this.prefer_keyword = false; this.page_classification = 0; }; OmniboxPageHandler_StartOmniboxQuery_Params.prototype.initFields_ = function(fields) { for(var field in fields) { if (this.hasOwnProperty(field)) this[field] = fields[field]; } }; OmniboxPageHandler_StartOmniboxQuery_Params.validate = function(messageValidator, offset) { var err; err = messageValidator.validateStructHeader(offset, codec.kStructHeaderSize); if (err !== validator.validationError.NONE) return err; var kVersionSizes = [ {version: 0, numBytes: 32} ]; err = messageValidator.validateStructVersion(offset, kVersionSizes); if (err !== validator.validationError.NONE) return err; // validate OmniboxPageHandler_StartOmniboxQuery_Params.input_string err = messageValidator.validateStringPointer(offset + codec.kStructHeaderSize + 0, false) if (err !== validator.validationError.NONE) return err; return validator.validationError.NONE; }; OmniboxPageHandler_StartOmniboxQuery_Params.encodedSize = codec.kStructHeaderSize + 24; OmniboxPageHandler_StartOmniboxQuery_Params.decode = function(decoder) { var packed; var val = new OmniboxPageHandler_StartOmniboxQuery_Params(); var numberOfBytes = decoder.readUint32(); var version = decoder.readUint32(); val.input_string = decoder.decodeStruct(codec.String); val.cursor_position = decoder.decodeStruct(codec.Int32); packed = decoder.readUint8(); val.prevent_inline_autocomplete = (packed >> 0) & 1 ? true : false; val.prefer_keyword = (packed >> 1) & 1 ? true : false; decoder.skip(1); decoder.skip(1); decoder.skip(1); val.page_classification = decoder.decodeStruct(codec.Int32); decoder.skip(1); decoder.skip(1); decoder.skip(1); decoder.skip(1); return val; }; OmniboxPageHandler_StartOmniboxQuery_Params.encode = function(encoder, val) { var packed; encoder.writeUint32(OmniboxPageHandler_StartOmniboxQuery_Params.encodedSize); encoder.writeUint32(0); encoder.encodeStruct(codec.String, val.input_string); encoder.encodeStruct(codec.Int32, val.cursor_position); packed = 0; packed |= (val.prevent_inline_autocomplete & 1) << 0 packed |= (val.prefer_keyword & 1) << 1 encoder.writeUint8(packed); encoder.skip(1); encoder.skip(1); encoder.skip(1); encoder.encodeStruct(codec.Int32, val.page_classification); encoder.skip(1); encoder.skip(1); encoder.skip(1); encoder.skip(1); }; function OmniboxPage_HandleNewAutocompleteResult_Params(values) { this.initDefaults_(); this.initFields_(values); } OmniboxPage_HandleNewAutocompleteResult_Params.prototype.initDefaults_ = function() { this.result = null; }; OmniboxPage_HandleNewAutocompleteResult_Params.prototype.initFields_ = function(fields) { for(var field in fields) { if (this.hasOwnProperty(field)) this[field] = fields[field]; } }; OmniboxPage_HandleNewAutocompleteResult_Params.validate = function(messageValidator, offset) { var err; err = messageValidator.validateStructHeader(offset, codec.kStructHeaderSize); if (err !== validator.validationError.NONE) return err; var kVersionSizes = [ {version: 0, numBytes: 16} ]; err = messageValidator.validateStructVersion(offset, kVersionSizes); if (err !== validator.validationError.NONE) return err; // validate OmniboxPage_HandleNewAutocompleteResult_Params.result err = messageValidator.validateStructPointer(offset + codec.kStructHeaderSize + 0, OmniboxResult, false); if (err !== validator.validationError.NONE) return err; return validator.validationError.NONE; }; OmniboxPage_HandleNewAutocompleteResult_Params.encodedSize = codec.kStructHeaderSize + 8; OmniboxPage_HandleNewAutocompleteResult_Params.decode = function(decoder) { var packed; var val = new OmniboxPage_HandleNewAutocompleteResult_Params(); var numberOfBytes = decoder.readUint32(); var version = decoder.readUint32(); val.result = decoder.decodeStructPointer(OmniboxResult); return val; }; OmniboxPage_HandleNewAutocompleteResult_Params.encode = function(encoder, val) { var packed; encoder.writeUint32(OmniboxPage_HandleNewAutocompleteResult_Params.encodedSize); encoder.writeUint32(0); encoder.encodeStructPointer(OmniboxResult, val.result); }; var kOmniboxPageHandler_SetClientPage_Name = 0; var kOmniboxPageHandler_StartOmniboxQuery_Name = 1; function OmniboxPageHandlerPtr(handleOrPtrInfo) { this.ptr = new bindings.InterfacePtrController(OmniboxPageHandler, handleOrPtrInfo); } function OmniboxPageHandlerProxy(receiver) { this.receiver_ = receiver; } OmniboxPageHandlerPtr.prototype.setClientPage = function() { return OmniboxPageHandlerProxy.prototype.setClientPage .apply(this.ptr.getProxy(), arguments); }; OmniboxPageHandlerProxy.prototype.setClientPage = function(page) { var params = new OmniboxPageHandler_SetClientPage_Params(); params.page = page; var builder = new codec.MessageBuilder( kOmniboxPageHandler_SetClientPage_Name, codec.align(OmniboxPageHandler_SetClientPage_Params.encodedSize)); builder.encodeStruct(OmniboxPageHandler_SetClientPage_Params, params); var message = builder.finish(); this.receiver_.accept(message); }; OmniboxPageHandlerPtr.prototype.startOmniboxQuery = function() { return OmniboxPageHandlerProxy.prototype.startOmniboxQuery .apply(this.ptr.getProxy(), arguments); }; OmniboxPageHandlerProxy.prototype.startOmniboxQuery = function(input_string, cursor_position, prevent_inline_autocomplete, prefer_keyword, page_classification) { var params = new OmniboxPageHandler_StartOmniboxQuery_Params(); params.input_string = input_string; params.cursor_position = cursor_position; params.prevent_inline_autocomplete = prevent_inline_autocomplete; params.prefer_keyword = prefer_keyword; params.page_classification = page_classification; var builder = new codec.MessageBuilder( kOmniboxPageHandler_StartOmniboxQuery_Name, codec.align(OmniboxPageHandler_StartOmniboxQuery_Params.encodedSize)); builder.encodeStruct(OmniboxPageHandler_StartOmniboxQuery_Params, params); var message = builder.finish(); this.receiver_.accept(message); }; function OmniboxPageHandlerStub(delegate) { this.delegate_ = delegate; } OmniboxPageHandlerStub.prototype.setClientPage = function(page) { return this.delegate_ && this.delegate_.setClientPage && this.delegate_.setClientPage(page); } OmniboxPageHandlerStub.prototype.startOmniboxQuery = function(input_string, cursor_position, prevent_inline_autocomplete, prefer_keyword, page_classification) { return this.delegate_ && this.delegate_.startOmniboxQuery && this.delegate_.startOmniboxQuery(input_string, cursor_position, prevent_inline_autocomplete, prefer_keyword, page_classification); } OmniboxPageHandlerStub.prototype.accept = function(message) { var reader = new codec.MessageReader(message); switch (reader.messageName) { case kOmniboxPageHandler_SetClientPage_Name: var params = reader.decodeStruct(OmniboxPageHandler_SetClientPage_Params); this.setClientPage(params.page); return true; case kOmniboxPageHandler_StartOmniboxQuery_Name: var params = reader.decodeStruct(OmniboxPageHandler_StartOmniboxQuery_Params); this.startOmniboxQuery(params.input_string, params.cursor_position, params.prevent_inline_autocomplete, params.prefer_keyword, params.page_classification); return true; default: return false; } }; OmniboxPageHandlerStub.prototype.acceptWithResponder = function(message, responder) { var reader = new codec.MessageReader(message); switch (reader.messageName) { default: return Promise.reject(Error("Unhandled message: " + reader.messageName)); } }; function validateOmniboxPageHandlerRequest(messageValidator) { var message = messageValidator.message; var paramsClass = null; switch (message.getName()) { case kOmniboxPageHandler_SetClientPage_Name: if (!message.expectsResponse() && !message.isResponse()) paramsClass = OmniboxPageHandler_SetClientPage_Params; break; case kOmniboxPageHandler_StartOmniboxQuery_Name: if (!message.expectsResponse() && !message.isResponse()) paramsClass = OmniboxPageHandler_StartOmniboxQuery_Params; break; } if (paramsClass === null) return validator.validationError.NONE; return paramsClass.validate(messageValidator, messageValidator.message.getHeaderNumBytes()); } function validateOmniboxPageHandlerResponse(messageValidator) { return validator.validationError.NONE; } var OmniboxPageHandler = { name: 'mojom::OmniboxPageHandler', ptrClass: OmniboxPageHandlerPtr, proxyClass: OmniboxPageHandlerProxy, stubClass: OmniboxPageHandlerStub, validateRequest: validateOmniboxPageHandlerRequest, validateResponse: null, }; OmniboxPageHandlerStub.prototype.validator = validateOmniboxPageHandlerRequest; OmniboxPageHandlerProxy.prototype.validator = null; var kOmniboxPage_HandleNewAutocompleteResult_Name = 0; function OmniboxPagePtr(handleOrPtrInfo) { this.ptr = new bindings.InterfacePtrController(OmniboxPage, handleOrPtrInfo); } function OmniboxPageProxy(receiver) { this.receiver_ = receiver; } OmniboxPagePtr.prototype.handleNewAutocompleteResult = function() { return OmniboxPageProxy.prototype.handleNewAutocompleteResult .apply(this.ptr.getProxy(), arguments); }; OmniboxPageProxy.prototype.handleNewAutocompleteResult = function(result) { var params = new OmniboxPage_HandleNewAutocompleteResult_Params(); params.result = result; var builder = new codec.MessageBuilder( kOmniboxPage_HandleNewAutocompleteResult_Name, codec.align(OmniboxPage_HandleNewAutocompleteResult_Params.encodedSize)); builder.encodeStruct(OmniboxPage_HandleNewAutocompleteResult_Params, params); var message = builder.finish(); this.receiver_.accept(message); }; function OmniboxPageStub(delegate) { this.delegate_ = delegate; } OmniboxPageStub.prototype.handleNewAutocompleteResult = function(result) { return this.delegate_ && this.delegate_.handleNewAutocompleteResult && this.delegate_.handleNewAutocompleteResult(result); } OmniboxPageStub.prototype.accept = function(message) { var reader = new codec.MessageReader(message); switch (reader.messageName) { case kOmniboxPage_HandleNewAutocompleteResult_Name: var params = reader.decodeStruct(OmniboxPage_HandleNewAutocompleteResult_Params); this.handleNewAutocompleteResult(params.result); return true; default: return false; } }; OmniboxPageStub.prototype.acceptWithResponder = function(message, responder) { var reader = new codec.MessageReader(message); switch (reader.messageName) { default: return Promise.reject(Error("Unhandled message: " + reader.messageName)); } }; function validateOmniboxPageRequest(messageValidator) { var message = messageValidator.message; var paramsClass = null; switch (message.getName()) { case kOmniboxPage_HandleNewAutocompleteResult_Name: if (!message.expectsResponse() && !message.isResponse()) paramsClass = OmniboxPage_HandleNewAutocompleteResult_Params; break; } if (paramsClass === null) return validator.validationError.NONE; return paramsClass.validate(messageValidator, messageValidator.message.getHeaderNumBytes()); } function validateOmniboxPageResponse(messageValidator) { return validator.validationError.NONE; } var OmniboxPage = { name: 'mojom::OmniboxPage', ptrClass: OmniboxPagePtr, proxyClass: OmniboxPageProxy, stubClass: OmniboxPageStub, validateRequest: validateOmniboxPageRequest, validateResponse: null, }; OmniboxPageStub.prototype.validator = validateOmniboxPageRequest; OmniboxPageProxy.prototype.validator = null; var exports = {}; exports.AutocompleteAdditionalInfo = AutocompleteAdditionalInfo; exports.AutocompleteMatch = AutocompleteMatch; exports.AutocompleteResultsForProvider = AutocompleteResultsForProvider; exports.OmniboxResult = OmniboxResult; exports.OmniboxPageHandler = OmniboxPageHandler; exports.OmniboxPageHandlerPtr = OmniboxPageHandlerPtr; exports.OmniboxPage = OmniboxPage; exports.OmniboxPagePtr = OmniboxPagePtr; return exports; });VO0~G0%Z&D6i$/MirNYw4)-i]Hr|w6Tہvg RX>\T!fN $*LV .HA%r#"A[1A1"88y4CX GXJE`_{=HYZ)m2kĝh xhG\6 li֝jOUbܐa(cI4795+W CbFTpV(#GUfA!L`V=;}b%~4͋MBOL35U<,8J/04vݖs#79(Shyur2TK$ 9X {~.T?s Y徝ǒr+!yP4 B]13QJ(z>|e%(=ȵQ8c`dogMVf$=)W:^Tc+"z]F|ʎ[ ʦlu[gb-ʗw %gg}.(aPkW.yA-{P5ɓh7 ֵS}ۃ;14 kYię}8Ch+GvB:iIJ // Copyright 2013 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. 'use strict'; /** * Takes the |componentsData| input argument which represents data about the * currently installed components and populates the html jstemplate with * that data. It expects an object structure like the above. * @param {Object} componentsData Detailed info about installed components. * Same expected format as returnComponentsData(). */ function renderTemplate(componentsData) { // This is the javascript code that processes the template: var input = new JsEvalContext(componentsData); var output = $('component-template').cloneNode(true); $('component-placeholder').innerHTML = ''; $('component-placeholder').appendChild(output); jstProcess(input, output); output.removeAttribute('hidden'); } /** * Asks the C++ ComponentsDOMHandler to get details about the installed * components. * The ComponentsDOMHandler should reply to returnComponentsData() (below). */ function requestComponentsData() { chrome.send('requestComponentsData'); } /** * Called by the WebUI to re-populate the page with data representing the * current state of installed components. * @param {Object} componentsData Detailed info about installed components. The * template expects each component's format to match the following * structure to correctly populate the page: * { * components: [ * { * name: 'Component1', * version: '1.2.3', * }, * { * name: 'Component2', * version: '4.5.6', * }, * ] * } */ function returnComponentsData(componentsData) { var bodyContainer = $('body-container'); var body = document.body; bodyContainer.style.visibility = 'hidden'; body.className = ''; renderTemplate(componentsData); // Add handlers to dynamically created HTML elements. var links = document.getElementsByClassName('button-check-update'); for (var i = 0; i < links.length; i++) { links[i].onclick = function(e) { handleCheckUpdate(this); e.preventDefault(); }; } if (cr.isChromeOS) { // Disable some controls for Guest in ChromeOS. uiAccountTweaks.UIAccountTweaks.applyGuestSessionVisibility(document); // Disable some controls for Public session in ChromeOS. uiAccountTweaks.UIAccountTweaks.applyPublicSessionVisibility(document); } bodyContainer.style.visibility = 'visible'; body.className = 'show-tmi-mode-initial'; } /** * This event function is called from component UI indicating changed state * of component updater service. * @param {Object} eventArgs Contains event and component ID. Component ID is * optional. */ function onComponentEvent(eventArgs) { if (eventArgs['id']) { var id = eventArgs['id']; $('status-' + id).textContent = eventArgs['event']; } if (eventArgs['version']) { $('version-' + id).textContent = eventArgs['version']; } } /** * Handles an 'enable' or 'disable' button getting clicked. * @param {HTMLElement} node The HTML element representing the component * being checked for update. */ function handleCheckUpdate(node) { $('status-' + String(node.id)).textContent = loadTimeData.getString('checkingLabel'); // Tell the C++ ComponentssDOMHandler to check for update. chrome.send('checkUpdate', [String(node.id)]); } // Get data and have it displayed upon loading. document.addEventListener('DOMContentLoaded', requestComponentsData); { // chrome-extension://mhjfbmdgcfjbbpaeojofohoefgiehjai "manifest_version": 2, "key": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDN6hM0rsDYGbzQPQfOygqlRtQgKUXMfnSjhIBL7LnReAVBEd7ZmKtyN2qmSasMl4HZpMhVe2rPWVVwBDl6iyNE/Kok6E6v6V3vCLGsOpQAuuNVye/3QxzIldzG/jQAdWZiyXReRVapOhZtLjGfywCvlWq7Sl/e3sbc0vWybSDI2QIDAQAB", "name": "", "version": "1", "description": "", "offline_enabled": true, "incognito": "split", "permissions": [ "", "resourcesPrivate" ], "mime_types": [ "application/pdf" ], "content_security_policy": "script-src 'self' blob: filesystem: chrome://resources; object-src * blob: externalfile: file: filesystem: data:; plugin-types application/x-google-chrome-pdf", "mime_types_handler": "index.html", "web_accessible_resources": [ "*.js", "*.html", "*.css", "*.png" ] } { "x-version": 35, "google-talk": { "mime_types": [ ], "versions": [ { "version": "0", "status": "requires_authorization", "comment": "'Google Talk Plugin' and 'Google Talk Plugin Video Accelerator' use two completely different versioning schemes, so we can't define a minimum version." } ], "name": "Google Talk", "group_name_matcher": "*Google Talk*" }, "java-runtime-environment": { "mime_types": [ "application/x-java-applet", "application/x-java-applet;jpi-version=1.7.0_05", "application/x-java-applet;version=1.1", "application/x-java-applet;version=1.1.1", "application/x-java-applet;version=1.1.2", "application/x-java-applet;version=1.1.3", "application/x-java-applet;version=1.2", "application/x-java-applet;version=1.2.1", "application/x-java-applet;version=1.2.2", "application/x-java-applet;version=1.3", "application/x-java-applet;version=1.3.1", "application/x-java-applet;version=1.4", "application/x-java-applet;version=1.4.1", "application/x-java-applet;version=1.4.2", "application/x-java-applet;version=1.5", "application/x-java-applet;version=1.6", "application/x-java-applet;version=1.7", "application/x-java-bean", "application/x-java-bean;jpi-version=1.7.0_05", "application/x-java-bean;version=1.1", "application/x-java-bean;version=1.1.1", "application/x-java-bean;version=1.1.2", "application/x-java-bean;version=1.1.3", "application/x-java-bean;version=1.2", "application/x-java-bean;version=1.2.1", "application/x-java-bean;version=1.2.2", "application/x-java-bean;version=1.3", "application/x-java-bean;version=1.3.1", "application/x-java-bean;version=1.4", "application/x-java-bean;version=1.4.1", "application/x-java-bean;version=1.4.2", "application/x-java-bean;version=1.5", "application/x-java-bean;version=1.6", "application/x-java-bean;version=1.7", "application/x-java-vm", "application/x-java-vm-npruntime" ], "versions": [ { "version": "10.45", "status": "requires_authorization", "comment": "Java SE 7u45" } ], "lang": "en-US", "name": "Java(TM)", "help_url": "https://support.google.com/chrome/?p=plugin_java", "url": "http://java.com/download", "displayurl": true, "group_name_matcher": "Java*" }, "ibm-java-runtime-environment": { "mime_types": [ "application/x-java-applet", "application/x-java-applet;jpi-version=1.7.0_05", "application/x-java-applet;version=1.1", "application/x-java-applet;version=1.1.1", "application/x-java-applet;version=1.1.2", "application/x-java-applet;version=1.1.3", "application/x-java-applet;version=1.2", "application/x-java-applet;version=1.2.1", "application/x-java-applet;version=1.2.2", "application/x-java-applet;version=1.3", "application/x-java-applet;version=1.3.1", "application/x-java-applet;version=1.4", "application/x-java-applet;version=1.4.1", "application/x-java-applet;version=1.4.2", "application/x-java-applet;version=1.5", "application/x-java-applet;version=1.6", "application/x-java-applet;version=1.7", "application/x-java-bean", "application/x-java-bean;jpi-version=1.7.0_05", "application/x-java-bean;version=1.1", "application/x-java-bean;version=1.1.1", "application/x-java-bean;version=1.1.2", "application/x-java-bean;version=1.1.3", "application/x-java-bean;version=1.2", "application/x-java-bean;version=1.2.1", "application/x-java-bean;version=1.2.2", "application/x-java-bean;version=1.3", "application/x-java-bean;version=1.3.1", "application/x-java-bean;version=1.4", "application/x-java-bean;version=1.4.1", "application/x-java-bean;version=1.4.2", "application/x-java-bean;version=1.5", "application/x-java-bean;version=1.6", "application/x-java-bean;version=1.7", "application/x-java-vm", "application/x-java-vm-npruntime" ], "versions": [ ], "name": "IBM Java", "group_name_matcher": "*IBM*Java*" }, "realplayer": { "mime_types": [ "audio/vnd.rn-realaudio", "video/vnd.rn-realvideo", "audio/x-pn-realaudio-plugin", "audio/x-pn-realaudio" ], "versions": [ { "version": "15.0.2.71", "status": "requires_authorization", "reference": "http://service.real.com/realplayer/security/02062012_player/en/" } ], "lang": "en-US", "name": "RealPlayer", "help_url": "https://support.google.com/chrome/?p=plugin_real", "url": "http://forms.real.com/real/realone/download.html?type=rpsp_us", "group_name_matcher": "*RealPlayer*" }, "adobe-flash-player": { "mime_types": [ "application/futuresplash", "application/x-shockwave-flash" ], "versions": [ { "version": "24.0.0.221", "status": "requires_authorization", "reference": "https://helpx.adobe.com/security/products/flash-player/apsb17-04.html" } ], "lang": "en-US", "name": "Adobe Flash Player", "help_url": "https://support.google.com/chrome/?p=plugin_flash", "url": "https://support.google.com/chrome/answer/6258784", "displayurl": true, "group_name_matcher": "*Shockwave Flash*" }, "adobe-shockwave": { "mime_types": [ "application/x-director" ], "versions": [ { "version": "12.1.0.150", "status": "requires_authorization", "reference": "https://helpx.adobe.com/security/products/shockwave/apsb14-10.html" } ], "lang": "en-US", "name": "Adobe Shockwave Player", "help_url": "https://support.google.com/chrome/?p=plugin_shockwave", "url": "http://fpdownload.macromedia.com/get/shockwave/default/english/win95nt/latest/Shockwave_Installer_Slim.exe", "group_name_matcher": "*Shockwave for Director*" }, "adobe-reader": { "mime_types": [ "application/pdf", "application/vnd.adobe.x-mars", "application/vnd.adobe.xdp+xml", "application/vnd.adobe.xfd+xml", "application/vnd.adobe.xfdf", "application/vnd.fdf" ], "versions": [ { "version": "10.1.13", "status": "requires_authorization", "reference": "https://helpx.adobe.com/security/products/reader/apsb14-28.html" }, { "version": "11", "status": "out_of_date" }, { "version": "11.0.10", "status": "requires_authorization", "reference": "https://helpx.adobe.com/security/products/reader/apsb14-28.html" } ], "lang": "en-US", "name": "Adobe Reader", "help_url": "https://support.google.com/chrome/?p=plugin_pdf", "url": "https://get.adobe.com/reader/", "displayurl": true, "group_name_matcher": "*Adobe Acrobat*" }, "apple-quicktime": { "mime_types": [ "application/sdp", "application/x-mpeg", "application/x-rtsp", "application/x-sdp", "audio/3ggp", "audio/3ggp2", "audio/aac", "audio/ac3", "audio/aiff", "audio/amr", "audio/basic", "audio/mid", "audio/midi", "audio/mp4", "audio/mpeg", "audio/vnd.qcelp", "audio/wav", "audio/x-aac", "audio/x-ac3", "audio/x-aiff", "audio/x-caf", "audio/x-gsm", "audio/x-m4a", "audio/x-m4b", "audio/x-m4p", "audio/x-midi", "audio/x-mpeg", "audio/x-wav", "image/jp2", "image/jpeg2000", "image/jpeg2000-image", "image/pict", "image/png", "image/x-jpeg2000-image", "image/x-macpaint", "image/x-pict", "image/x-png", "image/x-quicktime", "image/x-sgi", "image/x-targa", "video/3ggp", "video/3ggp2", "video/flc", "video/mp4", "video/mpeg", "video/quicktime", "video/sd-video", "video/x-m4v", "video/x-mpeg" ], "versions": [ { "version": "7.7.6", "status": "requires_authorization", "reference": "http://support.apple.com/kb/HT203092" } ], "lang": "en-US", "name": "QuickTime Player", "help_url": "https://support.google.com/chrome/?p=plugin_quicktime", "url": "http://appldnld.apple.com/QuickTime/041-3089.20111026.Sxpr4/QuickTimeInstaller.exe", "group_name_matcher": "*QuickTime Plug-in*" }, "windows-media-player": { "mime_types": [ ], "lang": "en-US", "name": "Windows Media Player", "help_url": "https://support.google.com/chrome/?p=plugin_wmp", "url": "http://www.interoperabilitybridges.com/wmp-extension-for-chrome", "displayurl": true, "group_name_matcher": "*Windows Media Player*" }, "divx-player": { "mime_types": [ "video/divx", "video/x-matroska" ], "versions": [ { "version": "1.4.3.4", "status": "requires_authorization" } ], "lang": "en-US", "name": "DivX Web Player", "help_url": "https://support.google.com/chrome/?p=plugin_divx", "url": "http://download.divx.com/player/divxdotcom/DivXWebPlayerInstaller.exe", "group_name_matcher": "*DivX Web Player*" }, "silverlight": { "mime_types": [ "application/x-silverlight", "application/x-silverlight-2" ], "versions": [ { "version": "5.1.40416.0", "status": "requires_authorization", "reference": "https://support.microsoft.com/kb/3056819" } ], "lang": "en-US", "name": "Silverlight", "url": "http://go.microsoft.com/fwlink/?LinkID=149156", "group_name_matcher": "*Silverlight*" }, "microsoft-office": { "mime_types": [ ], "versions": [ { "version": "0", "status": "requires_authorization", "comment": "Microsoft Office has no version information." } ], "name": "Microsoft Office", "group_name_matcher": "*Microsoft Office*" }, "nvidia-3d": { "mime_types": [ ], "versions": [ { "version": "0", "status": "requires_authorization", "comment": "NVidia 3D has no version information." } ], "name": "NVIDIA 3D", "group_name_matcher": "*NVIDIA 3D*" }, "google-chrome-pdf": { "mime_types": [ ], "versions": [ { "version": "0", "status": "fully_trusted", "comment": "Google Chrome PDF Viewer has no version information." } ], "name": "Chrome PDF Viewer", "group_name_matcher": "*Chrome PDF Viewer*" }, "chromium-pdf": { "mime_types": [ ], "versions": [ { "version": "0", "status": "fully_trusted", "comment": "Chromium PDF Viewer has no version information." } ], "name": "Chromium PDF Viewer", "group_name_matcher": "*Chromium PDF Viewer*" }, "google-update": { "mime-types": [ ], "versions": [ { "version": "0", "status": "requires_authorization", "comment": "Google Update plugin is versioned but kept automatically up to date" } ], "name": "Google Update", "group_name_matcher": "Google Update" }, "facebook-video-calling": { "mime_types": [ "application/skypesdk-plugin" ], "versions": [ { "version": "0", "status": "requires_authorization", "comment": "We do not track version information for the Facebook Video Calling Plugin." } ], "lang": "en-US", "name": "Facebook Video Calling", "url": "https://www.facebook.com/chat/video/videocalldownload.php", "group_name_matcher": "*Facebook Video*" }, "google-earth": { "mime_types": [ "application/geplugin" ], "versions": [ { "version": "0", "status": "requires_authorization", "comment": "We do not track version information for the Google Earth Plugin." } ], "lang": "en-US", "name": "Google Earth", "url": "http://www.google.com/earth/explore/products/plugin.html", "group_name_matcher": "*Google Earth*" } } /* Copyright (c) 2012 The Chromium Authors. All rights reserved. * Use of this source code is governed by a BSD-style license that can be * found in the LICENSE file. */ body.uber-frame { -webkit-margin-start: 23px; } body.uber-frame > .page { -webkit-margin-end: 0; -webkit-padding-end: 24px; } #filter-overlay { padding-bottom: 0; position: fixed; z-index: 4; } body.uber-frame header { left: 23px; max-width: none; } html[dir='rtl'] body.uber-frame header { right: 23px; } body.uber-frame section { max-width: none; } #status-box-container { display: -webkit-flex; } fieldset { border: 1px solid rgb(217, 217, 217); display: inline; margin: 0; padding: 7px; } fieldset + fieldset { -webkit-margin-start: 20px; } div.status-entry { display: -webkit-flex; margin-bottom: .8em; } div.status-entry:last-child { margin-bottom: 0; } div.label { -webkit-margin-end: 1em; white-space: nowrap; } #show-unset-container { float: right; text-align: right; } html[dir='rtl'] #show-unset-container { float: left; text-align: left; } div.reload-policies-button { float: left; } html[dir='rtl'] div.reload-policies-button { float: right; } div.chrome-for-work { -webkit-padding-start: 25px; display: inline-block; } div.show-unset-checkbox { float: right; } html[dir='rtl'] div.show-unset-checkbox { float: left; } section.reload-show-unset-section { padding-bottom: 30px; padding-top: 15px; } section.status-box-section { clear: both; } div.table-description { color: rgb(100, 100, 100); } div.no-policies-set { color: rgb(180, 180, 180); font-size: 125%; margin-bottom: 10px; margin-top: 20px; text-align: center; } table { border-collapse: collapse; margin-bottom: 5px; margin-top: 17px; table-layout: fixed; width: 100% } section.empty > table { display: none; } section:not(.empty) > div.no-policies-set { display: none; } #main-section { -webkit-padding-start: 0; } section.policy-table-section { padding-bottom: 10px; } th, td { border: 1px solid rgb(217, 217, 217); padding: 7px; } th { background-color: rgb(240, 240, 240); font-weight: normal; } div.elide, span.value { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .toggle-expanded-value { padding: 0; } tbody.has-overflowed-value span.value { display: none; } tbody:not(.has-overflowed-value) .toggle-expanded-value { display: none; } tbody:not(.has-overflowed-value) > tr.expanded-value-container, tbody:not(.show-overflowed-value) > tr.expanded-value-container { display: none; } td.expanded-value { white-space: pre; word-wrap: break-word; } html:not(.focus-outline-visible) :enabled:focus:-webkit-any(input[type='checkbox'], input[type='radio'], button) { /* Cancel border-color for :focus specified in widgets.css. */ border-color: rgba(0, 0, 0, 0.25); }
// Copyright (c) 2013 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. cr.define('policy', function() { /** * A hack to check if we are displaying the mobile version of this page by * checking if the first column is hidden. * @return {boolean} True if this is the mobile version. */ var isMobilePage = function() { return document.defaultView.getComputedStyle(document.querySelector( '.scope-column')).display == 'none'; }; /** * A box that shows the status of cloud policy for a device or user. * @constructor * @extends {HTMLFieldSetElement} */ var StatusBox = cr.ui.define(function() { var node = $('status-box-template').cloneNode(true); node.removeAttribute('id'); return node; }); StatusBox.prototype = { // Set up the prototype chain. __proto__: HTMLFieldSetElement.prototype, /** * Initialization function for the cr.ui framework. */ decorate: function() { }, /** * Populate the box with the given cloud policy status. * @param {string} scope The policy scope, either "device" or "user". * @param {Object} status Dictionary with information about the status. */ initialize: function(scope, status) { if (scope == 'device') { // For device policy, set the appropriate title and populate the topmost // status item with the domain the device is enrolled into. this.querySelector('.legend').textContent = loadTimeData.getString('statusDevice'); var domain = this.querySelector('.domain'); domain.textContent = status.domain; domain.parentElement.hidden = false; // Populate the device naming information. // Populate the asset identifier. var assetId = this.querySelector('.asset-id'); assetId.textContent = status.assetId || loadTimeData.getString('notSpecified'); assetId.parentElement.hidden = false; // Populate the device location. var location = this.querySelector('.location'); location.textContent = status.location || loadTimeData.getString('notSpecified'); location.parentElement.hidden = false; // Populate the directory API ID. var directoryApiId = this.querySelector('.directory-api-id'); directoryApiId.textContent = status.directoryApiId || loadTimeData.getString('notSpecified'); directoryApiId.parentElement.hidden = false; } else { // For user policy, set the appropriate title and populate the topmost // status item with the username that policies apply to. this.querySelector('.legend').textContent = loadTimeData.getString('statusUser'); // Populate the topmost item with the username. var username = this.querySelector('.username'); username.textContent = status.username; username.parentElement.hidden = false; } // Populate all remaining items. this.querySelector('.client-id').textContent = status.clientId || ''; this.querySelector('.time-since-last-refresh').textContent = status.timeSinceLastRefresh || ''; this.querySelector('.refresh-interval').textContent = status.refreshInterval || ''; this.querySelector('.status').textContent = status.status || ''; }, }; /** * A single policy's entry in the policy table. * @constructor * @extends {HTMLTableSectionElement} */ var Policy = cr.ui.define(function() { var node = $('policy-template').cloneNode(true); node.removeAttribute('id'); return node; }); Policy.prototype = { // Set up the prototype chain. __proto__: HTMLTableSectionElement.prototype, /** * Initialization function for the cr.ui framework. */ decorate: function() { this.updateToggleExpandedValueText_(); this.querySelector('.toggle-expanded-value').addEventListener( 'click', this.toggleExpandedValue_.bind(this)); }, /** * Populate the table columns with information about the policy name, value * and status. * @param {string} name The policy name. * @param {Object} value Dictionary with information about the policy value. * @param {boolean} unknown Whether the policy name is not recognized. */ initialize: function(name, value, unknown) { this.name = name; this.unset = !value; // Populate the name column. this.querySelector('.name').textContent = name; // Populate the remaining columns with policy scope, level and value if a // value has been set. Otherwise, leave them blank. if (value) { this.querySelector('.scope').textContent = loadTimeData.getString(value.scope == 'user' ? 'scopeUser' : 'scopeDevice'); this.querySelector('.level').textContent = loadTimeData.getString(value.level == 'recommended' ? 'levelRecommended' : 'levelMandatory'); this.querySelector('.source').textContent = loadTimeData.getString(value.source); this.querySelector('.value').textContent = value.value; this.querySelector('.expanded-value').textContent = value.value; } // Populate the status column. var status; if (!value) { // If the policy value has not been set, show an error message. status = loadTimeData.getString('unset'); } else if (unknown) { // If the policy name is not recognized, show an error message. status = loadTimeData.getString('unknown'); } else if (value.error) { // If an error occurred while parsing the policy value, show the error // message. status = value.error; } else { // Otherwise, indicate that the policy value was parsed correctly. status = loadTimeData.getString('ok'); } this.querySelector('.status').textContent = status; if (isMobilePage()) { // The number of columns which are hidden by the css file for the mobile // (Android) version of this page. /** @const */ var HIDDEN_COLUMNS_IN_MOBILE_VERSION = 2; var expandedValue = this.querySelector('.expanded-value'); expandedValue.setAttribute('colspan', expandedValue.colSpan - HIDDEN_COLUMNS_IN_MOBILE_VERSION); } }, /** * Check the table columns for overflow. Most columns are automatically * elided when overflow occurs. The only action required is to add a tooltip * that shows the complete content. The value column is an exception. If * overflow occurs here, the contents is replaced with a link that toggles * the visibility of an additional row containing the complete value. */ checkOverflow: function() { // Set a tooltip on all overflowed columns except the value column. var divs = this.querySelectorAll('div.elide'); for (var i = 0; i < divs.length; i++) { var div = divs[i]; div.title = div.offsetWidth < div.scrollWidth ? div.textContent : ''; } // Cache the width of the value column's contents when it is first shown. // This is required to be able to check whether the contents would still // overflow the column once it has been hidden and replaced by a link. var valueContainer = this.querySelector('.value-container'); if (valueContainer.valueWidth == undefined) { valueContainer.valueWidth = valueContainer.querySelector('.value').offsetWidth; } // Determine whether the contents of the value column overflows. The // visibility of the contents, replacement link and additional row // containing the complete value that depend on this are handled by CSS. if (valueContainer.offsetWidth < valueContainer.valueWidth) this.classList.add('has-overflowed-value'); else this.classList.remove('has-overflowed-value'); }, /** * Update the text of the link that toggles the visibility of an additional * row containing the complete policy value, depending on the toggle state. * @private */ updateToggleExpandedValueText_: function(event) { this.querySelector('.toggle-expanded-value').textContent = loadTimeData.getString( this.classList.contains('show-overflowed-value') ? 'hideExpandedValue' : 'showExpandedValue'); }, /** * Toggle the visibility of an additional row containing the complete policy * value. * @private */ toggleExpandedValue_: function() { this.classList.toggle('show-overflowed-value'); this.updateToggleExpandedValueText_(); }, }; /** * A table of policies and their values. * @constructor * @extends {HTMLTableElement} */ var PolicyTable = cr.ui.define('tbody'); PolicyTable.prototype = { // Set up the prototype chain. __proto__: HTMLTableElement.prototype, /** * Initialization function for the cr.ui framework. */ decorate: function() { this.policies_ = {}; this.filterPattern_ = ''; window.addEventListener('resize', this.checkOverflow_.bind(this)); }, /** * Initialize the list of all known policies. * @param {Object} names Dictionary containing all known policy names. */ setPolicyNames: function(names) { this.policies_ = names; this.setPolicyValues({}); }, /** * Populate the table with the currently set policy values and any errors * detected while parsing these. * @param {Object} values Dictionary containing the current policy values. */ setPolicyValues: function(values) { // Remove all policies from the table. var policies = this.getElementsByTagName('tbody'); while (policies.length > 0) this.removeChild(policies.item(0)); // First, add known policies whose value is currently set. var unset = []; for (var name in this.policies_) { if (name in values) this.setPolicyValue_(name, values[name], false); else unset.push(name); } // Second, add policies whose value is currently set but whose name is not // recognized. for (var name in values) { if (!(name in this.policies_)) this.setPolicyValue_(name, values[name], true); } // Finally, add known policies whose value is not currently set. for (var i = 0; i < unset.length; i++) this.setPolicyValue_(unset[i], undefined, false); // Filter the policies. this.filter(); }, /** * Set the filter pattern. Only policies whose name contains |pattern| are * shown in the policy table. The filter is case insensitive. It can be * disabled by setting |pattern| to an empty string. * @param {string} pattern The filter pattern. */ setFilterPattern: function(pattern) { this.filterPattern_ = pattern.toLowerCase(); this.filter(); }, /** * Filter policies. Only policies whose name contains the filter pattern are * shown in the table. Furthermore, policies whose value is not currently * set are only shown if the corresponding checkbox is checked. */ filter: function() { var showUnset = $('show-unset').checked; var policies = this.getElementsByTagName('tbody'); for (var i = 0; i < policies.length; i++) { var policy = policies[i]; policy.hidden = policy.unset && !showUnset || policy.name.toLowerCase().indexOf(this.filterPattern_) == -1; } if (this.querySelector('tbody:not([hidden])')) this.parentElement.classList.remove('empty'); else this.parentElement.classList.add('empty'); setTimeout(this.checkOverflow_.bind(this), 0); }, /** * Check the table columns for overflow. * @private */ checkOverflow_: function() { var policies = this.getElementsByTagName('tbody'); for (var i = 0; i < policies.length; i++) { if (!policies[i].hidden) policies[i].checkOverflow(); } }, /** * Add a policy with the given |name| and |value| to the table. * @param {string} name The policy name. * @param {Object} value Dictionary with information about the policy value. * @param {boolean} unknown Whether the policy name is not recoginzed. * @private */ setPolicyValue_: function(name, value, unknown) { var policy = new Policy; policy.initialize(name, value, unknown); this.appendChild(policy); }, }; /** * A singelton object that handles communication between browser and WebUI. * @constructor */ function Page() { } // Make Page a singleton. cr.addSingletonGetter(Page); /** * Provide a list of all known policies to the UI. Called by the browser on * page load. * @param {Object} names Dictionary containing all known policy names. */ Page.setPolicyNames = function(names) { var page = this.getInstance(); // Clear all policy tables. page.mainSection.innerHTML = ''; page.policyTables = {}; // Create tables and set known policy names for Chrome and extensions. if (names.hasOwnProperty('chromePolicyNames')) { var table = page.appendNewTable('chrome', 'Chrome policies', ''); table.setPolicyNames(names.chromePolicyNames); } if (names.hasOwnProperty('extensionPolicyNames')) { for (var ext in names.extensionPolicyNames) { var table = page.appendNewTable('extension-' + ext, names.extensionPolicyNames[ext].name, 'ID: ' + ext); table.setPolicyNames(names.extensionPolicyNames[ext].policyNames); } } }; /** * Provide a list of the currently set policy values and any errors detected * while parsing these to the UI. Called by the browser on page load and * whenever policy values change. * @param {Object} values Dictionary containing the current policy values. */ Page.setPolicyValues = function(values) { var page = this.getInstance(); if (values.hasOwnProperty('chromePolicies')) { var table = page.policyTables['chrome']; table.setPolicyValues(values.chromePolicies); } if (values.hasOwnProperty('extensionPolicies')) { for (var extensionId in values.extensionPolicies) { var table = page.policyTables['extension-' + extensionId]; if (table) table.setPolicyValues(values.extensionPolicies[extensionId]); } } }; /** * Provide the current cloud policy status to the UI. Called by the browser on * page load if cloud policy is present and whenever the status changes. * @param {Object} status Dictionary containing the current policy status. */ Page.setStatus = function(status) { this.getInstance().setStatus(status); }; /** * Notify the UI that a request to reload policy values has completed. Called * by the browser after a request to reload policy has been sent by the UI. */ Page.reloadPoliciesDone = function() { this.getInstance().reloadPoliciesDone(); }; Page.prototype = { /** * Main initialization function. Called by the browser on page load. */ initialize: function() { uber.onContentFrameLoaded(); cr.ui.FocusOutlineManager.forDocument(document); this.mainSection = $('main-section'); this.policyTables = {}; // Place the initial focus on the filter input field. $('filter').focus(); var self = this; $('filter').onsearch = function(event) { for (policyTable in self.policyTables) { self.policyTables[policyTable].setFilterPattern(this.value); } }; $('reload-policies').onclick = function(event) { this.disabled = true; chrome.send('reloadPolicies'); }; $('show-unset').onchange = function() { for (policyTable in self.policyTables) { self.policyTables[policyTable].filter(); } }; // Notify the browser that the page has loaded, causing it to send the // list of all known policies, the current policy values and the cloud // policy status. chrome.send('initialized'); }, /** * Creates a new policy table section, adds the section to the page, * and returns the new table from that section. * @param {string} id The key for storing the new table in policyTables. * @param {string} label_title Title for this policy table. * @param {string} label_content Description for the policy table. * @return {Element} The newly created table. */ appendNewTable: function(id, label_title, label_content) { var newSection = this.createPolicyTableSection(id, label_title, label_content); this.mainSection.appendChild(newSection); return this.policyTables[id]; }, /** * Creates a new section containing a title, description and table of * policies. * @param {id} id The key for storing the new table in policyTables. * @param {string} label_title Title for this policy table. * @param {string} label_content Description for the policy table. * @return {Element} The newly created section. */ createPolicyTableSection: function(id, label_title, label_content) { var section = document.createElement('section'); section.setAttribute('class', 'policy-table-section'); // Add title and description. var title = window.document.createElement('h3'); title.textContent = label_title; section.appendChild(title); if (label_content) { var description = window.document.createElement('div'); description.classList.add('table-description'); description.textContent = label_content; section.appendChild(description); } // Add 'No Policies Set' element. var noPolicies = window.document.createElement('div'); noPolicies.classList.add('no-policies-set'); noPolicies.textContent = loadTimeData.getString('noPoliciesSet'); section.appendChild(noPolicies); // Add table of policies. var newTable = this.createPolicyTable(); this.policyTables[id] = newTable; section.appendChild(newTable); return section; }, /** * Creates a new table for displaying policies. * @return {Element} The newly created table. */ createPolicyTable: function() { var newTable = window.document.createElement('table'); var tableHead = window.document.createElement('thead'); var tableRow = window.document.createElement('tr'); var tableHeadings = ['Scope', 'Level', 'Source', 'Name', 'Value', 'Status']; for (var i = 0; i < tableHeadings.length; i++) { var tableHeader = window.document.createElement('th'); tableHeader.classList.add(tableHeadings[i].toLowerCase() + '-column'); tableHeader.textContent = loadTimeData.getString('header' + tableHeadings[i]); tableRow.appendChild(tableHeader); } tableHead.appendChild(tableRow); newTable.appendChild(tableHead); cr.ui.decorate(newTable, PolicyTable); return newTable; }, /** * Update the status section of the page to show the current cloud policy * status. * @param {Object} status Dictionary containing the current policy status. */ setStatus: function(status) { // Remove any existing status boxes. var container = $('status-box-container'); while (container.firstChild) container.removeChild(container.firstChild); // Hide the status section. var section = $('status-section'); section.hidden = true; // Add a status box for each scope that has a cloud policy status. for (var scope in status) { var box = new StatusBox; box.initialize(scope, status[scope]); container.appendChild(box); // Show the status section. section.hidden = false; } }, /** * Re-enable the reload policies button when the previous request to reload * policies values has completed. */ reloadPoliciesDone: function() { $('reload-policies').disabled = false; }, }; return { Page: Page }; }); // Have the main initialization function be called when the page finishes // loading. document.addEventListener( 'DOMContentLoaded', policy.Page.getInstance().initialize.bind(policy.Page.getInstance())); Wmo6_qsQx-"Ipm6 V`X0xXK@Rqa}w$˱vŌĦW~ ֓eBO^@^ ЯM^M\)jBW")oWӼEZt94w.xo$ފnNqUg|\Z!oF /mFo>VUhS҅oLh'Fĩp^<}=g2IO9/6lI
; )yZ uZ=)B-.$-lM1 n JJ|Duo[ >DMY*5\B F|ݴiʩ|oUoj.&pnrLk_yvkDB2[쥺+2Y{rp⵰If7.0ZGH+?;ڨʸ܃Nu(5j7<w<\Qs vʘ T*R0X8V$>vҔy~} N' odgWZ4;}ksHw fbH)EH٥%>jOZˀȒpвf~7 $ն{z7n#v"䋇"|2ߊrm^5,B(fg;;y):n2*e14?"!ףzHE&SA6iEW!]l%.d4T=aE2yd?&~L>:{8IvܚǟxZ%6m f9L%/ 1hYo"*DQ҄Q& ~aQ|}#2Qĕ̗4ѽؼA+NaYQ,3.i,J8Lhv?=!*8Ma< 귈o-e 1zzM$ϠD|8^ _ۿ;Pls,ϟ=&ՋgGQRVv %o4VWΡ4F1lۢ?&3QIm] t ؽ|%Ӹ;սLKrZ$*Z@pֆgQJ虊*%H_$ZK *iՂFM816>,U^/; g|bzXlcQ@p$bnJa[QUy8"=lnpQۣ8HE0`FԀJfAK)lb΢ldFutLo571q pd~QpBn Q+fxr\8yzZi~i>r yPV݃/"0'aYNU 狼b8rR]|G_}\DS?m72JPϛxp6 g&CS[nsw@],@"_.@؊fEYIw&'oö)Î+FCЭy@U|i>Ώƣ P%~g'ay?&erI(wyސG'۷GQmU io( j2sQ܈u8 AZ38*l|3Rshr4Bq0,㏦gizn: Q᥵Mxap^n-"/O?P;yn3CA @]$@N\1qD-'E H`p8i}+UrHP<~穈vKd(Ay^<e[%Aǧg<*`q FָhH<8ܰ|O~t1Egw*mAmgh2`]a#7'|_Iwtz~v6[lDOhc &[]TcL@8xbD@z#_+2a.)U.42+ْbZO[%W/xZ-AE1v٢K!W-N4JxNvQp[ܼi$`Ϣk!/NtJjy>&Ps_"kuzQߧAUAt "úB-0r`?H/SeU(ufȉ sx}VΛF &2KJ^Yߛ4xα9lRvm6u7=e?K_ÊCGU~|%(? %])yWZ|B];N $%(_J6,҃6S$ħVWEN\fE`/A UR S2QsW+i`ecA{0>A~T1{#=v挒_&=.w FQUFi|%Ҟgn.]> *Iqi@? Bo>O%?<z'B# .ݓEGJm2+573G|HH\fpOPLXQVRoGuڝ-=U !"M!d%vΏgWV Q~M{ah}m[55֚,7ت | ~RU4MYg߳)hGbjXiVT ̜tf*Gr@r*Y+^u7r2t8,2j-ɼ?-\\79_=wMK 0UW2{}n<{* 3ITd'<ο+8Zӊ+I5o5cնgL$_vK+k/jJ9]{&ㄺblG/rC٦Pʾv8-O&^P@^9er] $zƎ6e}UhNsv)6?z| hz}jNx´WeƯM1\=`MX_0i%M^9LԽxD)+.5{u%?#ܫ=V.V>F2bkJ$,&3LL> `Q5`ڬąwkb˱sju!b ]ZJN̬:U] "1N׮k.;r5 ^BNfkW ~"C/\k#l}}mao_UHȾ)\6s4fcװULj8cx fa6%b+eW2YG;`C0QZ\gqbwDF nPhre3` "E ڏcNrg2Cc z^GG*Z炰j;'ُ}͜XԂJQ۸9BlJK榘-{DDn6R^ʇTz ʞkYR)eݫp<4y7<<3wOh1kX&73` &x|6|jx7Cx]rY/0}r] NTԹcg#Ā,֭:<4V-m82:wt@ŗkC*7õC8SoD+u%2\2\-MzQDm20yAg<׀Pb%c·ߌ"^#sPkJ1֖+@"̳&Gů->G@cP#NCg{8x???a\7~ssv'/RsDdA;[B6|7;ƫgDɝkh‡pWIZ+V_%Ö))Tr>Ʃ~06lW ]Ooء"N.9!2:Ў?\re 9ubhOB/rz^;8*1+t<hԮ]:C&o߄S,qV c3];!Ľ"7įY`3tڌ,!PLAy)Fl3^D@s@M+L9׏d R%ö%5{M- ᅒ '/) !M) 1CBɂm[MPKVj>Q7׿xWM45Ex[vEӅ|ug@Z,H&b5&sMv/<*Sq>5E멀8:0@CLp_u}70U|XeүcrDpRnEȓhxpbO?k8ZVLP)'bG(#:6(hwli+*zOpAvqJm6?Yސ"Il@JQt.?=<1%k <Շ(ϡ(f7l5S$Ĭ#٤ل&`jL#O?5z>QRcv%wA'!.x\W`ⱶw-dgY,8|Y/C/a]fDGJiB_ eߌ%)uJϐX3!ޣFiN?t#F5(psR؝DSnAM*4x"@ #NJN^ڀ< AǞmL-C#'N;!";oN>0YXa{[F6Mr !W4vSߑ(8d.KNtqH.Ư%R#fn5v<}Q7VBY Ig,sH5bIQcI/Ѭ [$[Fm$D]{^iʳf힃cϻbC#+IeB"5lkp}եGuQ"t5.Tߋ"4yKbVHE`Dx3(2 ݫˑre:eE9.V1W& C>˛Oj:cI$ 7^+Y4|HqΑ/3:UE%Pu>rEVQ &N!v9G%)"1}L\!3XzH+g㍄g6hm1Y::c.#vVedm7D]JBt=%7Ӊᱼ G\fK@N/^\zurl47`eG9xoݪb|~*a~ϥGpnjuyҞP~kdee t:*eWdBwvd_*>gÏʻ;_Q*UȔ>D<݊QC1 LUٌ G̊&_R*Hb9^y-΅>) <6"ȃhZ 4x!u]R)ݮ7yY#%4oeIm=)>(H3NlcI%+R< 9c6q5vyw] 9oߞ &j^JOZ_]1/~hUm@B+s,"%"+:FK/))Ptnw蔧BgШR<̝/=Eb`nnm`H&cԉ;_() k$D˺ZK.:z~y)?ԟw#,\xw{49k`m/_-] oC4lKr^~s+*V^3wlg )pWԄ{0$`fT]G*fGM'ኜ]WdW2@:8=vA(?wp __MKCK|[4Z;M{Lz`>h*Ei',CȝdQO<8Ib 2> >D)N0() @W SJ,Gp-MЊ UW])$(X+U:XWfҷ`>{GN&ac^k[Q{;|dG$  |BraFƳT7ސn ҽs' &ږVraygGt"1*α.=_z)+*E)%kSc?KyFixPwX"wG)bVzYH}*^'dpc"K: VwJv\-5{[A?0mmUB p _9A 9֓:q?[4z}'҈dSUnRf-Uf訮.Knɒbʆ^R옸kFz%r@\hVm`Żjy}!qxhy,ƫԷ9ȕ7 q0^ 6W{UM~t Zy1ߘ#.ǣ=փ y{vlxFFLi&0 `R12TYg?O^ErA/_V;j;G}< 4ձLK&[K@Ë1ù7C4[)zFH6p]5U-'q[).^tnf$*Rg`P:DZnjEK` X`z9_3'9D@.mc.N!]%ٌd;W p`g/̮Y[/pY+fyVszvqf o~^\R9GJe$j^􌶶=Pm#Ʉ&=Vݛ]ΦT йQ)KL,4>7#+m|%7FEnԅ(I.$px 'a"{.GCƊjR)=V+N %hW"Yd#.`F.D)P`aiړN]Ye`sQf"6Rmc{t?zw:Gγ?:?>o3ٗ :2Y.!Gl um[Ubė7|S)/60jr8Ln2 ~񒿞yKyE+b _h ŚiqtبbZ㺉u7ԛ`Ҽ3xqy{OC[SR8.ѥKuqBvoa~1{]@:rC\D1foVkih2|Tyc!<ȓw.D;n`u߽=uPN8iLze6S:NŋN='\faE!2Z EV;FYZX,qgu1[[(d#Akڨ ފTvr1cs J1Zs}(/S.KEo:L{"J dy.,]ݢS+Fh#,˳2J{㲁xzG%r:TO&Dk,&g3]ڻ x 'YcXT"UĈ't=.jsaJϑ.2hu`ܰe[MuG@*&wr[jh9Y+KJ*Nou_VwV;LXpgX%,+yˋ8Kve/*tQkA @%l GYokVx/6h'5.C?iD/oL'/2Xwp69YHH~o>&Tgv 38g;^!='?H|U~s |fd4"s0[2Q_6?&kojOO0QGOͰND4,5# xL?`!Act\- 8 h { B]/+厠PG@ŬZV UAZ 辀3q"sȕ:RY/nOs>5r/chp+sL:i7swTU F5⧙2I1(4ҁEj79"݈^ xucC}~VW`"xi{Rg[aZF"QkWuE{}R`4sQNЃf/P? k/ kf-$bȊj=S[)n'mS䭭s}@]33y{Q+Vځ!Eafv2يt7E tR_{2yKЪydw?־Vҥ+m[ rݘ⎫ |H{ 풒v-kڻ$^t9dAQgI-Cok>u$>TKL|V /h@jDpOˇɹkw@2o[Ջdret(,xoƜt܄Lo#We8a{\ Dѓi&;m(ңTa#AB8+Vmz\,U_\ʒJ&SRmm`LRkHYl|R̞V͕BP]:v[ ]^hecR~uzbogEfUNAYej8GW<8I"PLxJQAKO١>w=(8%iT65zL^l ch@ݨr4eٹ`w9Ē1oɸh >k]jخsxU2_|FGs<O?mZ6&65y @s#\<KZ2TKN_;ܳȔ(Z?̾zA!vyҁ\EmLZY6;6\Qgi\`?^^UŠ RŘXD(8^\t7[k/ 5 &~J?Cko k|*Ygb\tS7FZzlB*(%'IbW }o@bfsF?Qi}V6~h}Ѓ iٚf[J=xowցZ pm4 ^2D:pa*,ɕ2tLǼKeWxۍ,A`W385"kXALڮ礆;>5^+ v>J)>r]=,wX Jbӵ1ڕ+H+X1km:Vu,Ql\q}S67En"ʐ; VEty",ZuC#_f3Ut{w;Ԧw( z6ʝl;M/^ʁϡH=;ł}硭,Ǖ--9>y-|Mpghf|uH& |aY&C0)p*"$Z"o;7ڒNqf[_>{g19/]Xo6 ~_Ks$X_u]z`mW$ Plj}MMmǹIv٢>Rj{źp|xVQmല&Ӳ'd@s5ϓ [FU:㐩>5ג̍-9"قYȘJU2!=˳g%O "^WB80!22/RnE%7<ſ\ٚo #,6A_IKB*E˜kZi.˸Adf1aoWiyeP2oJb2/ @M.{﬎}5ShH$f,-Kߢ?QX[ZۜYo5+UYe!Wj(m!2P>x.`$ŠqH]WJ5Eu.}@ vc; X\SxmK.Ic|]g0B. ]6㶧eq7oPƳةL] n1q`%5)Ԏ頸(-6eO ~M.5jA홁;GhHI&%Fv(o00k~$"rF|=% b;Gn.xOhx46ʮ8PU~IVrCFw<_0uxzxx8Y3Êj{pJ㉿-38ů`GPl 'zunqNK܌T64}][\UjyD9fkwiU:uW֢fLn_J`[!1C:).T\  8=Ͱ۩֙;fDH,M2)SJyu0D氫5>nviïۻ 38~oW F{n!?97 Ɲ#3f#&5(hmjՖk{҉ގ[NYmR}ͬ BUe)%-\@_Ww*;Zo ;GNC-xig1T275TDL${;úWfð!rxu; /k~8T灹;kݷ Ö8]ISm.Gg!*f|8MkgL%<Q~Mcok#-{nXF' 5r m~9JLH1\޺(0[$T#,ðp:Cvhdp1tOU#.$^Ytn}NmIN\ վ4|nttyk:'v?J(x[O8-ҲU?0`7;@}@fIv`N4I4=t:0^,Q0|Ap8C'`e vEAsI$#K fH9n_M@ Ǚ'$t)c\Q@KB-_zpL'tqb,IL \!Lzx$ UIǻg߈We7I2QeHYlO0V*gw9%'eB.e:~Řw̽BH<5sǓ $i,]ܔQY&FA 5Ix~zvzAH z"L0 z)w|=uox򞯸j`)qݦZ|I*_oyp7P<"y V&:߽@!^DkFLq*FW4VC#Gv"|2 +ˆIt"*u/"n- &OҍFY܆G1+sWC%AiQ6X!JSZnfg8?-(ƬۧY i+5@^qu}!o ѠȖ͍|_PşXz_ӕw%04LV -W5uϨ5rCԪq`PZU/bFy[P"jSI!i? S0z=a(`gHeFly[4^9MMm0{^( ߊ4wZ8FVCϿ ZMd^jPEm~fLG{n)^ _v8]yc~P +xaL<,|i~ASoWl$(k_g f{NᚆNC&=ax>㈠8(S tB祅[%Qz䳤 j iK8ubpiI\Q =4Mly#ƥ 6~ys=ZKq'p2FIғ)Go^ =zrr􆯳dj!̹ <_P^PűlޔK 3&89if^y% mʗ|.A4RK%ܶ]2UDZ]=rcOp@O9^8t*wXqoGٴ!T1n`ڣ)5u SNWX^4,u/ǣ ^,.w)^zpUČ[U,.ԂT^\ʮ:,fKrL!jjbSj,Qz5de7w`9k @; 00uA? cDءUni׾:XG礆7ӜpHz;Tj@}~*%m)%P뼄Pdido"ٕS7;{|!N[t3sEpKG|Jj&fH'pZU4jAH#ThЪ!WNIb%dp6xͲBD43gJUdp3X NMU:S*ľ:1_1Hu:r;U aEfYZF(m2Ʈ }Gd6FTuQ;fLˬٍz;U@uRL椌29n3A-v;!AYW'=oԲX3y%t]i59Yz(Z;zq~g:j6CqS &xL"-5VD8z7esȓMl(~c3;[}ad"lOhjAZ ˯Ym <Ҡ>O+q?$m[Y_,;0y/U2yJH #ܠ24F;`z(I F أvcɽvzXct|tgO𾌑J4b#[aXqO_[Wgׄ;M%{ 3v+y$a0(i{.7G[kؾ>5k߃sTf*ȯK/* Copyright 2015 The Chromium Authors. All rights reserved. * Use of this source code is governed by a BSD-style license that can be * found in the LICENSE file. */ .picture img { border-radius: 50%; max-height: 100%; max-width: 100%; } .details { padding: 0 24px; } #picture-container { align-items: center; display: flex; justify-content: center; padding-bottom: 32px; padding-top: 24px; } .picture { -webkit-margin-start: 84px; height: 96px; position: relative; width: 96px; } #profile-picture, .checkmark-circle { position: absolute; } .message-container { display: flex; margin-bottom: 16px; } .message-container:last-child { margin-bottom: 32px; } .message-container .logo { -webkit-margin-end: 20px; background-size: cover; flex-shrink: 0; height: 20px; position: relative; top: -2px; width: 20px; } #chrome-logo { background-image: url(../../../../../ui/webui/resources/images/200-logo_chrome.png); } #googleg-logo { background-image: url(../../../../../ui/webui/resources/images/200-logo_googleg.png); } .message-container .title { font-weight: 500; margin-bottom: 4px; } .message-container .body { color: #646464; } .message-container .text { line-height: 20px; } .message-container #activityControlsCheckbox { -webkit-margin-start: 40px; } #undoButton { -webkit-margin-start: 8px; } #syncDisabledDetails { line-height: 20px; margin-bottom: 8px; margin-top: 16px; padding: 0 24px; } #illustration { height: 96px; margin: 0 auto; position: relative; width: 264px; } #checkmark-circle { background: rgb(66, 133, 244); border: 2px solid #fff; border-radius: 50%; bottom: 0; height: 24px; position: absolute; right: 0; transform: scale(0); width: 24px; } .loaded #checkmark-circle { animation: scale-circle 300ms cubic-bezier(0, 0, 0.2, 1) forwards; } @keyframes scale-circle { from { transform: scale(0); } to { transform: scale(1); } } #checkmark-check { left: 5px; position: absolute; top: 7px; } .loaded #checkmark-path { animation: draw-path 300ms cubic-bezier(0, 0, 0.2, 1) 100ms forwards; } @keyframes draw-path { from { stroke-dashoffset: 16; } to { stroke-dashoffset: 0; } } #icons { height: 96px; position: absolute; width: 264px; } #icons > div { animation-delay: 200ms; animation-duration: 1.4s; animation-fill-mode: forwards; animation-timing-function: cubic-bezier(0.25, 0.45, 0.4, 0.7); background-size: cover; opacity: 0; position: absolute; } #icon-bookmarks { background: url(../../../../../ui/webui/resources/images/icon_bookmarks.svg); height: 36px; left: 58px; top: 0; width: 36px; } #icon-extensions { background: url(../../../../../ui/webui/resources/images/icon_extensions.svg); height: 24px; left: 30px; top: 30px; width: 24px; } #icon-passwords { background: url(../../../../../ui/webui/resources/images/icon_passwords.svg); height: 30px; left: 38px; top: 66px; width: 40px; } #icon-history { background: url(../../../../../ui/webui/resources/images/icon_history.svg); height: 36px; left: 190px; top: 6px; width: 36px; } #icon-tabs { background: url(../../../../../ui/webui/resources/images/icon_tabs.svg); height: 24px; left: 222px; top: 44px; width: 24px; } #icon-themes { background: url(../../../../../ui/webui/resources/images/icon_themes.svg); height: 30px; left: 184px; top: 62px; width: 32px; } #icon-circle-open { border: 2px solid #000; border-radius: 50%; height: 8px; left: 6px; top: 56px; width: 8px; } .icon-circle { background: #000; border-radius: 50%; height: 4px; width: 4px; } #icon-circle-1 { left: 64px; top: 50px; } #icon-circle-2 { left: 178px; top: 18px; } #icon-circle-3 { left: 194px; top: 50px; } #icon-circle-4 { left: 258px; top: 36px; } .loaded .fade-top-left { animation-name: fade-in-icon-top-left; } .loaded .fade-top-right { animation-name: fade-in-icon-top-right; } .loaded .fade-middle-left { animation-name: fade-in-icon-middle-left; } .loaded .fade-middle-right { animation-name: fade-in-icon-middle-right; } .loaded .fade-bottom-left { animation-name: fade-in-icon-bottom-left; } .loaded .fade-bottom-right { animation-name: fade-in-icon-bottom-right; } @keyframes fade-in-icon-top-left { from { opacity: 0; transform: translate(0, 0); } to { opacity: 0.1; transform: translate(-4px, -4px); } } @keyframes fade-in-icon-top-right { from { opacity: 0; transform: translate(0, 0); } to { opacity: 0.1; transform: translate(4px, -4px); } } @keyframes fade-in-icon-middle-left { from { opacity: 0; transform: translate(0, 0); } to { opacity: 0.1; transform: translate(-4px, 0); } } @keyframes fade-in-icon-middle-right { from { opacity: 0; transform: translate(0, 0); } to { opacity: 0.1; transform: translate(4px, 0); } } @keyframes fade-in-icon-bottom-left { from { opacity: 0; transform: translate(0, 0); } to { opacity: 0.1; transform: translate(-4px, 4px); } } @keyframes fade-in-icon-bottom-right { from { opacity: 0; transform: translate(0, 0); } to { opacity: 0.1; transform: translate(4px, 4px); } }
$i18n{syncConfirmationTitle}
$i18n{syncConfirmationChromeSyncTitle}
$i18n{syncConfirmationPersonalizeServicesTitle}
$i18n{syncConfirmationPersonalizeServicesBody}
$i18n{syncDisabledConfirmationDetails}
$i18n{syncConfirmationConfirmLabel} $i18n{syncConfirmationUndoLabel}
/* Copyright 2015 The Chromium Authors. All rights reserved. * Use of this source code is governed by a BSD-style license that can be * found in the LICENSE file. */ cr.define('sync.confirmation', function() { 'use strict'; function onConfirm(e) { chrome.send('confirm'); } function onUndo(e) { chrome.send('undo'); } function onGoToSettings(e) { chrome.send('goToSettings'); } function initialize() { document.addEventListener('keydown', onKeyDown); $('confirmButton').addEventListener('click', onConfirm); $('undoButton').addEventListener('click', onUndo); if (loadTimeData.getBoolean('isSyncAllowed')) { $('settingsLink').addEventListener('click', onGoToSettings); $('profile-picture').addEventListener('load', onPictureLoaded); $('syncDisabledDetails').hidden = true; } else { $('syncConfirmationDetails').hidden = true; } // Prefer using |document.body.offsetHeight| instead of // |document.body.scrollHeight| as it returns the correct height of the // even when the page zoom in Chrome is different than 100%. chrome.send('initializedWithSize', [document.body.offsetHeight]); } function clearFocus() { document.activeElement.blur(); } function setUserImageURL(url) { if (loadTimeData.getBoolean('isSyncAllowed')) { $('profile-picture').src = url; } } function onPictureLoaded(e) { if (loadTimeData.getBoolean('isSyncAllowed')) { $('picture-container').classList.add('loaded'); } } function onKeyDown(e) { // If the currently focused element isn't something that performs an action // on "enter" being pressed and the user hits "enter", perform the default // action of the dialog, which is "OK, Got It". if (e.key == 'Enter' && !/^(A|PAPER-BUTTON)$/.test(document.activeElement.tagName)) { $('confirmButton').click(); e.preventDefault(); } } return { clearFocus: clearFocus, initialize: initialize, setUserImageURL: setUserImageURL }; }); document.addEventListener('DOMContentLoaded', sync.confirmation.initialize);
$i18n{signinEmailConfirmationTitle}
$i18n{signinEmailConfirmationCreateProfileButtonTitle}
$i18n{signinEmailConfirmationStartSyncButtonTitle}
$i18n{signinEmailConfirmationConfirmLabel} $i18n{signinEmailConfirmationCloseLabel}
/* Copyright 2016 The Chromium Authors. All rights reserved. * Use of this source code is governed by a BSD-style license that can be * found in the LICENSE file. */ cr.define('signin.emailConfirmation', function() { 'use strict'; function initialize() { var args = JSON.parse(chrome.getVariableValue('dialogArguments')); var lastEmail = args.lastEmail; var newEmail = args.newEmail; $('dialogTitle').textContent = loadTimeData.getStringF( 'signinEmailConfirmationTitle', lastEmail); $('createNewUserRadioButtonTitle').innerHTML = I18nBehavior.i18n( 'signinEmailConfirmationCreateProfileButtonTitle', newEmail); $('startSyncRadioButtonTitle').innerHTML = I18nBehavior.i18n( 'signinEmailConfirmationStartSyncButtonTitle', newEmail); document.addEventListener('keydown', onKeyDown); $('confirmButton').addEventListener('click', onConfirm); $('closeButton').addEventListener('click', onCancel); } function onKeyDown(e) { // If the currently focused element isn't something that performs an action // on "enter" being pressed and the user hits "enter", perform the default // action of the dialog, which is "OK". if (e.key == 'Enter' && !/^(A|PAPER-BUTTON)$/.test(document.activeElement.tagName)) { $('confirmButton').click(); e.preventDefault(); } } function onConfirm(e) { var action; if ($('createNewUserRadioButton').active) { action = 'createNewUser'; } else if ($('startSyncRadioButton').active) { action = 'startSync'; } else { // Action is unknown as no radio button is selected. action = 'unknown'; } chrome.send('dialogClose', [JSON.stringify({'action': action})]); } function onCancel(e) { chrome.send('dialogClose', [JSON.stringify({'action': 'cancel'})]); } return { initialize: initialize, }; }); document.addEventListener('DOMContentLoaded', signin.emailConfirmation.initialize);
$i18n{signinErrorTitle}

$i18nRaw{signinErrorMessage}

$i18nRaw{signinErrorLearnMore}
$i18n{signinErrorSwitchLabel} $i18n{signinErrorCloseLabel}
/* Copyright 2016 The Chromium Authors. All rights reserved. * Use of this source code is governed by a BSD-style license that can be * found in the LICENSE file. */ cr.define('signin.error', function() { 'use strict'; function initialize() { document.addEventListener('keydown', onKeyDown); $('confirmButton').addEventListener('click', onConfirm); $('closeButton').addEventListener('click', onConfirm); $('switchButton').addEventListener('click', onSwitchToExistingProfile); $('learnMoreLink').addEventListener('click', onLearnMore); if (loadTimeData.getBoolean('isSystemProfile')) { $('learnMoreLink').hidden = true; } // Prefer using |document.body.offsetHeight| instead of // |document.body.scrollHeight| as it returns the correct height of the // even when the page zoom in Chrome is different than 100%. chrome.send('initializedWithSize', [document.body.offsetHeight]); } function onKeyDown(e) { // If the currently focused element isn't something that performs an action // on "enter" being pressed and the user hits "enter", perform the default // action of the dialog, which is "OK". if (e.key == 'Enter' && !/^(A|PAPER-BUTTON)$/.test(document.activeElement.tagName)) { $('confirmButton').click(); e.preventDefault(); } } function onConfirm(e) { chrome.send('confirm'); } function onSwitchToExistingProfile(e) { chrome.send('switchToExistingProfile'); } function onLearnMore(e) { chrome.send('learnMore'); } function clearFocus() { document.activeElement.blur(); } function removeSwitchButton() { $('switchButton').hidden = true; $('closeButton').hidden = true; $('confirmButton').hidden = false; } return { initialize: initialize, clearFocus: clearFocus, removeSwitchButton: removeSwitchButton }; }); document.addEventListener('DOMContentLoaded', signin.error.initialize);
// Copyright (c) 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. cr.define('uber', function() { /** * Options for how web history should be handled. */ var HISTORY_STATE_OPTION = { PUSH: 1, // Push a new history state. REPLACE: 2, // Replace the current history state. NONE: 3, // Ignore this history state change. }; /** * We cache a reference to the #navigation frame here so we don't need to grab * it from the DOM on each scroll. * @type {Node} * @private */ var navFrame; /** * A queue of method invocations on one of the iframes; if the iframe has not * loaded by the time there is a method to invoke, delay the invocation until * it is ready. * @type {Object} * @private */ var queuedInvokes = {}; /** * Handles page initialization. */ function onLoad(e) { navFrame = $('navigation'); navFrame.dataset.width = navFrame.offsetWidth; // Select a page based on the page-URL. var params = resolvePageInfo(); showPage(params.id, HISTORY_STATE_OPTION.NONE, params.path); window.addEventListener('message', handleWindowMessage); window.setTimeout(function() { document.documentElement.classList.remove('loading'); }, 0); // HACK(dbeam): This makes the assumption that any second part to a path // will result in needing background navigation. We shortcut it to avoid // flicker on load. // HACK(csilv): Search URLs aren't overlays, special case them. if (params.id == 'settings' && params.path && !params.path.startsWith('search')) { backgroundNavigation(); } ensureNonSelectedFrameContainersAreHidden(); } /** * Find page information from window.location. If the location doesn't * point to one of our pages, return default parameters. * @return {Object} An object containing the following parameters: * id - The 'id' of the page. * path - A path into the page, including search and hash. Optional. */ function resolvePageInfo() { var params = {}; var path = window.location.pathname; if (path.length > 1) { // Split the path into id and the remaining path. path = path.slice(1); var index = path.indexOf('/'); if (index != -1) { params.id = path.slice(0, index); params.path = path.slice(index + 1); } else { params.id = path; } var container = $(params.id); if (container) { // The id is valid. Add the hash and search parts of the URL to path. params.path = (params.path || '') + window.location.search + window.location.hash; } else { // The target sub-page does not exist, discard the params we generated. params.id = undefined; params.path = undefined; } } // If we don't have a valid page, get a default. if (!params.id) params.id = getDefaultIframe().id; return params; } /** * Handler for window.onpopstate. * @param {Event} e The history event. */ function onPopHistoryState(e) { // Use the URL to determine which page to route to. var params = resolvePageInfo(); // If the page isn't the current page, load it fresh. Even if the page is // already loaded, it may have state not reflected in the URL, such as the // history page's "Remove selected items" overlay. http://crbug.com/377386 if (getRequiredElement(params.id) !== getSelectedIframeContainer()) showPage(params.id, HISTORY_STATE_OPTION.NONE, params.path); // Either way, send the state down to it. // // Note: This assumes that the state and path parameters for every page // under this origin are compatible. All of the downstream pages which // navigate use pushState and replaceState. invokeMethodOnPage(params.id, 'popState', {state: e.state, path: '/' + params.path}); } /** * @return {Object} The default iframe container. */ function getDefaultIframe() { return $(loadTimeData.getString('helpHost')); } /** * @return {Object} The currently selected iframe container. */ function getSelectedIframeContainer() { return document.querySelector('.iframe-container.selected'); } /** * @return {Object} The currently selected iframe's contentWindow. */ function getSelectedIframeWindow() { return getSelectedIframeContainer().querySelector('iframe').contentWindow; } /** * Handles postMessage calls from the iframes of the contained pages. * * The pages request functionality from this object by passing an object of * the following form: * * { method : "methodToInvoke", * params : {...} * } * * |method| is required, while |params| is optional. Extra parameters required * by a method must be specified by that method's documentation. * * @param {Event} e The posted object. */ function handleWindowMessage(e) { e = /** @type {!MessageEvent} */(e); if (e.data.method === 'beginInterceptingEvents') { backgroundNavigation(); } else if (e.data.method === 'stopInterceptingEvents') { foregroundNavigation(); } else if (e.data.method === 'ready') { pageReady(e.origin); } else if (e.data.method === 'updateHistory') { updateHistory(e.origin, e.data.params.state, e.data.params.path, e.data.params.replace); } else if (e.data.method === 'setTitle') { setTitle(e.origin, e.data.params.title); } else if (e.data.method === 'showPage') { showPage(e.data.params.pageId, HISTORY_STATE_OPTION.PUSH, e.data.params.path); } else if (e.data.method === 'navigationControlsLoaded') { onNavigationControlsLoaded(); } else if (e.data.method === 'adjustToScroll') { adjustToScroll(/** @type {number} */(e.data.params)); } else if (e.data.method === 'mouseWheel') { forwardMouseWheel(/** @type {Object} */(e.data.params)); } else if (e.data.method === 'mouseDown') { forwardMouseDown(); } else { console.error('Received unexpected message', e.data); } } /** * Sends the navigation iframe to the background. */ function backgroundNavigation() { navFrame.classList.add('background'); navFrame.firstChild.tabIndex = -1; navFrame.firstChild.setAttribute('aria-hidden', true); } /** * Retrieves the navigation iframe from the background. */ function foregroundNavigation() { navFrame.classList.remove('background'); navFrame.firstChild.tabIndex = 0; navFrame.firstChild.removeAttribute('aria-hidden'); } /** * Enables or disables animated transitions when changing content while * horizontally scrolled. * @param {boolean} enabled True if enabled, else false to disable. */ function setContentChanging(enabled) { navFrame.classList[enabled ? 'add' : 'remove']('changing-content'); if (isRTL()) { uber.invokeMethodOnWindow(navFrame.firstChild.contentWindow, 'setContentChanging', enabled); } } /** * Get an iframe based on the origin of a received post message. * @param {string} origin The origin of a post message. * @return {!Element} The frame associated to |origin| or null. */ function getIframeFromOrigin(origin) { assert(origin.substr(-1) != '/', 'invalid origin given'); var query = '.iframe-container > iframe[src^="' + origin + '/"]'; var element = document.querySelector(query); assert(element); return /** @type {!Element} */(element); } /** * Changes the path past the page title (i.e. chrome://chrome/settings/(.*)). * @param {Object} state The page's state object for the navigation. * @param {string} path The new /path/ to be set after the page name. * @param {number} historyOption The type of history modification to make. */ function changePathTo(state, path, historyOption) { assert(!path || path.substr(-1) != '/', 'invalid path given'); var histFunc; if (historyOption == HISTORY_STATE_OPTION.PUSH) histFunc = window.history.pushState; else if (historyOption == HISTORY_STATE_OPTION.REPLACE) histFunc = window.history.replaceState; assert(histFunc, 'invalid historyOption given ' + historyOption); var pageId = getSelectedIframeContainer().id; var args = [state, '', '/' + pageId + '/' + (path || '')]; histFunc.apply(window.history, args); } /** * Adds or replaces the current history entry based on a navigation from the * source iframe. * @param {string} origin The origin of the source iframe. * @param {Object} state The source iframe's state object. * @param {string} path The new "path" (e.g. "/createProfile"). * @param {boolean} replace Whether to replace the current history entry. */ function updateHistory(origin, state, path, replace) { assert(!path || path[0] != '/', 'invalid path sent from ' + origin); var historyOption = replace ? HISTORY_STATE_OPTION.REPLACE : HISTORY_STATE_OPTION.PUSH; // Only update the currently displayed path if this is the visible frame. var container = getIframeFromOrigin(origin).parentNode; if (container == getSelectedIframeContainer()) changePathTo(state, path, historyOption); } /** * Sets the title of the page. * @param {string} origin The origin of the source iframe. * @param {string} title The title of the page. */ function setTitle(origin, title) { // Cache the title for the client iframe, i.e., the iframe setting the // title. querySelector returns the actual iframe element, so use parentNode // to get back to the container. var container = getIframeFromOrigin(origin).parentNode; container.dataset.title = title; // Only update the currently displayed title if this is the visible frame. if (container == getSelectedIframeContainer()) document.title = title; } /** * Invokes a method on a subpage. If the subpage has not signaled readiness, * queue the message for when it does. * @param {string} pageId Should match an id of one of the iframe containers. * @param {string} method The name of the method to invoke. * @param {Object=} opt_params Optional property page of parameters to pass to * the invoked method. */ function invokeMethodOnPage(pageId, method, opt_params) { var frame = $(pageId).querySelector('iframe'); if (!frame || !frame.dataset.ready) { queuedInvokes[pageId] = (queuedInvokes[pageId] || []); queuedInvokes[pageId].push([method, opt_params]); } else { uber.invokeMethodOnWindow(frame.contentWindow, method, opt_params); } } /** * Called in response to a page declaring readiness. Calls any deferred method * invocations from invokeMethodOnPage. * @param {string} origin The origin of the source iframe. */ function pageReady(origin) { var frame = getIframeFromOrigin(origin); var container = frame.parentNode; frame.dataset.ready = true; var queue = queuedInvokes[container.id] || []; queuedInvokes[container.id] = undefined; for (var i = 0; i < queue.length; i++) { uber.invokeMethodOnWindow(frame.contentWindow, queue[i][0], queue[i][1]); } } /** * Selects and navigates a subpage. This is called from uber-frame. * @param {string} pageId Should match an id of one of the iframe containers. * @param {number} historyOption Indicates whether we should push or replace * browser history. * @param {string} path A sub-page path. */ function showPage(pageId, historyOption, path) { var container = $(pageId); // Lazy load of iframe contents. var sourceUrl = container.dataset.url + (path || ''); var frame = container.querySelector('iframe'); if (!frame) { frame = container.ownerDocument.createElement('iframe'); frame.name = pageId; frame.setAttribute('role', 'presentation'); container.appendChild(frame); frame.src = sourceUrl; } else { // There's no particularly good way to know what the current URL of the // content frame is as we don't have access to its contentWindow's // location, so just replace every time until necessary to do otherwise. frame.contentWindow.location.replace(sourceUrl); frame.dataset.ready = false; } // If the last selected container is already showing, ignore the rest. var lastSelected = document.querySelector('.iframe-container.selected'); if (lastSelected === container) return; if (lastSelected) { lastSelected.classList.remove('selected'); // Setting aria-hidden hides the container from assistive technology // immediately. The 'hidden' attribute is set after the transition // finishes - that ensures it's not possible to accidentally focus // an element in an unselected container. lastSelected.setAttribute('aria-hidden', 'true'); } // Containers that aren't selected have to be hidden so that their // content isn't focusable. container.hidden = false; container.setAttribute('aria-hidden', 'false'); // Trigger a layout after making it visible and before setting // the class to 'selected', so that it animates in. /** @suppress {uselessCode} */ container.offsetTop; container.classList.add('selected'); setContentChanging(true); adjustToScroll(0); var selectedWindow = getSelectedIframeWindow(); uber.invokeMethodOnWindow(selectedWindow, 'frameSelected'); selectedWindow.focus(); if (historyOption != HISTORY_STATE_OPTION.NONE) changePathTo({}, path, historyOption); if (container.dataset.title) document.title = container.dataset.title; assert('favicon' in container.dataset); var dataset = /** @type {{favicon: string}} */(container.dataset); $('favicon').href = 'chrome://theme/' + dataset.favicon; $('favicon2x').href = 'chrome://theme/' + dataset.favicon + '@2x'; updateNavigationControls(); } function onNavigationControlsLoaded() { updateNavigationControls(); } /** * Sends a message to uber-frame to update the appearance of the nav controls. * It should be called whenever the selected iframe changes. */ function updateNavigationControls() { var container = getSelectedIframeContainer(); uber.invokeMethodOnWindow(navFrame.firstChild.contentWindow, 'changeSelection', {pageId: container.id}); } /** * Forwarded scroll offset from a content frame's scroll handler. * @param {number} scrollOffset The scroll offset from the content frame. */ function adjustToScroll(scrollOffset) { // NOTE: The scroll is reset to 0 and easing turned on every time a user // switches frames. If we receive a non-zero value it has to have come from // a real user scroll, so we disable easing when this happens. if (scrollOffset != 0) setContentChanging(false); if (isRTL()) { uber.invokeMethodOnWindow(navFrame.firstChild.contentWindow, 'adjustToScroll', scrollOffset); var navWidth = Math.max(0, +navFrame.dataset.width + scrollOffset); navFrame.style.width = navWidth + 'px'; } else { navFrame.style.webkitTransform = 'translateX(' + -scrollOffset + 'px)'; } } /** * Forward scroll wheel events to subpages. * @param {Object} params Relevant parameters of wheel event. */ function forwardMouseWheel(params) { uber.invokeMethodOnWindow(getSelectedIframeWindow(), 'mouseWheel', params); } /** Forward mouse down events to subpages. */ function forwardMouseDown() { uber.invokeMethodOnWindow(getSelectedIframeWindow(), 'mouseDown'); } /** * Make sure that iframe containers that are not selected are * hidden, so that elements in those frames aren't part of the * focus order. Containers that are unselected later get hidden * when the transition ends. We also set the aria-hidden attribute * because that hides the container from assistive technology * immediately, rather than only after the transition ends. */ function ensureNonSelectedFrameContainersAreHidden() { var containers = document.querySelectorAll('.iframe-container'); for (var i = 0; i < containers.length; i++) { var container = containers[i]; if (!container.classList.contains('selected')) { container.hidden = true; container.setAttribute('aria-hidden', 'true'); } container.addEventListener('webkitTransitionEnd', function(event) { if (!event.target.classList.contains('selected')) event.target.hidden = true; }); } } return { onLoad: onLoad, onPopHistoryState: onPopHistoryState }; }); window.addEventListener('popstate', uber.onPopHistoryState); document.addEventListener('DOMContentLoaded', uber.onLoad);

// Copyright (c) 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. // This file contains the navigation controls that are visible on the left side // of the uber page. It exists separately from uber.js so that it may be loaded // in an iframe. Iframes can be layered on top of each other, but not mixed in // with page content, so all overlapping content on uber must be framed. // // Copyright (c) 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. // // Copyright (c) 2013 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. /** * @fileoverview Assertion support. */ /** * Verify |condition| is truthy and return |condition| if so. * @template T * @param {T} condition A condition to check for truthiness. Note that this * may be used to test whether a value is defined or not, and we don't want * to force a cast to Boolean. * @param {string=} opt_message A message to show on failure. * @return {T} A non-null |condition|. */ function assert(condition, opt_message) { if (!condition) { var message = 'Assertion failed'; if (opt_message) message = message + ': ' + opt_message; var error = new Error(message); var global = function() { return this; }(); if (global.traceAssertionsForTesting) console.warn(error.stack); throw error; } return condition; } /** * Call this from places in the code that should never be reached. * * For example, handling all the values of enum with a switch() like this: * * function getValueFromEnum(enum) { * switch (enum) { * case ENUM_FIRST_OF_TWO: * return first * case ENUM_LAST_OF_TWO: * return last; * } * assertNotReached(); * return document; * } * * This code should only be hit in the case of serious programmer error or * unexpected input. * * @param {string=} opt_message A message to show when this is hit. */ function assertNotReached(opt_message) { assert(false, opt_message || 'Unreachable code hit'); } /** * @param {*} value The value to check. * @param {function(new: T, ...)} type A user-defined constructor. * @param {string=} opt_message A message to show when this is hit. * @return {T} * @template T */ function assertInstanceof(value, type, opt_message) { // We don't use assert immediately here so that we avoid constructing an error // message if we don't have to. if (!(value instanceof type)) { assertNotReached( opt_message || 'Value ' + value + ' is not a[n] ' + (type.name || typeof type)); } return value; } /** * Alias for document.getElementById. Found elements must be HTMLElements. * @param {string} id The ID of the element to find. * @return {HTMLElement} The found element or null if not found. */ function $(id) { var el = document.getElementById(id); return el ? assertInstanceof(el, HTMLElement) : null; } // TODO(devlin): This should return SVGElement, but closure compiler is missing // those externs. /** * Alias for document.getElementById. Found elements must be SVGElements. * @param {string} id The ID of the element to find. * @return {Element} The found element or null if not found. */ function getSVGElement(id) { var el = document.getElementById(id); return el ? assertInstanceof(el, Element) : null; } /** * Add an accessible message to the page that will be announced to * users who have spoken feedback on, but will be invisible to all * other users. It's removed right away so it doesn't clutter the DOM. * @param {string} msg The text to be pronounced. */ function announceAccessibleMessage(msg) { var element = document.createElement('div'); element.setAttribute('aria-live', 'polite'); element.style.position = 'fixed'; element.style.left = '-9999px'; element.style.height = '0px'; element.innerText = msg; document.body.appendChild(element); window.setTimeout(function() { document.body.removeChild(element); }, 0); } /** * Generates a CSS url string. * @param {string} s The URL to generate the CSS url for. * @return {string} The CSS url string. */ function url(s) { // http://www.w3.org/TR/css3-values/#uris // Parentheses, commas, whitespace characters, single quotes (') and double // quotes (") appearing in a URI must be escaped with a backslash var s2 = s.replace(/(\(|\)|\,|\s|\'|\"|\\)/g, '\\$1'); // WebKit has a bug when it comes to URLs that end with \ // https://bugs.webkit.org/show_bug.cgi?id=28885 if (/\\\\$/.test(s2)) { // Add a space to work around the WebKit bug. s2 += ' '; } return 'url("' + s2 + '")'; } /** * Parses query parameters from Location. * @param {Location} location The URL to generate the CSS url for. * @return {Object} Dictionary containing name value pairs for URL */ function parseQueryParams(location) { var params = {}; var query = unescape(location.search.substring(1)); var vars = query.split('&'); for (var i = 0; i < vars.length; i++) { var pair = vars[i].split('='); params[pair[0]] = pair[1]; } return params; } /** * Creates a new URL by appending or replacing the given query key and value. * Not supporting URL with username and password. * @param {Location} location The original URL. * @param {string} key The query parameter name. * @param {string} value The query parameter value. * @return {string} The constructed new URL. */ function setQueryParam(location, key, value) { var query = parseQueryParams(location); query[encodeURIComponent(key)] = encodeURIComponent(value); var newQuery = ''; for (var q in query) { newQuery += (newQuery ? '&' : '?') + q + '=' + query[q]; } return location.origin + location.pathname + newQuery + location.hash; } /** * @param {Node} el A node to search for ancestors with |className|. * @param {string} className A class to search for. * @return {Element} A node with class of |className| or null if none is found. */ function findAncestorByClass(el, className) { return /** @type {Element} */ (findAncestor(el, function(el) { return el.classList && el.classList.contains(className); })); } /** * Return the first ancestor for which the {@code predicate} returns true. * @param {Node} node The node to check. * @param {function(Node):boolean} predicate The function that tests the * nodes. * @return {Node} The found ancestor or null if not found. */ function findAncestor(node, predicate) { var last = false; while (node != null && !(last = predicate(node))) { node = node.parentNode; } return last ? node : null; } function swapDomNodes(a, b) { var afterA = a.nextSibling; if (afterA == b) { swapDomNodes(b, a); return; } var aParent = a.parentNode; b.parentNode.replaceChild(a, b); aParent.insertBefore(b, afterA); } /** * Disables text selection and dragging, with optional whitelist callbacks. * @param {function(Event):boolean=} opt_allowSelectStart Unless this function * is defined and returns true, the onselectionstart event will be * surpressed. * @param {function(Event):boolean=} opt_allowDragStart Unless this function * is defined and returns true, the ondragstart event will be surpressed. */ function disableTextSelectAndDrag(opt_allowSelectStart, opt_allowDragStart) { // Disable text selection. document.onselectstart = function(e) { if (!(opt_allowSelectStart && opt_allowSelectStart.call(this, e))) e.preventDefault(); }; // Disable dragging. document.ondragstart = function(e) { if (!(opt_allowDragStart && opt_allowDragStart.call(this, e))) e.preventDefault(); }; } /** * TODO(dbeam): DO NOT USE. THIS IS DEPRECATED. Use an action-link instead. * Call this to stop clicks on links from scrolling to the top of * the page (and possibly showing a # in the link). */ function preventDefaultOnPoundLinkClicks() { document.addEventListener('click', function(e) { var anchor = findAncestor(/** @type {Node} */ (e.target), function(el) { return el.tagName == 'A'; }); // Use getAttribute() to prevent URL normalization. if (anchor && anchor.getAttribute('href') == '#') e.preventDefault(); }); } /** * Check the directionality of the page. * @return {boolean} True if Chrome is running an RTL UI. */ function isRTL() { return document.documentElement.dir == 'rtl'; } /** * Get an element that's known to exist by its ID. We use this instead of just * calling getElementById and not checking the result because this lets us * satisfy the JSCompiler type system. * @param {string} id The identifier name. * @return {!HTMLElement} the Element. */ function getRequiredElement(id) { return assertInstanceof( $(id), HTMLElement, 'Missing required element: ' + id); } /** * Query an element that's known to exist by a selector. We use this instead of * just calling querySelector and not checking the result because this lets us * satisfy the JSCompiler type system. * @param {string} selectors CSS selectors to query the element. * @param {(!Document|!DocumentFragment|!Element)=} opt_context An optional * context object for querySelector. * @return {!HTMLElement} the Element. */ function queryRequiredElement(selectors, opt_context) { var element = (opt_context || document).querySelector(selectors); return assertInstanceof( element, HTMLElement, 'Missing required element: ' + selectors); } // Handle click on a link. If the link points to a chrome: or file: url, then // call into the browser to do the navigation. ['click', 'auxclick'].forEach(function(eventName) { document.addEventListener(eventName, function(e) { if (e.button > 1) return; // Ignore buttons other than left and middle. if (e.defaultPrevented) return; var eventPath = e.path; var anchor = null; if (eventPath) { for (var i = 0; i < eventPath.length; i++) { var element = eventPath[i]; if (element.tagName === 'A' && element.href) { anchor = element; break; } } } // Fallback if Event.path is not available. var el = e.target; if (!anchor && el.nodeType == Node.ELEMENT_NODE && el.webkitMatchesSelector('A, A *')) { while (el.tagName != 'A') { el = el.parentElement; } anchor = el; } if (!anchor) return; anchor = /** @type {!HTMLAnchorElement} */ (anchor); if ((anchor.protocol == 'file:' || anchor.protocol == 'about:') && (e.button == 0 || e.button == 1)) { chrome.send('navigateToUrl', [ anchor.href, anchor.target, e.button, e.altKey, e.ctrlKey, e.metaKey, e.shiftKey ]); e.preventDefault(); } }); }); /** * Creates a new URL which is the old URL with a GET param of key=value. * @param {string} url The base URL. There is not sanity checking on the URL so * it must be passed in a proper format. * @param {string} key The key of the param. * @param {string} value The value of the param. * @return {string} The new URL. */ function appendParam(url, key, value) { var param = encodeURIComponent(key) + '=' + encodeURIComponent(value); if (url.indexOf('?') == -1) return url + '?' + param; return url + '&' + param; } /** * Creates an element of a specified type with a specified class name. * @param {string} type The node type. * @param {string} className The class name to use. * @return {Element} The created element. */ function createElementWithClassName(type, className) { var elm = document.createElement(type); elm.className = className; return elm; } /** * webkitTransitionEnd does not always fire (e.g. when animation is aborted * or when no paint happens during the animation). This function sets up * a timer and emulate the event if it is not fired when the timer expires. * @param {!HTMLElement} el The element to watch for webkitTransitionEnd. * @param {number=} opt_timeOut The maximum wait time in milliseconds for the * webkitTransitionEnd to happen. If not specified, it is fetched from |el| * using the transitionDuration style value. */ function ensureTransitionEndEvent(el, opt_timeOut) { if (opt_timeOut === undefined) { var style = getComputedStyle(el); opt_timeOut = parseFloat(style.transitionDuration) * 1000; // Give an additional 50ms buffer for the animation to complete. opt_timeOut += 50; } var fired = false; el.addEventListener('webkitTransitionEnd', function f(e) { el.removeEventListener('webkitTransitionEnd', f); fired = true; }); window.setTimeout(function() { if (!fired) cr.dispatchSimpleEvent(el, 'webkitTransitionEnd', true); }, opt_timeOut); } /** * Alias for document.scrollTop getter. * @param {!HTMLDocument} doc The document node where information will be * queried from. * @return {number} The Y document scroll offset. */ function scrollTopForDocument(doc) { return doc.documentElement.scrollTop || doc.body.scrollTop; } /** * Alias for document.scrollTop setter. * @param {!HTMLDocument} doc The document node where information will be * queried from. * @param {number} value The target Y scroll offset. */ function setScrollTopForDocument(doc, value) { doc.documentElement.scrollTop = doc.body.scrollTop = value; } /** * Alias for document.scrollLeft getter. * @param {!HTMLDocument} doc The document node where information will be * queried from. * @return {number} The X document scroll offset. */ function scrollLeftForDocument(doc) { return doc.documentElement.scrollLeft || doc.body.scrollLeft; } /** * Alias for document.scrollLeft setter. * @param {!HTMLDocument} doc The document node where information will be * queried from. * @param {number} value The target X scroll offset. */ function setScrollLeftForDocument(doc, value) { doc.documentElement.scrollLeft = doc.body.scrollLeft = value; } /** * Replaces '&', '<', '>', '"', and ''' characters with their HTML encoding. * @param {string} original The original string. * @return {string} The string with all the characters mentioned above replaced. */ function HTMLEscape(original) { return original.replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } /** * Shortens the provided string (if necessary) to a string of length at most * |maxLength|. * @param {string} original The original string. * @param {number} maxLength The maximum length allowed for the string. * @return {string} The original string if its length does not exceed * |maxLength|. Otherwise the first |maxLength| - 1 characters with '...' * appended. */ function elide(original, maxLength) { if (original.length <= maxLength) return original; return original.substring(0, maxLength - 1) + '\u2026'; } /** * Quote a string so it can be used in a regular expression. * @param {string} str The source string. * @return {string} The escaped string. */ function quoteString(str) { return str.replace(/([\\\.\+\*\?\[\^\]\$\(\)\{\}\=\!\<\>\|\:])/g, '\\$1'); } /** * Calls |callback| and stops listening the first time any event in |eventNames| * is triggered on |target|. * @param {!EventTarget} target * @param {!Array|string} eventNames Array or space-delimited string of * event names to listen to (e.g. 'click mousedown'). * @param {function(!Event)} callback Called at most once. The * optional return value is passed on by the listener. */ function listenOnce(target, eventNames, callback) { if (!Array.isArray(eventNames)) eventNames = eventNames.split(/ +/); var removeAllAndCallCallback = function(event) { eventNames.forEach(function(eventName) { target.removeEventListener(eventName, removeAllAndCallCallback, false); }); return callback(event); }; eventNames.forEach(function(eventName) { target.addEventListener(eventName, removeAllAndCallCallback, false); }); } // /* is_ios */ /** * Helper to convert callback-based define() API to a promise-based API. * @suppress {undefinedVars} * @param {!Array} moduleNames * @return {!Promise} */ function importModules(moduleNames) { return new Promise(function(resolve) { define(moduleNames, function() { resolve(Array.from(arguments)); }); }); } /** * @param {!Event} e * @return {boolean} Whether a modifier key was down when processing |e|. */ function hasKeyModifiers(e) { return !!(e.altKey || e.ctrlKey || e.metaKey || e.shiftKey); } // // Copyright (c) 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. /** * @fileoverview A collection of utility methods for UberPage and its contained * pages. */ cr.define('uber', function() { /** * Fixed position header elements on the page to be shifted by handleScroll. * @type {NodeList} */ var headerElements; /** * This should be called by uber content pages when DOM content has loaded. */ function onContentFrameLoaded() { headerElements = document.getElementsByTagName('header'); document.addEventListener('scroll', handleScroll); document.addEventListener('mousedown', handleMouseDownInFrame, true); invokeMethodOnParent('ready'); // Prevent the navigation from being stuck in a disabled state when a // content page is reloaded while an overlay is visible (crbug.com/246939). invokeMethodOnParent('stopInterceptingEvents'); // Trigger the scroll handler to tell the navigation if our page started // with some scroll (happens when you use tab restore). handleScroll(); window.addEventListener('message', handleWindowMessage); } /** * Handles scroll events on the document. This adjusts the position of all * headers and updates the parent frame when the page is scrolled. */ function handleScroll() { var scrollLeft = scrollLeftForDocument(document); var offset = scrollLeft * -1; for (var i = 0; i < headerElements.length; i++) { // As a workaround for http://crbug.com/231830, set the transform to // 'none' rather than 0px. headerElements[i].style.webkitTransform = offset ? 'translateX(' + offset + 'px)' : 'none'; } invokeMethodOnParent('adjustToScroll', scrollLeft); } /** * Tells the parent to focus the current frame if the mouse goes down in the * current frame (and it doesn't already have focus). * @param {Event} e A mousedown event. */ function handleMouseDownInFrame(e) { if (!e.isSynthetic && !document.hasFocus()) window.focus(); } /** * Handles 'message' events on window. * @param {Event} e The message event. */ function handleWindowMessage(e) { e = /** @type {!MessageEvent} */(e); if (e.data.method === 'frameSelected') { handleFrameSelected(); } else if (e.data.method === 'mouseWheel') { handleMouseWheel( /** @type {{deltaX: number, deltaY: number}} */(e.data.params)); } else if (e.data.method === 'mouseDown') { handleMouseDown(); } else if (e.data.method === 'popState') { handlePopState(e.data.params.state, e.data.params.path); } } /** * This is called when a user selects this frame via the navigation bar * frame (and is triggered via postMessage() from the uber page). */ function handleFrameSelected() { setScrollTopForDocument(document, 0); } /** * Called when a user mouse wheels (or trackpad scrolls) over the nav frame. * The wheel event is forwarded here and we scroll the body. * There's no way to figure out the actual scroll amount for a given delta. * It differs for every platform and even initWebKitWheelEvent takes a * pixel amount instead of a wheel delta. So we just choose something * reasonable and hope no one notices the difference. * @param {{deltaX: number, deltaY: number}} params A structure that holds * wheel deltas in X and Y. */ function handleMouseWheel(params) { window.scrollBy(-params.deltaX * 49 / 120, -params.deltaY * 49 / 120); } /** * Fire a synthetic mousedown on the body to dismiss transient things like * bubbles or menus that listen for mouse presses outside of their UI. We * dispatch a fake mousedown rather than a 'mousepressedinnavframe' so that * settings/history/extensions don't need to know about their embedder. */ function handleMouseDown() { var mouseEvent = new MouseEvent('mousedown'); mouseEvent.isSynthetic = true; document.dispatchEvent(mouseEvent); } /** * Called when the parent window restores some state saved by uber.pushState * or uber.replaceState. Simulates a popstate event. * @param {PopStateEvent} state A state object for replaceState and pushState. * @param {string} path The path the page navigated to. * @suppress {checkTypes} */ function handlePopState(state, path) { window.history.replaceState(state, '', path); window.dispatchEvent(new PopStateEvent('popstate', {state: state})); } /** * @return {boolean} Whether this frame has a parent. */ function hasParent() { return window != window.parent; } /** * Invokes a method on the parent window (UberPage). This is a convenience * method for API calls into the uber page. * @param {string} method The name of the method to invoke. * @param {?=} opt_params Optional property bag of parameters to pass to the * invoked method. */ function invokeMethodOnParent(method, opt_params) { if (!hasParent()) return; invokeMethodOnWindow(window.parent, method, opt_params, 'chrome://chrome'); } /** * Invokes a method on the target window. * @param {string} method The name of the method to invoke. * @param {?=} opt_params Optional property bag of parameters to pass to the * invoked method. * @param {string=} opt_url The origin of the target window. */ function invokeMethodOnWindow(targetWindow, method, opt_params, opt_url) { var data = {method: method, params: opt_params}; targetWindow.postMessage(data, opt_url ? opt_url : '*'); } /** * Updates the page's history state. If the page is embedded in a child, * forward the information to the parent for it to manage history for us. This * is a replacement of history.replaceState and history.pushState. * @param {Object} state A state object for replaceState and pushState. * @param {string} path The path the page navigated to. * @param {boolean} replace If true, navigate with replacement. */ function updateHistory(state, path, replace) { var historyFunction = replace ? window.history.replaceState : window.history.pushState; if (hasParent()) { // If there's a parent, always replaceState. The parent will do the actual // pushState. historyFunction = window.history.replaceState; invokeMethodOnParent('updateHistory', { state: state, path: path, replace: replace}); } historyFunction.call(window.history, state, '', '/' + path); } /** * Sets the current title for the page. If the page is embedded in a child, * forward the information to the parent. This is a replacement for setting * document.title. * @param {string} title The new title for the page. */ function setTitle(title) { document.title = title; invokeMethodOnParent('setTitle', {title: title}); } /** * Pushes new history state for the page. If the page is embedded in a child, * forward the information to the parent; when embedded, all history entries * are attached to the parent. This is a replacement of history.pushState. * @param {Object} state A state object for replaceState and pushState. * @param {string} path The path the page navigated to. */ function pushState(state, path) { updateHistory(state, path, false); } /** * Replaces the page's history state. If the page is embedded in a child, * forward the information to the parent; when embedded, all history entries * are attached to the parent. This is a replacement of history.replaceState. * @param {Object} state A state object for replaceState and pushState. * @param {string} path The path the page navigated to. */ function replaceState(state, path) { updateHistory(state, path, true); } return { invokeMethodOnParent: invokeMethodOnParent, invokeMethodOnWindow: invokeMethodOnWindow, onContentFrameLoaded: onContentFrameLoaded, pushState: pushState, replaceState: replaceState, setTitle: setTitle, }; }); cr.define('uber_frame', function() { /** * Handles page initialization. */ function onLoad() { var navigationItems = document.querySelectorAll('li'); for (var i = 0; i < navigationItems.length; ++i) { navigationItems[i].addEventListener('click', onNavItemClicked); } cr.ui.FocusOutlineManager.forDocument(this); window.addEventListener('message', handleWindowMessage); uber.invokeMethodOnParent('navigationControlsLoaded'); document.documentElement.addEventListener('mousewheel', onMouseWheel); document.documentElement.addEventListener('mousedown', onMouseDown); } /** * Handles clicks on the navigation controls (switches the page and updates * the URL). * @param {Event} e The click event. */ function onNavItemClicked(e) { // Though pointer-event: none; is applied to the .selected nav item, users // can still tab to them and press enter/space which simulates a click. if (e.target.classList.contains('selected')) return; // Extensions can override Uber content (e.g., if the user has a history // extension, it should display when the 'History' navigation is clicked). if (e.currentTarget.getAttribute('override') == 'yes') { window.open('chrome://' + e.currentTarget.getAttribute('controls'), '_blank'); return; } uber.invokeMethodOnParent('showPage', {pageId: e.currentTarget.getAttribute('controls')}); setSelection(/** @type {Element} */(e.currentTarget)); } /** * Handles postMessage from chrome://chrome. * @param {Event} e The post data. */ function handleWindowMessage(e) { if (e.data.method === 'changeSelection') changeSelection(e.data.params); else if (e.data.method === 'adjustToScroll') adjustToScroll(e.data.params); else if (e.data.method === 'setContentChanging') setContentChanging(e.data.params); else console.error('Received unexpected message', e.data); } /** * Changes the selected nav control. * @param {Object} params Must contain pageId. */ function changeSelection(params) { var navItem = document.querySelector('li[controls="' + params.pageId + '"]'); setSelection(navItem); showNavItems(); } /** * @return {Element} The currently selected nav item, if any. */ function getSelectedNavItem() { return document.querySelector('li.selected'); } /** * Sets selection on the given nav item. * @param {Element} newSelection The item to be selected. */ function setSelection(newSelection) { var items = document.querySelectorAll('li'); for (var i = 0; i < items.length; ++i) { items[i].classList.toggle('selected', items[i] == newSelection); items[i].setAttribute('aria-selected', items[i] == newSelection); } } /** * Shows nav items belonging to the same group as the selected item. */ function showNavItems() { var hideSettingsAndHelp = loadTimeData.getBoolean('hideSettingsAndHelp'); $('settings').hidden = hideSettingsAndHelp; $('help').hidden = hideSettingsAndHelp; $('extensions').hidden = loadTimeData.getBoolean('hideExtensions'); $('history').hidden = loadTimeData.getBoolean('hideHistory'); } /** * Adjusts this frame's content to scrolls from the outer frame. This is done * to obscure text in RTL as a user scrolls over the content of this frame (as * currently RTL scrollbars still draw on the right). * @param {number} scrollLeft document.body.scrollLeft of the content frame. */ function adjustToScroll(scrollLeft) { assert(isRTL()); document.body.style.webkitTransform = 'translateX(' + -scrollLeft + 'px)'; } /** * Enable/disable an animation to ease the nav bar back into view when * changing content while horizontally scrolled. * @param {boolean} enabled Whether easing should be enabled. */ function setContentChanging(enabled) { assert(isRTL()); document.documentElement.classList.toggle('changing-content', enabled); } /** * Handles mouse wheels on the top level element. Forwards them to uber.js. * @param {Event} e The mouse wheel event. */ function onMouseWheel(e) { uber.invokeMethodOnParent('mouseWheel', {deltaX: e.wheelDeltaX, deltaY: e.wheelDeltaY}); } /** * Handles mouse presses on the top level element. Forwards them to uber.js. * @param {Event} e The mouse down event. */ function onMouseDown(e) { uber.invokeMethodOnParent('mouseDown'); } /** * @return {Element} The currently selected iframe container. * @private */ function getSelectedIframe() { return document.querySelector('.iframe-container.selected'); } /** * Finds the
  • element whose 'controls' attribute is |controls| and sets * its 'override' attribute to |override|. * @param {string} controls The value of the 'controls' attribute of the * element to change. * @param {string} override The value to set for the 'override' attribute of * that element (either 'yes' or 'no'). */ function setNavigationOverride(controls, override) { var navItem = document.querySelector('li[controls="' + controls + '"]'); navItem.setAttribute('override', override); } return { onLoad: onLoad, setNavigationOverride: setNavigationOverride, }; }); document.addEventListener('DOMContentLoaded', uber_frame.onLoad); // Copyright (c) 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. /** * @fileoverview A collection of utility methods for UberPage and its contained * pages. */ cr.define('uber', function() { /** * Fixed position header elements on the page to be shifted by handleScroll. * @type {NodeList} */ var headerElements; /** * This should be called by uber content pages when DOM content has loaded. */ function onContentFrameLoaded() { headerElements = document.getElementsByTagName('header'); document.addEventListener('scroll', handleScroll); document.addEventListener('mousedown', handleMouseDownInFrame, true); invokeMethodOnParent('ready'); // Prevent the navigation from being stuck in a disabled state when a // content page is reloaded while an overlay is visible (crbug.com/246939). invokeMethodOnParent('stopInterceptingEvents'); // Trigger the scroll handler to tell the navigation if our page started // with some scroll (happens when you use tab restore). handleScroll(); window.addEventListener('message', handleWindowMessage); } /** * Handles scroll events on the document. This adjusts the position of all * headers and updates the parent frame when the page is scrolled. */ function handleScroll() { var scrollLeft = scrollLeftForDocument(document); var offset = scrollLeft * -1; for (var i = 0; i < headerElements.length; i++) { // As a workaround for http://crbug.com/231830, set the transform to // 'none' rather than 0px. headerElements[i].style.webkitTransform = offset ? 'translateX(' + offset + 'px)' : 'none'; } invokeMethodOnParent('adjustToScroll', scrollLeft); } /** * Tells the parent to focus the current frame if the mouse goes down in the * current frame (and it doesn't already have focus). * @param {Event} e A mousedown event. */ function handleMouseDownInFrame(e) { if (!e.isSynthetic && !document.hasFocus()) window.focus(); } /** * Handles 'message' events on window. * @param {Event} e The message event. */ function handleWindowMessage(e) { e = /** @type {!MessageEvent} */(e); if (e.data.method === 'frameSelected') { handleFrameSelected(); } else if (e.data.method === 'mouseWheel') { handleMouseWheel( /** @type {{deltaX: number, deltaY: number}} */(e.data.params)); } else if (e.data.method === 'mouseDown') { handleMouseDown(); } else if (e.data.method === 'popState') { handlePopState(e.data.params.state, e.data.params.path); } } /** * This is called when a user selects this frame via the navigation bar * frame (and is triggered via postMessage() from the uber page). */ function handleFrameSelected() { setScrollTopForDocument(document, 0); } /** * Called when a user mouse wheels (or trackpad scrolls) over the nav frame. * The wheel event is forwarded here and we scroll the body. * There's no way to figure out the actual scroll amount for a given delta. * It differs for every platform and even initWebKitWheelEvent takes a * pixel amount instead of a wheel delta. So we just choose something * reasonable and hope no one notices the difference. * @param {{deltaX: number, deltaY: number}} params A structure that holds * wheel deltas in X and Y. */ function handleMouseWheel(params) { window.scrollBy(-params.deltaX * 49 / 120, -params.deltaY * 49 / 120); } /** * Fire a synthetic mousedown on the body to dismiss transient things like * bubbles or menus that listen for mouse presses outside of their UI. We * dispatch a fake mousedown rather than a 'mousepressedinnavframe' so that * settings/history/extensions don't need to know about their embedder. */ function handleMouseDown() { var mouseEvent = new MouseEvent('mousedown'); mouseEvent.isSynthetic = true; document.dispatchEvent(mouseEvent); } /** * Called when the parent window restores some state saved by uber.pushState * or uber.replaceState. Simulates a popstate event. * @param {PopStateEvent} state A state object for replaceState and pushState. * @param {string} path The path the page navigated to. * @suppress {checkTypes} */ function handlePopState(state, path) { window.history.replaceState(state, '', path); window.dispatchEvent(new PopStateEvent('popstate', {state: state})); } /** * @return {boolean} Whether this frame has a parent. */ function hasParent() { return window != window.parent; } /** * Invokes a method on the parent window (UberPage). This is a convenience * method for API calls into the uber page. * @param {string} method The name of the method to invoke. * @param {?=} opt_params Optional property bag of parameters to pass to the * invoked method. */ function invokeMethodOnParent(method, opt_params) { if (!hasParent()) return; invokeMethodOnWindow(window.parent, method, opt_params, 'chrome://chrome'); } /** * Invokes a method on the target window. * @param {string} method The name of the method to invoke. * @param {?=} opt_params Optional property bag of parameters to pass to the * invoked method. * @param {string=} opt_url The origin of the target window. */ function invokeMethodOnWindow(targetWindow, method, opt_params, opt_url) { var data = {method: method, params: opt_params}; targetWindow.postMessage(data, opt_url ? opt_url : '*'); } /** * Updates the page's history state. If the page is embedded in a child, * forward the information to the parent for it to manage history for us. This * is a replacement of history.replaceState and history.pushState. * @param {Object} state A state object for replaceState and pushState. * @param {string} path The path the page navigated to. * @param {boolean} replace If true, navigate with replacement. */ function updateHistory(state, path, replace) { var historyFunction = replace ? window.history.replaceState : window.history.pushState; if (hasParent()) { // If there's a parent, always replaceState. The parent will do the actual // pushState. historyFunction = window.history.replaceState; invokeMethodOnParent('updateHistory', { state: state, path: path, replace: replace}); } historyFunction.call(window.history, state, '', '/' + path); } /** * Sets the current title for the page. If the page is embedded in a child, * forward the information to the parent. This is a replacement for setting * document.title. * @param {string} title The new title for the page. */ function setTitle(title) { document.title = title; invokeMethodOnParent('setTitle', {title: title}); } /** * Pushes new history state for the page. If the page is embedded in a child, * forward the information to the parent; when embedded, all history entries * are attached to the parent. This is a replacement of history.pushState. * @param {Object} state A state object for replaceState and pushState. * @param {string} path The path the page navigated to. */ function pushState(state, path) { updateHistory(state, path, false); } /** * Replaces the page's history state. If the page is embedded in a child, * forward the information to the parent; when embedded, all history entries * are attached to the parent. This is a replacement of history.replaceState. * @param {Object} state A state object for replaceState and pushState. * @param {string} path The path the page navigated to. */ function replaceState(state, path) { updateHistory(state, path, true); } return { invokeMethodOnParent: invokeMethodOnParent, invokeMethodOnWindow: invokeMethodOnWindow, onContentFrameLoaded: onContentFrameLoaded, pushState: pushState, replaceState: replaceState, setTitle: setTitle, }; }); N0DUQ)=$ĥ-vd;U"ĿRmw,3)?\X5\t@p$!;$QX!-ȦXTMc[ɤ,\e*v9wf(vNBIK[bjrBҰھk {ifB C,"2s`MuPY/$.3}!1S;!Y -BE&xv'TmslD 7n"[zF0 oK4L?&}uxysAYк㊚)ºd2&,| B2s46r~9u1KLWk&tZ%;^舒(ϡݛ)2&ǩ_'}& =& w#1O>a62S7N=it^e e+Hߤ"9ɴ?!Ѱ]2vߏ#4NqctVKo6ﯘCIWJz!FQď".lhS`4Zq >! g$ ~|UZ_D(&B@ 2Ѡ`/U-2#JmPK!}/> 3D%1 )zKr2.i\`X$'' 8?؆L%g'.-9aĥ)4AACIBeLb2,adfa.XUu_Lz ?2 LRI媋;jK!4Zeh # g ] tÍm13^}@c !VnGm\[ HE[^UA8܋@&ȁ ]Yx Q]06xQ|C{ xn0)i.<O?]C|W.z4vNPx&N$\{AOH4XŎ~<\&[4xhRInms8DقtQ,~O}4 j\v 0Ldg-`xct\ <_N,:Ac_^袏U"L"C fOhNth4Ij3{4DC@O= M^AV* Gx= DG䀖s|I|5+3*45x9<@+|oE-yWWJA9@VâF{MͅG $)qJ\T@U&z4-jpK*Mbר8I_o~-W-ޢr:fUL!*RhrS |ԯXcץKȭT.ld/Ӎ(4OI'XmOZì0'ô=9-!o8jI-7ojv9G3kK1Z0H7 ySR6&{tu']5Q{u֎O0ZA ?QiD`@K.zYF%~)(u-: R{2棯twk4Vp(֊5R[{g02 0!Knkn(!'P}tJ?+$/<' QThয়mmhMȄQ .[J,睊Tj_BbȷYItU3F7;X딍Ewն}8:F[~6P~='H -l8VYVwh}EoݝxՍQ'{.qt!yo,+^Ne y[w><3 5m/.r2ѕQCߘKQ-s!U߬˨덹Gf*h^t)eSygZgE1B}偕m~a:Z`d|q˴`S=y4o) w W:bsZb>C{ Æ >yplZYjYŃ User Actions Debug Page

    Listening for user actions...

    User ActionTimestamp (sec.)
    /* Copyright 2012 The Chromium Authors. All rights reserved. * Use of this source code is governed by a BSD-style license that can be * found in the LICENSE file. */ thead { white-space: nowrap; } th { background-color: #C0C0C0; } td { background-color: #F0F0F0; } // Copyright 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. /** * Javascript for user_actions.html, served from chrome://user-actions/ * This is used to debug user actions recording. It displays a live * stream of all user action events that occur in chromium while the * chrome://user-actions/ page is open. * * The simple object defined in this javascript file listens for * callbacks from the C++ code saying that a new user action was seen. */ cr.define('userActions', function() { 'use strict'; /** * Appends a row to the output table listing the user action observed * and the current timestamp. * @param {string} userAction the name of the user action observed. */ function observeUserAction(userAction) { var table = $('user-actions-table'); var tr = document.createElement('tr'); var td = document.createElement('td'); td.textContent = userAction; tr.appendChild(td); td = document.createElement('td'); td.textContent = Date.now() / 1000; // in seconds since epoch tr.appendChild(td); table.appendChild(tr); } return { observeUserAction: observeUserAction }; });

    // Copyright 2013 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. /** * Requests the list of uploads from the backend. */ function requestUploads() { chrome.send('requestWebRtcLogsList'); } /** * Callback from backend with the list of uploads. Builds the UI. * @param {array} uploads The list of uploads. * @param {string} version The browser version. */ function updateWebRtcLogsList(uploads, version) { $('log-banner').textContent = loadTimeData.getStringF('webrtcLogCountFormat', uploads.length); var logSection = $('log-list'); // Clear any previous list. logSection.textContent = ''; for (var i = 0; i < uploads.length; i++) { var upload = uploads[i]; var logBlock = document.createElement('div'); var title = document.createElement('h3'); title.textContent = loadTimeData.getStringF('webrtcLogHeaderFormat', upload['capture_time']); logBlock.appendChild(title); var localFileLine = document.createElement('p'); if (upload['local_file'].length == 0) { localFileLine.textContent = loadTimeData.getString('noLocalLogFileMessage'); } else { localFileLine.textContent = loadTimeData.getString('webrtcLogLocalFileLabelFormat') + ' '; var localFileLink = document.createElement('a'); localFileLink.href = 'file://' + upload['local_file']; localFileLink.textContent = upload['local_file']; localFileLine.appendChild(localFileLink); } logBlock.appendChild(localFileLine); var uploadLine = document.createElement('p'); if (upload['id'].length == 0) { uploadLine.textContent = loadTimeData.getString('webrtcLogNotUploadedMessage'); } else { uploadLine.textContent = loadTimeData.getStringF('webrtcLogUploadTimeFormat', upload['upload_time']) + '. ' + loadTimeData.getStringF('webrtcLogReportIdFormat', upload['id']) + '. '; var link = document.createElement('a'); var commentLines = [ 'Chrome Version: ' + version, // TODO(tbreisacher): fill in the OS automatically? 'Operating System: e.g., "Windows 7", "Mac OSX 10.6"', '', 'URL (if applicable) where the problem occurred:', '', 'Can you reproduce this problem?', '', 'What steps will reproduce this problem? (or if it\'s not ' + 'reproducible, what were you doing just before the problem)?', '', '1.', '2.', '3.', '', '*Please note that issues filed with no information filled in ' + 'above will be marked as WontFix*', '', '****DO NOT CHANGE BELOW THIS LINE****', 'report_id:' + upload.id ]; var params = { template: 'Defect report from user', comment: commentLines.join('\n'), }; var href = 'http://code.google.com/p/chromium/issues/entry'; for (var param in params) { href = appendParam(href, param, params[param]); } link.href = href; link.target = '_blank'; link.textContent = loadTimeData.getString('bugLinkText'); uploadLine.appendChild(link); } logBlock.appendChild(uploadLine); logSection.appendChild(logBlock); } $('no-logs').hidden = uploads.length != 0; } document.addEventListener('DOMContentLoaded', requestUploads); { "key": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCtl3tO0osjuzRsf6xtD2SKxPlTfuoy7AWoObysitBPvH5fE1NaAA1/2JkPWkVDhdLBWLaIBPYeXbzlHp3y4Vv/4XG+aN5qFE3z+1RU/NqkzVYHtIpVScf3DjTYtKVL66mzVGijSoAIwbFCC3LpGdaoe6Q1rSRDp76wR6jjFzsYwQIDAQAB", "name": "Web Store", "version": "0.2", "description": "Chrome Web Store", "icons": { "16": "webstore_icon_16.png", "128": "webstore_icon_128.png" }, "app": { "launch": { "web_url": "https://chrome.google.com/webstore" }, "urls": [ "https://chrome.google.com/webstore" ] }, "permissions": [ "webstorePrivate", "management", "system.cpu", "system.display", "system.memory", "system.network", "system.storage" ] } { "name": "CryptoTokenExtension", "description": "CryptoToken Component Extension", "version": "0.9.46", "key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAq7zRobvA+AVlvNqkHSSVhh1sEWsHSqz4oR/XptkDe/Cz3+gW9ZGumZ20NCHjaac8j1iiesdigp8B1LJsd/2WWv2Dbnto4f8GrQ5MVphKyQ9WJHwejEHN2K4vzrTcwaXqv5BSTXwxlxS/mXCmXskTfryKTLuYrcHEWK8fCHb+0gvr8b/kvsi75A1aMmb6nUnFJvETmCkOCPNX5CHTdy634Ts/x0fLhRuPlahk63rdf7agxQv5viVjQFk+tbgv6aa9kdSd11Js/RZ9yZjrFgHOBWgP4jTBqud4+HUglrzu8qynFipyNRLCZsaxhm+NItTyNgesxLdxZcwOz56KD1Q4IQIDAQAB", "manifest_version": 2, "permissions": [ "hid", "u2fDevices", "usb", "cryptotokenPrivate", "externally_connectable.all_urls", "tabs", "https://*/*", "http://*/*", { "usbDevices": [ { "vendorId": 4176, "productId": 529 } ] } ], "externally_connectable": { "matches": [ "" ], "ids": [ "fjajfjhkeibgmiggdfehjplbhmfkialk" ], "accepts_tls_channel_id": true }, "background": { "persistent": false, "scripts": [ "util.js", "b64.js", "sha256.js", "timer.js", "countdown.js", "countdowntimer.js", "devicestatuscodes.js", "approvedorigins.js", "errorcodes.js", "webrequest.js", "messagetypes.js", "factoryregistry.js", "closeable.js", "requesthelper.js", "enroller.js", "requestqueue.js", "signer.js", "origincheck.js", "textfetcher.js", "appid.js", "watchdog.js", "logging.js", "webrequestsender.js", "window-timer.js", "cryptotokenorigincheck.js", "cryptotokenapprovedorigins.js", "gnubbydevice.js", "hidgnubbydevice.js", "usbgnubbydevice.js", "gnubbies.js", "gnubby.js", "gnubby-u2f.js", "gnubbyfactory.js", "singlesigner.js", "multiplesigner.js", "generichelper.js", "inherits.js", "individualattest.js", "devicefactoryregistry.js", "usbhelper.js", "usbenrollhandler.js", "usbsignhandler.js", "usbgnubbyfactory.js", "googlecorpindividualattest.js", "cryptotokenbackground.js" ] }, "incognito": "split" } { // chrome-extension://mfffpogegjflfpflabcdkioaeobkgjik/ "key": "MIGdMA0GCSqGSIb3DQEBAQUAA4GLADCBhwKBgQC4L17nAfeTd6Xhtx96WhQ6DSr8KdHeQmfzgCkieKLCgUkWdwB9G1DCuh0EPMDn1MdtSwUAT7xE36APEzi0X/UpKjOVyX8tCC3aQcLoRAE0aJAvCcGwK7qIaQaczHmHKvPC2lrRdzSoMMTC5esvHX+ZqIBMi123FOL0dGW6OPKzIwIBIw==", "name": "GaiaAuthExtension", "version": "0.0.1", "manifest_version": 2, "background" : { "scripts": ["channel.js", "background.js"] }, "content_scripts": [ { "matches": [ "" ], "js": ["channel.js", "saml_injected.js"], "run_at": "document_start", "all_frames": true } ], "content_security_policy": "default-src 'self' blob: filesystem:; script-src 'self' blob: filesystem:; frame-src 'self' blob: filesystem: http: https:; style-src 'self' blob: filesystem:", "description": "GAIA Component Extension", "incognito": "split", "web_accessible_resources": [ "main.css", "main.html", "main.js", "offline.css", "offline.html", "offline.js", "success.html", "success.js", "util.js" ], "permissions": [ "", "webRequest", "webRequestBlocking" ] } // Copyright (c) 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. (function() { // Since all we want here is forwarding of certain commands, all can be done // in the anonymous function's scope. function wireUpWindow() { $('launch-button').addEventListener('click', function() { chrome.send('SetAsDefaultBrowser:LaunchSetDefaultBrowserFlow'); }); } window.addEventListener('DOMContentLoaded', wireUpWindow); })();

    // Copyright 2016 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. /** * @fileoverview 'control-bar' is the horizontal bar at the bottom of the user * manager screen. */ Polymer({ is: 'control-bar', behaviors: [ I18nBehavior, ], properties: { /** * True if 'Browse as Guest' button is displayed. * @type {boolean} */ showGuest: { type: Boolean, value: false }, /** * True if 'Add Person' button is displayed. * @type {boolean} */ showAddPerson: { type: Boolean, value: false }, /** @private {!signin.ProfileBrowserProxy} */ browserProxy_: Object, /** * True if the force sign in policy is enabled. * @private {boolean} */ isForceSigninEnabled_: { type: Boolean, value: function() { return loadTimeData.getBoolean('isForceSigninEnabled'); }, } }, /** @override */ created: function() { this.browserProxy_ = signin.ProfileBrowserProxyImpl.getInstance(); }, /** * Handler for 'Browse as Guest' button click event. * @param {!Event} event * @private */ onLaunchGuestTap_: function(event) { this.browserProxy_.areAllProfilesLocked().then( function(allProfilesLocked) { if (!allProfilesLocked || this.isForceSigninEnabled_) { this.browserProxy_.launchGuestUser(); } else { document.querySelector('error-dialog').show( this.i18n('browseAsGuestAllProfilesLockedError')); } }.bind(this)); }, /** * Handler for 'Add Person' button click event. * @param {!Event} event * @private */ onAddUserTap_: function(event) { this.browserProxy_.areAllProfilesLocked().then( function(allProfilesLocked) { if (!allProfilesLocked || this.isForceSigninEnabled_) { // Event is caught by user-manager-pages. this.fire('change-page', {page: 'create-user-page'}); } else { document.querySelector('error-dialog').show( this.i18n('addProfileAllProfilesLockedError')); } }.bind(this)); } }); // Copyright 2016 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. /** * @fileoverview 'create-profile' is a page that contains controls for creating * a (optionally supervised) profile, including choosing a name, and an avatar. */ /** @typedef {{url: string, label:string}} */ var AvatarIcon; (function() { /** * Sentinel signed-in user's index value. * @const {number} */ var NO_USER_SELECTED = -1; Polymer({ is: 'create-profile', behaviors: [ I18nBehavior, WebUIListenerBehavior ], properties: { /** * The current profile name. * @private {string} */ profileName_: { type: String, value: '' }, /** * The list of available profile icon Urls and labels. * @private {!Array} */ availableIcons_: { type: Array, value: function() { return []; } }, /** * The currently selected profile icon URL. May be a data URL. * @private {string} */ profileIconUrl_: { type: String, value: '' }, /** * True if the existing supervised users are being loaded. * @private {boolean} */ loadingSupervisedUsers_: { type: Boolean, value: false }, /** * True if a profile is being created or imported. * @private {boolean} */ createInProgress_: { type: Boolean, value: false }, /** * True if the error/warning message is displaying. * @private {boolean} */ isMessageVisble_: { type: Boolean, value: false }, /** * The current error/warning message. * @private {string} */ message_: { type: String, value: '' }, /** * if true, a desktop shortcut will be created for the new profile. * @private {boolean} */ createShortcut_: { type: Boolean, value: true }, /** * True if the new profile is a supervised profile. * @private {boolean} */ isSupervised_: { type: Boolean, value: false }, /** * The list of usernames and profile paths for currently signed-in users. * @private {!Array} */ signedInUsers_: { type: Array, value: function() { return []; } }, /** * Index of the selected signed-in user. * @private {number} */ signedInUserIndex_: { type: Number, value: NO_USER_SELECTED }, /** @private {!signin.ProfileBrowserProxy} */ browserProxy_: Object, /** * True if the profile shortcuts feature is enabled. * @private */ isProfileShortcutsEnabled_: { type: Boolean, value: function() { return loadTimeData.getBoolean('profileShortcutsEnabled'); }, readOnly: true }, /** * True if the force sign in policy is enabled. * @private {boolean} */ isForceSigninEnabled_: { type: Boolean, value: function() { return loadTimeData.getBoolean('isForceSigninEnabled'); }, } }, listeners: { 'tap': 'onTap_', 'importUserPopup.import': 'onImportUserPopupImport_' }, /** @override */ created: function() { this.browserProxy_ = signin.ProfileBrowserProxyImpl.getInstance(); }, /** @override */ ready: function() { this.addWebUIListener( 'create-profile-success', this.handleSuccess_.bind(this)); this.addWebUIListener( 'create-profile-warning', this.handleMessage_.bind(this)); this.addWebUIListener( 'create-profile-error', this.handleMessage_.bind(this)); this.addWebUIListener( 'profile-icons-received', this.handleProfileIcons_.bind(this)); this.addWebUIListener( 'profile-defaults-received', this.handleProfileDefaults_.bind(this)); this.addWebUIListener( 'signedin-users-received', this.handleSignedInUsers_.bind(this)); this.browserProxy_.getAvailableIcons(); this.browserProxy_.getSignedInUsers(); }, /** @override */ attached: function() { this.$.nameInput.focus(); }, /** * Handles tap events from: * - links within dynamic warning/error messages pushed from the browser. * - the 'noSignedInUserMessage' i18n string. * @param {!Event} event * @private */ onTap_: function(event) { var element = Polymer.dom(event).rootTarget; if (element.id == 'supervised-user-import-existing') { this.onImportUserTap_(event); event.preventDefault(); } else if (element.id == 'sign-in-to-chrome') { this.browserProxy_.openUrlInLastActiveProfileBrowser(element.href); event.preventDefault(); } else if (element.id == 'reauth') { var elementData = /** @type {{userEmail: string}} */ (element.dataset); this.browserProxy_.authenticateCustodian(elementData.userEmail); this.hideMessage_(); event.preventDefault(); } }, /** * Handler for when the profile icons are pushed from the browser. * @param {!Array} icons * @private */ handleProfileIcons_: function(icons) { this.availableIcons_ = icons; this.profileIconUrl_ = icons[0].url; }, /** * Handler for when the profile defaults are pushed from the browser. * @param {!ProfileInfo} profileInfo Default Info for the new profile. * @private */ handleProfileDefaults_: function(profileInfo) { this.profileName_ = profileInfo.name; }, /** * Handler for when signed-in users are pushed from the browser. * @param {!Array} signedInUsers * @private */ handleSignedInUsers_: function(signedInUsers) { this.signedInUsers_ = signedInUsers; }, /** * Returns the currently selected signed-in user. * @return {(!SignedInUser|undefined)} * @private */ signedInUser_: function(signedInUserIndex) { return this.signedInUsers_[signedInUserIndex]; }, /** * Handler for the 'Learn More' link tap event. * @param {!Event} event * @private */ onLearnMoreTap_: function(event) { this.fire('change-page', {page: 'supervised-learn-more-page'}); }, /** * Handler for the 'Import Supervised User' link tap event. * @param {!Event} event * @private */ onImportUserTap_: function(event) { if (this.signedInUserIndex_ == NO_USER_SELECTED) { // A custodian must be selected. this.handleMessage_(this.i18n('custodianAccountNotSelectedError')); } else { var signedInUser = this.signedInUser_(this.signedInUserIndex_); this.hideMessage_(); this.loadingSupervisedUsers_ = true; this.browserProxy_.getExistingSupervisedUsers(signedInUser.profilePath) .then(this.showImportSupervisedUserPopup_.bind(this), this.handleMessage_.bind(this)); } }, /** * Handler for the 'Save' button tap event. * @param {!Event} event * @private */ onSaveTap_: function(event) { if (!this.isSupervised_) { // The new profile is not supervised. Go ahead and create it. this.createProfile_(); } else if (this.signedInUserIndex_ == NO_USER_SELECTED) { // If the new profile is supervised, a custodian must be selected. this.handleMessage_(this.i18n('custodianAccountNotSelectedError')); } else { var signedInUser = this.signedInUser_(this.signedInUserIndex_); this.hideMessage_(); this.loadingSupervisedUsers_ = true; this.browserProxy_.getExistingSupervisedUsers(signedInUser.profilePath) .then(this.createProfileIfValidSupervisedUser_.bind(this), this.handleMessage_.bind(this)); } }, /** * Displays the import supervised user popup or an error message if there are * no existing supervised users. * @param {!Array} supervisedUsers The list of existing * supervised users. * @private */ showImportSupervisedUserPopup_: function(supervisedUsers) { this.loadingSupervisedUsers_ = false; if (supervisedUsers.length > 0) { this.$.importUserPopup.show(this.signedInUser_(this.signedInUserIndex_), supervisedUsers); } else { this.handleMessage_(this.i18n('noSupervisedUserImportText')); } }, /** * Checks if the entered name matches name of an existing supervised user. * If yes, the user is prompted to import the existing supervised user. * If no, the new supervised profile gets created. * @param {!Array} supervisedUsers The list of existing * supervised users. * @private */ createProfileIfValidSupervisedUser_: function(supervisedUsers) { for (var i = 0; i < supervisedUsers.length; ++i) { if (supervisedUsers[i].name != this.profileName_) continue; // Check if another supervised user also exists with that name. var nameIsUnique = true; // Handling the case when multiple supervised users with the same // name exist, but not all of them are on the device. // If at least one is not imported, we want to offer that // option to the user. This could happen due to a bug that allowed // creating SUs with the same name (https://crbug.com/557445). var allOnCurrentDevice = supervisedUsers[i].onCurrentDevice; for (var j = i + 1; j < supervisedUsers.length; ++j) { if (supervisedUsers[j].name == this.profileName_) { nameIsUnique = false; allOnCurrentDevice = allOnCurrentDevice && supervisedUsers[j].onCurrentDevice; } } var opts = { 'substitutions': [HTMLEscape(elide(this.profileName_, /* maxLength */ 50))], 'attrs': { 'id': function(node, value) { return node.tagName == 'A'; }, 'is': function(node, value) { return node.tagName == 'A' && value == 'action-link'; }, 'role': function(node, value) { return node.tagName == 'A' && value == 'link'; }, 'tabindex': function(node, value) { return node.tagName == 'A'; } } }; this.handleMessage_(allOnCurrentDevice ? this.i18n('managedProfilesExistingLocalSupervisedUser') : this.i18nAdvanced('manageProfilesExistingSupervisedUser', opts)); return; } // No existing supervised user's name matches the entered profile name. // Continue with creating the new supervised profile. this.createProfile_(); // Set this to false after createInProgress_ has been set to true in // order for the 'Save' button to remain disabled. this.loadingSupervisedUsers_ = false; }, /** * Creates the new profile. * @private */ createProfile_: function() { var custodianProfilePath = ''; if (this.signedInUserIndex_ != NO_USER_SELECTED) { custodianProfilePath = this.signedInUser_(this.signedInUserIndex_).profilePath; } this.hideMessage_(); this.createInProgress_ = true; var createShortcut = this.isProfileShortcutsEnabled_ && this.createShortcut_; this.browserProxy_.createProfile( this.profileName_, this.profileIconUrl_, createShortcut, this.isSupervised_, '', custodianProfilePath); }, /** * Handler for the 'import' event fired by #importUserPopup once a supervised * user is selected to be imported and the popup closes. * @param {!{detail: {supervisedUser: !SupervisedUser, * signedInUser: !SignedInUser}}} event * @private */ onImportUserPopupImport_: function(event) { var supervisedUser = event.detail.supervisedUser; var signedInUser = event.detail.signedInUser; this.hideMessage_(); this.createInProgress_ = true; var createShortcut = this.isProfileShortcutsEnabled_; this.browserProxy_.createProfile( supervisedUser.name, supervisedUser.iconURL, createShortcut, true /* isSupervised */, supervisedUser.id, signedInUser.profilePath); }, /** * Handler for the 'Cancel' button tap event. * @param {!Event} event * @private */ onCancelTap_: function(event) { if (this.createInProgress_) { this.createInProgress_ = false; this.browserProxy_.cancelCreateProfile(); } else if (this.loadingSupervisedUsers_) { this.loadingSupervisedUsers_ = false; this.browserProxy_.cancelLoadingSupervisedUsers(); } else { this.fire('change-page', {page: 'user-pods-page'}); } }, /** * Handles profile create/import success message pushed by the browser. * @param {!ProfileInfo} profileInfo Details of the created/imported profile. * @private */ handleSuccess_: function(profileInfo) { this.createInProgress_ = false; if (profileInfo.showConfirmation) { this.fire('change-page', {page: 'supervised-create-confirm-page', data: profileInfo}); } else { this.fire('change-page', {page: 'user-pods-page'}); } }, /** * Hides the warning/error message. * @private */ hideMessage_: function() { this.isMessageVisble_ = false; }, /** * Handles warning/error messages when a profile is being created/imported * or the existing supervised users are being loaded. * @param {*} message An HTML warning/error message. * @private */ handleMessage_: function(message) { this.createInProgress_ = false; this.loadingSupervisedUsers_ = false; this.message_ = '' + message; this.isMessageVisble_ = true; }, /** * Returns a translated message that contains link elements with the 'id' * attribute. * @param {string} id The ID of the string to translate. * @private */ i18nAllowIDAttr_: function(id) { var opts = { 'attrs': { 'id' : function(node, value) { return node.tagName == 'A'; } } }; return this.i18nAdvanced(id, opts); }, /** * Computed binding determining which profile icon button is toggled on. * @param {string} iconUrl icon URL of a given icon button. * @param {string} profileIconUrl Currently selected icon URL. * @return {boolean} * @private */ isActiveIcon_: function(iconUrl, profileIconUrl) { return iconUrl == profileIconUrl; }, /** * Computed binding determining whether the paper-spinner is active. * @param {boolean} createInProgress Is create in progress? * @param {boolean} loadingSupervisedUsers Are supervised users being loaded? * @return {boolean} * @private */ isSpinnerActive_: function(createInProgress, loadingSupervisedUsers) { return createInProgress || loadingSupervisedUsers; }, /** * Computed binding determining whether 'Save' button is disabled. * @param {boolean} createInProgress Is create in progress? * @param {boolean} loadingSupervisedUsers Are supervised users being loaded? * @param {string} profileName Profile Name. * @return {boolean} * @private */ isSaveDisabled_: function(createInProgress, loadingSupervisedUsers, profileName) { // TODO(mahmadi): Figure out a way to add 'paper-input-extracted' as a // dependency and cast to PaperInputElement instead. /** @type {{validate: function():boolean}} */ var nameInput = this.$.nameInput; return createInProgress || loadingSupervisedUsers || !profileName || !nameInput.validate(); }, /** * Returns True if the import existing supervised user link should be hidden. * @param {boolean} createInProgress True if create/import is in progress. * @param {boolean} loadingSupervisedUsers True if supervised users are being * loaded. * @param {number} signedInUserIndex Index of the selected signed-in user. * @return {boolean} * @private */ isImportUserLinkHidden_: function(createInProgress, loadingSupervisedUsers, signedInUserIndex) { return createInProgress || loadingSupervisedUsers || !this.signedInUser_(signedInUserIndex); }, /** * Computed binding that returns True if there are any signed-in users. * @param {!Array} signedInUsers signed-in users. * @return {boolean} * @private */ isSignedIn_: function(signedInUsers) { return signedInUsers.length > 0; } }); }()); // Copyright 2016 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. /** * @fileoverview 'error-dialog' is a dialog that displays error messages * in the user manager. */ (function() { Polymer({ is: 'error-dialog', properties: { /** * The message shown in the dialog. * @private {string} */ message_: { type: String, value: '' } }, /** * Displays the dialog populated with the given message. * @param {string} message Error message to show. */ show: function(message) { this.message_ = message; this.$.dialog.open(); } }); })(); // Copyright 2016 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. /** * @fileoverview 'import-supervised-user' is a dialog that allows user to select * a supervised profile from a list of profiles to import on the current device. */ (function() { /** * It means no supervised user is selected. * @const {number} */ var NO_USER_SELECTED = -1; Polymer({ is: 'import-supervised-user', behaviors: [ I18nBehavior, ], properties: { /** * The currently signed in user and the custodian. * @private {?SignedInUser} */ signedInUser_: { type: Object, value: function() { return null; } }, /** * The list of supervised users managed by signedInUser_. * @private {!Array} */ supervisedUsers_: { type: Array, value: function() { return []; } }, /** * Index of the selected supervised user. * @private {number} */ supervisedUserIndex_: { type: Number, value: NO_USER_SELECTED } }, /** override */ ready: function() { this.$.dialog.lastFocusableNode = this.$.cancel; }, /** * Displays the dialog. * @param {(!SignedInUser|undefined)} signedInUser * @param {!Array} supervisedUsers */ show: function(signedInUser, supervisedUsers) { this.supervisedUsers_ = supervisedUsers; this.supervisedUsers_.sort(function(a, b) { if (a.onCurrentDevice != b.onCurrentDevice) return a.onCurrentDevice ? 1 : -1; return a.name.localeCompare(b.name); }); this.supervisedUserIndex_ = NO_USER_SELECTED; this.signedInUser_ = signedInUser || null; if (this.signedInUser_) this.$.dialog.open(); }, /** * param {number} supervisedUserIndex Index of the selected supervised user. * @private * @return {boolean} Whether the 'Import' button should be disabled. */ isImportDisabled_: function(supervisedUserIndex) { var disabled = supervisedUserIndex == NO_USER_SELECTED; if (!disabled) { this.$.dialog.lastFocusableNode = this.$.import; } return disabled; }, /** * Called when the user clicks the 'Import' button. it proceeds with importing * the supervised user. * @private */ onImportTap_: function() { var supervisedUser = this.supervisedUsers_[this.supervisedUserIndex_]; if (this.signedInUser_ && supervisedUser) { this.$.dialog.close(); // Event is caught by create-profile. this.fire('import', {supervisedUser: supervisedUser, signedInUser: this.signedInUser_}); } } }); })(); // Copyright 2016 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. /** * @fileoverview Helper object and related behavior that encapsulate messaging * between JS and C++ for creating/importing profiles in the user-manager page. */ /** @typedef {{username: string, profilePath: string}} */ var SignedInUser; /** * @typedef {{name: string, * filePath: string, * isSupervised: boolean, * custodianUsername: string, * showConfirmation: boolean}} */ var ProfileInfo; /** * @typedef {{id: string, * name: string, * iconURL: string, * onCurrentDevice: boolean}} */ var SupervisedUser; cr.define('signin', function() { /** @interface */ function ProfileBrowserProxy() {} ProfileBrowserProxy.prototype = { /** * Gets the available profile icons to choose from. */ getAvailableIcons: function() { assertNotReached(); }, /** * Gets the current signed-in users. */ getSignedInUsers: function() { assertNotReached(); }, /** * Launches the guest user. */ launchGuestUser: function() { assertNotReached(); }, /** * @param {string} profilePath Profile Path of the custodian. * @return {!Promise>} The list of existing * supervised users. */ getExistingSupervisedUsers: function(profilePath) { assertNotReached(); }, /** * Creates a profile. * @param {string} profileName Name of the new profile. * @param {string} profileIconUrl URL of the selected icon of the new * profile. * @param {boolean} createShortcut if true a desktop shortcut will be * created. * @param {boolean} isSupervised True if the new profile is supervised. * @param {string} supervisedUserId ID of the supervised user to be * imported. * @param {string} custodianProfilePath Profile path of the custodian if * the new profile is supervised. */ createProfile: function(profileName, profileIconUrl, createShortcut, isSupervised, supervisedUserId, custodianProfilePath) { assertNotReached(); }, /** * Cancels creation of the new profile. */ cancelCreateProfile: function() { assertNotReached(); }, /** * Cancels loading supervised users. */ cancelLoadingSupervisedUsers: function() { assertNotReached(); }, /** * Initializes the UserManager * @param {string} locationHash */ initializeUserManager: function(locationHash) { assertNotReached(); }, /** * Launches the user with the given |profilePath| * @param {string} profilePath Profile Path of the user. */ launchUser: function(profilePath) { assertNotReached(); }, /** * Opens the given url in a new tab in the browser instance of the last * active profile. Hyperlinks don't work in the user manager since its * browser instance does not support tabs. * @param {string} url */ openUrlInLastActiveProfileBrowser: function(url) { assertNotReached(); }, /** * Switches to the profile with the given path. * @param {string} profilePath Path to the profile to switch to. */ switchToProfile: function(profilePath) { assertNotReached(); }, /** * @return {!Promise} Whether all (non-supervised and non-child) * profiles are locked. */ areAllProfilesLocked: function() { assertNotReached(); }, /** * Authenticates the custodian profile with the given email address. * @param {string} emailAddress Email address of the custodian profile. */ authenticateCustodian: function(emailAddress) { assertNotReached(); } }; /** * @constructor * @implements {signin.ProfileBrowserProxy} */ function ProfileBrowserProxyImpl() {} // The singleton instance_ is replaced with a test version of this wrapper // during testing. cr.addSingletonGetter(ProfileBrowserProxyImpl); ProfileBrowserProxyImpl.prototype = { /** @override */ getAvailableIcons: function() { chrome.send('requestDefaultProfileIcons'); }, /** @override */ getSignedInUsers: function() { chrome.send('requestSignedInProfiles'); }, /** @override */ launchGuestUser: function() { chrome.send('launchGuest'); }, /** @override */ getExistingSupervisedUsers: function(profilePath) { return cr.sendWithPromise('getExistingSupervisedUsers', profilePath); }, /** @override */ createProfile: function(profileName, profileIconUrl, createShortcut, isSupervised, supervisedUserId, custodianProfilePath) { chrome.send('createProfile', [profileName, profileIconUrl, createShortcut, isSupervised, supervisedUserId, custodianProfilePath]); }, /** @override */ cancelCreateProfile: function() { chrome.send('cancelCreateProfile'); }, /** @override */ cancelLoadingSupervisedUsers: function() { chrome.send('cancelLoadingSupervisedUsers'); }, /** @override */ initializeUserManager: function(locationHash) { chrome.send('userManagerInitialize', [locationHash]); }, /** @override */ launchUser: function(profilePath) { chrome.send('launchUser', [profilePath]); }, /** @override */ openUrlInLastActiveProfileBrowser: function(url) { chrome.send('openUrlInLastActiveProfileBrowser', [url]); }, /** @override */ switchToProfile: function(profilePath) { chrome.send('switchToProfile', [profilePath]); }, /** @override */ areAllProfilesLocked: function() { return cr.sendWithPromise('areAllProfilesLocked'); }, /** @override */ authenticateCustodian: function(emailAddress) { chrome.send('authenticateCustodian', [emailAddress]); } }; return { ProfileBrowserProxy: ProfileBrowserProxy, ProfileBrowserProxyImpl: ProfileBrowserProxyImpl, }; }); // Copyright 2016 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. /** * @fileoverview 'supervised-user-create-confirm' is a page that is displayed * upon successful creation of a supervised user. It contains information for * the custodian on where to configure browsing restrictions as well as how to * exit and childlock their profile. */ (function() { /** * Maximum length of the supervised user profile name or custodian's username. * @const {number} */ var MAX_NAME_LENGTH = 50; Polymer({ is: 'supervised-user-create-confirm', behaviors: [ I18nBehavior ], properties: { /** * Profile Info of the supervised user that is passed to the page. * @type {?ProfileInfo} */ profileInfo: { type: Object, value: function() { return null; } }, /** @private {!signin.ProfileBrowserProxy} */ browserProxy_: Object }, listeners: { 'tap': 'onTap_' }, /** @override */ created: function() { this.browserProxy_ = signin.ProfileBrowserProxyImpl.getInstance(); }, /** * Handles tap events from dynamically created links in the * supervisedUserCreatedText i18n string. * @param {!Event} event * @private */ onTap_: function(event) { var element = Polymer.dom(event).rootTarget; // Handle the tap event only if the target is a '
    ' element. if (element.nodeName == 'A') { this.browserProxy_.openUrlInLastActiveProfileBrowser(element.href); event.preventDefault(); } }, /** * Returns the shortened profile name or empty string if |profileInfo| is * null. * @param {?ProfileInfo} profileInfo * @return {string} * @private */ elideProfileName_: function(profileInfo) { var name = profileInfo ? profileInfo.name : ''; return elide(name, MAX_NAME_LENGTH); }, /** * Returns the shortened custodian username or empty string if |profileInfo| * is null. * @param {?ProfileInfo} profileInfo * @return {string} * @private */ elideCustodianUsername_: function(profileInfo) { var name = profileInfo ? profileInfo.custodianUsername : ''; return elide(name, MAX_NAME_LENGTH); }, /** * Computed binding returning the text of the title section. * @param {?ProfileInfo} profileInfo * @return {string} * @private */ titleText_: function(profileInfo) { return this.i18n('supervisedUserCreatedTitle', this.elideProfileName_(profileInfo)); }, /** * Computed binding returning the sanitized confirmation HTML message that is * safe to set as innerHTML. * @param {?ProfileInfo} profileInfo * @return {string} * @private */ confirmationMessage_: function(profileInfo) { return this.i18n('supervisedUserCreatedText', this.elideProfileName_(profileInfo), this.elideCustodianUsername_(profileInfo)); }, /** * Computed binding returning the text of the 'Switch To User' button. * @param {?ProfileInfo} profileInfo * @return {string} * @private */ switchUserText_: function(profileInfo) { return this.i18n('supervisedUserCreatedSwitch', this.elideProfileName_(profileInfo)); }, /** * Handler for the 'Ok' button tap event. * @param {!Event} event * @private */ onOkTap_: function(event) { // Event is caught by user-manager-pages. this.fire('change-page', {page: 'user-pods-page'}); }, /** * Handler for the 'Switch To User' button tap event. * @param {!Event} event * @private */ onSwitchUserTap_: function(event) { this.browserProxy_.switchToProfile(this.profileInfo.filePath); // Event is caught by user-manager-pages. this.fire('change-page', {page: 'user-pods-page'}); } }); })(); // Copyright 2016 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. /** * @fileoverview 'supervised-user-learn-more' is a page that contains * information about what a supervised user is, what happens when a supervised * user is created, and a link to the help center for more information. */ Polymer({ is: 'supervised-user-learn-more', properties: { /** @private {!signin.ProfileBrowserProxy} */ browserProxy_: Object }, listeners: { 'tap': 'onTap_' }, /** @override */ created: function() { this.browserProxy_ = signin.ProfileBrowserProxyImpl.getInstance(); }, /** * Handles tap events from dynamically created links in the * supervisedUserLearnMoreText i18n string. * @param {!Event} event * @private */ onTap_: function(event) { var element = Polymer.dom(event).rootTarget; // Handle the tap event only if the target is a '' element. if (element.nodeName == 'A') { this.browserProxy_.openUrlInLastActiveProfileBrowser(element.href); event.preventDefault(); } }, /** * Handler for the 'Done' button tap event. * @param {!Event} event * @private */ onDoneTap_: function(event) { // Event is caught by user-manager-pages. this.fire('change-page', {page: 'create-user-page'}); } }); // Copyright 2016 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. /** * @fileoverview 'user-manager-dialog' is a modal dialog for the user manager. */ Polymer({ is: 'user-manager-dialog', behaviors: [Polymer.PaperDialogBehavior], properties: { /** @override */ noCancelOnOutsideClick: { type: Boolean, value: true }, /** @override */ withBackdrop: { type: Boolean, value: true }, /** * The first node that can receive focus. * @type {!Node} */ firstFocusableNode: { type: Object, value: function() { return this.$.close; }, observer: 'firstFocusableNodeChanged_' }, /** * The last node that can receive focus. * @type {!Node} */ lastFocusableNode: { type: Object, value: function() { return this.$.close; }, observer: 'lastFocusableNodeChanged_' } }, /** * Returns the first and the last focusable elements in order to wrap the * focus for the dialog in Polymer.PaperDialogBehavior. * @override * @type {!Array} */ get _focusableNodes() { return [this.firstFocusableNode, this.lastFocusableNode]; }, /** * Observer for firstFocusableNode. Updates __firstFocusableNode in * Polymer.PaperDialogBehavior. */ firstFocusableNodeChanged_: function(newValue) { this.__firstFocusableNode = newValue; }, /** * Observer for lastFocusableNodeChanged_. Updates __lastFocusableNode in * Polymer.PaperDialogBehavior. */ lastFocusableNodeChanged_: function(newValue) { this.__lastFocusableNode = newValue; } }); $i18n{title}
    // Copyright 2016 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. // // Copyright 2014 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. /** * @fileoverview Base class for all login WebUI screens. */ cr.define('login', function() { /** @const */ var CALLBACK_CONTEXT_CHANGED = 'contextChanged'; /** @const */ var CALLBACK_USER_ACTED = 'userActed'; function doNothing() {}; function alwaysTruePredicate() { return true; } var querySelectorAll = HTMLDivElement.prototype.querySelectorAll; var Screen = function(sendPrefix) { this.sendPrefix_ = sendPrefix; this.screenContext_ = null; this.contextObservers_ = {}; }; Screen.prototype = { __proto__: HTMLDivElement.prototype, /** * Prefix added to sent to Chrome messages' names. */ sendPrefix_: null, /** * Context used by this screen. */ screenContext_: null, get context() { return this.screenContext_; }, /** * Dictionary of context observers that are methods of |this| bound to * |this|. */ contextObservers_: null, /** * Called during screen initialization. */ decorate: doNothing, /** * Returns minimal size that screen prefers to have. Default implementation * returns current screen size. * @return {{width: number, height: number}} */ getPreferredSize: function() { return {width: this.offsetWidth, height: this.offsetHeight}; }, /** * Called for currently active screen when screen size changed. */ onWindowResize: doNothing, /** * @final */ initialize: function() { return this.initializeImpl_.apply(this, arguments); }, /** * @final */ send: function() { return this.sendImpl_.apply(this, arguments); }, /** * @final */ addContextObserver: function() { return this.addContextObserverImpl_.apply(this, arguments); }, /** * @final */ removeContextObserver: function() { return this.removeContextObserverImpl_.apply(this, arguments); }, /** * @final */ commitContextChanges: function() { return this.commitContextChangesImpl_.apply(this, arguments); }, /** * Creates and returns new button element with given identifier * and on-click event listener, which sends notification about * user action to the C++ side. * * @param {string} id Identifier of a button. * @param {string} opt_action_id Identifier of user action. * @final */ declareButton: function(id, opt_action_id) { var button = this.ownerDocument.createElement('button'); button.id = id; this.declareUserAction(button, { action_id: opt_action_id, event: 'click' }); return button; }, /** * Adds event listener to an element which sends notification * about event to the C++ side. * * @param {Element} element An DOM element * @param {Object} options A dictionary of optional arguments: * {string} event: name of event that will be listened, * default: 'click'. * {string} action_id: name of an action which will be sent to * the C++ side. * {function} condition: a one-argument function which takes * event as an argument, notification is sent to the * C++ side iff condition is true, default: constant * true function. * @final */ declareUserAction: function(element, options) { var self = this; options = options || {}; var event = options.event || 'click'; var action_id = options.action_id || element.id; var condition = options.condition || alwaysTruePredicate; element.addEventListener(event, function(e) { if (condition(e)) self.sendImpl_(CALLBACK_USER_ACTED, action_id); e.stopPropagation(); }); }, /** * @override * @final */ querySelectorAll: function() { return this.querySelectorAllImpl_.apply(this, arguments); }, /** * Does the following things: * * Creates screen context. * * Looks for elements having "alias" property and adds them as the * proprties of the screen with name equal to value of "alias", i.e. HTML * element
    will be stored in this.myDiv. * * Looks for buttons having "action" properties and adds click handlers * to them. These handlers send |CALLBACK_USER_ACTED| messages to * C++ with "action" property's value as payload. * @private */ initializeImpl_: function() { if (cr.isChromeOS) this.screenContext_ = new login.ScreenContext(); this.decorate(); this.querySelectorAllImpl_('[alias]').forEach(function(element) { var alias = element.getAttribute('alias'); if (alias in this) throw Error('Alias "' + alias + '" of "' + this.name() + '" screen ' + 'shadows or redefines property that is already defined.'); this[alias] = element; this[element.getAttribute('alias')] = element; }, this); var self = this; this.querySelectorAllImpl_('button[action]').forEach(function(button) { button.addEventListener('click', function(e) { var action = this.getAttribute('action'); self.send(CALLBACK_USER_ACTED, action); e.stopPropagation(); }); }); }, /** * Sends message to Chrome, adding needed prefix to message name. All * arguments after |messageName| are packed into message parameters list. * * @param {string} messageName Name of message without a prefix. * @param {...*} varArgs parameters for message. * @private */ sendImpl_: function(messageName, varArgs) { if (arguments.length == 0) throw Error('Message name is not provided.'); var fullMessageName = this.sendPrefix_ + messageName; var payload = Array.prototype.slice.call(arguments, 1); chrome.send(fullMessageName, payload); }, /** * Starts observation of property with |key| of the context attached to * current screen. This method differs from "login.ScreenContext" in that * it automatically detects if observer is method of |this| and make * all needed actions to make it work correctly. So it's no need for client * to bind methods to |this| and keep resulting callback for * |removeObserver| call: * * this.addContextObserver('key', this.onKeyChanged_); * ... * this.removeContextObserver('key', this.onKeyChanged_); * @private */ addContextObserverImpl_: function(key, observer) { var realObserver = observer; var propertyName = this.getPropertyNameOf_(observer); if (propertyName) { if (!this.contextObservers_.hasOwnProperty(propertyName)) this.contextObservers_[propertyName] = observer.bind(this); realObserver = this.contextObservers_[propertyName]; } this.screenContext_.addObserver(key, realObserver); }, /** * Removes |observer| from the list of context observers. Supports not only * regular functions but also screen methods (see comment to * |addContextObserver|). * @private */ removeContextObserverImpl_: function(observer) { var realObserver = observer; var propertyName = this.getPropertyNameOf_(observer); if (propertyName) { if (!this.contextObservers_.hasOwnProperty(propertyName)) return; realObserver = this.contextObservers_[propertyName]; delete this.contextObservers_[propertyName]; } this.screenContext_.removeObserver(realObserver); }, /** * Sends recent context changes to C++ handler. * @private */ commitContextChangesImpl_: function() { if (!this.screenContext_.hasChanges()) return; this.sendImpl_(CALLBACK_CONTEXT_CHANGED, this.screenContext_.getChangesAndReset()); }, /** * Calls standart |querySelectorAll| method and returns its result converted * to Array. * @private */ querySelectorAllImpl_: function(selector) { var list = querySelectorAll.call(this, selector); return Array.prototype.slice.call(list); }, /** * Called when context changes are recieved from C++. * @private */ contextChanged_: function(diff) { this.screenContext_.applyChanges(diff); }, /** * If |value| is the value of some property of |this| returns property's * name. Otherwise returns empty string. * @private */ getPropertyNameOf_: function(value) { for (var key in this) if (this[key] === value) return key; return ''; } }; Screen.CALLBACK_USER_ACTED = CALLBACK_USER_ACTED; return { Screen: Screen }; }); cr.define('login', function() { return { /** * Creates class and object for screen. * Methods specified in EXTERNAL_API array of prototype * will be available from C++ part. * Example: * login.createScreen('ScreenName', 'screen-id', { * foo: function() { console.log('foo'); }, * bar: function() { console.log('bar'); } * EXTERNAL_API: ['foo']; * }); * login.ScreenName.register(); * var screen = $('screen-id'); * screen.foo(); // valid * login.ScreenName.foo(); // valid * screen.bar(); // valid * login.ScreenName.bar(); // invalid * * @param {string} name Name of created class. * @param {string} id Id of div representing screen. * @param {(function()|Object)} proto Prototype of object or function that * returns prototype. */ createScreen: function(name, id, template) { if (typeof template == 'function') template = template(); var apiNames = template.EXTERNAL_API || []; for (var i = 0; i < apiNames.length; ++i) { var methodName = apiNames[i]; if (typeof template[methodName] !== 'function') throw Error('External method "' + methodName + '" for screen "' + name + '" not a function or undefined.'); } function checkPropertyAllowed(propertyName) { if (propertyName.charAt(propertyName.length - 1) === '_' && (propertyName in login.Screen.prototype)) { throw Error('Property "' + propertyName + '" of "' + id + '" ' + 'shadows private property of login.Screen prototype.'); } }; var Constructor = function() { login.Screen.call(this, 'login.' + name + '.'); }; Constructor.prototype = Object.create(login.Screen.prototype); var api = {}; Object.getOwnPropertyNames(template).forEach(function(propertyName) { if (propertyName === 'EXTERNAL_API') return; checkPropertyAllowed(propertyName); var descriptor = Object.getOwnPropertyDescriptor(template, propertyName); Object.defineProperty(Constructor.prototype, propertyName, descriptor); if (apiNames.indexOf(propertyName) >= 0) { api[propertyName] = function() { var screen = $(id); return screen[propertyName].apply(screen, arguments); }; } }); Constructor.prototype.name = function() { return id; }; api.contextChanged = function() { var screen = $(id); screen.contextChanged_.apply(screen, arguments); } api.register = function(opt_lazy_init) { var screen = $(id); screen.__proto__ = new Constructor(); if (opt_lazy_init !== undefined && opt_lazy_init) screen.deferredInitialization = function() { screen.initialize(); } else screen.initialize(); Oobe.getInstance().registerScreen(screen); }; cr.define('login', function() { var result = {}; result[name] = api; return result; }); } }; }); // // Copyright 2014 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. /** * @fileoverview Bubble implementation. */ // TODO(xiyuan): Move this into shared. cr.define('cr.ui', function() { /** * Creates a bubble div. * @constructor * @extends {HTMLDivElement} */ var Bubble = cr.ui.define('div'); /** * Bubble key codes. * @enum {number} */ var Keys = { TAB: 'Tab', ENTER: 'Enter', ESC: 'Escape', SPACE: ' ' }; /** * Bubble attachment side. * @enum {number} */ Bubble.Attachment = { RIGHT: 0, LEFT: 1, TOP: 2, BOTTOM: 3 }; Bubble.prototype = { __proto__: HTMLDivElement.prototype, // Anchor element for this bubble. anchor_: undefined, // If defined, sets focus to this element once bubble is closed. Focus is // set to this element only if there's no any other focused element. elementToFocusOnHide_: undefined, // With help of these elements we create closed artificial tab-cycle through // bubble elements. firstBubbleElement_: undefined, lastBubbleElement_: undefined, // Whether to hide bubble when key is pressed. hideOnKeyPress_: true, /** @override */ decorate: function() { this.docKeyDownHandler_ = this.handleDocKeyDown_.bind(this); this.selfClickHandler_ = this.handleSelfClick_.bind(this); this.ownerDocument.addEventListener('click', this.handleDocClick_.bind(this)); // Set useCapture to true because scroll event does not bubble. this.ownerDocument.addEventListener('scroll', this.handleScroll_.bind(this), true); this.ownerDocument.addEventListener('keydown', this.docKeyDownHandler_); window.addEventListener('blur', this.handleWindowBlur_.bind(this)); this.addEventListener('webkitTransitionEnd', this.handleTransitionEnd_.bind(this)); // Guard timer for 200ms + epsilon. ensureTransitionEndEvent(this, 250); }, /** * Element that should be focused on hide. * @type {HTMLElement} */ set elementToFocusOnHide(value) { this.elementToFocusOnHide_ = value; }, /** * Element that should be focused on shift-tab of first bubble element * to create artificial closed tab-cycle through bubble. * Usually close-button. * @type {HTMLElement} */ set lastBubbleElement(value) { this.lastBubbleElement_ = value; }, /** * Element that should be focused on tab of last bubble element * to create artificial closed tab-cycle through bubble. * Same element as first focused on bubble opening. * @type {HTMLElement} */ set firstBubbleElement(value) { this.firstBubbleElement_ = value; }, /** * Whether to hide bubble when key is pressed. * @type {boolean} */ set hideOnKeyPress(value) { this.hideOnKeyPress_ = value; }, /** * Whether to hide bubble when clicked inside bubble element. * Default is true. * @type {boolean} */ set hideOnSelfClick(value) { if (value) this.removeEventListener('click', this.selfClickHandler_); else this.addEventListener('click', this.selfClickHandler_); }, /** * Handler for click event which prevents bubble auto hide. * @private */ handleSelfClick_: function(e) { // Allow clicking on [x] button. if (e.target && e.target.classList.contains('close-button')) return; e.stopPropagation(); }, /** * Sets the attachment of the bubble. * @param {!Attachment} attachment Bubble attachment. */ setAttachment_: function(attachment) { var styleClassList = ['bubble-right', 'bubble-left', 'bubble-top', 'bubble-bottom']; for (var i = 0; i < styleClassList.length; ++i) this.classList.toggle(styleClassList[i], i == attachment); }, /** * Shows the bubble for given anchor element. * @param {!Object} pos Bubble position (left, top, right, bottom in px). * @param {HTMLElement} opt_content Content to show in bubble. * If not specified, bubble element content is shown. * @param {Attachment=} opt_attachment Bubble attachment (on which side of * the element it should be displayed). * @param {boolean=} opt_oldstyle Optional flag to force old style bubble, * i.e. pre-MD-style. * @private */ showContentAt_: function(pos, opt_content, opt_attachment, opt_oldstyle) { this.style.top = this.style.left = this.style.right = this.style.bottom = 'auto'; for (var k in pos) { if (typeof pos[k] == 'number') this.style[k] = pos[k] + 'px'; } if (opt_content !== undefined) this.replaceContent(opt_content); if (opt_oldstyle) { this.setAttribute('oldstyle', ''); this.setAttachment_(opt_attachment); } this.hidden = false; this.classList.remove('faded'); }, /** * Replaces error message content with the given DOM element. * @param {HTMLElement} content Content to show in bubble. */ replaceContent: function(content) { this.innerHTML = ''; this.appendChild(content); }, /** * Shows the bubble for given anchor element. Bubble content is not cleared. * @param {!HTMLElement} el Anchor element of the bubble. * @param {!Attachment} attachment Bubble attachment (on which side of the * element it should be displayed). * @param {number=} opt_offset Offset of the bubble. * @param {number=} opt_padding Optional padding of the bubble. */ showForElement: function(el, attachment, opt_offset, opt_padding) { /* showForElement() is used only to display Accessibility popup in * oobe_screen_network*. It requires old-style bubble, so it is safe * to always set this flag here. */ this.showContentForElement( el, attachment, undefined, opt_offset, opt_padding, undefined, true); }, /** * Shows the bubble for given anchor element. * @param {!HTMLElement} el Anchor element of the bubble. * @param {!Attachment} attachment Bubble attachment (on which side of the * element it should be displayed). * @param {HTMLElement} opt_content Content to show in bubble. * If not specified, bubble element content is shown. * @param {number=} opt_offset Offset of the bubble attachment point from * left (for vertical attachment) or top (for horizontal attachment) * side of the element. If not specified, the bubble is positioned to * be aligned with the left/top side of the element but not farther than * half of its width/height. * @param {number=} opt_padding Optional padding of the bubble. * @param {boolean=} opt_match_width Optional flag to force the bubble have * the same width as the element it it attached to. * @param {boolean=} opt_oldstyle Optional flag to force old style bubble, * i.e. pre-MD-style. */ showContentForElement: function(el, attachment, opt_content, opt_offset, opt_padding, opt_match_width, opt_oldstyle) { /** @const */ var ARROW_OFFSET = 25; /** @const */ var DEFAULT_PADDING = 18; if (opt_padding == undefined) opt_padding = DEFAULT_PADDING; if (!opt_oldstyle) opt_padding += 10; var origin = cr.ui.login.DisplayManager.getPosition(el); var offset = opt_offset == undefined ? [Math.min(ARROW_OFFSET, el.offsetWidth / 2), Math.min(ARROW_OFFSET, el.offsetHeight / 2)] : [opt_offset, opt_offset]; var pos = {}; if (isRTL()) { switch (attachment) { case Bubble.Attachment.TOP: pos.right = origin.right + offset[0] - ARROW_OFFSET; pos.bottom = origin.bottom + el.offsetHeight + opt_padding; break; case Bubble.Attachment.RIGHT: pos.top = origin.top + offset[1] - ARROW_OFFSET; pos.right = origin.right + el.offsetWidth + opt_padding; break; case Bubble.Attachment.BOTTOM: pos.right = origin.right + offset[0] - ARROW_OFFSET; pos.top = origin.top + el.offsetHeight + opt_padding; break; case Bubble.Attachment.LEFT: pos.top = origin.top + offset[1] - ARROW_OFFSET; pos.left = origin.left + el.offsetWidth + opt_padding; break; } } else { switch (attachment) { case Bubble.Attachment.TOP: pos.left = origin.left + offset[0] - ARROW_OFFSET; pos.bottom = origin.bottom + el.offsetHeight + opt_padding; break; case Bubble.Attachment.RIGHT: pos.top = origin.top + offset[1] - ARROW_OFFSET; pos.left = origin.left + el.offsetWidth + opt_padding; break; case Bubble.Attachment.BOTTOM: pos.left = origin.left + offset[0] - ARROW_OFFSET; pos.top = origin.top + el.offsetHeight + opt_padding; break; case Bubble.Attachment.LEFT: pos.top = origin.top + offset[1] - ARROW_OFFSET; pos.right = origin.right + el.offsetWidth + opt_padding; break; } } this.style.width = ''; this.removeAttribute('match-width'); if (opt_match_width) { this.setAttribute('match-width', ''); var elWidth = window.getComputedStyle(el, null).getPropertyValue('width'); var paddingLeft = parseInt(window.getComputedStyle(this, null) .getPropertyValue('padding-left')); var paddingRight = parseInt(window.getComputedStyle(this, null) .getPropertyValue('padding-right')); if (elWidth) this.style.width = (parseInt(elWidth) - paddingLeft - paddingRight) + 'px'; } this.anchor_ = el; this.showContentAt_(pos, opt_content, attachment, opt_oldstyle); }, /** * Shows the bubble for given anchor element. * @param {!HTMLElement} el Anchor element of the bubble. * @param {string} text Text content to show in bubble. * @param {!Attachment} attachment Bubble attachment (on which side of the * element it should be displayed). * @param {number=} opt_offset Offset of the bubble attachment point from * left (for vertical attachment) or top (for horizontal attachment) * side of the element. If not specified, the bubble is positioned to * be aligned with the left/top side of the element but not farther than * half of its weight/height. * @param {number=} opt_padding Optional padding of the bubble. */ showTextForElement: function(el, text, attachment, opt_offset, opt_padding) { var span = this.ownerDocument.createElement('span'); span.textContent = text; this.showContentForElement(el, attachment, span, opt_offset, opt_padding); }, /** * Hides the bubble. */ hide: function() { if (!this.classList.contains('faded')) this.classList.add('faded'); }, /** * Hides the bubble anchored to the given element (if any). * @param {!Object} el Anchor element. */ hideForElement: function(el) { if (!this.hidden && this.anchor_ == el) this.hide(); }, /** * Handler for faded transition end. * @private */ handleTransitionEnd_: function(e) { if (this.classList.contains('faded')) { this.hidden = true; if (this.elementToFocusOnHide_) this.elementToFocusOnHide_.focus(); } }, /** * Handler of scroll event. * @private */ handleScroll_: function(e) { if (!this.hidden) this.hide(); }, /** * Handler of document click event. * @private */ handleDocClick_: function(e) { // Ignore clicks on anchor element. if (e.target == this.anchor_) return; if (!this.hidden) this.hide(); }, /** * Handle of document keydown event. * @private */ handleDocKeyDown_: function(e) { if (this.hidden) return; if (this.hideOnKeyPress_) { this.hide(); return; } // Artificial tab-cycle. if (e.key == Keys.TAB && e.shiftKey == true && e.target == this.firstBubbleElement_) { this.lastBubbleElement_.focus(); e.preventDefault(); } if (e.key == Keys.TAB && e.shiftKey == false && e.target == this.lastBubbleElement_) { this.firstBubbleElement_.focus(); e.preventDefault(); } // Close bubble on ESC or on hitting spacebar or Enter at close-button. if (e.key == Keys.ESC || ((e.key == Keys.ENTER || e.key == Keys.SPACE) && e.target && e.target.classList.contains('close-button'))) this.hide(); }, /** * Handler of window blur event. * @private */ handleWindowBlur_: function(e) { if (!this.hidden) this.hide(); } }; return { Bubble: Bubble }; }); // // Copyright 2014 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. /** * @fileoverview JS helpers used on login. */ cr.define('cr.ui.LoginUITools', function() { return { /** * Computes max-height for an element so that it doesn't overlap shelf. * @param {element} DOM element * @param {wholeWindow} Whether the element can go outside outer-container. */ getMaxHeightBeforeShelfOverlapping : function(element, wholeWindow) { var maxAllowedHeight = $('outer-container').offsetHeight - element.getBoundingClientRect().top - parseInt(window.getComputedStyle(element).marginTop) - parseInt(window.getComputedStyle(element).marginBottom); if (wholeWindow) { maxAllowedHeight += parseInt(window.getComputedStyle($('outer-container')).bottom); } return maxAllowedHeight; }, /** * Computes max-width for an element so that it does fit the * outer-container. * @param {element} DOM element */ getMaxWidthToFit : function(element) { var maxAllowedWidth = $('outer-container').offsetWidth - element.getBoundingClientRect().left - parseInt(window.getComputedStyle(element).marginLeft) - parseInt(window.getComputedStyle(element).marginRight); return maxAllowedWidth; }, } }); // // Copyright 2014 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. /** * @fileoverview Display manager for WebUI OOBE and login. */ // TODO(xiyuan): Find a better to share those constants. /** @const */ var SCREEN_OOBE_NETWORK = 'connect'; /** @const */ var SCREEN_OOBE_HID_DETECTION = 'hid-detection'; /** @const */ var SCREEN_OOBE_EULA = 'eula'; /** @const */ var SCREEN_OOBE_ENABLE_DEBUGGING = 'debugging'; /** @const */ var SCREEN_OOBE_UPDATE = 'update'; /** @const */ var SCREEN_OOBE_RESET = 'reset'; /** @const */ var SCREEN_OOBE_ENROLLMENT = 'oauth-enrollment'; /** @const */ var SCREEN_OOBE_KIOSK_ENABLE = 'kiosk-enable'; /** @const */ var SCREEN_OOBE_AUTO_ENROLLMENT_CHECK = 'auto-enrollment-check'; /** @const */ var SCREEN_GAIA_SIGNIN = 'gaia-signin'; /** @const */ var SCREEN_ACCOUNT_PICKER = 'account-picker'; /** @const */ var SCREEN_USER_IMAGE_PICKER = 'user-image'; /** @const */ var SCREEN_ERROR_MESSAGE = 'error-message'; /** @const */ var SCREEN_TPM_ERROR = 'tpm-error-message'; /** @const */ var SCREEN_PASSWORD_CHANGED = 'password-changed'; /** @const */ var SCREEN_CREATE_SUPERVISED_USER_FLOW = 'supervised-user-creation'; /** @const */ var SCREEN_APP_LAUNCH_SPLASH = 'app-launch-splash'; /** @const */ var SCREEN_ARC_KIOSK_SPLASH = 'arc-kiosk-splash'; /** @const */ var SCREEN_CONFIRM_PASSWORD = 'confirm-password'; /** @const */ var SCREEN_FATAL_ERROR = 'fatal-error'; /** @const */ var SCREEN_KIOSK_ENABLE = 'kiosk-enable'; /** @const */ var SCREEN_TERMS_OF_SERVICE = 'terms-of-service'; /** @const */ var SCREEN_ARC_TERMS_OF_SERVICE = 'arc-tos'; /** @const */ var SCREEN_WRONG_HWID = 'wrong-hwid'; /** @const */ var SCREEN_DEVICE_DISABLED = 'device-disabled'; /** @const */ var SCREEN_UNRECOVERABLE_CRYPTOHOME_ERROR = 'unrecoverable-cryptohome-error'; /** @const */ var SCREEN_ACTIVE_DIRECTORY_PASSWORD_CHANGE = 'ad-password-change'; /* Accelerator identifiers. Must be kept in sync with webui_login_view.cc. */ /** @const */ var ACCELERATOR_CANCEL = 'cancel'; /** @const */ var ACCELERATOR_ENABLE_DEBBUGING = 'debugging'; /** @const */ var ACCELERATOR_TOGGLE_EASY_BOOTSTRAP = 'toggle_easy_bootstrap'; /** @const */ var ACCELERATOR_ENROLLMENT = 'enrollment'; /** @const */ var ACCELERATOR_ENROLLMENT_AD = 'enrollment_ad'; /** @const */ var ACCELERATOR_KIOSK_ENABLE = 'kiosk_enable'; /** @const */ var ACCELERATOR_VERSION = 'version'; /** @const */ var ACCELERATOR_RESET = 'reset'; /** @const */ var ACCELERATOR_DEVICE_REQUISITION = 'device_requisition'; /** @const */ var ACCELERATOR_DEVICE_REQUISITION_REMORA = 'device_requisition_remora'; /** @const */ var ACCELERATOR_DEVICE_REQUISITION_SHARK = 'device_requisition_shark'; /** @const */ var ACCELERATOR_APP_LAUNCH_BAILOUT = 'app_launch_bailout'; /** @const */ var ACCELERATOR_APP_LAUNCH_NETWORK_CONFIG = 'app_launch_network_config'; /** @const */ var ACCELERATOR_BOOTSTRAPPING_SLAVE = "bootstrapping_slave"; /* Signin UI state constants. Used to control header bar UI. */ /** @const */ var SIGNIN_UI_STATE = { HIDDEN: 0, GAIA_SIGNIN: 1, ACCOUNT_PICKER: 2, WRONG_HWID_WARNING: 3, SUPERVISED_USER_CREATION_FLOW: 4, SAML_PASSWORD_CONFIRM: 5, PASSWORD_CHANGED: 6, ENROLLMENT: 7, ERROR: 8 }; /* Possible UI states of the error screen. */ /** @const */ var ERROR_SCREEN_UI_STATE = { UNKNOWN: 'ui-state-unknown', UPDATE: 'ui-state-update', SIGNIN: 'ui-state-signin', SUPERVISED_USER_CREATION_FLOW: 'ui-state-supervised', KIOSK_MODE: 'ui-state-kiosk-mode', LOCAL_STATE_ERROR: 'ui-state-local-state-error', AUTO_ENROLLMENT_ERROR: 'ui-state-auto-enrollment-error', ROLLBACK_ERROR: 'ui-state-rollback-error' }; /* Possible types of UI. */ /** @const */ var DISPLAY_TYPE = { UNKNOWN: 'unknown', OOBE: 'oobe', LOGIN: 'login', LOCK: 'lock', USER_ADDING: 'user-adding', APP_LAUNCH_SPLASH: 'app-launch-splash', ARC_KIOSK_SPLASH: 'arc-kiosk-splash', DESKTOP_USER_MANAGER: 'login-add-user' }; /** @const */ var USER_ACTION_ROLLBACK_TOGGLED = 'rollback-toggled'; cr.define('cr.ui.login', function() { var Bubble = cr.ui.Bubble; /** * Maximum time in milliseconds to wait for step transition to finish. * The value is used as the duration for ensureTransitionEndEvent below. * It needs to be inline with the step screen transition duration time * defined in css file. The current value in css is 200ms. To avoid emulated * webkitTransitionEnd fired before real one, 250ms is used. * @const */ var MAX_SCREEN_TRANSITION_DURATION = 250; /** * Groups of screens (screen IDs) that should have the same dimensions. * @type Array> * @const */ var SCREEN_GROUPS = [[SCREEN_OOBE_NETWORK, SCREEN_OOBE_EULA, SCREEN_OOBE_UPDATE, SCREEN_OOBE_AUTO_ENROLLMENT_CHECK] ]; /** * Group of screens (screen IDs) where factory-reset screen invocation is * available. * @type Array * @const */ var RESET_AVAILABLE_SCREEN_GROUP = [ SCREEN_OOBE_NETWORK, SCREEN_OOBE_EULA, SCREEN_OOBE_UPDATE, SCREEN_OOBE_ENROLLMENT, SCREEN_OOBE_AUTO_ENROLLMENT_CHECK, SCREEN_GAIA_SIGNIN, SCREEN_ACCOUNT_PICKER, SCREEN_KIOSK_ENABLE, SCREEN_ERROR_MESSAGE, SCREEN_USER_IMAGE_PICKER, SCREEN_TPM_ERROR, SCREEN_PASSWORD_CHANGED, SCREEN_TERMS_OF_SERVICE, SCREEN_ARC_TERMS_OF_SERVICE, SCREEN_WRONG_HWID, SCREEN_CONFIRM_PASSWORD, SCREEN_FATAL_ERROR ]; /** * Group of screens (screen IDs) where enable debuggingscreen invocation is * available. * @type Array * @const */ var ENABLE_DEBUGGING_AVAILABLE_SCREEN_GROUP = [ SCREEN_OOBE_HID_DETECTION, SCREEN_OOBE_NETWORK, SCREEN_OOBE_EULA, SCREEN_OOBE_UPDATE, SCREEN_TERMS_OF_SERVICE ]; /** * Group of screens (screen IDs) that are not participating in * left-current-right animation. * @type Array * @const */ var NOT_ANIMATED_SCREEN_GROUP = [ SCREEN_OOBE_ENABLE_DEBUGGING, SCREEN_OOBE_RESET, ]; /** * OOBE screens group index. */ var SCREEN_GROUP_OOBE = 0; /** * Constructor a display manager that manages initialization of screens, * transitions, error messages display. * * @constructor */ function DisplayManager() { } DisplayManager.prototype = { /** * Registered screens. */ screens_: [], /** * Current OOBE step, index in the screens array. * @type {number} */ currentStep_: 0, /** * Whether version label can be toggled by ACCELERATOR_VERSION. * @type {boolean} */ allowToggleVersion_: false, /** * Whether keyboard navigation flow is enforced. * @type {boolean} */ forceKeyboardFlow_: false, /** * Whether the virtual keyboard is displayed. * @type {boolean} */ virtualKeyboardShown: false, /** * Type of UI. * @type {string} */ displayType_: DISPLAY_TYPE.UNKNOWN, /** * Error message (bubble) was shown. This is checked in tests. */ errorMessageWasShownForTesting_: false, get displayType() { return this.displayType_; }, set displayType(displayType) { this.displayType_ = displayType; document.documentElement.setAttribute('screen', displayType); }, get newKioskUI() { return loadTimeData.getString('newKioskUI') == 'on'; }, /** * Returns dimensions of screen exluding header bar. * @type {Object} */ get clientAreaSize() { var container = $('outer-container'); return {width: container.offsetWidth, height: container.offsetHeight}; }, /** * Gets current screen element. * @type {HTMLElement} */ get currentScreen() { return $(this.screens_[this.currentStep_]); }, /** * Hides/shows header (Shutdown/Add User/Cancel buttons). * @param {boolean} hidden Whether header is hidden. */ get headerHidden() { return $('login-header-bar').hidden; }, set headerHidden(hidden) { $('login-header-bar').hidden = hidden; }, set pinHidden(hidden) { this.virtualKeyboardShown = hidden; $('pod-row').setFocusedPodPinVisibility(!hidden); }, /** * Sets the current size of the client area (display size). * @param {number} width client area width * @param {number} height client area height */ setClientAreaSize: function(width, height) { var clientArea = $('outer-container'); var bottom = parseInt(window.getComputedStyle(clientArea).bottom); clientArea.style.minHeight = cr.ui.toCssPx(height - bottom); }, /** * Toggles background of main body between transparency and solid. * @param {boolean} solid Whether to show a solid background. */ set solidBackground(solid) { if (solid) document.body.classList.add('solid'); else document.body.classList.remove('solid'); }, /** * Forces keyboard based OOBE navigation. * @param {boolean} value True if keyboard navigation flow is forced. */ set forceKeyboardFlow(value) { this.forceKeyboardFlow_ = value; if (value) { keyboard.initializeKeyboardFlow(false); cr.ui.DropDown.enableKeyboardFlow(); for (var i = 0; i < this.screens_.length; ++i) { var screen = $(this.screens_[i]); if (screen.enableKeyboardFlow) screen.enableKeyboardFlow(); } } }, /** * Returns true if keyboard flow is enabled. * @return {boolean} */ get forceKeyboardFlow() { return this.forceKeyboardFlow_; }, /** * Shows/hides version labels. * @param {boolean} show Whether labels should be visible by default. If * false, visibility can be toggled by ACCELERATOR_VERSION. */ showVersion: function(show) { $('version-labels').hidden = !show; this.allowToggleVersion_ = !show; }, /** * Handle accelerators. * @param {string} name Accelerator name. */ handleAccelerator: function(name) { if (this.currentScreen.ignoreAccelerators) { return; } var currentStepId = this.screens_[this.currentStep_]; if (name == ACCELERATOR_CANCEL) { if (this.currentScreen.cancel) { this.currentScreen.cancel(); } } else if (name == ACCELERATOR_ENABLE_DEBBUGING) { if (ENABLE_DEBUGGING_AVAILABLE_SCREEN_GROUP.indexOf( currentStepId) != -1) { chrome.send('toggleEnableDebuggingScreen'); } } else if (name == ACCELERATOR_ENROLLMENT || name == ACCELERATOR_ENROLLMENT_AD) { if (name == ACCELERATOR_ENROLLMENT_AD) chrome.send('toggleEnrollmentAd'); if (currentStepId == SCREEN_GAIA_SIGNIN || currentStepId == SCREEN_ACCOUNT_PICKER) { chrome.send('toggleEnrollmentScreen'); } else if (currentStepId == SCREEN_OOBE_NETWORK || currentStepId == SCREEN_OOBE_EULA) { // In this case update check will be skipped and OOBE will // proceed straight to enrollment screen when EULA is accepted. chrome.send('skipUpdateEnrollAfterEula'); } } else if (name == ACCELERATOR_KIOSK_ENABLE) { if (currentStepId == SCREEN_GAIA_SIGNIN || currentStepId == SCREEN_ACCOUNT_PICKER) { chrome.send('toggleKioskEnableScreen'); } } else if (name == ACCELERATOR_VERSION) { if (this.allowToggleVersion_) $('version-labels').hidden = !$('version-labels').hidden; } else if (name == ACCELERATOR_RESET) { if (currentStepId == SCREEN_OOBE_RESET) $('reset').send(login.Screen.CALLBACK_USER_ACTED, USER_ACTION_ROLLBACK_TOGGLED); else if (RESET_AVAILABLE_SCREEN_GROUP.indexOf(currentStepId) != -1) chrome.send('toggleResetScreen'); } else if (name == ACCELERATOR_DEVICE_REQUISITION) { if (this.isOobeUI()) this.showDeviceRequisitionPrompt_(); } else if (name == ACCELERATOR_DEVICE_REQUISITION_REMORA) { if (this.isOobeUI()) this.showDeviceRequisitionRemoraPrompt_( 'deviceRequisitionRemoraPromptText', 'remora'); } else if (name == ACCELERATOR_DEVICE_REQUISITION_SHARK) { if (this.isOobeUI()) this.showDeviceRequisitionRemoraPrompt_( 'deviceRequisitionSharkPromptText', 'shark'); } else if (name == ACCELERATOR_APP_LAUNCH_BAILOUT) { if (currentStepId == SCREEN_APP_LAUNCH_SPLASH) chrome.send('cancelAppLaunch'); if (currentStepId == SCREEN_ARC_KIOSK_SPLASH) chrome.send('cancelArcKioskLaunch'); } else if (name == ACCELERATOR_APP_LAUNCH_NETWORK_CONFIG) { if (currentStepId == SCREEN_APP_LAUNCH_SPLASH) chrome.send('networkConfigRequest'); } else if (name == ACCELERATOR_TOGGLE_EASY_BOOTSTRAP) { if (currentStepId == SCREEN_GAIA_SIGNIN) chrome.send('toggleEasyBootstrap'); } else if (name == ACCELERATOR_BOOTSTRAPPING_SLAVE) { chrome.send('setOobeBootstrappingSlave'); } }, /** * Appends buttons to the button strip. * @param {Array} buttons Array with the buttons to append. * @param {string} screenId Id of the screen that buttons belong to. */ appendButtons_: function(buttons, screenId) { if (buttons) { var buttonStrip = $(screenId + '-controls'); if (buttonStrip) { for (var i = 0; i < buttons.length; ++i) buttonStrip.appendChild(buttons[i]); } } }, /** * Disables or enables control buttons on the specified screen. * @param {HTMLElement} screen Screen which controls should be affected. * @param {boolean} disabled Whether to disable controls. */ disableButtons_: function(screen, disabled) { var buttons = document.querySelectorAll( '#' + screen.id + '-controls button:not(.preserve-disabled-state)'); for (var i = 0; i < buttons.length; ++i) { buttons[i].disabled = disabled; } }, screenIsAnimated_: function(screenId) { return NOT_ANIMATED_SCREEN_GROUP.indexOf(screenId) != -1; }, /** * Updates a step's css classes to reflect left, current, or right position. * @param {number} stepIndex step index. * @param {string} state one of 'left', 'current', 'right'. */ updateStep_: function(stepIndex, state) { var stepId = this.screens_[stepIndex]; var step = $(stepId); var header = $('header-' + stepId); var states = ['left', 'right', 'current']; for (var i = 0; i < states.length; ++i) { if (states[i] != state) { step.classList.remove(states[i]); header.classList.remove(states[i]); } } step.classList.add(state); header.classList.add(state); }, /** * Switches to the next OOBE step. * @param {number} nextStepIndex Index of the next step. */ toggleStep_: function(nextStepIndex, screenData) { var currentStepId = this.screens_[this.currentStep_]; var nextStepId = this.screens_[nextStepIndex]; var oldStep = $(currentStepId); var newStep = $(nextStepId); var newHeader = $('header-' + nextStepId); // Disable controls before starting animation. this.disableButtons_(oldStep, true); if (oldStep.onBeforeHide) oldStep.onBeforeHide(); $('oobe').className = nextStepId; // Need to do this before calling newStep.onBeforeShow() so that new step // is back in DOM tree and has correct offsetHeight / offsetWidth. newStep.hidden = false; if (newStep.onBeforeShow) newStep.onBeforeShow(screenData); newStep.classList.remove('hidden'); if (this.isOobeUI() && this.screenIsAnimated_(nextStepId) && this.screenIsAnimated_(currentStepId)) { // Start gliding animation for OOBE steps. if (nextStepIndex > this.currentStep_) { for (var i = this.currentStep_; i < nextStepIndex; ++i) this.updateStep_(i, 'left'); this.updateStep_(nextStepIndex, 'current'); } else if (nextStepIndex < this.currentStep_) { for (var i = this.currentStep_; i > nextStepIndex; --i) this.updateStep_(i, 'right'); this.updateStep_(nextStepIndex, 'current'); } } else { // Start fading animation for login display or reset screen. oldStep.classList.add('faded'); newStep.classList.remove('faded'); if (!this.screenIsAnimated_(nextStepId)) { newStep.classList.remove('left'); newStep.classList.remove('right'); } } this.disableButtons_(newStep, false); // Adjust inner container height based on new step's height. this.updateScreenSize(newStep); if (newStep.onAfterShow) newStep.onAfterShow(screenData); // Workaround for gaia and network screens. // Due to other origin iframe and long ChromeVox focusing correspondingly // passive aria-label title is not pronounced. // Gaia hack can be removed on fixed crbug.com/316726. if (nextStepId == SCREEN_GAIA_SIGNIN || nextStepId == SCREEN_OOBE_ENROLLMENT) { newStep.setAttribute( 'aria-label', loadTimeData.getString('signinScreenTitle')); } else if (nextStepId == SCREEN_OOBE_NETWORK) { newStep.setAttribute( 'aria-label', loadTimeData.getString('networkScreenAccessibleTitle')); } // Default control to be focused (if specified). var defaultControl = newStep.defaultControl; var outerContainer = $('outer-container'); var innerContainer = $('inner-container'); var isOOBE = this.isOobeUI(); if (this.currentStep_ != nextStepIndex && !oldStep.classList.contains('hidden')) { if (oldStep.classList.contains('animated')) { innerContainer.classList.add('animation'); oldStep.addEventListener('webkitTransitionEnd', function f(e) { oldStep.removeEventListener('webkitTransitionEnd', f); if (oldStep.classList.contains('faded') || oldStep.classList.contains('left') || oldStep.classList.contains('right')) { innerContainer.classList.remove('animation'); oldStep.classList.add('hidden'); if (!isOOBE) oldStep.hidden = true; } // Refresh defaultControl. It could have changed. var defaultControl = newStep.defaultControl; if (defaultControl) defaultControl.focus(); }); ensureTransitionEndEvent(oldStep, MAX_SCREEN_TRANSITION_DURATION); } else { oldStep.classList.add('hidden'); oldStep.hidden = true; if (defaultControl) defaultControl.focus(); } } else { // First screen on OOBE launch. if (this.isOobeUI() && innerContainer.classList.contains('down')) { innerContainer.classList.remove('down'); innerContainer.addEventListener( 'webkitTransitionEnd', function f(e) { innerContainer.removeEventListener('webkitTransitionEnd', f); outerContainer.classList.remove('down'); $('progress-dots').classList.remove('down'); chrome.send('loginVisible', ['oobe']); // Refresh defaultControl. It could have changed. var defaultControl = newStep.defaultControl; if (defaultControl) defaultControl.focus(); }); ensureTransitionEndEvent(innerContainer, MAX_SCREEN_TRANSITION_DURATION); } else { if (defaultControl) defaultControl.focus(); chrome.send('loginVisible', ['oobe']); } } this.currentStep_ = nextStepIndex; $('step-logo').hidden = newStep.classList.contains('no-logo'); $('oobe').dispatchEvent( new CustomEvent('screenchanged', {detail: this.currentScreen.id})); chrome.send('updateCurrentScreen', [this.currentScreen.id]); }, /** * Make sure that screen is initialized and decorated. * @param {Object} screen Screen params dict, e.g. {id: screenId, data: {}}. */ preloadScreen: function(screen) { var screenEl = $(screen.id); if (screenEl.deferredInitialization !== undefined) { screenEl.deferredInitialization(); delete screenEl.deferredInitialization; } }, /** * Show screen of given screen id. * @param {Object} screen Screen params dict, e.g. {id: screenId, data: {}}. */ showScreen: function(screen) { // Do not allow any other screen to clobber the device disabled screen. if (this.currentScreen.id == SCREEN_DEVICE_DISABLED) return; var screenId = screen.id; // Make sure the screen is decorated. this.preloadScreen(screen); if (screen.data !== undefined && screen.data.disableAddUser) DisplayManager.updateAddUserButtonStatus(true); // Show sign-in screen instead of account picker if pod row is empty. if (screenId == SCREEN_ACCOUNT_PICKER && $('pod-row').pods.length == 0 && cr.isChromeOS) { // Manually hide 'add-user' header bar, because of the case when // 'Cancel' button is used on the offline login page. $('add-user-header-bar-item').hidden = true; Oobe.showSigninUI(); return; } var data = screen.data; var index = this.getScreenIndex_(screenId); if (index >= 0) this.toggleStep_(index, data); }, /** * Gets index of given screen id in screens_. * @param {string} screenId Id of the screen to look up. * @private */ getScreenIndex_: function(screenId) { for (var i = 0; i < this.screens_.length; ++i) { if (this.screens_[i] == screenId) return i; } return -1; }, /** * Register an oobe screen. * @param {Element} el Decorated screen element. */ registerScreen: function(el) { var screenId = el.id; this.screens_.push(screenId); var header = document.createElement('span'); header.id = 'header-' + screenId; header.textContent = el.header ? el.header : ''; header.className = 'header-section'; $('header-sections').appendChild(header); var dot = document.createElement('div'); dot.id = screenId + '-dot'; dot.className = 'progdot'; var progressDots = $('progress-dots'); if (progressDots) progressDots.appendChild(dot); this.appendButtons_(el.buttons, screenId); }, /** * Updates inner container size based on the size of the current screen and * other screens in the same group. * Should be executed on screen change / screen size change. * @param {!HTMLElement} screen Screen that is being shown. */ updateScreenSize: function(screen) { // Have to reset any previously predefined screen size first // so that screen contents would define it instead. $('inner-container').style.height = ''; $('inner-container').style.width = ''; screen.style.width = ''; screen.style.height = ''; $('outer-container').classList.toggle( 'fullscreen', screen.classList.contains('fullscreen')); var width = screen.getPreferredSize().width; var height = screen.getPreferredSize().height; for (var i = 0, screenGroup; screenGroup = SCREEN_GROUPS[i]; i++) { if (screenGroup.indexOf(screen.id) != -1) { // Set screen dimensions to maximum dimensions within this group. for (var j = 0, screen2; screen2 = $(screenGroup[j]); j++) { width = Math.max(width, screen2.getPreferredSize().width); height = Math.max(height, screen2.getPreferredSize().height); } break; } } $('inner-container').style.height = height + 'px'; $('inner-container').style.width = width + 'px'; // This requires |screen| to have 'box-sizing: border-box'. screen.style.width = width + 'px'; screen.style.height = height + 'px'; }, /** * Updates localized content of the screens like headers, buttons and links. * Should be executed on language change. */ updateLocalizedContent_: function() { for (var i = 0, screenId; screenId = this.screens_[i]; ++i) { var screen = $(screenId); var buttonStrip = $(screenId + '-controls'); if (buttonStrip) buttonStrip.innerHTML = ''; // TODO(nkostylev): Update screen headers for new OOBE design. this.appendButtons_(screen.buttons, screenId); if (screen.updateLocalizedContent) screen.updateLocalizedContent(); } var currentScreenId = this.screens_[this.currentStep_]; var currentScreen = $(currentScreenId); this.updateScreenSize(currentScreen); // Trigger network drop-down to reload its state // so that strings are reloaded. // Will be reloaded if drowdown is actually shown. cr.ui.DropDown.refresh(); }, /** * Initialized first group of OOBE screens. */ initializeOOBEScreens: function() { if (this.isOobeUI() && $('inner-container').classList.contains('down')) { for (var i = 0, screen; screen = $(SCREEN_GROUPS[SCREEN_GROUP_OOBE][i]); i++) { screen.hidden = false; } } }, /** * Prepares screens to use in login display. */ prepareForLoginDisplay_: function() { for (var i = 0, screenId; screenId = this.screens_[i]; ++i) { var screen = $(screenId); screen.classList.add('faded'); screen.classList.remove('right'); screen.classList.remove('left'); } }, /** * Shows the device requisition prompt. */ showDeviceRequisitionPrompt_: function() { if (!this.deviceRequisitionDialog_) { this.deviceRequisitionDialog_ = new cr.ui.dialogs.PromptDialog(document.body); this.deviceRequisitionDialog_.setOkLabel( loadTimeData.getString('deviceRequisitionPromptOk')); this.deviceRequisitionDialog_.setCancelLabel( loadTimeData.getString('deviceRequisitionPromptCancel')); } this.deviceRequisitionDialog_.show( loadTimeData.getString('deviceRequisitionPromptText'), this.deviceRequisition_, this.onConfirmDeviceRequisitionPrompt_.bind(this)); }, /** * Confirmation handle for the device requisition prompt. * @param {string} value The value entered by the user. * @private */ onConfirmDeviceRequisitionPrompt_: function(value) { this.deviceRequisition_ = value; chrome.send('setDeviceRequisition', [value == '' ? 'none' : value]); }, /** * Called when window size changed. Notifies current screen about change. * @private */ onWindowResize_: function() { var currentScreenId = this.screens_[this.currentStep_]; var currentScreen = $(currentScreenId); if (currentScreen) currentScreen.onWindowResize(); }, /* * Updates the device requisition string shown in the requisition prompt. * @param {string} requisition The device requisition. */ updateDeviceRequisition: function(requisition) { this.deviceRequisition_ = requisition; }, /** * Shows the special remora/shark device requisition prompt. * @private */ showDeviceRequisitionRemoraPrompt_: function(promptText, requisition) { if (!this.deviceRequisitionRemoraDialog_) { this.deviceRequisitionRemoraDialog_ = new cr.ui.dialogs.ConfirmDialog(document.body); this.deviceRequisitionRemoraDialog_.setOkLabel( loadTimeData.getString('deviceRequisitionRemoraPromptOk')); this.deviceRequisitionRemoraDialog_.setCancelLabel( loadTimeData.getString('deviceRequisitionRemoraPromptCancel')); } this.deviceRequisitionRemoraDialog_.show( loadTimeData.getString(promptText), function() { // onShow chrome.send('setDeviceRequisition', [requisition]); }, function() { // onCancel chrome.send('setDeviceRequisition', ['none']); }); }, /** * Returns true if Oobe UI is shown. */ isOobeUI: function() { return document.body.classList.contains('oobe-display'); }, /** * Sets or unsets given |className| for top-level container. Useful for * customizing #inner-container with CSS rules. All classes set with with * this method will be removed after screen change. * @param {string} className Class to toggle. * @param {boolean} enabled Whether class should be enabled or disabled. */ toggleClass: function(className, enabled) { $('oobe').classList.toggle(className, enabled); } }; /** * Initializes display manager. */ DisplayManager.initialize = function() { var givenDisplayType = DISPLAY_TYPE.UNKNOWN; if (document.documentElement.hasAttribute('screen')) { // Display type set in HTML property. givenDisplayType = document.documentElement.getAttribute('screen'); } else { // Extracting display type from URL. givenDisplayType = window.location.pathname.substr(1); } var instance = Oobe.getInstance(); Object.getOwnPropertyNames(DISPLAY_TYPE).forEach(function(type) { if (DISPLAY_TYPE[type] == givenDisplayType) { instance.displayType = givenDisplayType; } }); if (instance.displayType == DISPLAY_TYPE.UNKNOWN) { console.error("Unknown display type '" + givenDisplayType + "'. Setting default."); instance.displayType = DISPLAY_TYPE.LOGIN; } instance.initializeOOBEScreens(); window.addEventListener('resize', instance.onWindowResize_.bind(instance)); }; /** * Returns offset (top, left) of the element. * @param {!Element} element HTML element. * @return {!Object} The offset (top, left). */ DisplayManager.getOffset = function(element) { var x = 0; var y = 0; while (element && !isNaN(element.offsetLeft) && !isNaN(element.offsetTop)) { x += element.offsetLeft - element.scrollLeft; y += element.offsetTop - element.scrollTop; element = element.offsetParent; } return { top: y, left: x }; }; /** * Returns position (top, left, right, bottom) of the element. * @param {!Element} element HTML element. * @return {!Object} Element position (top, left, right, bottom). */ DisplayManager.getPosition = function(element) { var offset = DisplayManager.getOffset(element); return { top: offset.top, right: window.innerWidth - element.offsetWidth - offset.left, bottom: window.innerHeight - element.offsetHeight - offset.top, left: offset.left }; }; /** * Disables signin UI. */ DisplayManager.disableSigninUI = function() { $('login-header-bar').disabled = true; $('pod-row').disabled = true; }; /** * Shows signin UI. * @param {string} opt_email An optional email for signin UI. */ DisplayManager.showSigninUI = function(opt_email) { var currentScreenId = Oobe.getInstance().currentScreen.id; if (currentScreenId == SCREEN_GAIA_SIGNIN) $('login-header-bar').signinUIState = SIGNIN_UI_STATE.GAIA_SIGNIN; else if (currentScreenId == SCREEN_ACCOUNT_PICKER) $('login-header-bar').signinUIState = SIGNIN_UI_STATE.ACCOUNT_PICKER; chrome.send('showAddUser', [opt_email]); }; /** * Resets sign-in input fields. * @param {boolean} forceOnline Whether online sign-in should be forced. * If |forceOnline| is false previously used sign-in type will be used. */ DisplayManager.resetSigninUI = function(forceOnline) { var currentScreenId = Oobe.getInstance().currentScreen.id; if ($(SCREEN_GAIA_SIGNIN)) $(SCREEN_GAIA_SIGNIN).reset( currentScreenId == SCREEN_GAIA_SIGNIN, forceOnline); $('login-header-bar').disabled = false; $('pod-row').reset(currentScreenId == SCREEN_ACCOUNT_PICKER); }; /** * Shows sign-in error bubble. * @param {number} loginAttempts Number of login attemps tried. * @param {string} message Error message to show. * @param {string} link Text to use for help link. * @param {number} helpId Help topic Id associated with help link. */ DisplayManager.showSignInError = function(loginAttempts, message, link, helpId) { var error = document.createElement('div'); var messageDiv = document.createElement('div'); messageDiv.className = 'error-message-bubble'; messageDiv.textContent = message; error.appendChild(messageDiv); if (link) { messageDiv.classList.add('error-message-bubble-padding'); var helpLink = document.createElement('a'); helpLink.href = '#'; helpLink.textContent = link; helpLink.addEventListener('click', function(e) { chrome.send('launchHelpApp', [helpId]); e.preventDefault(); }); error.appendChild(helpLink); } error.setAttribute('aria-live', 'assertive'); var currentScreen = Oobe.getInstance().currentScreen; if (currentScreen && typeof currentScreen.showErrorBubble === 'function') { currentScreen.showErrorBubble(loginAttempts, error); this.errorMessageWasShownForTesting_ = true; } }; /** * Shows password changed screen that offers migration. * @param {boolean} showError Whether to show the incorrect password error. * @param {string} email What user does reauth. Being used for display in the * new UI. */ DisplayManager.showPasswordChangedScreen = function(showError, email) { login.PasswordChangedScreen.show(showError, email); }; /** * Shows dialog to create a supervised user. */ DisplayManager.showSupervisedUserCreationScreen = function() { login.SupervisedUserCreationScreen.show(); }; /** * Shows TPM error screen. */ DisplayManager.showTpmError = function() { login.TPMErrorMessageScreen.show(); }; /** * Shows password change screen for Active Directory users. * @param {string} username Display name of the user whose password is being * changed. */ DisplayManager.showActiveDirectoryPasswordChangeScreen = function(username) { login.ActiveDirectoryPasswordChangeScreen.show(username); }; /** * Clears error bubble. */ DisplayManager.clearErrors = function() { $('bubble').hide(); this.errorMessageWasShownForTesting_ = false; var bubbles = document.querySelectorAll('.bubble-shown'); for (var i = 0; i < bubbles.length; ++i) bubbles[i].classList.remove('bubble-shown'); }; /** * Sets text content for a div with |labelId|. * @param {string} labelId Id of the label div. * @param {string} labelText Text for the label. */ DisplayManager.setLabelText = function(labelId, labelText) { $(labelId).textContent = labelText; }; /** * Sets the text content of the enterprise info message and asset ID. * @param {string} messageText The message text. * @param {string} assetId The device asset ID. */ DisplayManager.setEnterpriseInfo = function(messageText, assetId) { $('asset-id').textContent = ((assetId == "") ? "" : loadTimeData.getStringF('assetIdLabel', assetId)); }; /** * Disable Add users button if said. * @param {boolean} disable true to disable */ DisplayManager.updateAddUserButtonStatus = function(disable) { $('add-user-button').disabled = disable; $('add-user-button').classList[ disable ? 'add' : 'remove']('button-restricted'); $('add-user-button').title = disable ? loadTimeData.getString('disabledAddUserTooltip') : ''; } /** * Clears password field in user-pod. */ DisplayManager.clearUserPodPassword = function() { $('pod-row').clearFocusedPod(); }; /** * Restores input focus to currently selected pod. */ DisplayManager.refocusCurrentPod = function() { $('pod-row').refocusCurrentPod(); }; // Export return { DisplayManager: DisplayManager }; }); // // Copyright 2014 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. /** * @fileoverview Account picker screen implementation. */ login.createScreen('AccountPickerScreen', 'account-picker', function() { /** * Maximum number of offline login failures before online login. * @type {number} * @const */ var MAX_LOGIN_ATTEMPTS_IN_POD = 3; /** * Distance between error bubble and user POD. * @type {number} * @const */ var BUBBLE_POD_OFFSET = 4; return { EXTERNAL_API: [ 'loadUsers', 'runAppForTesting', 'setApps', 'setShouldShowApps', 'showAppError', 'updateUserImage', 'setCapsLockState', 'forceLockedUserPodFocus', 'removeUser', 'showBannerMessage', 'showUserPodCustomIcon', 'hideUserPodCustomIcon', 'disablePinKeyboardForUser', 'setAuthType', 'setTouchViewState', 'setPublicSessionDisplayName', 'setPublicSessionLocales', 'setPublicSessionKeyboardLayouts', ], preferredWidth_: 0, preferredHeight_: 0, // Whether this screen is shown for the first time. firstShown_: true, // Whether this screen is currently being shown. showing_: false, /** @override */ decorate: function() { login.PodRow.decorate($('pod-row')); }, /** @override */ getPreferredSize: function() { return {width: this.preferredWidth_, height: this.preferredHeight_}; }, /** @override */ onWindowResize: function() { $('pod-row').onWindowResize(); // Reposition the error bubble, if it is showing. Since we are just // moving the bubble, the number of login attempts tried doesn't matter. var errorBubble = $('bubble'); if (errorBubble && !errorBubble.hidden) this.showErrorBubble(0, undefined /* Reuses the existing message. */); }, /** * Sets preferred size for account picker screen. */ setPreferredSize: function(width, height) { this.preferredWidth_ = width; this.preferredHeight_ = height; }, /** * When the account picker is being used to lock the screen, pressing the * exit accelerator key will sign out the active user as it would when * they are signed in. */ exit: function() { // Check and disable the sign out button so that we can never have two // sign out requests generated in a row. if ($('pod-row').lockedPod && !$('sign-out-user-button').disabled) { $('sign-out-user-button').disabled = true; chrome.send('signOutUser'); } }, /* Cancel user adding if ESC was pressed. */ cancel: function() { if (Oobe.getInstance().displayType == DISPLAY_TYPE.USER_ADDING) chrome.send('cancelUserAdding'); }, /** * Event handler that is invoked just after the frame is shown. * @param {string} data Screen init payload. */ onAfterShow: function(data) { $('pod-row').handleAfterShow(); }, /** * Event handler that is invoked just before the frame is shown. * @param {string} data Screen init payload. */ onBeforeShow: function(data) { this.showing_ = true; chrome.send('loginUIStateChanged', ['account-picker', true]); $('login-header-bar').signinUIState = SIGNIN_UI_STATE.ACCOUNT_PICKER; // Header bar should be always visible on Account Picker screen. Oobe.getInstance().headerHidden = false; chrome.send('hideCaptivePortal'); var podRow = $('pod-row'); podRow.handleBeforeShow(); // In case of the preselected pod onShow will be called once pod // receives focus. if (!podRow.preselectedPod) this.onShow(); }, /** * Event handler invoked when the page is shown and ready. */ onShow: function() { if (!this.showing_) { // This method may be called asynchronously when the pod row finishes // initializing. However, at that point, the screen may have been hidden // again already. If that happens, ignore the onShow() call. return; } chrome.send('getTouchViewState'); if (!this.firstShown_) return; this.firstShown_ = false; // Ensure that login is actually visible. window.requestAnimationFrame(function() { chrome.send('accountPickerReady'); chrome.send('loginVisible', ['account-picker']); }); }, /** * Event handler that is invoked just before the frame is hidden. */ onBeforeHide: function() { $('pod-row').clearFocusedPod(); this.showing_ = false; chrome.send('loginUIStateChanged', ['account-picker', false]); $('login-header-bar').signinUIState = SIGNIN_UI_STATE.HIDDEN; $('pod-row').handleHide(); }, /** * Shows sign-in error bubble. * @param {number} loginAttempts Number of login attemps tried. * @param {HTMLElement} content Content to show in bubble. */ showErrorBubble: function(loginAttempts, error) { var activatedPod = $('pod-row').activatedPod; if (!activatedPod) { $('bubble').showContentForElement($('pod-row'), cr.ui.Bubble.Attachment.RIGHT, error); return; } // Show web authentication if this is not a supervised user. if (loginAttempts > MAX_LOGIN_ATTEMPTS_IN_POD && !activatedPod.user.supervisedUser) { chrome.send('maxIncorrectPasswordAttempts', [activatedPod.user.emailAddress]); activatedPod.showSigninUI(); } else { if (loginAttempts == 1) { chrome.send('firstIncorrectPasswordAttempt', [activatedPod.user.emailAddress]); } // Update the pod row display if incorrect password. $('pod-row').setFocusedPodErrorDisplay(true); /** @const */ var BUBBLE_OFFSET = 25; // -8 = 4(BUBBLE_POD_OFFSET) - 2(bubble margin) // - 10(internal bubble adjustment) var bubblePositioningPadding = -8; var bubbleAnchor; var attachment; if (activatedPod.pinContainer) { // Anchor the bubble to the input field. bubbleAnchor = ( activatedPod.getElementsByClassName('auth-container'))[0]; if (!bubbleAnchor) { console.error('auth-container not found!'); bubbleAnchor = activatedPod.mainInput; } attachment = cr.ui.Bubble.Attachment.RIGHT; } else { // Anchor the bubble to the pod instead of the input. bubbleAnchor = activatedPod; attachment = cr.ui.Bubble.Attachment.BOTTOM; } var bubble = $('bubble'); // Cannot use cr.ui.LoginUITools.get* on bubble until it is attached to // the element. getMaxHeight/Width rely on the correct up/left element // side positioning that doesn't happen until bubble is attached. var maxHeight = cr.ui.LoginUITools.getMaxHeightBeforeShelfOverlapping(bubbleAnchor) - bubbleAnchor.offsetHeight - BUBBLE_POD_OFFSET; var maxWidth = cr.ui.LoginUITools.getMaxWidthToFit(bubbleAnchor) - bubbleAnchor.offsetWidth - BUBBLE_POD_OFFSET; // Change bubble visibility temporary to calculate height. var bubbleVisibility = bubble.style.visibility; bubble.style.visibility = 'hidden'; bubble.hidden = false; // Now we need the bubble to have the new content before calculating // size. Undefined |error| == reuse old content. if (error !== undefined) bubble.replaceContent(error); // Get bubble size. var bubbleOffsetHeight = parseInt(bubble.offsetHeight); var bubbleOffsetWidth = parseInt(bubble.offsetWidth); // Restore attributes. bubble.style.visibility = bubbleVisibility; bubble.hidden = true; if (attachment == cr.ui.Bubble.Attachment.BOTTOM) { // Move error bubble if it overlaps the shelf. if (maxHeight < bubbleOffsetHeight) attachment = cr.ui.Bubble.Attachment.TOP; } else { // Move error bubble if it doesn't fit screen. if (maxWidth < bubbleOffsetWidth) { bubblePositioningPadding = 2; attachment = cr.ui.Bubble.Attachment.LEFT; } } var showBubbleCallback = function() { activatedPod.removeEventListener("webkitTransitionEnd", showBubbleCallback); $('bubble').showContentForElement(bubbleAnchor, attachment, error, BUBBLE_OFFSET, bubblePositioningPadding, true); }; activatedPod.addEventListener("webkitTransitionEnd", showBubbleCallback); ensureTransitionEndEvent(activatedPod); } }, /** * Loads the PIN keyboard if any of the users can login with a PIN. Disables * the PIN keyboard for users who are not allowed to use PIN unlock. * @param {array} users Array of user instances. */ initializePinKeyboardStateForUsers_: function(users) { // It is possible that the PIN keyboard HTML has already been loaded. If // that is the case, we want to show the user pods with the PIN keyboard // immediately without running the PIN show/hide effect. document.body.classList.add('disable-pin-animation'); setTimeout(function() { document.body.classList.remove('disable-pin-animation'); }); for (var i = 0; i < users.length; ++i) { var user = users[i]; if (user.showPin) { showPinKeyboardAsync(); } else { // Disable pin for users who cannot authenticate with PIN. For // example, users who have not set up PIN or users who have not // entered their account recently. Otherwise, the PIN keyboard will // will appear for any user if there is at least one user who has PIN // enabled. this.disablePinKeyboardForUser(user.username); } } }, /** * Loads given users in pod row. * @param {array} users Array of user. * @param {boolean} showGuest Whether to show guest session button. */ loadUsers: function(users, showGuest) { $('pod-row').loadPods(users); $('login-header-bar').showGuestButton = showGuest; // On Desktop, #login-header-bar has a shadow if there are 8+ profiles. if (Oobe.getInstance().displayType == DISPLAY_TYPE.DESKTOP_USER_MANAGER) $('login-header-bar').classList.toggle('shadow', users.length > 8); this.initializePinKeyboardStateForUsers_(users); }, /** * Runs app with a given id from the list of loaded apps. * @param {!string} app_id of an app to run. * @param {boolean=} opt_diagnostic_mode Whether to run the app in * diagnostic mode. Default is false. */ runAppForTesting: function(app_id, opt_diagnostic_mode) { $('pod-row').findAndRunAppForTesting(app_id, opt_diagnostic_mode); }, /** * Adds given apps to the pod row. * @param {array} apps Array of apps. */ setApps: function(apps) { $('pod-row').setApps(apps); }, /** * Sets the flag of whether app pods should be visible. * @param {boolean} shouldShowApps Whether to show app pods. */ setShouldShowApps: function(shouldShowApps) { $('pod-row').setShouldShowApps(shouldShowApps); }, /** * Shows the given kiosk app error message. * @param {!string} message Error message to show. */ showAppError: function(message) { // TODO(nkostylev): Figure out a way to show kiosk app launch error // pointing to the kiosk app pod. /** @const */ var BUBBLE_PADDING = 12; $('bubble').showTextForElement($('pod-row'), message, cr.ui.Bubble.Attachment.BOTTOM, $('pod-row').offsetWidth / 2, BUBBLE_PADDING); }, /** * Updates current image of a user. * @param {string} username User for which to update the image. */ updateUserImage: function(username) { $('pod-row').updateUserImage(username); }, /** * Updates Caps Lock state (for Caps Lock hint in password input field). * @param {boolean} enabled Whether Caps Lock is on. */ setCapsLockState: function(enabled) { $('pod-row').classList.toggle('capslock-on', enabled); }, /** * Enforces focus on user pod of locked user. */ forceLockedUserPodFocus: function() { var row = $('pod-row'); if (row.lockedPod) row.focusPod(row.lockedPod, true); }, /** * Remove given user from pod row if it is there. * @param {string} user name. */ removeUser: function(username) { $('pod-row').removeUserPod(username); }, /** * Displays a banner containing |message|. If the banner is already present * this function updates the message in the banner. This function is used * by the chrome.screenlockPrivate.showMessage API. * @param {string} message Text to be displayed */ showBannerMessage: function(message) { var banner = $('signin-banner'); banner.textContent = message; banner.classList.toggle('message-set', true); }, /** * Shows a custom icon in the user pod of |username|. This function * is used by the chrome.screenlockPrivate API. * @param {string} username Username of pod to add button * @param {!{id: !string, * hardlockOnClick: boolean, * isTrialRun: boolean, * tooltip: ({text: string, autoshow: boolean} | undefined)}} icon * The icon parameters. */ showUserPodCustomIcon: function(username, icon) { $('pod-row').showUserPodCustomIcon(username, icon); }, /** * Hides the custom icon in the user pod of |username| added by * showUserPodCustomIcon(). This function is used by the * chrome.screenlockPrivate API. * @param {string} username Username of pod to remove button */ hideUserPodCustomIcon: function(username) { $('pod-row').hideUserPodCustomIcon(username); }, /** * Sets the authentication type used to authenticate the user. * @param {string} username Username of selected user * @param {number} authType Authentication type, must be a valid value in * the AUTH_TYPE enum in user_pod_row.js. * @param {string} value The initial value to use for authentication. */ setAuthType: function(username, authType, value) { $('pod-row').setAuthType(username, authType, value); }, /** * Sets the state of touch view mode. * @param {boolean} isTouchViewEnabled true if the mode is on. */ setTouchViewState: function(isTouchViewEnabled) { $('pod-row').setTouchViewState(isTouchViewEnabled); }, /** * Removes the PIN keyboard so the user can no longer enter a PIN. * @param {!user} user The user who can no longer enter a PIN. */ disablePinKeyboardForUser: function(user) { $('pod-row').removePinKeyboard(user); }, /** * Updates the display name shown on a public session pod. * @param {string} userID The user ID of the public session * @param {string} displayName The new display name */ setPublicSessionDisplayName: function(userID, displayName) { $('pod-row').setPublicSessionDisplayName(userID, displayName); }, /** * Updates the list of locales available for a public session. * @param {string} userID The user ID of the public session * @param {!Object} locales The list of available locales * @param {string} defaultLocale The locale to select by default * @param {boolean} multipleRecommendedLocales Whether |locales| contains * two or more recommended locales */ setPublicSessionLocales: function(userID, locales, defaultLocale, multipleRecommendedLocales) { $('pod-row').setPublicSessionLocales(userID, locales, defaultLocale, multipleRecommendedLocales); }, /** * Updates the list of available keyboard layouts for a public session pod. * @param {string} userID The user ID of the public session * @param {string} locale The locale to which this list of keyboard layouts * applies * @param {!Object} list List of available keyboard layouts */ setPublicSessionKeyboardLayouts: function(userID, locale, list) { $('pod-row').setPublicSessionKeyboardLayouts(userID, locale, list); } }; }); // // Copyright 2014 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. /** * @fileoverview User pod row implementation. */ cr.define('login', function() { /** * Number of displayed columns depending on user pod count. * @type {Array} * @const */ var COLUMNS = [0, 1, 2, 3, 4, 5, 4, 4, 4, 5, 5, 6, 6, 5, 5, 6, 6, 6, 6]; /** * Mapping between number of columns in pod-row and margin between user pods * for such layout. * @type {Array} * @const */ var MARGIN_BY_COLUMNS = [undefined, 40, 40, 40, 40, 40, 12]; /** * Mapping between number of columns in the desktop pod-row and margin * between user pods for such layout. * @type {Array} * @const */ var DESKTOP_MARGIN_BY_COLUMNS = [undefined, 32, 32, 32, 32, 32, 32]; /** * Maximal number of columns currently supported by pod-row. * @type {number} * @const */ var MAX_NUMBER_OF_COLUMNS = 6; /** * Maximal number of rows if sign-in banner is displayed alonside. * @type {number} * @const */ var MAX_NUMBER_OF_ROWS_UNDER_SIGNIN_BANNER = 2; /** * Variables used for pod placement processing. Width and height should be * synced with computed CSS sizes of pods. */ var CROS_POD_WIDTH = 180; var DESKTOP_POD_WIDTH = 180; var MD_DESKTOP_POD_WIDTH = 160; var PUBLIC_EXPANDED_BASIC_WIDTH = 500; var PUBLIC_EXPANDED_ADVANCED_WIDTH = 610; var CROS_POD_HEIGHT = 213; var DESKTOP_POD_HEIGHT = 226; var MD_DESKTOP_POD_HEIGHT = 200; var POD_ROW_PADDING = 10; var DESKTOP_ROW_PADDING = 32; var CUSTOM_ICON_CONTAINER_SIZE = 40; var CROS_PIN_POD_HEIGHT = 417; /** * Minimal padding between user pod and virtual keyboard. * @type {number} * @const */ var USER_POD_KEYBOARD_MIN_PADDING = 20; /** * Maximum time for which the pod row remains hidden until all user images * have been loaded. * @type {number} * @const */ var POD_ROW_IMAGES_LOAD_TIMEOUT_MS = 3000; /** * Public session help topic identifier. * @type {number} * @const */ var HELP_TOPIC_PUBLIC_SESSION = 3041033; /** * Tab order for user pods. Update these when adding new controls. * @enum {number} * @const */ var UserPodTabOrder = { POD_INPUT: 1, // Password input field, Action box menu button, submit // button next to password input field and the pod // itself. PIN_KEYBOARD: 2, // Pin keyboard below the password input field. POD_CUSTOM_ICON: 3, // Pod custom icon next to password input field. HEADER_BAR: 4, // Buttons on the header bar (Shutdown, Add User). POD_MENU_ITEM: 5 // User pad menu items (User info, Remove user). }; /** * Supported authentication types. Keep in sync with the enum in * chrome/browser/signin/screenlock_bridge.h * @enum {number} * @const */ var AUTH_TYPE = { OFFLINE_PASSWORD: 0, ONLINE_SIGN_IN: 1, NUMERIC_PIN: 2, USER_CLICK: 3, EXPAND_THEN_USER_CLICK: 4, FORCE_OFFLINE_PASSWORD: 5 }; /** * Names of authentication types. */ var AUTH_TYPE_NAMES = { 0: 'offlinePassword', 1: 'onlineSignIn', 2: 'numericPin', 3: 'userClick', 4: 'expandThenUserClick', 5: 'forceOfflinePassword' }; // Focus and tab order are organized as follows: // // (1) all user pods have tab index 1 so they are traversed first; // (2) when a user pod is activated, its tab index is set to -1 and its // main input field gets focus and tab index 1; // (3) if user pod custom icon is interactive, it has tab index 2 so it // follows the input. // (4) buttons on the header bar have tab index 3 so they follow the custom // icon, or user pod if custom icon is not interactive; // (5) Action box buttons have tab index 4 and follow header bar buttons; // (6) lastly, focus jumps to the Status Area and back to user pods. // // 'Focus' event is handled by a capture handler for the whole document // and in some cases 'mousedown' event handlers are used instead of 'click' // handlers where it's necessary to prevent 'focus' event from being fired. /** * Helper function to remove a class from given element. * @param {!HTMLElement} el Element whose class list to change. * @param {string} cl Class to remove. */ function removeClass(el, cl) { el.classList.remove(cl); } /** * Creates a user pod. * @constructor * @extends {HTMLDivElement} */ var UserPod = cr.ui.define(function() { var node = $('user-pod-template').cloneNode(true); node.removeAttribute('id'); return node; }); /** * Stops event propagation from the any user pod child element. * @param {Event} e Event to handle. */ function stopEventPropagation(e) { // Prevent default so that we don't trigger a 'focus' event. e.preventDefault(); e.stopPropagation(); } /** * Creates an element for custom icon shown in a user pod next to the input * field. * @constructor * @extends {HTMLDivElement} */ var UserPodCustomIcon = cr.ui.define(function() { var node = document.createElement('div'); node.classList.add('custom-icon-container'); node.hidden = true; // Create the actual icon element and add it as a child to the container. var iconNode = document.createElement('div'); iconNode.classList.add('custom-icon'); node.appendChild(iconNode); return node; }); /** * The supported user pod custom icons. * {@code id} properties should be in sync with values set by C++ side. * {@code class} properties are CSS classes used to set the icons' background. * @const {Array<{id: !string, class: !string}>} */ UserPodCustomIcon.ICONS = [ {id: 'locked', class: 'custom-icon-locked'}, {id: 'locked-to-be-activated', class: 'custom-icon-locked-to-be-activated'}, {id: 'locked-with-proximity-hint', class: 'custom-icon-locked-with-proximity-hint'}, {id: 'unlocked', class: 'custom-icon-unlocked'}, {id: 'hardlocked', class: 'custom-icon-hardlocked'}, {id: 'spinner', class: 'custom-icon-spinner'} ]; /** * The hover state for the icon. When user hovers over the icon, a tooltip * should be shown after a short delay. This enum is used to keep track of * the tooltip status related to hover state. * @enum {string} */ UserPodCustomIcon.HoverState = { /** The user is not hovering over the icon. */ NO_HOVER: 'no_hover', /** The user is hovering over the icon but the tooltip is not activated. */ HOVER: 'hover', /** * User is hovering over the icon and the tooltip is activated due to the * hover state (which happens with delay after user starts hovering). */ HOVER_TOOLTIP: 'hover_tooltip' }; /** * If the icon has a tooltip that should be automatically shown, the tooltip * is shown even when there is no user action (i.e. user is not hovering over * the icon), after a short delay. The tooltip should be hidden after some * time. Note that the icon will not be considered autoshown if it was * previously shown as a result of the user action. * This enum is used to keep track of this state. * @enum {string} */ UserPodCustomIcon.TooltipAutoshowState = { /** The tooltip should not be or was not automatically shown. */ DISABLED: 'disabled', /** * The tooltip should be automatically shown, but the timeout for showing * the tooltip has not yet passed. */ ENABLED: 'enabled', /** The tooltip was automatically shown. */ ACTIVE : 'active' }; UserPodCustomIcon.prototype = { __proto__: HTMLDivElement.prototype, /** * The id of the icon being shown. * @type {string} * @private */ iconId_: '', /** * A reference to the timeout for updating icon hover state. Non-null * only if there is an active timeout. * @type {?number} * @private */ updateHoverStateTimeout_: null, /** * A reference to the timeout for updating icon tooltip autoshow state. * Non-null only if there is an active timeout. * @type {?number} * @private */ updateTooltipAutoshowStateTimeout_: null, /** * Callback for click and 'Enter' key events that gets set if the icon is * interactive. * @type {?function()} * @private */ actionHandler_: null, /** * The current tooltip state. * @type {{active: function(): boolean, * autoshow: !UserPodCustomIcon.TooltipAutoshowState, * hover: !UserPodCustomIcon.HoverState, * text: string}} * @private */ tooltipState_: { /** * Utility method for determining whether the tooltip is active, either as * a result of hover state or being autoshown. * @return {boolean} */ active: function() { return this.autoshow == UserPodCustomIcon.TooltipAutoshowState.ACTIVE || this.hover == UserPodCustomIcon.HoverState.HOVER_TOOLTIP; }, /** * @type {!UserPodCustomIcon.TooltipAutoshowState} */ autoshow: UserPodCustomIcon.TooltipAutoshowState.DISABLED, /** * @type {!UserPodCustomIcon.HoverState} */ hover: UserPodCustomIcon.HoverState.NO_HOVER, /** * The tooltip text. * @type {string} */ text: '' }, /** @override */ decorate: function() { this.iconElement.addEventListener( 'mouseover', this.updateHoverState_.bind(this, UserPodCustomIcon.HoverState.HOVER)); this.iconElement.addEventListener( 'mouseout', this.updateHoverState_.bind(this, UserPodCustomIcon.HoverState.NO_HOVER)); this.iconElement.addEventListener('mousedown', this.handleMouseDown_.bind(this)); this.iconElement.addEventListener('click', this.handleClick_.bind(this)); this.iconElement.addEventListener('keydown', this.handleKeyDown_.bind(this)); // When the icon is focused using mouse, there should be no outline shown. // Preventing default mousedown event accomplishes this. this.iconElement.addEventListener('mousedown', function(e) { e.preventDefault(); }); }, /** * Getter for the icon element's div. * @return {HTMLDivElement} */ get iconElement() { return this.querySelector('.custom-icon'); }, /** * Updates the icon element class list to properly represent the provided * icon. * @param {!string} id The id of the icon that should be shown. Should be * one of the ids listed in {@code UserPodCustomIcon.ICONS}. */ setIcon: function(id) { this.iconId_ = id; UserPodCustomIcon.ICONS.forEach(function(icon) { this.iconElement.classList.toggle(icon.class, id == icon.id); }, this); }, /** * Sets the ARIA label for the icon. * @param {!string} ariaLabel */ setAriaLabel: function(ariaLabel) { this.iconElement.setAttribute('aria-label', ariaLabel); }, /** * Shows the icon. */ show: function() { // Show the icon if the current iconId is valid. var validIcon = false; UserPodCustomIcon.ICONS.forEach(function(icon) { validIcon = validIcon || this.iconId_ == icon.id; }, this); this.hidden = validIcon ? false : true; }, /** * Updates the icon tooltip. If {@code autoshow} parameter is set the * tooltip is immediatelly shown. If tooltip text is not set, the method * ensures the tooltip gets hidden. If tooltip is shown prior to this call, * it remains shown, but the tooltip text is updated. * @param {!{text: string, autoshow: boolean}} tooltip The tooltip * parameters. */ setTooltip: function(tooltip) { this.iconElement.classList.toggle('icon-with-tooltip', !!tooltip.text); this.updateTooltipAutoshowState_( tooltip.autoshow ? UserPodCustomIcon.TooltipAutoshowState.ENABLED : UserPodCustomIcon.TooltipAutoshowState.DISABLED); this.tooltipState_.text = tooltip.text; this.updateTooltip_(); }, /** * Sets up icon tabIndex attribute and handler for click and 'Enter' key * down events. * @param {?function()} callback If icon should be interactive, the * function to get called on click and 'Enter' key down events. Should * be null to make the icon non interactive. */ setInteractive: function(callback) { this.iconElement.classList.toggle('interactive-custom-icon', !!callback); // Update tabIndex property if needed. if (!!this.actionHandler_ != !!callback) { if (callback) { this.iconElement.setAttribute('tabIndex', UserPodTabOrder.POD_CUSTOM_ICON); } else { this.iconElement.removeAttribute('tabIndex'); } } // Set the new action handler. this.actionHandler_ = callback; }, /** * Hides the icon and cleans its state. */ hide: function() { this.hideTooltip_(); this.clearUpdateHoverStateTimeout_(); this.clearUpdateTooltipAutoshowStateTimeout_(); this.setInteractive(null); this.hidden = true; }, /** * Clears timeout for showing a tooltip if one is set. Used to cancel * showing the tooltip when the user starts typing the password. */ cancelDelayedTooltipShow: function() { this.updateTooltipAutoshowState_( UserPodCustomIcon.TooltipAutoshowState.DISABLED); this.clearUpdateHoverStateTimeout_(); }, /** * Handles mouse down event in the icon element. * @param {Event} e The mouse down event. * @private */ handleMouseDown_: function(e) { this.updateHoverState_(UserPodCustomIcon.HoverState.NO_HOVER); this.updateTooltipAutoshowState_( UserPodCustomIcon.TooltipAutoshowState.DISABLED); // Stop the event propagation so in the case the click ends up on the // user pod (outside the custom icon) auth is not attempted. stopEventPropagation(e); }, /** * Handles click event on the icon element. No-op if * {@code this.actionHandler_} is not set. * @param {Event} e The click event. * @private */ handleClick_: function(e) { if (!this.actionHandler_) return; this.actionHandler_(); stopEventPropagation(e); }, /** * Handles key down event on the icon element. Only 'Enter' key is handled. * No-op if {@code this.actionHandler_} is not set. * @param {Event} e The key down event. * @private */ handleKeyDown_: function(e) { if (!this.actionHandler_ || e.key != 'Enter') return; this.actionHandler_(e); stopEventPropagation(e); }, /** * Changes the tooltip hover state and updates tooltip visibility if needed. * @param {!UserPodCustomIcon.HoverState} state * @private */ updateHoverState_: function(state) { this.clearUpdateHoverStateTimeout_(); this.sanitizeTooltipStateIfBubbleHidden_(); if (state == UserPodCustomIcon.HoverState.HOVER) { if (this.tooltipState_.active()) { this.tooltipState_.hover = UserPodCustomIcon.HoverState.HOVER_TOOLTIP; } else { this.updateHoverStateSoon_( UserPodCustomIcon.HoverState.HOVER_TOOLTIP); } return; } if (state != UserPodCustomIcon.HoverState.NO_HOVER && state != UserPodCustomIcon.HoverState.HOVER_TOOLTIP) { console.error('Invalid hover state ' + state); return; } this.tooltipState_.hover = state; this.updateTooltip_(); }, /** * Sets up a timeout for updating icon hover state. * @param {!UserPodCustomIcon.HoverState} state * @private */ updateHoverStateSoon_: function(state) { if (this.updateHoverStateTimeout_) clearTimeout(this.updateHoverStateTimeout_); this.updateHoverStateTimeout_ = setTimeout(this.updateHoverState_.bind(this, state), 1000); }, /** * Clears a timeout for updating icon hover state if there is one set. * @private */ clearUpdateHoverStateTimeout_: function() { if (this.updateHoverStateTimeout_) { clearTimeout(this.updateHoverStateTimeout_); this.updateHoverStateTimeout_ = null; } }, /** * Changes the tooltip autoshow state and changes tooltip visibility if * needed. * @param {!UserPodCustomIcon.TooltipAutoshowState} state * @private */ updateTooltipAutoshowState_: function(state) { this.clearUpdateTooltipAutoshowStateTimeout_(); this.sanitizeTooltipStateIfBubbleHidden_(); if (state == UserPodCustomIcon.TooltipAutoshowState.DISABLED) { if (this.tooltipState_.autoshow != state) { this.tooltipState_.autoshow = state; this.updateTooltip_(); } return; } if (this.tooltipState_.active()) { if (this.tooltipState_.autoshow != UserPodCustomIcon.TooltipAutoshowState.ACTIVE) { this.tooltipState_.autoshow = UserPodCustomIcon.TooltipAutoshowState.DISABLED; } else { // If the tooltip is already automatically shown, the timeout for // removing it should be reset. this.updateTooltipAutoshowStateSoon_( UserPodCustomIcon.TooltipAutoshowState.DISABLED); } return; } if (state == UserPodCustomIcon.TooltipAutoshowState.ENABLED) { this.updateTooltipAutoshowStateSoon_( UserPodCustomIcon.TooltipAutoshowState.ACTIVE); } else if (state == UserPodCustomIcon.TooltipAutoshowState.ACTIVE) { this.updateTooltipAutoshowStateSoon_( UserPodCustomIcon.TooltipAutoshowState.DISABLED); } this.tooltipState_.autoshow = state; this.updateTooltip_(); }, /** * Sets up a timeout for updating tooltip autoshow state. * @param {!UserPodCustomIcon.TooltipAutoshowState} state * @private */ updateTooltipAutoshowStateSoon_: function(state) { if (this.updateTooltipAutoshowStateTimeout_) clearTimeout(this.updateTooltupAutoshowStateTimeout_); var timeout = state == UserPodCustomIcon.TooltipAutoshowState.DISABLED ? 5000 : 1000; this.updateTooltipAutoshowStateTimeout_ = setTimeout(this.updateTooltipAutoshowState_.bind(this, state), timeout); }, /** * Clears the timeout for updating tooltip autoshow state if one is set. * @private */ clearUpdateTooltipAutoshowStateTimeout_: function() { if (this.updateTooltipAutoshowStateTimeout_) { clearTimeout(this.updateTooltipAutoshowStateTimeout_); this.updateTooltipAutoshowStateTimeout_ = null; } }, /** * If tooltip bubble is hidden, this makes sure that hover and tooltip * autoshow states are not the ones that imply an active tooltip. * Used to handle a case where the tooltip bubble is hidden by an event that * does not update one of the states (e.g. click outside the pod will not * update tooltip autoshow state). Should be called before making * tooltip state updates. * @private */ sanitizeTooltipStateIfBubbleHidden_: function() { if (!$('bubble').hidden) return; if (this.tooltipState_.hover == UserPodCustomIcon.HoverState.HOVER_TOOLTIP && this.tooltipState_.text) { this.tooltipState_.hover = UserPodCustomIcon.HoverState.NO_HOVER; this.clearUpdateHoverStateTimeout_(); } if (this.tooltipState_.autoshow == UserPodCustomIcon.TooltipAutoshowState.ACTIVE) { this.tooltipState_.autoshow = UserPodCustomIcon.TooltipAutoshowState.DISABLED; this.clearUpdateTooltipAutoshowStateTimeout_(); } }, /** * Returns whether the user pod to which the custom icon belongs is focused. * @return {boolean} * @private */ isParentPodFocused_: function() { if ($('account-picker').hidden) return false; var parentPod = this.parentNode; while (parentPod && !parentPod.classList.contains('pod')) parentPod = parentPod.parentNode; return parentPod && parentPod.parentNode.isFocused(parentPod); }, /** * Depending on {@code this.tooltipState_}, it updates tooltip visibility * and text. * @private */ updateTooltip_: function() { if (this.hidden || !this.isParentPodFocused_()) return; if (!this.tooltipState_.active() || !this.tooltipState_.text) { this.hideTooltip_(); return; } // Show the tooltip bubble. var bubbleContent = document.createElement('div'); bubbleContent.textContent = this.tooltipState_.text; /** @const */ var BUBBLE_OFFSET = CUSTOM_ICON_CONTAINER_SIZE / 2; // TODO(tengs): Introduce a special reauth state for the account picker, // instead of showing the tooltip bubble here (crbug.com/409427). /** @const */ var BUBBLE_PADDING = 8 + (this.iconId_ ? 0 : 23); $('bubble').showContentForElement(this, cr.ui.Bubble.Attachment.RIGHT, bubbleContent, BUBBLE_OFFSET, BUBBLE_PADDING); }, /** * Hides the tooltip. * @private */ hideTooltip_: function() { $('bubble').hideForElement(this); } }; /** * Unique salt added to user image URLs to prevent caching. Dictionary with * user names as keys. * @type {Object} */ UserPod.userImageSalt_ = {}; UserPod.prototype = { __proto__: HTMLDivElement.prototype, /** * Whether click on the pod can issue a user click auth attempt. The * attempt can be issued iff the pod was focused when the click * started (i.e. on mouse down event). * @type {boolean} * @private */ userClickAuthAllowed_: false, /** @override */ decorate: function() { this.tabIndex = UserPodTabOrder.POD_INPUT; this.actionBoxAreaElement.tabIndex = UserPodTabOrder.POD_INPUT; this.addEventListener('keydown', this.handlePodKeyDown_.bind(this)); this.addEventListener('click', this.handleClickOnPod_.bind(this)); this.addEventListener('mousedown', this.handlePodMouseDown_.bind(this)); if (this.pinKeyboard) { this.pinKeyboard.passwordElement = this.passwordElement; this.pinKeyboard.addEventListener('pin-change', this.handleInputChanged_.bind(this)); this.pinKeyboard.tabIndex = UserPodTabOrder.PIN_KEYBOARD; } this.actionBoxAreaElement.addEventListener('mousedown', stopEventPropagation); this.actionBoxAreaElement.addEventListener('click', this.handleActionAreaButtonClick_.bind(this)); this.actionBoxAreaElement.addEventListener('keydown', this.handleActionAreaButtonKeyDown_.bind(this)); this.actionBoxMenuTitleElement.addEventListener('keydown', this.handleMenuTitleElementKeyDown_.bind(this)); this.actionBoxMenuTitleElement.addEventListener('blur', this.handleMenuTitleElementBlur_.bind(this)); this.actionBoxMenuRemoveElement.addEventListener('click', this.handleRemoveCommandClick_.bind(this)); this.actionBoxMenuRemoveElement.addEventListener('keydown', this.handleRemoveCommandKeyDown_.bind(this)); this.actionBoxMenuRemoveElement.addEventListener('blur', this.handleRemoveCommandBlur_.bind(this)); this.actionBoxRemoveUserWarningButtonElement.addEventListener('click', this.handleRemoveUserConfirmationClick_.bind(this)); this.actionBoxRemoveUserWarningButtonElement.addEventListener('keydown', this.handleRemoveUserConfirmationKeyDown_.bind(this)); var customIcon = this.customIconElement; customIcon.parentNode.replaceChild(new UserPodCustomIcon(), customIcon); }, /** * Initializes the pod after its properties set and added to a pod row. */ initialize: function() { this.passwordElement.addEventListener('keydown', this.parentNode.handleKeyDown.bind(this.parentNode)); this.passwordElement.addEventListener('keypress', this.handlePasswordKeyPress_.bind(this)); this.passwordElement.addEventListener('input', this.handleInputChanged_.bind(this)); this.passwordElement.addEventListener('mouseup', this.handleInputMouseUp_.bind(this)); if (this.submitButton) { this.submitButton.addEventListener('click', this.handleSubmitButtonClick_.bind(this)); this.submitButton.tabIndex = UserPodTabOrder.POD_INPUT; } this.imageElement.addEventListener('load', this.parentNode.handlePodImageLoad.bind(this.parentNode, this)); var initialAuthType = this.user.initialAuthType || AUTH_TYPE.OFFLINE_PASSWORD; this.setAuthType(initialAuthType, null); this.userClickAuthAllowed_ = false; // Lazy load the assets needed for the polymer submit button. var isLockScreen = (Oobe.getInstance().displayType == DISPLAY_TYPE.LOCK); if (cr.isChromeOS && isLockScreen && !cr.ui.login.ResourceLoader.alreadyLoadedAssets( 'custom-elements-user-pod')) { cr.ui.login.ResourceLoader.registerAssets({ id: 'custom-elements-user-pod', html: [{ url: 'custom_elements_user_pod.html' }] }); cr.ui.login.ResourceLoader.loadAssetsOnIdle('custom-elements-user-pod'); } }, /** * Whether the user pod is disabled. * @type {boolean} */ disabled_: false, get disabled() { return this.disabled_; }, set disabled(value) { this.disabled_ = value; this.querySelectorAll('button,input').forEach(function(element) { element.disabled = value }); // Special handling for submit button - the submit button should be // enabled only if there is the password value set. var submitButton = this.submitButton; if (submitButton) submitButton.disabled = value || !this.passwordElement.value; }, /** * Resets tab order for pod elements to its initial state. */ resetTabOrder: function() { // Note: the |mainInput| can be the pod itself. this.mainInput.tabIndex = -1; this.tabIndex = UserPodTabOrder.POD_INPUT; }, /** * Handles keypress event (i.e. any textual input) on password input. * @param {Event} e Keypress Event object. * @private */ handlePasswordKeyPress_: function(e) { // When tabbing from the system tray a tab key press is received. Suppress // this so as not to type a tab character into the password field. if (e.keyCode == 9) { e.preventDefault(); return; } this.customIconElement.cancelDelayedTooltipShow(); }, /** * Handles a click event on submit button. * @param {Event} e Click event. */ handleSubmitButtonClick_: function(e) { this.parentNode.setActivatedPod(this, e); }, /** * Top edge margin number of pixels. * @type {?number} */ set top(top) { this.style.top = cr.ui.toCssPx(top); }, /** * Top edge margin number of pixels. */ get top() { return parseInt(this.style.top); }, /** * Left edge margin number of pixels. * @type {?number} */ set left(left) { this.style.left = cr.ui.toCssPx(left); }, /** * Left edge margin number of pixels. */ get left() { return parseInt(this.style.left); }, /** * Height number of pixels. */ get height() { return this.offsetHeight; }, /** * Gets image element. * @type {!HTMLImageElement} */ get imageElement() { return this.querySelector('.user-image'); }, /** * Gets name element. * @type {!HTMLDivElement} */ get nameElement() { return this.querySelector('.name'); }, /** * Gets reauth name hint element. * @type {!HTMLDivElement} */ get reauthNameHintElement() { return this.querySelector('.reauth-name-hint'); }, /** * Gets the container holding the password field. * @type {!HTMLInputElement} */ get passwordEntryContainerElement() { return this.querySelector('.password-entry-container'); }, /** * Gets password field. * @type {!HTMLInputElement} */ get passwordElement() { return this.querySelector('.password'); }, /** * Gets submit button. * @type {!HTMLInputElement} */ get submitButton() { return this.querySelector('.submit-button'); }, /** * Gets the password label, which is used to show a message where the * password field is normally. * @type {!HTMLInputElement} */ get passwordLabelElement() { return this.querySelector('.password-label'); }, get pinContainer() { return this.querySelector('.pin-container'); }, /** * Gets the pin-keyboard of the pod. * @type {!HTMLElement} */ get pinKeyboard() { return this.querySelector('pin-keyboard'); }, /** * Gets user online sign in hint element. * @type {!HTMLDivElement} */ get reauthWarningElement() { return this.querySelector('.reauth-hint-container'); }, /** * Gets the container holding the launch app button. * @type {!HTMLButtonElement} */ get launchAppButtonContainerElement() { return this.querySelector('.launch-app-button-container'); }, /** * Gets launch app button. * @type {!HTMLButtonElement} */ get launchAppButtonElement() { return this.querySelector('.launch-app-button'); }, /** * Gets action box area. * @type {!HTMLInputElement} */ get actionBoxAreaElement() { return this.querySelector('.action-box-area'); }, /** * Gets user type icon area. * @type {!HTMLDivElement} */ get userTypeIconAreaElement() { return this.querySelector('.user-type-icon-area'); }, /** * Gets user type bubble like multi-profiles policy restriction message. * @type {!HTMLDivElement} */ get userTypeBubbleElement() { return this.querySelector('.user-type-bubble'); }, /** * Gets action box menu. * @type {!HTMLDivElement} */ get actionBoxMenu() { return this.querySelector('.action-box-menu'); }, /** * Gets action box menu title (user name and email). * @type {!HTMLDivElement} */ get actionBoxMenuTitleElement() { return this.querySelector('.action-box-menu-title'); }, /** * Gets action box menu title, user name item. * @type {!HTMLSpanElement} */ get actionBoxMenuTitleNameElement() { return this.querySelector('.action-box-menu-title-name'); }, /** * Gets action box menu title, user email item. * @type {!HTMLSpanElement} */ get actionBoxMenuTitleEmailElement() { return this.querySelector('.action-box-menu-title-email'); }, /** * Gets action box menu, remove user command item. * @type {!HTMLInputElement} */ get actionBoxMenuCommandElement() { return this.querySelector('.action-box-menu-remove-command'); }, /** * Gets action box menu, remove user command item div. * @type {!HTMLInputElement} */ get actionBoxMenuRemoveElement() { return this.querySelector('.action-box-menu-remove'); }, /** * Gets action box menu, remove user command item div. * @type {!HTMLInputElement} */ get actionBoxRemoveUserWarningElement() { return this.querySelector('.action-box-remove-user-warning'); }, /** * Gets action box menu, remove user command item div. * @type {!HTMLInputElement} */ get actionBoxRemoveUserWarningButtonElement() { return this.querySelector('.remove-warning-button'); }, /** * Gets the custom icon. This icon is normally hidden, but can be shown * using the chrome.screenlockPrivate API. * @type {!HTMLDivElement} */ get customIconElement() { return this.querySelector('.custom-icon-container'); }, /** * Gets the elements used for statistics display. * @type {Object.} */ get statsMapElements() { return { 'BrowsingHistory': this.querySelector('.action-box-remove-user-warning-history'), 'Passwords': this.querySelector('.action-box-remove-user-warning-passwords'), 'Bookmarks': this.querySelector('.action-box-remove-user-warning-bookmarks'), 'Settings': this.querySelector('.action-box-remove-user-warning-settings') } }, /** * Updates the user pod element. */ update: function() { this.imageElement.src = 'chrome://userimage/' + this.user.username + '?id=' + UserPod.userImageSalt_[this.user.username]; this.nameElement.textContent = this.user_.displayName; this.reauthNameHintElement.textContent = this.user_.displayName; this.classList.toggle('signed-in', this.user_.signedIn); if (this.isAuthTypeUserClick) this.passwordLabelElement.textContent = this.authValue; this.updateActionBoxArea(); this.passwordElement.setAttribute('aria-label', loadTimeData.getStringF( 'passwordFieldAccessibleName', this.user_.emailAddress)); this.customizeUserPodPerUserType(); }, updateActionBoxArea: function() { if (this.user_.publicAccount || this.user_.isApp) { this.actionBoxAreaElement.hidden = true; return; } this.actionBoxMenuRemoveElement.hidden = !this.user_.canRemove; this.actionBoxAreaElement.setAttribute( 'aria-label', loadTimeData.getStringF( 'podMenuButtonAccessibleName', this.user_.emailAddress)); this.actionBoxMenuRemoveElement.setAttribute( 'aria-label', loadTimeData.getString( 'podMenuRemoveItemAccessibleName')); this.actionBoxMenuTitleNameElement.textContent = this.user_.isOwner ? loadTimeData.getStringF('ownerUserPattern', this.user_.displayName) : this.user_.displayName; this.actionBoxMenuTitleEmailElement.textContent = this.user_.emailAddress; this.actionBoxMenuTitleEmailElement.hidden = this.user_.legacySupervisedUser; this.actionBoxMenuCommandElement.textContent = loadTimeData.getString('removeUser'); }, customizeUserPodPerUserType: function() { if (this.user_.childUser && !this.user_.isDesktopUser) { this.setUserPodIconType('child'); } else if (this.user_.legacySupervisedUser && !this.user_.isDesktopUser) { this.setUserPodIconType('legacySupervised'); this.classList.add('legacy-supervised'); } else if (this.multiProfilesPolicyApplied) { // Mark user pod as not focusable which in addition to the grayed out // filter makes it look in disabled state. this.classList.add('multiprofiles-policy-applied'); this.setUserPodIconType('policy'); if (this.user.multiProfilesPolicy == 'primary-only') this.querySelector('.mp-policy-primary-only-msg').hidden = false; else if (this.user.multiProfilesPolicy == 'owner-primary-only') this.querySelector('.mp-owner-primary-only-msg').hidden = false; else this.querySelector('.mp-policy-not-allowed-msg').hidden = false; } else if (this.user_.isApp) { this.setUserPodIconType('app'); } }, isPinReady: function() { return this.pinKeyboard && this.pinKeyboard.offsetHeight > 0; }, set showError(visible) { if (this.submitButton) this.submitButton.classList.toggle('error-shown', visible); }, toggleTransitions: function(enable) { this.classList.toggle('flying-pin-pod', enable); }, updatePinClass_: function(element, enable) { element.classList.toggle('pin-enabled', enable); element.classList.toggle('pin-disabled', !enable); }, setPinVisibility: function(visible) { if (this.isPinShown() == visible) return; // Do not show pin if virtual keyboard is there. if (visible && Oobe.getInstance().virtualKeyboardShown) return; var elements = this.getElementsByClassName('pin-tag'); for (var i = 0; i < elements.length; ++i) this.updatePinClass_(elements[i], visible); this.updatePinClass_(this, visible); // Set the focus to the input element after showing/hiding pin keyboard. this.mainInput.focus(); // Change the password placeholder based on pin keyboard visibility. this.passwordElement.placeholder = loadTimeData.getString(visible ? 'pinKeyboardPlaceholderPinPassword' : 'passwordHint'); chrome.send('setForceDisableVirtualKeyboard', [visible]); }, isPinShown: function() { return this.classList.contains('pin-enabled'); }, setUserPodIconType: function(userTypeClass) { this.userTypeIconAreaElement.classList.add(userTypeClass); this.userTypeIconAreaElement.hidden = false; }, /** * The user that this pod represents. * @type {!Object} */ user_: undefined, get user() { return this.user_; }, set user(userDict) { this.user_ = userDict; this.update(); }, /** * Returns true if multi-profiles sign in is currently active and this * user pod is restricted per policy. * @type {boolean} */ get multiProfilesPolicyApplied() { var isMultiProfilesUI = (Oobe.getInstance().displayType == DISPLAY_TYPE.USER_ADDING); return isMultiProfilesUI && !this.user_.isMultiProfilesAllowed; }, /** * Gets main input element. * @type {(HTMLButtonElement|HTMLInputElement)} */ get mainInput() { if (this.isAuthTypePassword) { return this.passwordElement; } else if (this.isAuthTypeOnlineSignIn) { return this; } else if (this.isAuthTypeUserClick) { return this.passwordLabelElement; } }, /** * Whether action box button is in active state. * @type {boolean} */ get isActionBoxMenuActive() { return this.actionBoxAreaElement.classList.contains('active'); }, set isActionBoxMenuActive(active) { if (active == this.isActionBoxMenuActive) return; if (active) { this.actionBoxMenuRemoveElement.hidden = !this.user_.canRemove; this.actionBoxRemoveUserWarningElement.hidden = true; // Clear focus first if another pod is focused. if (!this.parentNode.isFocused(this)) { this.parentNode.focusPod(undefined, true); this.actionBoxAreaElement.focus(); } // Hide user-type-bubble. this.userTypeBubbleElement.classList.remove('bubble-shown'); this.actionBoxAreaElement.classList.add('active'); // Invisible focus causes ChromeVox to read user name and email. this.actionBoxMenuTitleElement.tabIndex = UserPodTabOrder.POD_MENU_ITEM; this.actionBoxMenuTitleElement.focus(); // If the user pod is on either edge of the screen, then the menu // could be displayed partially ofscreen. this.actionBoxMenu.classList.remove('left-edge-offset'); this.actionBoxMenu.classList.remove('right-edge-offset'); var offsetLeft = cr.ui.login.DisplayManager.getOffset(this.actionBoxMenu).left; var menuWidth = this.actionBoxMenu.offsetWidth; if (offsetLeft < 0) this.actionBoxMenu.classList.add('left-edge-offset'); else if (offsetLeft + menuWidth > window.innerWidth) this.actionBoxMenu.classList.add('right-edge-offset'); } else { this.actionBoxAreaElement.classList.remove('active'); this.actionBoxAreaElement.classList.remove('menu-moved-up'); this.actionBoxMenu.classList.remove('menu-moved-up'); } }, /** * Whether action box button is in hovered state. * @type {boolean} */ get isActionBoxMenuHovered() { return this.actionBoxAreaElement.classList.contains('hovered'); }, set isActionBoxMenuHovered(hovered) { if (hovered == this.isActionBoxMenuHovered) return; if (hovered) { this.actionBoxAreaElement.classList.add('hovered'); this.classList.add('hovered'); } else { if (this.multiProfilesPolicyApplied) this.userTypeBubbleElement.classList.remove('bubble-shown'); this.actionBoxAreaElement.classList.remove('hovered'); this.classList.remove('hovered'); } }, /** * Set the authentication type for the pod. * @param {number} An auth type value defined in the AUTH_TYPE enum. * @param {string} authValue The initial value used for the auth type. */ setAuthType: function(authType, authValue) { this.authType_ = authType; this.authValue_ = authValue; this.setAttribute('auth-type', AUTH_TYPE_NAMES[this.authType_]); this.update(); this.reset(this.parentNode.isFocused(this)); }, /** * The auth type of the user pod. This value is one of the enum * values in AUTH_TYPE. * @type {number} */ get authType() { return this.authType_; }, /** * The initial value used for the pod's authentication type. * eg. a prepopulated password input when using password authentication. */ get authValue() { return this.authValue_; }, /** * True if the the user pod uses a password to authenticate. * @type {bool} */ get isAuthTypePassword() { return this.authType_ == AUTH_TYPE.OFFLINE_PASSWORD || this.authType_ == AUTH_TYPE.FORCE_OFFLINE_PASSWORD; }, /** * True if the the user pod uses a user click to authenticate. * @type {bool} */ get isAuthTypeUserClick() { return this.authType_ == AUTH_TYPE.USER_CLICK; }, /** * True if the the user pod uses a online sign in to authenticate. * @type {bool} */ get isAuthTypeOnlineSignIn() { return this.authType_ == AUTH_TYPE.ONLINE_SIGN_IN; }, /** * Updates the image element of the user. */ updateUserImage: function() { UserPod.userImageSalt_[this.user.username] = new Date().getTime(); this.update(); }, /** * Focuses on input element. */ focusInput: function() { // Move tabIndex from the whole pod to the main input. // Note: the |mainInput| can be the pod itself. this.tabIndex = -1; this.mainInput.tabIndex = UserPodTabOrder.POD_INPUT; this.mainInput.focus(); }, /** * Activates the pod. * @param {Event} e Event object. * @return {boolean} True if activated successfully. */ activate: function(e) { if (this.isAuthTypeOnlineSignIn) { this.showSigninUI(); } else if (this.isAuthTypeUserClick) { Oobe.disableSigninUI(); this.classList.toggle('signing-in', true); chrome.send('attemptUnlock', [this.user.username]); } else if (this.isAuthTypePassword) { var pinValue = this.pinKeyboard ? this.pinKeyboard.value : ''; var password = this.passwordElement.value || pinValue; if (!password) return false; Oobe.disableSigninUI(); chrome.send('authenticateUser', [this.user.username, password, this.isPinShown()]); } else { console.error('Activating user pod with invalid authentication type: ' + this.authType); } return true; }, showSupervisedUserSigninWarning: function() { // Legacy supervised user token has been invalidated. // Make sure that pod is focused i.e. "Sign in" button is seen. this.parentNode.focusPod(this); var error = document.createElement('div'); var messageDiv = document.createElement('div'); messageDiv.className = 'error-message-bubble'; messageDiv.textContent = loadTimeData.getString('supervisedUserExpiredTokenWarning'); error.appendChild(messageDiv); $('bubble').showContentForElement( this.reauthWarningElement, cr.ui.Bubble.Attachment.TOP, error, this.reauthWarningElement.offsetWidth / 2, 4); // Move warning bubble up if it overlaps the shelf. var maxHeight = cr.ui.LoginUITools.getMaxHeightBeforeShelfOverlapping($('bubble')); if (maxHeight < $('bubble').offsetHeight) { $('bubble').showContentForElement( this.reauthWarningElement, cr.ui.Bubble.Attachment.BOTTOM, error, this.reauthWarningElement.offsetWidth / 2, 4); } }, /** * Shows signin UI for this user. */ showSigninUI: function() { if (this.user.legacySupervisedUser && !this.user.isDesktopUser) { this.showSupervisedUserSigninWarning(); } else { // Special case for multi-profiles sign in. We show users even if they // are not allowed per policy. Restrict those users from starting GAIA. if (this.multiProfilesPolicyApplied) return; this.parentNode.showSigninUI(this.user.emailAddress); } }, /** * Resets the input field and updates the tab order of pod controls. * @param {boolean} takeFocus If true, input field takes focus. */ reset: function(takeFocus) { this.passwordElement.value = ''; if (this.pinKeyboard) this.pinKeyboard.value = ''; this.updateInput_(); this.classList.toggle('signing-in', false); if (takeFocus) { if (!this.multiProfilesPolicyApplied) this.focusInput(); // This will set a custom tab order. } else this.resetTabOrder(); }, /** * Removes a user using the correct identifier based on user type. * @param {Object} user User to be removed. */ removeUser: function(user) { chrome.send('removeUser', [user.isDesktopUser ? user.profilePath : user.username]); }, /** * Handles a click event on action area button. * @param {Event} e Click event. */ handleActionAreaButtonClick_: function(e) { if (this.parentNode.disabled) return; this.isActionBoxMenuActive = !this.isActionBoxMenuActive; e.stopPropagation(); }, /** * Handles a keydown event on action area button. * @param {Event} e KeyDown event. */ handleActionAreaButtonKeyDown_: function(e) { if (this.disabled) return; switch (e.key) { case 'Enter': case ' ': if (this.parentNode.focusedPod_ && !this.isActionBoxMenuActive) this.isActionBoxMenuActive = true; e.stopPropagation(); break; case 'ArrowUp': case 'ArrowDown': if (this.isActionBoxMenuActive) { this.actionBoxMenuRemoveElement.tabIndex = UserPodTabOrder.POD_MENU_ITEM; this.actionBoxMenuRemoveElement.focus(); } e.stopPropagation(); break; // Ignore these two, so ChromeVox hotkeys don't close the menu before // they can navigate through it. case 'Shift': case 'Meta': break; case 'Escape': this.actionBoxAreaElement.focus(); this.isActionBoxMenuActive = false; e.stopPropagation(); break; case 'Tab': if (!this.parentNode.alwaysFocusSinglePod) this.parentNode.focusPod(); default: this.isActionBoxMenuActive = false; break; } }, /** * Handles a keydown event on menu title. * @param {Event} e KeyDown event. */ handleMenuTitleElementKeyDown_: function(e) { if (this.disabled) return; if (e.key != 'Tab') { this.handleActionAreaButtonKeyDown_(e); return; } if (e.shiftKey == false) { if (this.actionBoxMenuRemoveElement.hidden) { this.isActionBoxMenuActive = false; } else { this.actionBoxMenuRemoveElement.tabIndex = UserPodTabOrder.POD_MENU_ITEM; this.actionBoxMenuRemoveElement.focus(); e.preventDefault(); } } else { this.isActionBoxMenuActive = false; this.focusInput(); e.preventDefault(); } }, /** * Handles a blur event on menu title. * @param {Event} e Blur event. */ handleMenuTitleElementBlur_: function(e) { if (this.disabled) return; this.actionBoxMenuTitleElement.tabIndex = -1; }, /** * Handles a click event on remove user command. * @param {Event} e Click event. */ handleRemoveCommandClick_: function(e) { this.showRemoveWarning_(); }, /** * Move the action box menu up if needed. */ moveActionMenuUpIfNeeded_: function() { // Skip checking (computationally expensive) if already moved up. if (this.actionBoxMenu.classList.contains('menu-moved-up')) return; // Move up the menu if it overlaps shelf. var maxHeight = cr.ui.LoginUITools.getMaxHeightBeforeShelfOverlapping( this.actionBoxMenu, true); var actualHeight = parseInt( window.getComputedStyle(this.actionBoxMenu).height); if (maxHeight < actualHeight) { this.actionBoxMenu.classList.add('menu-moved-up'); this.actionBoxAreaElement.classList.add('menu-moved-up'); } }, /** * Shows remove user warning. Used for legacy supervised users * and non-device-owner on CrOS, and for all users on desktop. */ showRemoveWarning_: function() { this.actionBoxMenuRemoveElement.hidden = true; this.actionBoxRemoveUserWarningElement.hidden = false; if (!this.user.isDesktopUser) { this.moveActionMenuUpIfNeeded_(); if (!this.user.legacySupervisedUser) { this.querySelector( '.action-box-remove-user-warning-text').style.display = 'none'; this.querySelector( '.action-box-remove-user-warning-table-nonsync').style.display = 'none'; var message = loadTimeData.getString('removeNonOwnerUserWarningText'); this.updateRemoveNonOwnerUserWarningMessage_(this.user.profilePath, message); } } else { // Show extra statistics information for desktop users this.querySelector( '.action-box-remove-non-owner-user-warning-text').hidden = true; this.RemoveWarningDialogSetMessage_(true, false); // set a global handler for the callback window.updateRemoveWarningDialog = this.updateRemoveWarningDialog_.bind(this); chrome.send('removeUserWarningLoadStats', [this.user.profilePath]); } chrome.send('logRemoveUserWarningShown'); }, /** * Refresh the statistics in the remove user warning dialog. * @param {string} profilePath The filepath of the URL (must be verified). * @param {Object} profileStats Statistics associated with profileURL. */ updateRemoveWarningDialog_: function(profilePath, profileStats) { if (profilePath !== this.user.profilePath) return; var stats_elements = this.statsMapElements; // Update individual statistics var hasErrors = false; for (var key in profileStats) { if (stats_elements.hasOwnProperty(key)) { if (profileStats[key].success) { this.user.statistics[key] = profileStats[key]; } else if (!this.user.statistics[key].success) { hasErrors = true; stats_elements[key].textContent = ''; } } } this.RemoveWarningDialogSetMessage_(false, hasErrors); }, /** * Set the new message in the dialog. * @param {boolean} Whether this is the first output, that requires setting * a in-progress message. * @param {boolean} Whether any actual query to the statistics have failed. * Should be true only if there is an error and the corresponding statistic * is also unavailable in ProfileAttributesStorage. */ RemoveWarningDialogSetMessage_: function(isInitial, hasErrors) { var stats_elements = this.statsMapElements; var total_count = 0; var num_stats_loaded = 0; for (var key in stats_elements) { if (this.user.statistics[key].success) { var count = this.user.statistics[key].count; stats_elements[key].textContent = count; total_count += count; num_stats_loaded++; } } // this.classList is used for selecting the appropriate dialog. if (total_count) this.classList.remove('has-no-stats'); var is_synced_user = this.user.emailAddress !== ""; // Write total number if all statistics are loaded. if (num_stats_loaded === Object.keys(stats_elements).length) { if (!total_count) { this.classList.add('has-no-stats'); var message = loadTimeData.getString( is_synced_user ? 'removeUserWarningTextSyncNoStats' : 'removeUserWarningTextNonSyncNoStats'); this.updateRemoveWarningDialogSetMessage_(this.user.profilePath, message); } else { window.updateRemoveWarningDialogSetMessage = this.updateRemoveWarningDialogSetMessage_.bind(this); chrome.send('getRemoveWarningDialogMessage',[{ profilePath: this.user.profilePath, isSyncedUser: is_synced_user, hasErrors: hasErrors, totalCount: total_count }]); } } else if (isInitial) { if (!this.user.isProfileLoaded) { message = loadTimeData.getString( is_synced_user ? 'removeUserWarningTextSyncNoStats' : 'removeUserWarningTextNonSyncNoStats'); this.updateRemoveWarningDialogSetMessage_(this.user.profilePath, message); } else { message = loadTimeData.getString( is_synced_user ? 'removeUserWarningTextSyncCalculating' : 'removeUserWarningTextNonSyncCalculating'); substitute = loadTimeData.getString( 'removeUserWarningTextCalculating'); this.updateRemoveWarningDialogSetMessage_(this.user.profilePath, message, substitute); } } }, /** * Refresh the message in the remove user warning dialog. * @param {string} profilePath The filepath of the URL (must be verified). * @param {string} message The message to be written. * @param {number|string=} count The number or string to replace $1 in * |message|. Can be omitted if $1 is not present in |message|. */ updateRemoveWarningDialogSetMessage_: function(profilePath, message, count) { if (profilePath !== this.user.profilePath) return; // Add localized messages where $1 will be replaced with // and $2 will be replaced with // . var element = this.querySelector('.action-box-remove-user-warning-text'); element.textContent = ''; messageParts = message.split(/(\$[12])/); var numParts = messageParts.length; for (var j = 0; j < numParts; j++) { if (messageParts[j] === '$1') { var elementToAdd = document.createElement('span'); elementToAdd.classList.add('total-count'); elementToAdd.textContent = count; element.appendChild(elementToAdd); } else if (messageParts[j] === '$2') { var elementToAdd = document.createElement('span'); elementToAdd.classList.add('email'); elementToAdd.textContent = this.user.emailAddress; element.appendChild(elementToAdd); } else { element.appendChild(document.createTextNode(messageParts[j])); } } this.moveActionMenuUpIfNeeded_(); }, /** * Update the message in the "remove non-owner user warning" dialog on CrOS. * @param {string} profilePath The filepath of the URL (must be verified). * @param (string) message The message to be written. */ updateRemoveNonOwnerUserWarningMessage_: function(profilePath, message) { if (profilePath !== this.user.profilePath) return; // Add localized messages where $1 will be replaced with // . var element = this.querySelector( '.action-box-remove-non-owner-user-warning-text'); element.textContent = ''; messageParts = message.split(/(\$[1])/); var numParts = messageParts.length; for (var j = 0; j < numParts; j++) { if (messageParts[j] == '$1') { var elementToAdd = document.createElement('span'); elementToAdd.classList.add('email'); elementToAdd.textContent = this.user.emailAddress; element.appendChild(elementToAdd); } else { element.appendChild(document.createTextNode(messageParts[j])); } } this.moveActionMenuUpIfNeeded_(); }, /** * Handles a click event on remove user confirmation button. * @param {Event} e Click event. */ handleRemoveUserConfirmationClick_: function(e) { if (this.isActionBoxMenuActive) { this.isActionBoxMenuActive = false; this.removeUser(this.user); e.stopPropagation(); } }, /** * Handles a keydown event on remove user confirmation button. * @param {Event} e KeyDown event. */ handleRemoveUserConfirmationKeyDown_: function(e) { if (!this.isActionBoxMenuActive) return; // Only handle pressing 'Enter' or 'Space', and let all other events // bubble to the action box menu. if (e.key == 'Enter' || e.key == ' ') { this.isActionBoxMenuActive = false; this.removeUser(this.user); e.stopPropagation(); // Prevent default so that we don't trigger a 'click' event. e.preventDefault(); } }, /** * Handles a keydown event on remove command. * @param {Event} e KeyDown event. */ handleRemoveCommandKeyDown_: function(e) { if (this.disabled) return; switch (e.key) { case 'Enter': e.preventDefault(); this.showRemoveWarning_(); e.stopPropagation(); break; case 'ArrowUp': case 'ArrowDown': e.stopPropagation(); break; // Ignore these two, so ChromeVox hotkeys don't close the menu before // they can navigate through it. case 'Shift': case 'Meta': break; case 'Escape': this.actionBoxAreaElement.focus(); this.isActionBoxMenuActive = false; e.stopPropagation(); break; default: this.actionBoxAreaElement.focus(); this.isActionBoxMenuActive = false; break; } }, /** * Handles a blur event on remove command. * @param {Event} e Blur event. */ handleRemoveCommandBlur_: function(e) { if (this.disabled) return; this.actionBoxMenuRemoveElement.tabIndex = -1; }, /** * Handles mouse down event. It sets whether the user click auth will be * allowed on the next mouse click event. The auth is allowed iff the pod * was focused on the mouse down event starting the click. * @param {Event} e The mouse down event. */ handlePodMouseDown_: function(e) { this.userClickAuthAllowed_ = this.parentNode.isFocused(this); }, /** * Called when the input of the password element changes. Updates the submit * button color and state and hides the error popup bubble. */ updateInput_: function() { if (this.submitButton) this.submitButton.disabled = this.passwordElement.value.length <= 0; this.showError = false; $('bubble').hide(); }, /** * Handles input event on the password element. * @param {Event} e Input event. */ handleInputChanged_: function(e) { this.updateInput_(); }, /** * Handles mouse up event on the password element. * @param {Event} e Mouse up event. */ handleInputMouseUp_: function(e) { // If the PIN keyboard is shown and the user clicks on the password // element, the virtual keyboard should pop up if it is enabled, so we // must disable the virtual keyboard override. if (this.isPinShown()) { chrome.send('setForceDisableVirtualKeyboard', [false]); } }, /** * Handles click event on a user pod. * @param {Event} e Click event. */ handleClickOnPod_: function(e) { if (this.parentNode.disabled) return; if (!this.isActionBoxMenuActive) { if (this.isAuthTypeOnlineSignIn) { this.showSigninUI(); } else if (this.isAuthTypeUserClick && this.userClickAuthAllowed_) { // Note that this.userClickAuthAllowed_ is set in mouse down event // handler. this.parentNode.setActivatedPod(this); } else if (this.pinKeyboard && e.target == this.pinKeyboard.submitButton) { // Sets the pod as activated if the submit button is clicked so that // it simulates what the enter button does for the password/pin. this.parentNode.setActivatedPod(this); } if (this.multiProfilesPolicyApplied) this.userTypeBubbleElement.classList.add('bubble-shown'); // Prevent default so that we don't trigger 'focus' event and // stop propagation so that the 'click' event does not bubble // up and accidentally closes the bubble tooltip. stopEventPropagation(e); } }, /** * Handles keydown event for a user pod. * @param {Event} e Key event. */ handlePodKeyDown_: function(e) { if (!this.isAuthTypeUserClick || this.disabled) return; switch (e.key) { case 'Enter': case ' ': if (this.parentNode.isFocused(this)) this.parentNode.setActivatedPod(this); break; } } }; /** * Creates a public account user pod. * @constructor * @extends {UserPod} */ var PublicAccountUserPod = cr.ui.define(function() { var node = UserPod(); var extras = $('public-account-user-pod-extras-template').children; for (var i = 0; i < extras.length; ++i) { var el = extras[i].cloneNode(true); node.appendChild(el); } return node; }); PublicAccountUserPod.prototype = { __proto__: UserPod.prototype, /** * "Enter" button in expanded side pane. * @type {!HTMLButtonElement} */ get enterButtonElement() { return this.querySelector('.enter-button'); }, /** * Boolean flag of whether the pod is showing the side pane. The flag * controls whether 'expanded' class is added to the pod's class list and * resets tab order because main input element changes when the 'expanded' * state changes. * @type {boolean} */ get expanded() { return this.classList.contains('expanded'); }, set expanded(expanded) { if (this.expanded == expanded) return; this.resetTabOrder(); this.classList.toggle('expanded', expanded); if (expanded) { // Show the advanced expanded pod directly if there are at least two // recommended locales. This will be the case in multilingual // environments where users are likely to want to choose among locales. if (this.querySelector('.language-select').multipleRecommendedLocales) this.classList.add('advanced'); this.usualLeft = this.left; this.makeSpaceForExpandedPod_(); } else if (typeof(this.usualLeft) != 'undefined') { this.left = this.usualLeft; } var self = this; this.classList.add('animating'); this.addEventListener('webkitTransitionEnd', function f(e) { self.removeEventListener('webkitTransitionEnd', f); self.classList.remove('animating'); // Accessibility focus indicator does not move with the focused // element. Sends a 'focus' event on the currently focused element // so that accessibility focus indicator updates its location. if (document.activeElement) document.activeElement.dispatchEvent(new Event('focus')); }); // Guard timer set to animation duration + 20ms. ensureTransitionEndEvent(this, 200); }, get advanced() { return this.classList.contains('advanced'); }, /** @override */ get mainInput() { if (this.expanded) return this.enterButtonElement; else return this.nameElement; }, /** @override */ decorate: function() { UserPod.prototype.decorate.call(this); this.classList.add('public-account'); this.nameElement.addEventListener('keydown', (function(e) { if (e.key == 'Enter') { this.parentNode.setActivatedPod(this, e); // Stop this keydown event from bubbling up to PodRow handler. e.stopPropagation(); // Prevent default so that we don't trigger a 'click' event on the // newly focused "Enter" button. e.preventDefault(); } }).bind(this)); var learnMore = this.querySelector('.learn-more'); learnMore.addEventListener('mousedown', stopEventPropagation); learnMore.addEventListener('click', this.handleLearnMoreEvent); learnMore.addEventListener('keydown', this.handleLearnMoreEvent); learnMore = this.querySelector('.expanded-pane-learn-more'); learnMore.addEventListener('click', this.handleLearnMoreEvent); learnMore.addEventListener('keydown', this.handleLearnMoreEvent); var languageSelect = this.querySelector('.language-select'); languageSelect.tabIndex = UserPodTabOrder.POD_INPUT; languageSelect.manuallyChanged = false; languageSelect.addEventListener( 'change', function() { languageSelect.manuallyChanged = true; this.getPublicSessionKeyboardLayouts_(); }.bind(this)); var keyboardSelect = this.querySelector('.keyboard-select'); keyboardSelect.tabIndex = UserPodTabOrder.POD_INPUT; keyboardSelect.loadedLocale = null; var languageAndInput = this.querySelector('.language-and-input'); languageAndInput.tabIndex = UserPodTabOrder.POD_INPUT; languageAndInput.addEventListener('click', this.transitionToAdvanced_.bind(this)); var monitoringLearnMore = this.querySelector('.monitoring-learn-more'); monitoringLearnMore.tabIndex = UserPodTabOrder.POD_INPUT; monitoringLearnMore.addEventListener( 'click', this.onMonitoringLearnMoreClicked_.bind(this)); this.enterButtonElement.addEventListener('click', (function(e) { this.enterButtonElement.disabled = true; var locale = this.querySelector('.language-select').value; var keyboardSelect = this.querySelector('.keyboard-select'); // The contents of |keyboardSelect| is updated asynchronously. If its // locale does not match |locale|, it has not updated yet and the // currently selected keyboard layout may not be applicable to |locale|. // Do not return any keyboard layout in this case and let the backend // choose a suitable layout. var keyboardLayout = keyboardSelect.loadedLocale == locale ? keyboardSelect.value : ''; chrome.send('launchPublicSession', [this.user.username, locale, keyboardLayout]); }).bind(this)); }, /** @override **/ initialize: function() { UserPod.prototype.initialize.call(this); id = this.user.username + '-keyboard'; this.querySelector('.keyboard-select-label').htmlFor = id; this.querySelector('.keyboard-select').setAttribute('id', id); var id = this.user.username + '-language'; this.querySelector('.language-select-label').htmlFor = id; var languageSelect = this.querySelector('.language-select'); languageSelect.setAttribute('id', id); this.populateLanguageSelect(this.user.initialLocales, this.user.initialLocale, this.user.initialMultipleRecommendedLocales); }, /** @override **/ update: function() { UserPod.prototype.update.call(this); this.querySelector('.expanded-pane-name').textContent = this.user_.displayName; this.querySelector('.info').textContent = loadTimeData.getStringF('publicAccountInfoFormat', this.user_.enterpriseDomain); }, /** @override */ focusInput: function() { // Move tabIndex from the whole pod to the main input. this.tabIndex = -1; this.mainInput.tabIndex = UserPodTabOrder.POD_INPUT; this.mainInput.focus(); }, /** @override */ reset: function(takeFocus) { if (!takeFocus) this.expanded = false; this.enterButtonElement.disabled = false; UserPod.prototype.reset.call(this, takeFocus); }, /** @override */ activate: function(e) { if (!this.expanded) { this.expanded = true; this.focusInput(); } return true; }, /** @override */ handleClickOnPod_: function(e) { if (this.parentNode.disabled) return; this.parentNode.focusPod(this); this.parentNode.setActivatedPod(this, e); // Prevent default so that we don't trigger 'focus' event. e.preventDefault(); }, /** * Updates the display name shown on the pod. * @param {string} displayName The new display name */ setDisplayName: function(displayName) { this.user_.displayName = displayName; this.update(); }, /** * Handle mouse and keyboard events for the learn more button. Triggering * the button causes information about public sessions to be shown. * @param {Event} event Mouse or keyboard event. */ handleLearnMoreEvent: function(event) { switch (event.type) { // Show informaton on left click. Let any other clicks propagate. case 'click': if (event.button != 0) return; break; // Show informaton when or is pressed. Let any other // key presses propagate. case 'keydown': switch (event.keyCode) { case 13: // Return. case 32: // Space. break; default: return; } break; } chrome.send('launchHelpApp', [HELP_TOPIC_PUBLIC_SESSION]); stopEventPropagation(event); }, makeSpaceForExpandedPod_: function() { var width = this.classList.contains('advanced') ? PUBLIC_EXPANDED_ADVANCED_WIDTH : PUBLIC_EXPANDED_BASIC_WIDTH; var isDesktopUserManager = Oobe.getInstance().displayType == DISPLAY_TYPE.DESKTOP_USER_MANAGER; var rowPadding = isDesktopUserManager ? DESKTOP_ROW_PADDING : POD_ROW_PADDING; if (this.left + width > $('pod-row').offsetWidth - rowPadding) this.left = $('pod-row').offsetWidth - rowPadding - width; }, /** * Transition the expanded pod from the basic to the advanced view. */ transitionToAdvanced_: function() { var pod = this; var languageAndInputSection = this.querySelector('.language-and-input-section'); this.classList.add('transitioning-to-advanced'); setTimeout(function() { pod.classList.add('advanced'); pod.makeSpaceForExpandedPod_(); languageAndInputSection.addEventListener('webkitTransitionEnd', function observer() { languageAndInputSection.removeEventListener('webkitTransitionEnd', observer); pod.classList.remove('transitioning-to-advanced'); pod.querySelector('.language-select').focus(); }); // Guard timer set to animation duration + 20ms. ensureTransitionEndEvent(languageAndInputSection, 380); }, 0); }, /** * Show a dialog when user clicks on learn more (monitoring) button. */ onMonitoringLearnMoreClicked_: function() { if (!this.dialogContainer_) { this.dialogContainer_ = document.createElement('div'); this.dialogContainer_.classList.add('monitoring-dialog-container'); var topContainer = document.querySelector('#scroll-container'); topContainer.appendChild(this.dialogContainer_); } // Public Session POD in advanced view has a different size so add a dummy // parent element to enable different CSS settings. this.dialogContainer_.classList.toggle( 'advanced', this.classList.contains('advanced')) var html = ''; var infoItems = ['publicAccountMonitoringInfoItem1', 'publicAccountMonitoringInfoItem2', 'publicAccountMonitoringInfoItem3', 'publicAccountMonitoringInfoItem4']; for (item of infoItems) { html += '

    '; html += loadTimeData.getString(item); html += '

    '; } var title = loadTimeData.getString('publicAccountMonitoringInfo'); this.dialog_ = new cr.ui.dialogs.BaseDialog(this.dialogContainer_); this.dialog_.showHtml(title, html, undefined, this.onMonitoringDialogClosed_.bind(this)); this.parentNode.disabled = true; }, /** * Cleanup after the monitoring warning dialog is closed. */ onMonitoringDialogClosed_: function() { this.parentNode.disabled = false; this.dialog_ = undefined; }, /** * Retrieves the list of keyboard layouts available for the currently * selected locale. */ getPublicSessionKeyboardLayouts_: function() { var selectedLocale = this.querySelector('.language-select').value; if (selectedLocale == this.querySelector('.keyboard-select').loadedLocale) { // If the list of keyboard layouts was loaded for the currently selected // locale, it is already up to date. return; } chrome.send('getPublicSessionKeyboardLayouts', [this.user.username, selectedLocale]); }, /** * Populates the keyboard layout "select" element with a list of layouts. * @param {string} locale The locale to which this list of keyboard layouts * applies * @param {!Object} list List of available keyboard layouts */ populateKeyboardSelect: function(locale, list) { if (locale != this.querySelector('.language-select').value) { // The selected locale has changed and the list of keyboard layouts is // not applicable. This method will be called again when a list of // keyboard layouts applicable to the selected locale is retrieved. return; } var keyboardSelect = this.querySelector('.keyboard-select'); keyboardSelect.loadedLocale = locale; keyboardSelect.innerHTML = ''; for (var i = 0; i < list.length; ++i) { var item = list[i]; keyboardSelect.appendChild( new Option(item.title, item.value, item.selected, item.selected)); } }, /** * Populates the language "select" element with a list of locales. * @param {!Object} locales The list of available locales * @param {string} defaultLocale The locale to select by default * @param {boolean} multipleRecommendedLocales Whether |locales| contains * two or more recommended locales */ populateLanguageSelect: function(locales, defaultLocale, multipleRecommendedLocales) { var languageSelect = this.querySelector('.language-select'); // If the user manually selected a locale, do not change the selection. // Otherwise, select the new |defaultLocale|. var selected = languageSelect.manuallyChanged ? languageSelect.value : defaultLocale; languageSelect.innerHTML = ''; var group = languageSelect; for (var i = 0; i < locales.length; ++i) { var item = locales[i]; if (item.optionGroupName) { group = document.createElement('optgroup'); group.label = item.optionGroupName; languageSelect.appendChild(group); } else { group.appendChild(new Option(item.title, item.value, item.value == selected, item.value == selected)); } } languageSelect.multipleRecommendedLocales = multipleRecommendedLocales; // Retrieve a list of keyboard layouts applicable to the locale that is // now selected. this.getPublicSessionKeyboardLayouts_(); } }; /** * Creates a user pod to be used only in desktop chrome. * @constructor * @extends {UserPod} */ var DesktopUserPod = cr.ui.define(function() { // Don't just instantiate a UserPod(), as this will call decorate() on the // parent object, and add duplicate event listeners. var node = $('user-pod-template').cloneNode(true); node.removeAttribute('id'); return node; }); DesktopUserPod.prototype = { __proto__: UserPod.prototype, /** @override */ initialize: function() { if (this.user.needsSignin) { if (this.user.hasLocalCreds) { this.user.initialAuthType = AUTH_TYPE.OFFLINE_PASSWORD; } else { this.user.initialAuthType = AUTH_TYPE.ONLINE_SIGN_IN; } } UserPod.prototype.initialize.call(this); }, /** @override */ get mainInput() { if (this.user.needsSignin) return this.passwordElement; else return this.nameElement; }, /** @override */ update: function() { this.imageElement.src = this.user.userImage; this.nameElement.textContent = this.user.displayName; this.reauthNameHintElement.textContent = this.user.displayName; var isLockedUser = this.user.needsSignin; var isLegacySupervisedUser = this.user.legacySupervisedUser; var isChildUser = this.user.childUser; var isSyncedUser = this.user.emailAddress !== ""; var isProfileLoaded = this.user.isProfileLoaded; this.classList.toggle('locked', isLockedUser); this.classList.toggle('legacy-supervised', isLegacySupervisedUser); this.classList.toggle('child', isChildUser); this.classList.toggle('synced', isSyncedUser); this.classList.toggle('has-no-stats', !isProfileLoaded && !this.user.statistics.length); if (this.isAuthTypeUserClick) this.passwordLabelElement.textContent = this.authValue; this.passwordElement.setAttribute('aria-label', loadTimeData.getStringF( 'passwordFieldAccessibleName', this.user_.emailAddress)); UserPod.prototype.updateActionBoxArea.call(this); }, /** @override */ activate: function(e) { if (!this.user.needsSignin) { Oobe.launchUser(this.user.profilePath); } else if (this.user.hasLocalCreds && !this.passwordElement.value) { return false; } else { chrome.send('authenticatedLaunchUser', [this.user.profilePath, this.user.emailAddress, this.passwordElement.value]); } this.passwordElement.value = ''; return true; }, /** @override */ handleClickOnPod_: function(e) { if (this.parentNode.disabled) return; Oobe.clearErrors(); this.parentNode.lastFocusedPod_ = this; // If this is a locked pod and there are local credentials, show the // password field. Otherwise call activate() which will open up a browser // window or show the reauth dialog, as needed. if (!(this.user.needsSignin && this.user.hasLocalCreds) && !this.isActionBoxMenuActive) { this.activate(e); } if (this.isAuthTypeUserClick) chrome.send('attemptUnlock', [this.user.emailAddress]); }, }; /** * Creates a user pod that represents kiosk app. * @constructor * @extends {UserPod} */ var KioskAppPod = cr.ui.define(function() { var node = UserPod(); return node; }); KioskAppPod.prototype = { __proto__: UserPod.prototype, /** @override */ decorate: function() { UserPod.prototype.decorate.call(this); this.launchAppButtonElement.addEventListener('click', this.activate.bind(this)); }, /** @override */ update: function() { this.imageElement.src = this.user.iconUrl; this.imageElement.alt = this.user.label; this.imageElement.title = this.user.label; this.passwordEntryContainerElement.hidden = true; this.launchAppButtonContainerElement.hidden = false; this.nameElement.textContent = this.user.label; this.reauthNameHintElement.textContent = this.user.label; UserPod.prototype.updateActionBoxArea.call(this); UserPod.prototype.customizeUserPodPerUserType.call(this); }, /** @override */ get mainInput() { return this.launchAppButtonElement; }, /** @override */ focusInput: function() { // Move tabIndex from the whole pod to the main input. this.tabIndex = -1; this.mainInput.tabIndex = UserPodTabOrder.POD_INPUT; this.mainInput.focus(); }, /** @override */ get forceOnlineSignin() { return false; }, /** @override */ activate: function(e) { var diagnosticMode = e && e.ctrlKey; this.launchApp_(this.user, diagnosticMode); return true; }, /** @override */ handleClickOnPod_: function(e) { if (this.parentNode.disabled) return; Oobe.clearErrors(); this.parentNode.lastFocusedPod_ = this; this.activate(e); }, /** * Launch the app. If |diagnosticMode| is true, ask user to confirm. * @param {Object} app App data. * @param {boolean} diagnosticMode Whether to run the app in diagnostic * mode. */ launchApp_: function(app, diagnosticMode) { if (!diagnosticMode) { chrome.send('launchKioskApp', [app.id, false]); return; } var oobe = $('oobe'); if (!oobe.confirmDiagnosticMode_) { oobe.confirmDiagnosticMode_ = new cr.ui.dialogs.ConfirmDialog(document.body); oobe.confirmDiagnosticMode_.setOkLabel( loadTimeData.getString('confirmKioskAppDiagnosticModeYes')); oobe.confirmDiagnosticMode_.setCancelLabel( loadTimeData.getString('confirmKioskAppDiagnosticModeNo')); } oobe.confirmDiagnosticMode_.show( loadTimeData.getStringF('confirmKioskAppDiagnosticModeFormat', app.label), function() { chrome.send('launchKioskApp', [app.id, true]); }); }, }; /** * Creates a new pod row element. * @constructor * @extends {HTMLDivElement} */ var PodRow = cr.ui.define('podrow'); PodRow.prototype = { __proto__: HTMLDivElement.prototype, // Whether this user pod row is shown for the first time. firstShown_: true, // True if inside focusPod(). insideFocusPod_: false, // Focused pod. focusedPod_: undefined, // Activated pod, i.e. the pod of current login attempt. activatedPod_: undefined, // Pod that was most recently focused, if any. lastFocusedPod_: undefined, // Pods whose initial images haven't been loaded yet. podsWithPendingImages_: [], // Whether pod placement has been postponed. podPlacementPostponed_: false, // Standard user pod height/width. userPodHeight_: 0, userPodWidth_: 0, // Array of apps that are shown in addition to other user pods. apps_: [], // True to show app pods along with user pods. shouldShowApps_: true, // Array of users that are shown (public/supervised/regular). users_: [], // If we're in Touch View mode. touchViewEnabled_: false, /** @override */ decorate: function() { // Event listeners that are installed for the time period during which // the element is visible. this.listeners_ = { focus: [this.handleFocus_.bind(this), true /* useCapture */], click: [this.handleClick_.bind(this), true], mousemove: [this.handleMouseMove_.bind(this), false], keydown: [this.handleKeyDown.bind(this), false] }; var isDesktopUserManager = Oobe.getInstance().displayType == DISPLAY_TYPE.DESKTOP_USER_MANAGER; var isNewDesktopUserManager = Oobe.getInstance().newDesktopUserManager; this.userPodHeight_ = isDesktopUserManager ? isNewDesktopUserManager ? MD_DESKTOP_POD_HEIGHT : DESKTOP_POD_HEIGHT : CROS_POD_HEIGHT; this.userPodWidth_ = isDesktopUserManager ? isNewDesktopUserManager ? MD_DESKTOP_POD_WIDTH : DESKTOP_POD_WIDTH : CROS_POD_WIDTH; }, /** * Returns all the pods in this pod row. * @type {NodeList} */ get pods() { return Array.prototype.slice.call(this.children); }, /** * Return true if user pod row has only single user pod in it, which should * always be focused except desktop and touch view modes. * @type {boolean} */ get alwaysFocusSinglePod() { var isDesktopUserManager = Oobe.getInstance().displayType == DISPLAY_TYPE.DESKTOP_USER_MANAGER; return (isDesktopUserManager || this.touchViewEnabled_) ? false : this.children.length == 1; }, /** * Returns pod with the given app id. * @param {!string} app_id Application id to be matched. * @return {Object} Pod with the given app id. null if pod hasn't been * found. */ getPodWithAppId_: function(app_id) { for (var i = 0, pod; pod = this.pods[i]; ++i) { if (pod.user.isApp && pod.user.id == app_id) return pod; } return null; }, /** * Returns pod with the given username (null if there is no such pod). * @param {string} username Username to be matched. * @return {Object} Pod with the given username. null if pod hasn't been * found. */ getPodWithUsername_: function(username) { for (var i = 0, pod; pod = this.pods[i]; ++i) { if (pod.user.username == username) return pod; } return null; }, /** * True if the the pod row is disabled (handles no user interaction). * @type {boolean} */ disabled_: false, get disabled() { return this.disabled_; }, set disabled(value) { this.disabled_ = value; this.pods.forEach(function(pod) { pod.disabled = value; }); }, /** * Creates a user pod from given email. * @param {!Object} user User info dictionary. */ createUserPod: function(user) { var userPod; if (user.isDesktopUser) userPod = new DesktopUserPod({user: user}); else if (user.publicAccount) userPod = new PublicAccountUserPod({user: user}); else if (user.isApp) userPod = new KioskAppPod({user: user}); else userPod = new UserPod({user: user}); userPod.hidden = false; return userPod; }, /** * Add an existing user pod to this pod row. * @param {!Object} user User info dictionary. */ addUserPod: function(user) { var userPod = this.createUserPod(user); this.appendChild(userPod); userPod.initialize(); }, /** * Enables or disables transitions on every pod instance. * @param {boolean} enable */ toggleTransitions: function(enable) { for (var i = 0; i < this.pods.length; ++i) this.pods[i].toggleTransitions(enable); }, /** * Performs visual changes on the user pod if there is an error. * @param {boolean} visible Whether to show or hide the display. */ setFocusedPodErrorDisplay: function(visible) { if (this.focusedPod_) this.focusedPod_.showError = visible; }, /** * Shows or hides the pin keyboard for the current focused pod. * @param {boolean} visible */ setFocusedPodPinVisibility: function(visible) { if (this.focusedPod_ && this.focusedPod_.user.showPin) this.focusedPod_.setPinVisibility(visible); }, /** * Runs app with a given id from the list of loaded apps. * @param {!string} app_id of an app to run. * @param {boolean=} opt_diagnosticMode Whether to run the app in * diagnostic mode. Default is false. */ findAndRunAppForTesting: function(app_id, opt_diagnosticMode) { var app = this.getPodWithAppId_(app_id); if (app) { var activationEvent = cr.doc.createEvent('MouseEvents'); var ctrlKey = opt_diagnosticMode; activationEvent.initMouseEvent('click', true, true, null, 0, 0, 0, 0, 0, ctrlKey, false, false, false, 0, null); app.dispatchEvent(activationEvent); } }, /** * Remove the pin keyboard from the pod with the given |username|. * @param {!user} username * @param {boolean} visible */ removePinKeyboard: function(username) { var pod = this.getPodWithUsername_(username); if (!pod) { console.warn('Attempt to remove pin keyboard of missing pod.'); return; } // Remove the child, so that the virtual keyboard cannot bring it up // again after three tries. if (pod.pinContainer) pod.removeChild(pod.pinContainer); pod.setPinVisibility(false); }, /** * Removes user pod from pod row. * @param {!user} username */ removeUserPod: function(username) { var podToRemove = this.getPodWithUsername_(username); if (podToRemove == null) { console.warn('Attempt to remove pod that does not exist'); return; } this.removeChild(podToRemove); if (this.pods.length > 0) this.placePods_(); }, /** * Returns index of given pod or -1 if not found. * @param {UserPod} pod Pod to look up. * @private */ indexOf_: function(pod) { for (var i = 0; i < this.pods.length; ++i) { if (pod == this.pods[i]) return i; } return -1; }, /** * Populates pod row with given existing users and start init animation. * @param {array} users Array of existing user emails. */ loadPods: function(users) { this.users_ = users; this.rebuildPods(); }, /** * Scrolls focused user pod into view. */ scrollFocusedPodIntoView: function() { var pod = this.focusedPod_; if (!pod) return; // First check whether focused pod is already fully visible. var visibleArea = $('scroll-container'); // Visible area may not defined at user manager screen on all platforms. // Windows, Mac and Linux do not have visible area. if (!visibleArea) return; var scrollTop = visibleArea.scrollTop; var clientHeight = visibleArea.clientHeight; var podTop = $('oobe').offsetTop + pod.offsetTop; var padding = USER_POD_KEYBOARD_MIN_PADDING; if (podTop + pod.height + padding <= scrollTop + clientHeight && podTop - padding >= scrollTop) { return; } // Scroll so that user pod is as centered as possible. visibleArea.scrollTop = podTop - (clientHeight - pod.offsetHeight) / 2; }, /** * Rebuilds pod row using users_ and apps_ that were previously set or * updated. */ rebuildPods: function() { var emptyPodRow = this.pods.length == 0; // Clear existing pods. this.innerHTML = ''; this.focusedPod_ = undefined; this.activatedPod_ = undefined; this.lastFocusedPod_ = undefined; // Switch off animation Oobe.getInstance().toggleClass('flying-pods', false); // Populate the pod row. for (var i = 0; i < this.users_.length; ++i) this.addUserPod(this.users_[i]); for (var i = 0, pod; pod = this.pods[i]; ++i) this.podsWithPendingImages_.push(pod); // TODO(nkostylev): Edge case handling when kiosk apps are not fitting. if (this.shouldShowApps_) { for (var i = 0; i < this.apps_.length; ++i) this.addUserPod(this.apps_[i]); } // Make sure we eventually show the pod row, even if some image is stuck. setTimeout(function() { $('pod-row').classList.remove('images-loading'); }, POD_ROW_IMAGES_LOAD_TIMEOUT_MS); var isAccountPicker = $('login-header-bar').signinUIState == SIGNIN_UI_STATE.ACCOUNT_PICKER; // Immediately recalculate pods layout only when current UI is account // picker. Otherwise postpone it. if (isAccountPicker) { this.placePods_(); this.maybePreselectPod(); // Without timeout changes in pods positions will be animated even // though it happened when 'flying-pods' class was disabled. setTimeout(function() { Oobe.getInstance().toggleClass('flying-pods', true); }, 0); } else { this.podPlacementPostponed_ = true; // Update [Cancel] button state. if ($('login-header-bar').signinUIState == SIGNIN_UI_STATE.GAIA_SIGNIN && emptyPodRow && this.pods.length > 0) { login.GaiaSigninScreen.updateControlsState(); } } }, /** * Adds given apps to the pod row. * @param {array} apps Array of apps. */ setApps: function(apps) { this.apps_ = apps; this.rebuildPods(); chrome.send('kioskAppsLoaded'); // Check whether there's a pending kiosk app error. window.setTimeout(function() { chrome.send('checkKioskAppLaunchError'); }, 500); }, /** * Sets whether should show app pods. * @param {boolean} shouldShowApps Whether app pods should be shown. */ setShouldShowApps: function(shouldShowApps) { if (this.shouldShowApps_ == shouldShowApps) return; this.shouldShowApps_ = shouldShowApps; this.rebuildPods(); }, /** * Shows a custom icon on a user pod besides the input field. * @param {string} username Username of pod to add button * @param {!{id: !string, * hardlockOnClick: boolean, * isTrialRun: boolean, * ariaLabel: string | undefined, * tooltip: ({text: string, autoshow: boolean} | undefined)}} icon * The icon parameters. */ showUserPodCustomIcon: function(username, icon) { var pod = this.getPodWithUsername_(username); if (pod == null) { console.error('Unable to show user pod button: user pod not found.'); return; } if (!icon.id && !icon.tooltip) return; if (icon.id) pod.customIconElement.setIcon(icon.id); if (icon.isTrialRun) { pod.customIconElement.setInteractive( this.onDidClickLockIconDuringTrialRun_.bind(this, username)); } else if (icon.hardlockOnClick) { pod.customIconElement.setInteractive( this.hardlockUserPod_.bind(this, username)); } else { pod.customIconElement.setInteractive(null); } var ariaLabel = icon.ariaLabel || (icon.tooltip && icon.tooltip.text); if (ariaLabel) pod.customIconElement.setAriaLabel(ariaLabel); else console.warn('No ARIA label for user pod custom icon.'); pod.customIconElement.show(); // This has to be called after |show| in case the tooltip should be shown // immediatelly. pod.customIconElement.setTooltip( icon.tooltip || {text: '', autoshow: false}); }, /** * Hard-locks user pod for the user. If user pod is hard-locked, it can be * only unlocked using password, and the authentication type cannot be * changed. * @param {!string} username The user's username. * @private */ hardlockUserPod_: function(username) { chrome.send('hardlockPod', [username]); }, /** * Records a metric indicating that the user clicked on the lock icon during * the trial run for Easy Unlock. * @param {!string} username The user's username. * @private */ onDidClickLockIconDuringTrialRun_: function(username) { chrome.send('recordClickOnLockIcon', [username]); }, /** * Hides the custom icon in the user pod added by showUserPodCustomIcon(). * @param {string} username Username of pod to remove button */ hideUserPodCustomIcon: function(username) { var pod = this.getPodWithUsername_(username); if (pod == null) { console.error('Unable to hide user pod button: user pod not found.'); return; } // TODO(tengs): Allow option for a fading transition. pod.customIconElement.hide(); }, /** * Sets the authentication type used to authenticate the user. * @param {string} username Username of selected user * @param {number} authType Authentication type, must be one of the * values listed in AUTH_TYPE enum. * @param {string} value The initial value to use for authentication. */ setAuthType: function(username, authType, value) { var pod = this.getPodWithUsername_(username); if (pod == null) { console.error('Unable to set auth type: user pod not found.'); return; } pod.setAuthType(authType, value); }, /** * Sets the state of touch view mode. * @param {boolean} isTouchViewEnabled true if the mode is on. */ setTouchViewState: function(isTouchViewEnabled) { this.touchViewEnabled_ = isTouchViewEnabled; this.pods.forEach(function(pod, index) { pod.actionBoxAreaElement.classList.toggle('forced', isTouchViewEnabled); }); }, /** * Updates the display name shown on a public session pod. * @param {string} userID The user ID of the public session * @param {string} displayName The new display name */ setPublicSessionDisplayName: function(userID, displayName) { var pod = this.getPodWithUsername_(userID); if (pod != null) pod.setDisplayName(displayName); }, /** * Updates the list of locales available for a public session. * @param {string} userID The user ID of the public session * @param {!Object} locales The list of available locales * @param {string} defaultLocale The locale to select by default * @param {boolean} multipleRecommendedLocales Whether |locales| contains * two or more recommended locales */ setPublicSessionLocales: function(userID, locales, defaultLocale, multipleRecommendedLocales) { var pod = this.getPodWithUsername_(userID); if (pod != null) { pod.populateLanguageSelect(locales, defaultLocale, multipleRecommendedLocales); } }, /** * Updates the list of available keyboard layouts for a public session pod. * @param {string} userID The user ID of the public session * @param {string} locale The locale to which this list of keyboard layouts * applies * @param {!Object} list List of available keyboard layouts */ setPublicSessionKeyboardLayouts: function(userID, locale, list) { var pod = this.getPodWithUsername_(userID); if (pod != null) pod.populateKeyboardSelect(locale, list); }, /** * Called when window was resized. */ onWindowResize: function() { var layout = this.calculateLayout_(); if (layout.columns != this.columns || layout.rows != this.rows) this.placePods_(); // Wrap this in a set timeout so the function is called after the pod is // finished transitioning so that we work with the final pod dimensions. // If there is no focused pod that may be transitioning when this function // is called, we can call scrollFocusedPodIntoView() right away. var timeOut = 0; if (this.focusedPod_) { var style = getComputedStyle(this.focusedPod_); timeOut = parseFloat(style.transitionDuration) * 1000; } setTimeout(function() { this.scrollFocusedPodIntoView(); }.bind(this), timeOut); }, /** * Returns width of podrow having |columns| number of columns. * @private */ columnsToWidth_: function(columns) { var isDesktopUserManager = Oobe.getInstance().displayType == DISPLAY_TYPE.DESKTOP_USER_MANAGER; var margin = isDesktopUserManager ? DESKTOP_MARGIN_BY_COLUMNS[columns] : MARGIN_BY_COLUMNS[columns]; var rowPadding = isDesktopUserManager ? DESKTOP_ROW_PADDING : POD_ROW_PADDING; return 2 * rowPadding + columns * this.userPodWidth_ + (columns - 1) * margin; }, /** * Returns height of podrow having |rows| number of rows. * @private */ rowsToHeight_: function(rows) { var isDesktopUserManager = Oobe.getInstance().displayType == DISPLAY_TYPE.DESKTOP_USER_MANAGER; var rowPadding = isDesktopUserManager ? DESKTOP_ROW_PADDING : POD_ROW_PADDING; return 2 * rowPadding + rows * this.userPodHeight_; }, /** * Calculates number of columns and rows that podrow should have in order to * hold as much its pods as possible for current screen size. Also it tries * to choose layout that looks good. * @return {{columns: number, rows: number}} */ calculateLayout_: function() { var preferredColumns = this.pods.length < COLUMNS.length ? COLUMNS[this.pods.length] : COLUMNS[COLUMNS.length - 1]; var maxWidth = Oobe.getInstance().clientAreaSize.width; var columns = preferredColumns; while (maxWidth < this.columnsToWidth_(columns) && columns > 1) --columns; var rows = Math.floor((this.pods.length - 1) / columns) + 1; if (getComputedStyle( $('signin-banner'), null).getPropertyValue('display') != 'none') { rows = Math.min(rows, MAX_NUMBER_OF_ROWS_UNDER_SIGNIN_BANNER); } if (!Oobe.getInstance().newDesktopUserManager) { var maxHeigth = Oobe.getInstance().clientAreaSize.height; while (maxHeigth < this.rowsToHeight_(rows) && rows > 1) --rows; } // One more iteration if it's not enough cells to place all pods. while (maxWidth >= this.columnsToWidth_(columns + 1) && columns * rows < this.pods.length && columns < MAX_NUMBER_OF_COLUMNS) { ++columns; } return {columns: columns, rows: rows}; }, /** * Places pods onto their positions onto pod grid. * @private */ placePods_: function() { var isDesktopUserManager = Oobe.getInstance().displayType == DISPLAY_TYPE.DESKTOP_USER_MANAGER; if (isDesktopUserManager && !Oobe.getInstance().userPodsPageVisible) return; var layout = this.calculateLayout_(); var columns = this.columns = layout.columns; var rows = this.rows = layout.rows; var maxPodsNumber = columns * rows; var margin = isDesktopUserManager ? DESKTOP_MARGIN_BY_COLUMNS[columns] : MARGIN_BY_COLUMNS[columns]; this.parentNode.setPreferredSize( this.columnsToWidth_(columns), this.rowsToHeight_(rows)); var height = this.userPodHeight_; var width = this.userPodWidth_; var pinPodLocation = { column: columns + 1, row: rows + 1 }; if (this.focusedPod_ && this.focusedPod_.isPinShown()) pinPodLocation = this.findPodLocation_(this.focusedPod_, columns, rows); this.pods.forEach(function(pod, index) { if (index >= maxPodsNumber) { pod.hidden = true; return; } pod.hidden = false; if (pod.offsetHeight != height && pod.offsetHeight != CROS_PIN_POD_HEIGHT) { console.error('Pod offsetHeight (' + pod.offsetHeight + ') and POD_HEIGHT (' + height + ') are not equal.'); } if (pod.offsetWidth != width) { console.error('Pod offsetWidth (' + pod.offsetWidth + ') and POD_WIDTH (' + width + ') are not equal.'); } var column = index % columns; var row = Math.floor(index / columns); var rowPadding = isDesktopUserManager ? DESKTOP_ROW_PADDING : POD_ROW_PADDING; pod.left = rowPadding + column * (width + margin); // On desktop, we want the rows to always be equally spaced. pod.top = isDesktopUserManager ? row * (height + rowPadding) : row * height + rowPadding; }); Oobe.getInstance().updateScreenSize(this.parentNode); }, /** * Number of columns. * @type {?number} */ set columns(columns) { // Cannot use 'columns' here. this.setAttribute('ncolumns', columns); }, get columns() { return parseInt(this.getAttribute('ncolumns')); }, /** * Number of rows. * @type {?number} */ set rows(rows) { // Cannot use 'rows' here. this.setAttribute('nrows', rows); }, get rows() { return parseInt(this.getAttribute('nrows')); }, /** * Whether the pod is currently focused. * @param {UserPod} pod Pod to check for focus. * @return {boolean} Pod focus status. */ isFocused: function(pod) { return this.focusedPod_ == pod; }, /** * Focuses a given user pod or clear focus when given null. * @param {UserPod=} podToFocus User pod to focus (undefined clears focus). * @param {boolean=} opt_force If true, forces focus update even when * podToFocus is already focused. * @param {boolean=} opt_skipInputFocus If true, don't focus on the input * box of user pod. */ focusPod: function(podToFocus, opt_force, opt_skipInputFocus) { if (this.isFocused(podToFocus) && !opt_force) { // Calling focusPod w/o podToFocus means reset. if (!podToFocus) Oobe.clearErrors(); return; } // Make sure there's only one focusPod operation happening at a time. if (this.insideFocusPod_) { return; } this.insideFocusPod_ = true; for (var i = 0, pod; pod = this.pods[i]; ++i) { if (!this.alwaysFocusSinglePod) { pod.isActionBoxMenuActive = false; } if (pod != podToFocus) { pod.isActionBoxMenuHovered = false; pod.classList.remove('focused'); pod.setPinVisibility(false); // On Desktop, the faded style is not set correctly, so we should // manually fade out non-focused pods if there is a focused pod. if (pod.user.isDesktopUser && podToFocus) pod.classList.add('faded'); else pod.classList.remove('faded'); pod.reset(false); } } // Clear any error messages for previous pod. if (!this.isFocused(podToFocus)) Oobe.clearErrors(); this.focusedPod_ = podToFocus; if (podToFocus) { // Only show the keyboard if it is fully loaded. if (podToFocus.isPinReady()) podToFocus.setPinVisibility(true); podToFocus.classList.remove('faded'); podToFocus.classList.add('focused'); if (!podToFocus.multiProfilesPolicyApplied) { podToFocus.classList.toggle('signing-in', false); if (!opt_skipInputFocus) podToFocus.focusInput(); } else { podToFocus.userTypeBubbleElement.classList.add('bubble-shown'); // Note it is not necessary to skip this focus request when // |opt_skipInputFocus| is true. When |multiProfilesPolicyApplied| // is false, it doesn't focus on the password input box by default. podToFocus.focus(); } // focusPod() automatically loads wallpaper if (!podToFocus.user.isApp) chrome.send('focusPod', [podToFocus.user.username]); this.firstShown_ = false; this.lastFocusedPod_ = podToFocus; this.scrollFocusedPodIntoView(); } this.insideFocusPod_ = false; }, /** * Resets wallpaper to the last active user's wallpaper, if any. */ loadLastWallpaper: function() { if (this.lastFocusedPod_ && !this.lastFocusedPod_.user.isApp) chrome.send('loadWallpaper', [this.lastFocusedPod_.user.username]); }, /** * Returns the currently activated pod. * @type {UserPod} */ get activatedPod() { return this.activatedPod_; }, /** * Sets currently activated pod. * @param {UserPod} pod Pod to check for focus. * @param {Event} e Event object. */ setActivatedPod: function(pod, e) { if (this.disabled) { console.error('Cannot activate pod while sign-in UI is disabled.'); return; } if (pod && pod.activate(e)) this.activatedPod_ = pod; }, /** * The pod of the signed-in user, if any; null otherwise. * @type {?UserPod} */ get lockedPod() { for (var i = 0, pod; pod = this.pods[i]; ++i) { if (pod.user.signedIn) return pod; } return null; }, /** * The pod that is preselected on user pod row show. * @type {?UserPod} */ get preselectedPod() { var isDesktopUserManager = Oobe.getInstance().displayType == DISPLAY_TYPE.DESKTOP_USER_MANAGER; if (isDesktopUserManager) { // On desktop, don't pre-select a pod if it's the only one. if (this.pods.length == 1) return null; // The desktop User Manager can send an URI encoded profile path in the // url hash, that indicates a pod that should be initially focused. var focusedProfilePath = decodeURIComponent(window.location.hash.substr(1)); for (var i = 0, pod; pod = this.pods[i]; ++i) { if (focusedProfilePath === pod.user.profilePath) return pod; } return null; } var lockedPod = this.lockedPod; if (lockedPod) return lockedPod; for (i = 0; pod = this.pods[i]; ++i) { if (!pod.multiProfilesPolicyApplied) return pod; } return this.pods[0]; }, /** * Resets input UI. * @param {boolean} takeFocus True to take focus. */ reset: function(takeFocus) { this.disabled = false; if (this.activatedPod_) this.activatedPod_.reset(takeFocus); }, /** * Restores input focus to current selected pod, if there is any. */ refocusCurrentPod: function() { if (this.focusedPod_ && !this.focusedPod_.multiProfilesPolicyApplied) { this.focusedPod_.focusInput(); } }, /** * Clears focused pod password field. */ clearFocusedPod: function() { if (!this.disabled && this.focusedPod_) this.focusedPod_.reset(true); }, /** * Shows signin UI. * @param {string} email Email for signin UI. */ showSigninUI: function(email) { // Clear any error messages that might still be around. Oobe.clearErrors(); this.disabled = true; this.lastFocusedPod_ = this.getPodWithUsername_(email); Oobe.showSigninUI(email); }, /** * Updates current image of a user. * @param {string} username User for which to update the image. */ updateUserImage: function(username) { var pod = this.getPodWithUsername_(username); if (pod) pod.updateUserImage(); }, /** * Handler of click event. * @param {Event} e Click Event object. * @private */ handleClick_: function(e) { if (this.disabled) return; // Clear all menus if the click is outside pod menu and its // button area. if (!findAncestorByClass(e.target, 'action-box-menu') && !findAncestorByClass(e.target, 'action-box-area')) { for (var i = 0, pod; pod = this.pods[i]; ++i) pod.isActionBoxMenuActive = false; } // Clears focus if not clicked on a pod and if there's more than one pod. var pod = findAncestorByClass(e.target, 'pod'); if ((!pod || pod.parentNode != this) && !this.alwaysFocusSinglePod) { this.focusPod(); } if (pod) pod.isActionBoxMenuHovered = true; // Return focus back to single pod. if (this.alwaysFocusSinglePod && !pod) { if ($('login-header-bar').contains(e.target)) return; this.focusPod(this.focusedPod_, true /* force */); this.focusedPod_.userTypeBubbleElement.classList.remove('bubble-shown'); this.focusedPod_.isActionBoxMenuHovered = false; } }, /** * Handler of mouse move event. * @param {Event} e Click Event object. * @private */ handleMouseMove_: function(e) { if (this.disabled) return; if (e.movementX == 0 && e.movementY == 0) return; // Defocus (thus hide) action box, if it is focused on a user pod // and the pointer is not hovering over it. var pod = findAncestorByClass(e.target, 'pod'); if (document.activeElement && document.activeElement.parentNode != pod && document.activeElement.classList.contains('action-box-area')) { document.activeElement.parentNode.focus(); } if (pod) pod.isActionBoxMenuHovered = true; // Hide action boxes on other user pods. for (var i = 0, p; p = this.pods[i]; ++i) if (p != pod && !p.isActionBoxMenuActive) p.isActionBoxMenuHovered = false; }, /** * Handles focus event. * @param {Event} e Focus Event object. * @private */ handleFocus_: function(e) { if (this.disabled) return; if (e.target.parentNode == this) { // Focus on a pod if (e.target.classList.contains('focused')) { if (!e.target.multiProfilesPolicyApplied) e.target.focusInput(); else e.target.userTypeBubbleElement.classList.add('bubble-shown'); } else this.focusPod(e.target); return; } var pod = findAncestorByClass(e.target, 'pod'); if (pod && pod.parentNode == this) { // Focus on a control of a pod but not on the action area button. if (!pod.classList.contains('focused')) { if (e.target.classList.contains('action-box-area') || e.target.classList.contains('remove-warning-button')) { // focusPod usually moves focus on the password input box which // triggers virtual keyboard to show up. But the focus may move to a // non text input element shortly by e.target.focus. Hence, a // virtual keyboard flicking might be observed. We need to manually // prevent focus on password input box to avoid virtual keyboard // flicking in this case. See crbug.com/396016 for details. this.focusPod(pod, false, true /* opt_skipInputFocus */); } else { this.focusPod(pod); } pod.userTypeBubbleElement.classList.remove('bubble-shown'); e.target.focus(); } return; } // Clears pod focus when we reach here. It means new focus is neither // on a pod nor on a button/input for a pod. // Do not "defocus" user pod when it is a single pod. // That means that 'focused' class will not be removed and // input field/button will always be visible. if (!this.alwaysFocusSinglePod) this.focusPod(); else { // Hide user-type-bubble in case this is one pod and we lost focus of // it. this.focusedPod_.userTypeBubbleElement.classList.remove('bubble-shown'); } }, /** * Handler of keydown event. * @param {Event} e KeyDown Event object. */ handleKeyDown: function(e) { if (this.disabled) return; var editing = e.target.tagName == 'INPUT' && e.target.value; switch (e.key) { case 'ArrowLeft': if (!editing) { if (this.focusedPod_ && this.focusedPod_.previousElementSibling) this.focusPod(this.focusedPod_.previousElementSibling); else this.focusPod(this.lastElementChild); e.stopPropagation(); } break; case 'ArrowRight': if (!editing) { if (this.focusedPod_ && this.focusedPod_.nextElementSibling) this.focusPod(this.focusedPod_.nextElementSibling); else this.focusPod(this.firstElementChild); e.stopPropagation(); } break; case 'Enter': if (this.focusedPod_) { var targetTag = e.target.tagName; if (e.target == this.focusedPod_.passwordElement || (this.focusedPod_.pinKeyboard && e.target == this.focusedPod_.pinKeyboard.inputElement) || (targetTag != 'INPUT' && targetTag != 'BUTTON' && targetTag != 'A')) { this.setActivatedPod(this.focusedPod_, e); e.stopPropagation(); } } break; case 'Escape': if (!this.alwaysFocusSinglePod) this.focusPod(); break; } }, /** * Called right after the pod row is shown. */ handleAfterShow: function() { var focusedPod = this.focusedPod_; // Without timeout changes in pods positions will be animated even though // it happened when 'flying-pods' class was disabled. setTimeout(function() { Oobe.getInstance().toggleClass('flying-pods', true); if (focusedPod) ensureTransitionEndEvent(focusedPod); }, 0); // Force input focus for user pod on show and once transition ends. if (focusedPod) { var screen = this.parentNode; var self = this; focusedPod.addEventListener('webkitTransitionEnd', function f(e) { focusedPod.removeEventListener('webkitTransitionEnd', f); focusedPod.reset(true); // Notify screen that it is ready. screen.onShow(); }); } }, /** * Called right before the pod row is shown. */ handleBeforeShow: function() { Oobe.getInstance().toggleClass('flying-pods', false); for (var event in this.listeners_) { this.ownerDocument.addEventListener( event, this.listeners_[event][0], this.listeners_[event][1]); } $('login-header-bar').buttonsTabIndex = UserPodTabOrder.HEADER_BAR; if (this.podPlacementPostponed_) { this.podPlacementPostponed_ = false; this.placePods_(); this.maybePreselectPod(); } }, /** * Called when the element is hidden. */ handleHide: function() { for (var event in this.listeners_) { this.ownerDocument.removeEventListener( event, this.listeners_[event][0], this.listeners_[event][1]); } $('login-header-bar').buttonsTabIndex = 0; }, /** * Called when a pod's user image finishes loading. */ handlePodImageLoad: function(pod) { var index = this.podsWithPendingImages_.indexOf(pod); if (index == -1) { return; } this.podsWithPendingImages_.splice(index, 1); if (this.podsWithPendingImages_.length == 0) { this.classList.remove('images-loading'); } }, /** * Preselects pod, if needed. */ maybePreselectPod: function() { var pod = this.preselectedPod; this.focusPod(pod); // Hide user-type-bubble in case all user pods are disabled and we focus // first pod. if (pod && pod.multiProfilesPolicyApplied) { pod.userTypeBubbleElement.classList.remove('bubble-shown'); } } }; return { PodRow: PodRow }; }); cr.define('cr.ui', function() { var DisplayManager = cr.ui.login.DisplayManager; /** * Maximum possible height of the #login-header-bar, including the padding * and the border. * @const {number} */ var MAX_LOGIN_HEADER_BAR_HEIGHT = 57; /** * Manages initialization of screens, transitions, and error messages. * @constructor * @extends {DisplayManager} */ function UserManager() {} cr.addSingletonGetter(UserManager); UserManager.prototype = { __proto__: DisplayManager.prototype, /** * Indicates that this is the Material Design Desktop User Manager. * @type {boolean} */ newDesktopUserManager: true, /** * Indicates whether the user pods page is visible. * @type {boolean} */ userPodsPageVisible: true, /** * @override * Overrides clientAreaSize in DisplayManager. When a new profile is created * the user pods page may not be visible yet, so user-pods cannot be * placed correctly. Therefore, we use dimensions of the #animated-pages. * @type {{width: number, height: number}} */ get clientAreaSize() { var userManagerPages = document.querySelector('user-manager-pages'); var width = userManagerPages.offsetWidth; // Deduct the maximum possible height of the #login-header-bar from the // height of #animated-pages. Result is the remaining visible height. var height = userManagerPages.offsetHeight - MAX_LOGIN_HEADER_BAR_HEIGHT; return {width: width, height: height}; } }; /** * Listens for the page change event to see if the user pods page is visible. * Updates userPodsPageVisible property accordingly and if the page is visible * re-arranges the user pods. * @param {!Event} event The event containing ID of the selected page. */ UserManager.onPageChanged_ = function(event) { var userPodsPageVisible = event.detail.page == 'user-pods-page'; cr.ui.UserManager.getInstance().userPodsPageVisible = userPodsPageVisible; if (userPodsPageVisible) $('pod-row').rebuildPods(); }; /** * Initializes the UserManager. */ UserManager.initialize = function() { cr.ui.login.DisplayManager.initialize(); login.AccountPickerScreen.register(); cr.ui.Bubble.decorate($('bubble')); signin.ProfileBrowserProxyImpl.getInstance().initializeUserManager( window.location.hash); cr.addWebUIListener('show-error-dialog', cr.ui.UserManager.showErrorDialog); }; /** * Shows the given screen. * @param {boolean} showGuest True if 'Browse as Guest' button should be * displayed. * @param {boolean} showAddPerson True if 'Add Person' button should be * displayed. */ UserManager.showUserManagerScreen = function(showGuest, showAddPerson) { UserManager.getInstance().showScreen({id: 'account-picker', data: {disableAddUser: false}}); // Hide control options if the user does not have the right permissions. var controlBar = document.querySelector('control-bar'); controlBar.showGuest = showGuest; controlBar.showAddPerson = showAddPerson; // Disable the context menu, as the Print/Inspect element items don't // make sense when displayed as a widget. document.addEventListener('contextmenu', function(e) {e.preventDefault();}); if (window.location.hash == '#tutorial') document.querySelector('user-manager-tutorial').startTutorial(); else if (window.location.hash == '#create-user') { document.querySelector('user-manager-pages').setSelectedPage( 'create-user-page'); } }; /** * Open a new browser for the given profile. * @param {string} profilePath The profile's path. */ UserManager.launchUser = function(profilePath) { signin.ProfileBrowserProxyImpl.getInstance().launchUser(profilePath); }; /** * Disables signin UI. */ UserManager.disableSigninUI = function() { DisplayManager.disableSigninUI(); }; /** * Shows signin UI. * @param {string=} opt_email An optional email for signin UI. */ UserManager.showSigninUI = function(opt_email) { DisplayManager.showSigninUI(opt_email); }; /** * Shows sign-in error bubble. * @param {number} loginAttempts Number of login attempts tried. * @param {string} message Error message to show. * @param {string} link Text to use for help link. * @param {number} helpId Help topic Id associated with help link. */ UserManager.showSignInError = function(loginAttempts, message, link, helpId) { DisplayManager.showSignInError(loginAttempts, message, link, helpId); }; /** * Clears error bubble as well as optional menus that could be open. */ UserManager.clearErrors = function() { DisplayManager.clearErrors(); }; /** * Shows the error dialog populated with the given message. * @param {string} message Error message to show. */ UserManager.showErrorDialog = function(message) { document.querySelector('error-dialog').show(message); }; // Export return { UserManager: UserManager }; }); // Alias to Oobe for use in src/ui/login/account_picker/user_pod_row.js var Oobe = cr.ui.UserManager; // Allow selection events on components with editable text (password field) // bug (http://code.google.com/p/chromium/issues/detail?id=125863) disableTextSelectAndDrag(function(e) { var src = e.target; return src instanceof HTMLTextAreaElement || src instanceof HTMLInputElement && /text|password|search/.test(src.type); }); document.addEventListener('DOMContentLoaded', cr.ui.UserManager.initialize); document.addEventListener('change-page', cr.ui.UserManager.onPageChanged_); // Copyright 2016 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. /** * @fileoverview 'user-manager-pages' is the element that controls paging in the * user manager screen. */ Polymer({ is: 'user-manager-pages', properties: { /** * ID of the currently selected page. * @private {string} */ selectedPage_: { type: String, value: 'user-pods-page' }, /** * Data passed to the currently selected page. * @private {?Object} */ pageData_: { type: Object, value: null } }, listeners: { 'change-page': 'onChangePage_' }, /** * Handler for the change-page event. * @param {Event} e The event containing ID of the page that is to be selected * and the optional data to be passed to the page. * @private */ onChangePage_: function(e) { this.setSelectedPage(e.detail.page, e.detail.data); }, /** * Sets the selected page. * @param {string} pageId ID of the page that is to be selected. * @param {Object=} opt_pageData Optional data to be passed to the page. */ setSelectedPage: function(pageId, opt_pageData) { this.pageData_ = opt_pageData || null; this.selectedPage_ = pageId; }, /** * Returns True if the first argument is present in the given set of values. * @param {string} selectedPage ID of the currently selected page. * @param {...string} var_args Pages IDs to check the first argument against. * @return {boolean} */ isPresentIn_: function(selectedPage, var_args) { var pages = Array.prototype.slice.call(arguments, 1); return pages.indexOf(selectedPage) !== -1; } }); // Copyright 2016 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. /** * @fileoverview 'user-manager-tutorial' is the element that controls the * tutorial steps for the user manager page. */ (function() { /** @enum {string} */ var TutorialSteps = { YOUR_CHROME: 'yourChrome', FRIENDS: 'friends', GUESTS: 'guests', COMPLETE: 'complete', NOT_YOU: 'notYou' }; Polymer({ is: 'user-manager-tutorial', properties: { /** * True if the tutorial is currently hidden. * @private {boolean} */ hidden_: { type: Boolean, value: true }, /** * Current tutorial step ID. * @type {string} */ currentStep_: { type: String, value: '' }, /** * Enum values for the step IDs. * @private {TutorialSteps} */ steps_: { readOnly: true, type: Object, value: TutorialSteps } }, /** * Determines whether a given step is displaying. * @param {string} currentStep Index of the current step * @param {string} step Name of the given step * @return {boolean} * @private */ isStepHidden_: function(currentStep, step) { return currentStep != step; }, /** * Navigates to the next step. * @param {!Event} event * @private */ onNextTap_: function(event) { var element = Polymer.dom(event).rootTarget; this.currentStep_ = element.dataset.next; }, /** * Handler for the link in the last step. Takes user to the create-profile * page in order to add a new profile. * @param {!Event} event * @private */ onAddUserTap_: function(event) { this.onDissmissTap_(); // Event is caught by user-manager-pages. this.fire('change-page', {page: 'create-user-page'}); }, /** * Starts the tutorial. */ startTutorial: function() { this.currentStep_ = TutorialSteps.YOUR_CHROME; this.hidden_ = false; // If there's only one pod, show the steps to the side of the pod. // Otherwise, center the steps and disable interacting with the pods // while the tutorial is showing. var podRow = /** @type {{focusPod: !function(), pods: !Array}} */ ($('pod-row')); this.classList.toggle('single-pod', podRow.pods.length == 1); podRow.focusPod(); // No focused pods. $('inner-container').classList.add('disabled'); }, /** * Ends the tutorial. * @private */ onDissmissTap_: function() { $('inner-container').classList.remove('disabled'); this.hidden_ = true; } }); })();

    /* Copyright 2013 The Chromium Authors. All rights reserved. * Use of this source code is governed by a BSD-style license that can be * found in the LICENSE file. */ .header { color: rgb(74, 142, 230); font-size: 100%; margin-bottom: 0; } #token-list { width: 100%; } tr:nth-child(odd) { background: rgb(239, 243, 255); } td.label { font-weight: bold; vertical-align: top; } td.token-actions { text-align: center; } // Copyright 2013 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. cr.define('identity_internals', function() { 'use strict'; /** * Creates an identity token item. * @param {!Object} tokenInfo Object containing token information. * @constructor */ function TokenListItem(tokenInfo) { var el = cr.doc.createElement('div'); el.data_ = tokenInfo; el.__proto__ = TokenListItem.prototype; el.decorate(); return el; } TokenListItem.prototype = { __proto__: HTMLDivElement.prototype, /** @override */ decorate: function() { this.textContent = ''; this.id = this.data_.accessToken; var table = this.ownerDocument.createElement('table'); var tbody = this.ownerDocument.createElement('tbody'); tbody.appendChild(this.createEntry_( 'accessToken', this.data_.accessToken, 'access-token')); tbody.appendChild(this.createEntry_( 'extensionName', this.data_.extensionName, 'extension-name')); tbody.appendChild(this.createEntry_( 'extensionId', this.data_.extensionId, 'extension-id')); tbody.appendChild(this.createEntry_( 'tokenStatus', this.data_.status, 'token-status')); tbody.appendChild(this.createEntry_( 'expirationTime', this.data_.expirationTime, 'expiration-time')); tbody.appendChild(this.createEntryForScopes_()); table.appendChild(tbody); var tfoot = this.ownerDocument.createElement('tfoot'); tfoot.appendChild(this.createButtons_()); table.appendChild(tfoot); this.appendChild(table); }, /** * Creates an entry for a single property of the token. * @param {string} label An i18n label of the token's property name. * @param {string} value A value of the token property. * @param {string} accessor Additional class to tag the field for testing. * @return {HTMLElement} An HTML element with the property name and value. */ createEntry_: function(label, value, accessor) { var row = this.ownerDocument.createElement('tr'); var labelField = this.ownerDocument.createElement('td'); labelField.classList.add('label'); labelField.textContent = loadTimeData.getString(label); row.appendChild(labelField); var valueField = this.ownerDocument.createElement('td'); valueField.classList.add('value'); valueField.classList.add(accessor); valueField.textContent = value; row.appendChild(valueField); return row; }, /** * Creates an entry for a list of token scopes. * @return {!HTMLElement} An HTML element with scopes. */ createEntryForScopes_: function() { var row = this.ownerDocument.createElement('tr'); var labelField = this.ownerDocument.createElement('td'); labelField.classList.add('label'); labelField.textContent = loadTimeData.getString('scopes'); row.appendChild(labelField); var valueField = this.ownerDocument.createElement('td'); valueField.classList.add('value'); valueField.classList.add('scope-list'); this.data_.scopes.forEach(function(scope) { valueField.appendChild(this.ownerDocument.createTextNode(scope)); valueField.appendChild(this.ownerDocument.createElement('br')); }, this); row.appendChild(valueField); return row; }, /** * Creates buttons for the token. * @return {HTMLElement} An HTML element with actionable buttons for the * token. */ createButtons_: function() { var row = this.ownerDocument.createElement('tr'); var buttonHolder = this.ownerDocument.createElement('td'); buttonHolder.colSpan = 2; buttonHolder.classList.add('token-actions'); buttonHolder.appendChild(this.createRevokeButton_()); row.appendChild(buttonHolder); return row; }, /** * Creates a revoke button with an event sending a revoke token message * to the controller. * @return {!HTMLButtonElement} The created revoke button. * @private */ createRevokeButton_: function() { var revokeButton = this.ownerDocument.createElement('button'); revokeButton.classList.add('revoke-button'); revokeButton.addEventListener('click', function() { chrome.send('identityInternalsRevokeToken', [this.data_.extensionId, this.data_.accessToken]); }.bind(this)); revokeButton.textContent = loadTimeData.getString('revoke'); return revokeButton; }, }; /** * Creates a new list of identity tokens. * @param {Object=} opt_propertyBag Optional properties. * @constructor * @extends {cr.ui.div} */ var TokenList = cr.ui.define('div'); TokenList.prototype = { __proto__: HTMLDivElement.prototype, /** @override */ decorate: function() { this.textContent = ''; this.showTokenNodes_(); }, /** * Populates the list of tokens. */ showTokenNodes_: function() { this.data_.forEach(function(tokenInfo) { this.appendChild(new TokenListItem(tokenInfo)); }, this); }, /** * Removes a token node related to the specifed token ID from both the * internals data source as well as the user internface. * @param {string} accessToken The id of the token to remove. * @private */ removeTokenNode_: function(accessToken) { var tokenIndex; for (var index = 0; index < this.data_.length; index++) { if (this.data_[index].accessToken == accessToken) { tokenIndex = index; break; } } // Remove from the data_ source if token found. if (tokenIndex) this.data_.splice(tokenIndex, 1); // Remove from the user interface. var tokenNode = $(accessToken); if (tokenNode) this.removeChild(tokenNode); }, }; var tokenList_; /** * Initializes the UI by asking the contoller for list of identity tokens. */ function initialize() { chrome.send('identityInternalsGetTokens'); tokenList_ = $('token-list'); tokenList_.data_ = []; tokenList_.__proto__ = TokenList.prototype; tokenList_.decorate(); } /** * Callback function accepting a list of tokens to be displayed. * @param {!Token[]} tokens A list of tokens to be displayed */ function returnTokens(tokens) { tokenList_.data_ = tokens; tokenList_.showTokenNodes_(); } /** * Callback function that removes a token from UI once it has been revoked. * @param {!Array} accessTokens Array with a single element, which is * an access token to be removed. */ function tokenRevokeDone(accessTokens) { assert(accessTokens.length > 0); tokenList_.removeTokenNode_(accessTokens[0]); } // Return an object with all of the exports. return { initialize: initialize, returnTokens: returnTokens, tokenRevokeDone: tokenRevokeDone, }; }); document.addEventListener('DOMContentLoaded', identity_internals.initialize);
    // Copyright 2014 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. var DeviceLogUI = (function() { 'use strict'; /** * Creates a tag for the log level. * * @param {string} level A string that represents log level. * @return {HTMLSpanElement} The created span element. */ var createLevelTag = function(level) { var levelClassName = 'log-level-' + level.toLowerCase(); var tag = document.createElement('span'); tag.textContent = level; tag.className = 'level-tag ' + levelClassName; return tag; }; /** * Creates a tag for the log type. * * @param {string} level A string that represents log type. * @return {HTMLSpanElement} The created span element. */ var createTypeTag = function(type) { var typeClassName = 'log-type-' + type.toLowerCase(); var tag = document.createElement('span'); tag.textContent = type; tag.className = 'type-tag ' + typeClassName; return tag; }; /** * Creates an element that contains the time, the event, the level and * the description of the given log entry. * * @param {Object} logEntry An object that represents a single line of log. * @return {?HTMLParagraphElement} The created p element that represents * the log entry, or null if the entry should be skipped. */ var createLogEntryText = function(logEntry) { var level = logEntry['level']; var levelCheckbox = 'log-level-' + level.toLowerCase(); if ($(levelCheckbox) && !$(levelCheckbox).checked) return null; var type = logEntry['type']; var typeCheckbox = 'log-type-' + type.toLowerCase(); if ($(typeCheckbox) && !$(typeCheckbox).checked) return null; var res = document.createElement('p'); var textWrapper = document.createElement('span'); var fileinfo = ''; if ($('log-fileinfo').checked) fileinfo = logEntry['file']; var timestamp = ''; if ($('log-timedetail').checked) timestamp = logEntry['timestamp']; else timestamp = logEntry['timestampshort']; textWrapper.textContent = loadTimeData.getStringF( 'logEntryFormat', timestamp, fileinfo, logEntry['event']); res.appendChild(createTypeTag(type)); res.appendChild(createLevelTag(level)); res.appendChild(textWrapper); return res; }; /** * Creates event log entries. * * @param {Array} logEntries An array of strings that represent log * log events in JSON format. */ var createEventLog = function(logEntries) { var container = $('log-container'); container.textContent = ''; for (var i = 0; i < logEntries.length; ++i) { var entry = createLogEntryText(JSON.parse(logEntries[i])); if (entry) container.appendChild(entry); } }; /** * Callback function, triggered when the log is received. * * @param {Object} data A JSON structure of event log entries. */ var getLogCallback = function(data) { try { createEventLog(JSON.parse(data)); } catch(e) { var container = $('log-container'); container.textContent = 'No log entries'; } }; /** * Requests a log update. */ var requestLog = function() { chrome.send('DeviceLog.getLog'); }; /** * Sets refresh rate if the interval is found in the url. */ var setRefresh = function() { var interval = parseQueryParams(window.location)['refresh']; if (interval && interval != '') setInterval(requestLog, parseInt(interval) * 1000); }; /** * Gets log information from WebUI. */ document.addEventListener('DOMContentLoaded', function() { // Show all levels except 'debug' by default. $('log-level-error').checked = true; $('log-level-user').checked = true; $('log-level-event').checked = true; $('log-level-debug').checked = false; // Show all types by default. var checkboxes = document.querySelectorAll( '#log-checkbox-container input[type="checkbox"][id*="log-type"]'); for (var i = 0; i < checkboxes.length; ++i) checkboxes[i].checked = true; $('log-fileinfo').checked = false; $('log-timedetail').checked = false; $('log-refresh').onclick = requestLog; checkboxes = document.querySelectorAll( '#log-checkbox-container input[type="checkbox"]'); for (var i = 0; i < checkboxes.length; ++i) checkboxes[i].onclick = requestLog; setRefresh(); requestLog(); }); return { getLogCallback: getLogCallback }; })(); /* Copyright 2014 The Chromium Authors. All rights reserved. * Use of this source code is governed by a BSD-style license that can be * found in the LICENSE file. */ html { height: 100%; } body { display: flex; flex-direction: column; height: 100%; margin: 0; } #header { margin: 5px; } /* Checkboxes */ #log-checkbox-container { margin: 5px; } #log-checkbox-container button { -webkit-margin-end: 8px; } #log-checkbox-container label { vertical-align: middle; } #log-checkbox-show { font-weight: bold; } #log-checkbox-container input { margin-bottom: 1px; vertical-align: middle; } /* Log */ #log-container { border: 1px solid rgb(220, 220, 220); flex: 1 1 100%; font-size: 12px; margin: 5px; overflow: auto; padding: 10px; } #log-container p { font-family: monospace; line-height: 20px; margin: 2px; } /* Log Level tags */ .level-tag { -webkit-margin-end: 5px; border: 1px solid; border-radius: 2px; float: left; height: 14px; margin-top: 2px; padding: 0 4px 2px 4px; width: 50px; } .log-level-error { color: red; } .log-level-user { color: blue; } .log-level-event { color: black; } .log-level-debug { color: grey; } /* Log Type tags */ .type-tag { -webkit-margin-end: 5px; border: 1px solid; border-radius: 2px; float: left; font-weight: bold; height: 14px; margin-top: 2px; padding: 0 4px 2px 4px; width: 65px; } .log-type-login { color: darkgreen; } .log-type-network { color: darkblue; } .log-type-power { color: purple; } { "name": "Smart Lock", "description": "This app allows you to unlock your device when in proximity to your phone.", "version": "1.0", "key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqOUeUl1nC6qTz6WwVUIaAJ4ukXVzgeCAumX4TZlCHFk5DLHImHLDBxakyVGaQFLS9iEQ3tDTsJLIoA+FkbWKNX7bvDW/qM89CeVNZsIZRGw898m8J78N6dJHwP9aZSI8CpoMK2KvjANpuj1tdWs1OM6v65zRUu6y4Mq876dr5AcPiuznGxl8jekagBwGu8jqMySsJxLazj/EfQ3W1E7mpyHd0Z4C1qNwJoFlUQeMjn6gfPZqa06BLU6YznzCUesiyjFK3d1vzbN54ZkVxhcA6ekwLKYLqKykBFLmIQG0gkNNePzcGXju8p34dGJgkcZw0sOXrtNaLSe1su0zfcniIwIDAQAB", "oauth2": { "client_id": "383927464186-v05g3e5emhrrblqmpnvq7666jktlpc7q.apps.googleusercontent.com", "auto_approve": true, "scopes": [ "https://www.googleapis.com/auth/proximity_auth", "https://www.googleapis.com/auth/cryptauth" ] }, "permissions": [ // Public APIs: "alarms", "browser", "gcm", "identity", "notifications", "storage", "system.display", // Private APIs: "bluetoothPrivate", "chromeosInfoPrivate", "easyUnlockPrivate", "feedbackPrivate", "metricsPrivate", "preferencesPrivate", "screenlockPrivate", "systemPrivate" ], "app": { "background": { "scripts": ["easy_unlock_background.js"] } }, "bluetooth": { "socket" : true, "low_energy" : true, "uuids": [ "704EE561-3782-405A-A14B-2D47A2DDCDDF", // Unlock UUID "29422880-D56D-11E3-9C1A-0800200C9A66" // Setup UUID ] }, "offline_enabled": true, "display_in_launcher": false, "icons": { "32": "icons/easyunlock_app_icon_32.png", "48": "icons/easyunlock_app_icon_48.png", "64": "icons/easyunlock_app_icon_64.png", "96": "icons/easyunlock_app_icon_96.png", "128": "icons/easyunlock_app_icon_128.png", "256": "icons/easyunlock_app_icon_256.png" } } { "name": "Smart Lock", "description": "This app allows you to sign-in to a device when in proximity to your phone.", "version": "1.1", "key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqOUeUl1nC6qTz6WwVUIaAJ4ukXVzgeCAumX4TZlCHFk5DLHImHLDBxakyVGaQFLS9iEQ3tDTsJLIoA+FkbWKNX7bvDW/qM89CeVNZsIZRGw898m8J78N6dJHwP9aZSI8CpoMK2KvjANpuj1tdWs1OM6v65zRUu6y4Mq876dr5AcPiuznGxl8jekagBwGu8jqMySsJxLazj/EfQ3W1E7mpyHd0Z4C1qNwJoFlUQeMjn6gfPZqa06BLU6YznzCUesiyjFK3d1vzbN54ZkVxhcA6ekwLKYLqKykBFLmIQG0gkNNePzcGXju8p34dGJgkcZw0sOXrtNaLSe1su0zfcniIwIDAQAB", "permissions": [ // Public APIs: "alarms", "browser", "gcm", "identity", "notifications", "storage", "system.display", // Private APIs: "bluetoothPrivate", "chromeosInfoPrivate", "easyUnlockPrivate", "feedbackPrivate", "metricsPrivate", "preferencesPrivate", "screenlockPrivate", "systemPrivate" ], "app": { "background": { "scripts": ["easy_unlock_background.js"] } }, "bluetooth": { "socket" : true, "low_energy" : true, "uuids": [ "704EE561-3782-405A-A14B-2D47A2DDCDDF" // Unlock UUID ] }, "offline_enabled": true, "display_in_launcher": false, "incognito": "split", "icons": { "32": "icons/easyunlock_app_icon_32.png", "48": "icons/easyunlock_app_icon_48.png", "64": "icons/easyunlock_app_icon_64.png", "96": "icons/easyunlock_app_icon_96.png", "128": "icons/easyunlock_app_icon_128.png", "256": "icons/easyunlock_app_icon_256.png" } } /* Copyright 2015 The Chromium Authors. All rights reserved. * Use of this source code is governed by a BSD-style license that can be * found in the LICENSE file. */ :root { --dialog-padding-end: 26px; --dialog-padding-start: 16px; --navigation-icon-button-size: 36px; --non-navigation-icon-size: 16px; } .button { color: var(--paper-blue-700); cursor: pointer; text-align: center; } [hidden] { display: none !important; } /* Copyright 2015 The Chromium Authors. All rights reserved. * Use of this source code is governed by a BSD-style license that can be * found in the LICENSE file. */ body { font-family: Roboto; font-size: 0.75em; margin: 0; } #media-router-container { background-color: white; box-shadow: 0 3px 4px 0 rgba(0, 0, 0, 0.14), 0 1px 8px 0 rgba(0, 0, 0, 0.12), 0 3px 3px -2px rgba(0, 0, 0, 0.4); display: flex; flex-direction: column; margin-bottom: 1px; width: calc(100% - 1px); } // Copyright 2015 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. // Any strings used here will already be localized. Values such as // CastMode.type or IDs will be defined elsewhere and determined later. cr.exportPath('media_router'); /** * This corresponds to the C++ MediaCastMode, with the exception of AUTO. * See below for details. Note to support fast bitset operations, the values * here are (1 << [corresponding value in MR]). * @enum {number} */ media_router.CastModeType = { // Note: AUTO mode is only used to configure the sink list container to show // all sinks. Individual sinks are configured with a specific cast mode // (DEFAULT, TAB_MIRROR, DESKTOP_MIRROR). AUTO: -1, DEFAULT: 0x1, TAB_MIRROR: 0x2, DESKTOP_MIRROR: 0x4, }; /** * The ESC key maps to KeyboardEvent.key value 'Escape'. * @const {string} */ media_router.KEY_ESC = 'Escape'; /** * This corresponds to the C++ MediaRouterMetrics * MediaRouterRouteCreationOutcome. * @enum {number} */ media_router.MediaRouterRouteCreationOutcome = { SUCCESS: 0, FAILURE_NO_ROUTE: 1, FAILURE_INVALID_SINK: 2, }; /** * This corresponds to the C++ MediaRouterMetrics MediaRouterUserAction. * @enum {number} */ media_router.MediaRouterUserAction = { CHANGE_MODE: 0, START_LOCAL: 1, STOP_LOCAL: 2, CLOSE: 3, STATUS_REMOTE: 4, REPLACE_LOCAL_ROUTE: 5, }; /** * The possible states of the Media Router dialog. Used to determine which * components to show. * @enum {string} */ media_router.MediaRouterView = { CAST_MODE_LIST: 'cast-mode-list', FILTER: 'filter', ISSUE: 'issue', ROUTE_DETAILS: 'route-details', SINK_LIST: 'sink-list', }; /** * The minimum number of sinks to have to enable the search input strictly for * filtering (i.e. the Media Router doesn't support search so the search input * only filters existing sinks). * @const {number} */ media_router.MINIMUM_SINKS_FOR_SEARCH = 20; /** * This corresponds to the C++ MediaSink IconType, and the order must stay in * sync. * @enum {number} */ media_router.SinkIconType = { CAST: 0, CAST_AUDIO_GROUP: 1, CAST_AUDIO: 2, HANGOUT: 3, GENERIC: 4, }; /** * @enum {string} */ media_router.SinkStatus = { IDLE: 'idle', ACTIVE: 'active', REQUEST_PENDING: 'request_pending' }; cr.define('media_router', function() { 'use strict'; /** * @param {number} type The type of cast mode. * @param {string} description The description of the cast mode. * @param {?string} host The hostname of the site to cast. * @constructor * @struct */ var CastMode = function(type, description, host) { /** @type {number} */ this.type = type; /** @type {string} */ this.description = description; /** @type {?string} */ this.host = host || null; }; /** * Placeholder object for AUTO cast mode. See comment in CastModeType. * @const {!media_router.CastMode} */ var AUTO_CAST_MODE = new CastMode(media_router.CastModeType.AUTO, loadTimeData.getString('autoCastMode'), null); /** * @param {number} id The ID of this issue. * @param {string} title The issue title. * @param {string} message The issue message. * @param {number} defaultActionType The type of default action. * @param {number|undefined} secondaryActionType The type of optional action. * @param {?string} routeId The route ID to which this issue * pertains. If not set, this is a global issue. * @param {boolean} isBlocking True if this issue blocks other UI. * @param {?number} helpPageId The numeric help center ID. * @constructor * @struct */ var Issue = function(id, title, message, defaultActionType, secondaryActionType, routeId, isBlocking, helpPageId) { /** @type {number} */ this.id = id; /** @type {string} */ this.title = title; /** @type {string} */ this.message = message; /** @type {number} */ this.defaultActionType = defaultActionType; /** @type {number|undefined} */ this.secondaryActionType = secondaryActionType; /** @type {?string} */ this.routeId = routeId; /** @type {boolean} */ this.isBlocking = isBlocking; /** @type {?number} */ this.helpPageId = helpPageId; }; /** * @param {string} id The media route ID. * @param {string} sinkId The ID of the media sink running this route. * @param {string} description The short description of this route. * @param {?number} tabId The ID of the tab in which web app is running and * accessing the route. * @param {boolean} isLocal True if this is a locally created route. * @param {boolean} canJoin True if this route can be joined. * @param {?string} customControllerPath non-empty if this route has custom * controller. * @constructor * @struct */ var Route = function(id, sinkId, description, tabId, isLocal, canJoin, customControllerPath) { /** @type {string} */ this.id = id; /** @type {string} */ this.sinkId = sinkId; /** @type {string} */ this.description = description; /** @type {?number} */ this.tabId = tabId; /** @type {boolean} */ this.isLocal = isLocal; /** @type {boolean} */ this.canJoin = canJoin; /** @type {number|undefined} */ this.currentCastMode = undefined; /** @type {?string} */ this.customControllerPath = customControllerPath; }; /** * @param {string} id The ID of the media sink. * @param {string} name The name of the sink. * @param {?string} description Optional description of the sink. * @param {?string} domain Optional domain of the sink. * @param {media_router.SinkIconType} iconType the type of icon for the sink. * @param {media_router.SinkStatus} status The readiness state of the sink. * @param {number} castModes Bitset of cast modes compatible with the sink. * @constructor * @struct */ var Sink = function(id, name, description, domain, iconType, status, castModes) { /** @type {string} */ this.id = id; /** @type {string} */ this.name = name; /** @type {?string} */ this.description = description; /** @type {?string} */ this.domain = domain; /** @type {!media_router.SinkIconType} */ this.iconType = iconType; /** @type {!media_router.SinkStatus} */ this.status = status; /** @type {number} */ this.castModes = castModes; /** @type {boolean} */ this.isPseudoSink = false; }; /** * @param {number} tabId The current tab ID. * @param {string} domain The domain of the current tab. * @constructor * @struct */ var TabInfo = function(tabId, domain) { /** @type {number} */ this.tabId = tabId; /** @type {string} */ this.domain = domain; }; return { AUTO_CAST_MODE: AUTO_CAST_MODE, CastMode: CastMode, Issue: Issue, Route: Route, Sink: Sink, TabInfo: TabInfo, }; }); // Copyright 2015 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. // // Copyright 2015 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. // Any strings used here will already be localized. Values such as // CastMode.type or IDs will be defined elsewhere and determined later. cr.exportPath('media_router'); /** * This corresponds to the C++ MediaCastMode, with the exception of AUTO. * See below for details. Note to support fast bitset operations, the values * here are (1 << [corresponding value in MR]). * @enum {number} */ media_router.CastModeType = { // Note: AUTO mode is only used to configure the sink list container to show // all sinks. Individual sinks are configured with a specific cast mode // (DEFAULT, TAB_MIRROR, DESKTOP_MIRROR). AUTO: -1, DEFAULT: 0x1, TAB_MIRROR: 0x2, DESKTOP_MIRROR: 0x4, }; /** * The ESC key maps to KeyboardEvent.key value 'Escape'. * @const {string} */ media_router.KEY_ESC = 'Escape'; /** * This corresponds to the C++ MediaRouterMetrics * MediaRouterRouteCreationOutcome. * @enum {number} */ media_router.MediaRouterRouteCreationOutcome = { SUCCESS: 0, FAILURE_NO_ROUTE: 1, FAILURE_INVALID_SINK: 2, }; /** * This corresponds to the C++ MediaRouterMetrics MediaRouterUserAction. * @enum {number} */ media_router.MediaRouterUserAction = { CHANGE_MODE: 0, START_LOCAL: 1, STOP_LOCAL: 2, CLOSE: 3, STATUS_REMOTE: 4, REPLACE_LOCAL_ROUTE: 5, }; /** * The possible states of the Media Router dialog. Used to determine which * components to show. * @enum {string} */ media_router.MediaRouterView = { CAST_MODE_LIST: 'cast-mode-list', FILTER: 'filter', ISSUE: 'issue', ROUTE_DETAILS: 'route-details', SINK_LIST: 'sink-list', }; /** * The minimum number of sinks to have to enable the search input strictly for * filtering (i.e. the Media Router doesn't support search so the search input * only filters existing sinks). * @const {number} */ media_router.MINIMUM_SINKS_FOR_SEARCH = 20; /** * This corresponds to the C++ MediaSink IconType, and the order must stay in * sync. * @enum {number} */ media_router.SinkIconType = { CAST: 0, CAST_AUDIO_GROUP: 1, CAST_AUDIO: 2, HANGOUT: 3, GENERIC: 4, }; /** * @enum {string} */ media_router.SinkStatus = { IDLE: 'idle', ACTIVE: 'active', REQUEST_PENDING: 'request_pending' }; cr.define('media_router', function() { 'use strict'; /** * @param {number} type The type of cast mode. * @param {string} description The description of the cast mode. * @param {?string} host The hostname of the site to cast. * @constructor * @struct */ var CastMode = function(type, description, host) { /** @type {number} */ this.type = type; /** @type {string} */ this.description = description; /** @type {?string} */ this.host = host || null; }; /** * Placeholder object for AUTO cast mode. See comment in CastModeType. * @const {!media_router.CastMode} */ var AUTO_CAST_MODE = new CastMode(media_router.CastModeType.AUTO, loadTimeData.getString('autoCastMode'), null); /** * @param {number} id The ID of this issue. * @param {string} title The issue title. * @param {string} message The issue message. * @param {number} defaultActionType The type of default action. * @param {number|undefined} secondaryActionType The type of optional action. * @param {?string} routeId The route ID to which this issue * pertains. If not set, this is a global issue. * @param {boolean} isBlocking True if this issue blocks other UI. * @param {?number} helpPageId The numeric help center ID. * @constructor * @struct */ var Issue = function(id, title, message, defaultActionType, secondaryActionType, routeId, isBlocking, helpPageId) { /** @type {number} */ this.id = id; /** @type {string} */ this.title = title; /** @type {string} */ this.message = message; /** @type {number} */ this.defaultActionType = defaultActionType; /** @type {number|undefined} */ this.secondaryActionType = secondaryActionType; /** @type {?string} */ this.routeId = routeId; /** @type {boolean} */ this.isBlocking = isBlocking; /** @type {?number} */ this.helpPageId = helpPageId; }; /** * @param {string} id The media route ID. * @param {string} sinkId The ID of the media sink running this route. * @param {string} description The short description of this route. * @param {?number} tabId The ID of the tab in which web app is running and * accessing the route. * @param {boolean} isLocal True if this is a locally created route. * @param {boolean} canJoin True if this route can be joined. * @param {?string} customControllerPath non-empty if this route has custom * controller. * @constructor * @struct */ var Route = function(id, sinkId, description, tabId, isLocal, canJoin, customControllerPath) { /** @type {string} */ this.id = id; /** @type {string} */ this.sinkId = sinkId; /** @type {string} */ this.description = description; /** @type {?number} */ this.tabId = tabId; /** @type {boolean} */ this.isLocal = isLocal; /** @type {boolean} */ this.canJoin = canJoin; /** @type {number|undefined} */ this.currentCastMode = undefined; /** @type {?string} */ this.customControllerPath = customControllerPath; }; /** * @param {string} id The ID of the media sink. * @param {string} name The name of the sink. * @param {?string} description Optional description of the sink. * @param {?string} domain Optional domain of the sink. * @param {media_router.SinkIconType} iconType the type of icon for the sink. * @param {media_router.SinkStatus} status The readiness state of the sink. * @param {number} castModes Bitset of cast modes compatible with the sink. * @constructor * @struct */ var Sink = function(id, name, description, domain, iconType, status, castModes) { /** @type {string} */ this.id = id; /** @type {string} */ this.name = name; /** @type {?string} */ this.description = description; /** @type {?string} */ this.domain = domain; /** @type {!media_router.SinkIconType} */ this.iconType = iconType; /** @type {!media_router.SinkStatus} */ this.status = status; /** @type {number} */ this.castModes = castModes; /** @type {boolean} */ this.isPseudoSink = false; }; /** * @param {number} tabId The current tab ID. * @param {string} domain The domain of the current tab. * @constructor * @struct */ var TabInfo = function(tabId, domain) { /** @type {number} */ this.tabId = tabId; /** @type {string} */ this.domain = domain; }; return { AUTO_CAST_MODE: AUTO_CAST_MODE, CastMode: CastMode, Issue: Issue, Route: Route, Sink: Sink, TabInfo: TabInfo, }; }); // // Copyright 2015 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. // API invoked by the browser MediaRouterWebUIMessageHandler to communicate // with this UI. cr.define('media_router.ui', function() { 'use strict'; // The media-router-container element. var container = null; // The media-router-header element. var header = null; /** * Handles response of previous create route attempt. * * @param {string} sinkId The ID of the sink to which the Media Route was * creating a route. * @param {?media_router.Route} route The newly created route that * corresponds to the sink if route creation succeeded; null otherwise. * @param {boolean} isForDisplay Whether or not |route| is for display. */ function onCreateRouteResponseReceived(sinkId, route, isForDisplay) { container.onCreateRouteResponseReceived(sinkId, route, isForDisplay); } /** * Handles the search response by forwarding |sinkId| to the container. * * @param {string} sinkId The ID of the sink found by search. */ function receiveSearchResult(sinkId) { container.onReceiveSearchResult(sinkId); } /** * Sets the cast mode list. * * @param {!Array} castModeList */ function setCastModeList(castModeList) { container.castModeList = castModeList; } /** * Sets |container| and |header|. * * @param {!MediaRouterContainerElement} mediaRouterContainer * @param {!MediaRouterHeaderElement} mediaRouterHeader */ function setElements(mediaRouterContainer, mediaRouterHeader) { container = mediaRouterContainer; header = mediaRouterHeader; } /** * Populates the WebUI with data obtained about the first run flow. * * @param {{firstRunFlowCloudPrefLearnMoreUrl: string, * firstRunFlowLearnMoreUrl: string, * wasFirstRunFlowAcknowledged: boolean, * showFirstRunFlowCloudPref: boolean}} data * Parameters in data: * firstRunFlowCloudPrefLearnMoreUrl - url to open when the cloud services * pref learn more link is clicked. * firstRunFlowLearnMoreUrl - url to open when the first run flow learn * more link is clicked. * wasFirstRunFlowAcknowledged - true if first run flow was previously * acknowledged by user. * showFirstRunFlowCloudPref - true if the cloud pref option should be * shown. */ function setFirstRunFlowData(data) { container.firstRunFlowCloudPrefLearnMoreUrl = data['firstRunFlowCloudPrefLearnMoreUrl']; container.firstRunFlowLearnMoreUrl = data['firstRunFlowLearnMoreUrl']; container.showFirstRunFlowCloudPref = data['showFirstRunFlowCloudPref']; // Some users acknowledged the first run flow before the cloud prefs // setting was implemented. These users will see the first run flow // again. container.showFirstRunFlow = !data['wasFirstRunFlowAcknowledged'] || container.showFirstRunFlowCloudPref; } /** * Populates the WebUI with data obtained from Media Router. * * @param {{deviceMissingUrl: string, * sinksAndIdentity: { * sinks: !Array, * showEmail: boolean, * userEmail: string, * showDomain: boolean * }, * routes: !Array, * castModes: !Array, * useTabMirroring: boolean}} data * Parameters in data: * deviceMissingUrl - url to be opened on "Device missing?" clicked. * sinksAndIdentity - list of sinks to be displayed and user identity. * routes - list of routes that are associated with the sinks. * castModes - list of available cast modes. * useTabMirroring - whether the cast mode should be set to TAB_MIRROR. */ function setInitialData(data) { container.deviceMissingUrl = data['deviceMissingUrl']; container.castModeList = data['castModes']; this.setSinkListAndIdentity(data['sinksAndIdentity']); container.routeList = data['routes']; container.maybeShowRouteDetailsOnOpen(); if (data['useTabMirroring']) container.selectCastMode(media_router.CastModeType.TAB_MIRROR); media_router.browserApi.onInitialDataReceived(); } /** * Sets current issue to |issue|, or clears the current issue if |issue| is * null. * * @param {?media_router.Issue} issue */ function setIssue(issue) { container.issue = issue; } /** * Sets the list of currently active routes. * * @param {!Array} routeList */ function setRouteList(routeList) { container.routeList = routeList; } /** * Sets the list of discovered sinks along with properties of whether to hide * identity of the user email and domain. * * @param {{sinks: !Array, * showEmail: boolean, * userEmail: string, * showDomain: boolean}} data * Parameters in data: * sinks - list of sinks to be displayed. * showEmail - true if the user email should be shown. * userEmail - email of the user if the user is signed in. * showDomain - true if the user domain should be shown. */ function setSinkListAndIdentity(data) { container.showDomain = data['showDomain']; container.allSinks = data['sinks']; header.userEmail = data['userEmail']; header.showEmail = data['showEmail']; } /** * Updates the max height of the dialog * * @param {number} height */ function updateMaxHeight(height) { container.updateMaxDialogHeight(height); } return { onCreateRouteResponseReceived: onCreateRouteResponseReceived, receiveSearchResult: receiveSearchResult, setCastModeList: setCastModeList, setElements: setElements, setFirstRunFlowData: setFirstRunFlowData, setInitialData: setInitialData, setIssue: setIssue, setRouteList: setRouteList, setSinkListAndIdentity: setSinkListAndIdentity, updateMaxHeight: updateMaxHeight, }; }); // API invoked by this UI to communicate with the browser WebUI message handler. cr.define('media_router.browserApi', function() { 'use strict'; /** * Indicates that the user has acknowledged the first run flow. * * @param {boolean} optedIntoCloudServices Whether or not the user opted into * cloud services. */ function acknowledgeFirstRunFlow(optedIntoCloudServices) { chrome.send('acknowledgeFirstRunFlow', [optedIntoCloudServices]); } /** * Acts on the given issue. * * @param {number} issueId * @param {number} actionType Type of action that the user clicked. * @param {?number} helpPageId The numeric help center ID. */ function actOnIssue(issueId, actionType, helpPageId) { chrome.send('actOnIssue', [{issueId: issueId, actionType: actionType, helpPageId: helpPageId}]); } /** * Modifies |route| by changing its source to the one identified by * |selectedCastMode|. * * @param {!media_router.Route} route The route being modified. * @param {number} selectedCastMode The value of the cast mode the user * selected. */ function changeRouteSource(route, selectedCastMode) { chrome.send('requestRoute', [{sinkId: route.sinkId, selectedCastMode: selectedCastMode}]); } /** * Closes the dialog. * * @param {boolean} pressEscToClose Whether the user pressed ESC to close the * dialog. */ function closeDialog(pressEscToClose) { chrome.send('closeDialog', [pressEscToClose]); } /** * Closes the given route. * * @param {!media_router.Route} route */ function closeRoute(route) { chrome.send('closeRoute', [{routeId: route.id, isLocal: route.isLocal}]); } /** * Joins the given route. * * @param {!media_router.Route} route */ function joinRoute(route) { chrome.send('joinRoute', [{sinkId: route.sinkId, routeId: route.id}]); } /** * Indicates that the initial data has been received. */ function onInitialDataReceived() { chrome.send('onInitialDataReceived'); } /** * Reports when the user clicks outside the dialog. */ function reportBlur() { chrome.send('reportBlur'); } /** * Reports the index of the selected sink. * * @param {number} sinkIndex */ function reportClickedSinkIndex(sinkIndex) { chrome.send('reportClickedSinkIndex', [sinkIndex]); } /** * Reports that the user used the filter input. */ function reportFilter() { chrome.send('reportFilter'); } /** * Reports the initial dialog view. * * @param {string} view */ function reportInitialState(view) { chrome.send('reportInitialState', [view]); } /** * Reports the initial action the user took. * * @param {number} action */ function reportInitialAction(action) { chrome.send('reportInitialAction', [action]); } /** * Reports the navigation to the specified view. * * @param {string} view */ function reportNavigateToView(view) { chrome.send('reportNavigateToView', [view]); } /** * Reports whether or not a route was created successfully. * * @param {boolean} success */ function reportRouteCreation(success) { chrome.send('reportRouteCreation', [success]); } /** * Reports the outcome of a create route response. * * @param {number} outcome */ function reportRouteCreationOutcome(outcome) { chrome.send('reportRouteCreationOutcome', [outcome]); } /** * Reports the cast mode that the user selected. * * @param {number} castModeType */ function reportSelectedCastMode(castModeType) { chrome.send('reportSelectedCastMode', [castModeType]); } /** * Reports the current number of sinks. * * @param {number} sinkCount */ function reportSinkCount(sinkCount) { chrome.send('reportSinkCount', [sinkCount]); } /** * Reports the time it took for the user to select a sink after the sink list * is populated and shown. * * @param {number} timeMs */ function reportTimeToClickSink(timeMs) { chrome.send('reportTimeToClickSink', [timeMs]); } /** * Reports the time, in ms, it took for the user to close the dialog without * taking any other action. * * @param {number} timeMs */ function reportTimeToInitialActionClose(timeMs) { chrome.send('reportTimeToInitialActionClose', [timeMs]); } /** * Requests data to initialize the WebUI with. * The data will be returned via media_router.ui.setInitialData. */ function requestInitialData() { chrome.send('requestInitialData'); } /** * Requests that a media route be started with the given sink. * * @param {string} sinkId The sink ID. * @param {number} selectedCastMode The value of the cast mode the user * selected. */ function requestRoute(sinkId, selectedCastMode) { chrome.send('requestRoute', [{sinkId: sinkId, selectedCastMode: selectedCastMode}]); } /** * Requests that the media router search all providers for a sink matching * |searchCriteria| that can be used with the media source associated with the * cast mode |selectedCastMode|. If such a sink is found, a route is also * created between the sink and the media source. * * @param {string} sinkId Sink ID of the pseudo sink generating the request. * @param {string} searchCriteria Search criteria for the route providers. * @param {string} domain User's current hosted domain. * @param {number} selectedCastMode The value of the cast mode to be used with * the sink. */ function searchSinksAndCreateRoute( sinkId, searchCriteria, domain, selectedCastMode) { chrome.send('searchSinksAndCreateRoute', [{sinkId: sinkId, searchCriteria: searchCriteria, domain: domain, selectedCastMode: selectedCastMode}]); } return { acknowledgeFirstRunFlow: acknowledgeFirstRunFlow, actOnIssue: actOnIssue, changeRouteSource: changeRouteSource, closeDialog: closeDialog, closeRoute: closeRoute, joinRoute: joinRoute, onInitialDataReceived: onInitialDataReceived, reportBlur: reportBlur, reportClickedSinkIndex: reportClickedSinkIndex, reportFilter: reportFilter, reportInitialAction: reportInitialAction, reportInitialState: reportInitialState, reportNavigateToView: reportNavigateToView, reportRouteCreation: reportRouteCreation, reportRouteCreationOutcome: reportRouteCreationOutcome, reportSelectedCastMode: reportSelectedCastMode, reportSinkCount: reportSinkCount, reportTimeToClickSink: reportTimeToClickSink, reportTimeToInitialActionClose: reportTimeToInitialActionClose, requestInitialData: requestInitialData, requestRoute: requestRoute, searchSinksAndCreateRoute: searchSinksAndCreateRoute, }; }); // Handles user events for the Media Router UI. cr.define('media_router', function() { 'use strict'; /** * The media-router-container element. Initialized after polymer is ready. * @type {?MediaRouterContainerElement} */ var container = null; /** * Initializes the Media Router WebUI and requests initial media * router content, such as the media sink and media route lists. */ function initialize() { // For non-Mac platforms, request data immediately after initialization. if (!cr.isMac) onRequestInitialData(); container = /** @type {!MediaRouterContainerElement} */ ($('media-router-container')); media_router.ui.setElements(container, /** @type {!MediaRouterHeaderElement} */(container.header)); container.addEventListener('acknowledge-first-run-flow', onAcknowledgeFirstRunFlow); container.addEventListener('back-click', onNavigateToSinkList); container.addEventListener('cast-mode-selected', onCastModeSelected); container.addEventListener('change-route-source-click', onChangeRouteSourceClick); container.addEventListener('close-dialog', onCloseDialog); container.addEventListener('close-route', onCloseRoute); container.addEventListener('create-route', onCreateRoute); container.addEventListener('issue-action-click', onIssueActionClick); container.addEventListener('join-route-click', onJoinRouteClick); container.addEventListener('navigate-sink-list-to-details', onNavigateToDetails); container.addEventListener('navigate-to-cast-mode-list', onNavigateToCastMode); container.addEventListener('report-filter', onFilter); container.addEventListener('report-initial-action', onInitialAction); container.addEventListener('report-initial-action-close', onInitialActionClose); container.addEventListener('report-route-creation', onReportRouteCreation); container.addEventListener('report-sink-click-time', onSinkClickTimeReported); container.addEventListener('report-sink-count', onSinkCountReported); container.addEventListener('report-resolved-route', onReportRouteCreationOutcome); container.addEventListener('request-initial-data', onRequestInitialData); container.addEventListener('search-sinks-and-create-route', onSearchSinksAndCreateRoute); container.addEventListener('show-initial-state', onShowInitialState); container.addEventListener('sink-click', onSinkClick); window.addEventListener('blur', onWindowBlur); } /** * Requests that the Media Router searches for a sink with criteria * |event.detail.name|. * @param {!Event} event * Parameters in |event|.detail: * id - id of the pseudo sink generating the request. * name - sink search criteria. * domain - user's current domain. * selectedCastMode - type of cast mode selected by the user. */ function onSearchSinksAndCreateRoute(event) { /** @type {{id: string, domain: string, name: string, * selectedCastMode: number}} */ var detail = event.detail; media_router.browserApi.searchSinksAndCreateRoute( detail.id, detail.name, detail.domain, detail.selectedCastMode); } /** * Reports the selected cast mode. * Called when the user selects a cast mode from the picker. * * @param {!Event} event * Parameters in |event|.detail: * castModeType - type of cast mode selected by the user. */ function onCastModeSelected(event) { /** @type {{castModeType: number}} */ var detail = event.detail; media_router.browserApi.reportSelectedCastMode(detail.castModeType); } /** * Reports the route for which the users wants to replace the source and the * cast mode that should be used for the new source. * * @param {!Event} event The event object. * Parameters in |event|.detail: * route - route to modify. * selectedCastMode - type of cast mode selected by the user. */ function onChangeRouteSourceClick(event) { /** @type {{route: !media_router.Route, selectedCastMode: number}} */ var detail = event.detail; media_router.browserApi.changeRouteSource( detail.route, detail.selectedCastMode); } /** * Updates the preference that the user has seen the first run flow. * Called when the user clicks on the acknowledgement button on the first run * flow. * * @param {!Event} event * Parameters in |event|.detail: * optedIntoCloudServices - whether or not the user opted into cloud * services. */ function onAcknowledgeFirstRunFlow(event) { /** @type {{optedIntoCloudServices: boolean}} */ var detail = event.detail; media_router.browserApi.acknowledgeFirstRunFlow( detail.optedIntoCloudServices); } /** * Closes the dialog. * Called when the user clicks the close button on the dialog. Reports * whether the user closed the dialog via the ESC key. * * @param {!Event} event * Parameters in |event|.detail: * pressEscToClose - whether or not the user pressed ESC to close the * dialog. */ function onCloseDialog(event) { /** @type {{pressEscToClose: boolean}} */ var detail = event.detail; container.maybeReportUserFirstAction( media_router.MediaRouterUserAction.CLOSE); media_router.browserApi.closeDialog(detail.pressEscToClose); } /** * Reports when the user uses the filter input to filter the sink list. This * is reported at most once each time the user enters the filter view, and * only if text is actually entered in the filter input. */ function onFilter() { media_router.browserApi.reportFilter(); } /** * Reports the first action the user takes after opening the dialog. * Called when the user explicitly interacts with the dialog to perform an * action. * * @param {!Event} event * Parameters in |event|.detail: * action - the first action taken by the user. */ function onInitialAction(event) { /** @type {{action: number}} */ var detail = event.detail; media_router.browserApi.reportInitialAction(detail.action); } /** * Reports the time it took for the user to close the dialog if that was the * first action the user took after opening the dialog. * Called when the user closes the dialog without taking any other action. * * @param {!Event} event * Parameters in |event|.detail: * timeMs - time in ms for the user to close the dialog. */ function onInitialActionClose(event) { /** @type {{timeMs: number}} */ var detail = event.detail; media_router.browserApi.reportTimeToInitialActionClose(detail.timeMs); } /** * Acts on an issue and dismisses it from the UI. * Called when the user performs an action on an issue. * * @param {!Event} event * Parameters in |event|.detail: * id - issue ID. * actionType - type of action performed by the user. * helpPageId - the numeric help center ID. */ function onIssueActionClick(event) { /** @type {{id: number, actionType: number, helpPageId: number}} */ var detail = event.detail; media_router.browserApi.actOnIssue(detail.id, detail.actionType, detail.helpPageId); container.issue = null; } /** * Creates a media route. * Called when the user requests to create a media route. * * @param {!Event} event * Parameters in |event|.detail: * sinkId - sink ID selected by the user. * selectedCastModeValue - cast mode selected by the user. */ function onCreateRoute(event) { /** @type {{sinkId: string, selectedCastModeValue, number}} */ var detail = event.detail; media_router.browserApi.requestRoute(detail.sinkId, detail.selectedCastModeValue); } /** * Stops a route. * Called when the user requests to stop a media route. * * @param {!Event} event * Parameters in |event|.detail: * route - The route to close. */ function onCloseRoute(event) { /** @type {{route: !media_router.Route}} */ var detail = event.detail; media_router.browserApi.closeRoute(detail.route); } /** * Starts casting to an existing route. * Called when the user requests to start casting to a media route that is * joinable. * * @param {!Event} event * Parameters in |event|.detail: * route - The route to connect to if possible. */ function onJoinRouteClick(event) { /** @type {{route: !media_router.Route}} */ var detail = event.detail; media_router.browserApi.joinRoute(detail.route); } /** * Reports the user navigation to the cast mode view. * Called when the user clicks the drop arrow to navigate to the cast mode * view on the dialog. */ function onNavigateToCastMode() { media_router.browserApi.reportNavigateToView( media_router.MediaRouterView.CAST_MODE_LIST); } /** * Reports the user navigation the route details view. * Called when the user clicks on a sink to navigate to the route details * view. */ function onNavigateToDetails() { media_router.browserApi.reportNavigateToView( media_router.MediaRouterView.ROUTE_DETAILS); } /** * Reports the user navigation the sink list view. * Called when the user clicks on the back button from the route details view * to the sink list view. */ function onNavigateToSinkList() { media_router.browserApi.reportNavigateToView( media_router.MediaRouterView.SINK_LIST); } /** * Reports whether or not the route creation was successful. * * @param {!Event} event * Parameters in |event|.detail: * success - whether or not the route creation was successful. */ function onReportRouteCreation(event) { var detail = event.detail; media_router.browserApi.reportRouteCreation(detail.success); } /** * Reports success or the type of failure for route creation response. * Called when the route is resolved; either the route creation was a success * or if there was no route or the route's corresponding sink is invalid; * either the sink does not exist or was not the sink we were looking for. * * @param {!Event} event * Parameters in |event|.detail: * outcome - the outcome of a create route response. * */ function onReportRouteCreationOutcome(event) { /** @type {{outcome: number}} */ var detail = event.detail; media_router.browserApi.reportRouteCreationOutcome(detail.outcome); } /** * Requests for initial data to load into the dialog. */ function onRequestInitialData() { media_router.browserApi.requestInitialData(); } /** * Reports the initial state of the dialog after it is opened. * Called after initial data is populated. * * @param {!Event} event * Parameters in |event|.detail: * currentView - the current dialog's current view. */ function onShowInitialState(event) { /** @type {{currentView: string}} */ var detail = event.detail; media_router.browserApi.reportInitialState(detail.currentView); } /** * Reports the index of the sink that was clicked. * Called when the user selects a sink on the sink list. * * @param {!Event} event * Paramters in |event|.detail: * index - the index of the clicked sink. */ function onSinkClick(event) { /** @type {{index: number}} */ var detail = event.detail; media_router.browserApi.reportClickedSinkIndex(detail.index); } /** * Reports the time it took for the user to select a sink to create a route * after the list was popuated and shown. * * @param {!Event} event * Paramters in |event|.detail: * timeMs - the time it took for the user to select a sink. */ function onSinkClickTimeReported(event) { /** @type {{timeMs: number}} */ var detail = event.detail; media_router.browserApi.reportTimeToClickSink(detail.timeMs); } /** * Reports the current sink count. * Called 3 seconds after the dialog is initially opened. * * @param {!Event} event * Parameters in |event|.detail: * sinkCount - the number of sinks. */ function onSinkCountReported(event) { /** @type {{sinkCount: number}} */ var detail = event.detail; media_router.browserApi.reportSinkCount(detail.sinkCount); } /** * Reports when the user clicks outside the dialog. */ function onWindowBlur() { media_router.browserApi.reportBlur(); } return { initialize: initialize, }; }); window.addEventListener('load', media_router.initialize); // Copyright 2015 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. // API invoked by the browser MediaRouterWebUIMessageHandler to communicate // with this UI. cr.define('media_router.ui', function() { 'use strict'; // The media-router-container element. var container = null; // The media-router-header element. var header = null; /** * Handles response of previous create route attempt. * * @param {string} sinkId The ID of the sink to which the Media Route was * creating a route. * @param {?media_router.Route} route The newly created route that * corresponds to the sink if route creation succeeded; null otherwise. * @param {boolean} isForDisplay Whether or not |route| is for display. */ function onCreateRouteResponseReceived(sinkId, route, isForDisplay) { container.onCreateRouteResponseReceived(sinkId, route, isForDisplay); } /** * Handles the search response by forwarding |sinkId| to the container. * * @param {string} sinkId The ID of the sink found by search. */ function receiveSearchResult(sinkId) { container.onReceiveSearchResult(sinkId); } /** * Sets the cast mode list. * * @param {!Array} castModeList */ function setCastModeList(castModeList) { container.castModeList = castModeList; } /** * Sets |container| and |header|. * * @param {!MediaRouterContainerElement} mediaRouterContainer * @param {!MediaRouterHeaderElement} mediaRouterHeader */ function setElements(mediaRouterContainer, mediaRouterHeader) { container = mediaRouterContainer; header = mediaRouterHeader; } /** * Populates the WebUI with data obtained about the first run flow. * * @param {{firstRunFlowCloudPrefLearnMoreUrl: string, * firstRunFlowLearnMoreUrl: string, * wasFirstRunFlowAcknowledged: boolean, * showFirstRunFlowCloudPref: boolean}} data * Parameters in data: * firstRunFlowCloudPrefLearnMoreUrl - url to open when the cloud services * pref learn more link is clicked. * firstRunFlowLearnMoreUrl - url to open when the first run flow learn * more link is clicked. * wasFirstRunFlowAcknowledged - true if first run flow was previously * acknowledged by user. * showFirstRunFlowCloudPref - true if the cloud pref option should be * shown. */ function setFirstRunFlowData(data) { container.firstRunFlowCloudPrefLearnMoreUrl = data['firstRunFlowCloudPrefLearnMoreUrl']; container.firstRunFlowLearnMoreUrl = data['firstRunFlowLearnMoreUrl']; container.showFirstRunFlowCloudPref = data['showFirstRunFlowCloudPref']; // Some users acknowledged the first run flow before the cloud prefs // setting was implemented. These users will see the first run flow // again. container.showFirstRunFlow = !data['wasFirstRunFlowAcknowledged'] || container.showFirstRunFlowCloudPref; } /** * Populates the WebUI with data obtained from Media Router. * * @param {{deviceMissingUrl: string, * sinksAndIdentity: { * sinks: !Array, * showEmail: boolean, * userEmail: string, * showDomain: boolean * }, * routes: !Array, * castModes: !Array, * useTabMirroring: boolean}} data * Parameters in data: * deviceMissingUrl - url to be opened on "Device missing?" clicked. * sinksAndIdentity - list of sinks to be displayed and user identity. * routes - list of routes that are associated with the sinks. * castModes - list of available cast modes. * useTabMirroring - whether the cast mode should be set to TAB_MIRROR. */ function setInitialData(data) { container.deviceMissingUrl = data['deviceMissingUrl']; container.castModeList = data['castModes']; this.setSinkListAndIdentity(data['sinksAndIdentity']); container.routeList = data['routes']; container.maybeShowRouteDetailsOnOpen(); if (data['useTabMirroring']) container.selectCastMode(media_router.CastModeType.TAB_MIRROR); media_router.browserApi.onInitialDataReceived(); } /** * Sets current issue to |issue|, or clears the current issue if |issue| is * null. * * @param {?media_router.Issue} issue */ function setIssue(issue) { container.issue = issue; } /** * Sets the list of currently active routes. * * @param {!Array} routeList */ function setRouteList(routeList) { container.routeList = routeList; } /** * Sets the list of discovered sinks along with properties of whether to hide * identity of the user email and domain. * * @param {{sinks: !Array, * showEmail: boolean, * userEmail: string, * showDomain: boolean}} data * Parameters in data: * sinks - list of sinks to be displayed. * showEmail - true if the user email should be shown. * userEmail - email of the user if the user is signed in. * showDomain - true if the user domain should be shown. */ function setSinkListAndIdentity(data) { container.showDomain = data['showDomain']; container.allSinks = data['sinks']; header.userEmail = data['userEmail']; header.showEmail = data['showEmail']; } /** * Updates the max height of the dialog * * @param {number} height */ function updateMaxHeight(height) { container.updateMaxDialogHeight(height); } return { onCreateRouteResponseReceived: onCreateRouteResponseReceived, receiveSearchResult: receiveSearchResult, setCastModeList: setCastModeList, setElements: setElements, setFirstRunFlowData: setFirstRunFlowData, setInitialData: setInitialData, setIssue: setIssue, setRouteList: setRouteList, setSinkListAndIdentity: setSinkListAndIdentity, updateMaxHeight: updateMaxHeight, }; }); // API invoked by this UI to communicate with the browser WebUI message handler. cr.define('media_router.browserApi', function() { 'use strict'; /** * Indicates that the user has acknowledged the first run flow. * * @param {boolean} optedIntoCloudServices Whether or not the user opted into * cloud services. */ function acknowledgeFirstRunFlow(optedIntoCloudServices) { chrome.send('acknowledgeFirstRunFlow', [optedIntoCloudServices]); } /** * Acts on the given issue. * * @param {number} issueId * @param {number} actionType Type of action that the user clicked. * @param {?number} helpPageId The numeric help center ID. */ function actOnIssue(issueId, actionType, helpPageId) { chrome.send('actOnIssue', [{issueId: issueId, actionType: actionType, helpPageId: helpPageId}]); } /** * Modifies |route| by changing its source to the one identified by * |selectedCastMode|. * * @param {!media_router.Route} route The route being modified. * @param {number} selectedCastMode The value of the cast mode the user * selected. */ function changeRouteSource(route, selectedCastMode) { chrome.send('requestRoute', [{sinkId: route.sinkId, selectedCastMode: selectedCastMode}]); } /** * Closes the dialog. * * @param {boolean} pressEscToClose Whether the user pressed ESC to close the * dialog. */ function closeDialog(pressEscToClose) { chrome.send('closeDialog', [pressEscToClose]); } /** * Closes the given route. * * @param {!media_router.Route} route */ function closeRoute(route) { chrome.send('closeRoute', [{routeId: route.id, isLocal: route.isLocal}]); } /** * Joins the given route. * * @param {!media_router.Route} route */ function joinRoute(route) { chrome.send('joinRoute', [{sinkId: route.sinkId, routeId: route.id}]); } /** * Indicates that the initial data has been received. */ function onInitialDataReceived() { chrome.send('onInitialDataReceived'); } /** * Reports when the user clicks outside the dialog. */ function reportBlur() { chrome.send('reportBlur'); } /** * Reports the index of the selected sink. * * @param {number} sinkIndex */ function reportClickedSinkIndex(sinkIndex) { chrome.send('reportClickedSinkIndex', [sinkIndex]); } /** * Reports that the user used the filter input. */ function reportFilter() { chrome.send('reportFilter'); } /** * Reports the initial dialog view. * * @param {string} view */ function reportInitialState(view) { chrome.send('reportInitialState', [view]); } /** * Reports the initial action the user took. * * @param {number} action */ function reportInitialAction(action) { chrome.send('reportInitialAction', [action]); } /** * Reports the navigation to the specified view. * * @param {string} view */ function reportNavigateToView(view) { chrome.send('reportNavigateToView', [view]); } /** * Reports whether or not a route was created successfully. * * @param {boolean} success */ function reportRouteCreation(success) { chrome.send('reportRouteCreation', [success]); } /** * Reports the outcome of a create route response. * * @param {number} outcome */ function reportRouteCreationOutcome(outcome) { chrome.send('reportRouteCreationOutcome', [outcome]); } /** * Reports the cast mode that the user selected. * * @param {number} castModeType */ function reportSelectedCastMode(castModeType) { chrome.send('reportSelectedCastMode', [castModeType]); } /** * Reports the current number of sinks. * * @param {number} sinkCount */ function reportSinkCount(sinkCount) { chrome.send('reportSinkCount', [sinkCount]); } /** * Reports the time it took for the user to select a sink after the sink list * is populated and shown. * * @param {number} timeMs */ function reportTimeToClickSink(timeMs) { chrome.send('reportTimeToClickSink', [timeMs]); } /** * Reports the time, in ms, it took for the user to close the dialog without * taking any other action. * * @param {number} timeMs */ function reportTimeToInitialActionClose(timeMs) { chrome.send('reportTimeToInitialActionClose', [timeMs]); } /** * Requests data to initialize the WebUI with. * The data will be returned via media_router.ui.setInitialData. */ function requestInitialData() { chrome.send('requestInitialData'); } /** * Requests that a media route be started with the given sink. * * @param {string} sinkId The sink ID. * @param {number} selectedCastMode The value of the cast mode the user * selected. */ function requestRoute(sinkId, selectedCastMode) { chrome.send('requestRoute', [{sinkId: sinkId, selectedCastMode: selectedCastMode}]); } /** * Requests that the media router search all providers for a sink matching * |searchCriteria| that can be used with the media source associated with the * cast mode |selectedCastMode|. If such a sink is found, a route is also * created between the sink and the media source. * * @param {string} sinkId Sink ID of the pseudo sink generating the request. * @param {string} searchCriteria Search criteria for the route providers. * @param {string} domain User's current hosted domain. * @param {number} selectedCastMode The value of the cast mode to be used with * the sink. */ function searchSinksAndCreateRoute( sinkId, searchCriteria, domain, selectedCastMode) { chrome.send('searchSinksAndCreateRoute', [{sinkId: sinkId, searchCriteria: searchCriteria, domain: domain, selectedCastMode: selectedCastMode}]); } return { acknowledgeFirstRunFlow: acknowledgeFirstRunFlow, actOnIssue: actOnIssue, changeRouteSource: changeRouteSource, closeDialog: closeDialog, closeRoute: closeRoute, joinRoute: joinRoute, onInitialDataReceived: onInitialDataReceived, reportBlur: reportBlur, reportClickedSinkIndex: reportClickedSinkIndex, reportFilter: reportFilter, reportInitialAction: reportInitialAction, reportInitialState: reportInitialState, reportNavigateToView: reportNavigateToView, reportRouteCreation: reportRouteCreation, reportRouteCreationOutcome: reportRouteCreationOutcome, reportSelectedCastMode: reportSelectedCastMode, reportSinkCount: reportSinkCount, reportTimeToClickSink: reportTimeToClickSink, reportTimeToInitialActionClose: reportTimeToInitialActionClose, requestInitialData: requestInitialData, requestRoute: requestRoute, searchSinksAndCreateRoute: searchSinksAndCreateRoute, }; }); // Copyright 2015 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. // This Polymer element is used to show information about issues related // to casting. Polymer({ is: 'issue-banner', properties: { /** * Maps an issue action type to the resource identifier of the text shown * in the action button. * This is a property of issue-banner because it is used in tests. This * property should always be set before |issue| is set or updated. * @private {!Array} */ actionTypeToButtonTextResource_: { type: Array, readOnly: true, value: function() { return ['dismissButton', 'learnMoreText']; }, }, /** * The text shown in the default action button. * @private {string|undefined} */ defaultActionButtonText_: { type: String, }, /** * The issue to show. * @type {?media_router.Issue|undefined} */ issue: { type: Object, observer: 'updateActionButtonText_', }, /** * The text shown in the secondary action button. * @private {string|undefined} */ secondaryActionButtonText_: { type: String, }, }, behaviors: [ I18nBehavior, ], /** * @param {?media_router.Issue} issue * @return {boolean} Whether or not to hide the blocking issue UI. * @private */ computeIsBlockingIssueHidden_: function(issue) { return !issue || !issue.isBlocking; }, /** * @param {?media_router.Issue} issue The current issue. * @return {string} The class for the overall issue-banner. * @private */ computeIssueClass_: function(issue) { if (!issue) return ''; return issue.isBlocking ? 'blocking' : 'non-blocking'; }, /** * @param {?media_router.Issue} issue * @return {boolean} Whether or not to hide the non-blocking issue UI. * @private */ computeOptionalActionHidden_: function(issue) { return !issue || issue.secondaryActionType === undefined; }, /** * Fires an issue-action-click event. * * @param {number} actionType The type of issue action. * @private */ fireIssueActionClick_: function(actionType) { this.fire('issue-action-click', { id: this.issue.id, actionType: actionType, helpPageId: this.issue.helpPageId }); }, /** * Called when a default issue action is clicked. * * @param {!Event} event The event object. * @private */ onClickDefaultAction_: function(event) { this.fireIssueActionClick_(this.issue.defaultActionType); }, /** * Called when an optional issue action is clicked. * * @param {!Event} event The event object. * @private */ onClickOptAction_: function(event) { this.fireIssueActionClick_( /** @type {number} */(this.issue.secondaryActionType)); }, /** * Called when |issue| is updated. This updates the default and secondary * action button text. * * @private */ updateActionButtonText_: function() { var defaultText = ''; var secondaryText = ''; if (this.issue) { defaultText = this.i18n(this.actionTypeToButtonTextResource_[ this.issue.defaultActionType]); if (this.issue.secondaryActionType !== undefined) { secondaryText = this.i18n(this.actionTypeToButtonTextResource_[ this.issue.secondaryActionType]); } } this.defaultActionButtonText_ = defaultText; this.secondaryActionButtonText_ = secondaryText; }, }); /* Copyright 2015 The Chromium Authors. All rights reserved. * Use of this source code is governed by a BSD-style license that can be * found in the LICENSE file. */ .blocking { background-color: white; overflow: hidden; position: relative; text-align: center; } .blocking > #buttons { padding-bottom: 24px; padding-top: 20px; } .blocking > div > #title { color: rgba(0, 0, 0, 0.87); line-height: 1.125em; padding: 10px; vertical-align: middle; } #blocking-icon { color: var(--google-red-500); height: 75px; padding-top: 24px; width: 75px; } .non-blocking { background-color: var(--paper-grey-800); padding: 16px; width: inherit; } .non-blocking > #buttons { display: flex; flex-direction: row; justify-content: flex-end; width: 100%; } .non-blocking > #buttons > .button { color: var(--paper-blue-300); } .non-blocking > #buttons > #default-button { -webkit-margin-end: 24px; } .non-blocking > div > #title { -webkit-margin-end: 12px; -webkit-padding-end: 12px; color: rgba(255, 255, 255, 0.87); overflow: hidden; } paper-button { margin: 0; } /* Copyright 2015 The Chromium Authors. All rights reserved. * Use of this source code is governed by a BSD-style license that can be * found in the LICENSE file. */ .active-sink { color: var(--paper-blue-700); } .cast-mode-icon, .sink-icon { -webkit-padding-end: 12px; -webkit-padding-start: var(--dialog-padding-start); height: var(--non-navigation-icon-size); width: var(--non-navigation-icon-size); } #cast-mode-list { padding-bottom: 12px; padding-top: 4px; } #container-header { position: fixed; width: 100%; } #content { position: relative; } #device-missing { align-items: center; background-color: white; display: flex; justify-content: center; padding: 60px 0; } #device-missing a { color: var(--paper-blue-700); margin: 8px 0; text-align: center; text-decoration: none; } #first-run-button { background-color: white; } #first-run-button-container { display: flex; flex-direction: row; justify-content: flex-end; } #first-run-cloud-checkbox, #first-run-flow-cloud-pref, #first-run-text { font-size: 1.0em; line-height: 1.5em; } #first-run-cloud-checkbox, #first-run-text, #first-run-title { color: white; padding-bottom: 24px; } #first-run-cloud-checkbox::shadow #checkboxLabel { -webkit-padding-start: var(-dialog-padding-start); } #first-run-flow { background-color: var(--paper-blue-700); box-sizing: border-box; padding: 24px 16px 4px 16px; position: fixed; width: 100%; } #first-run-flow a { color: white; text-decoration: none; } #first-run-flow-cloud-pref { color: white; display: flex; } .first-run-learn-more { font-weight: bold; text-transform: uppercase; } #first-run-title { font-size: 1.25em; } #issue-banner { width: 100%; } #issue-banner.non-blocking { bottom: 0; display: block; margin-top: 0; } #no-search-matches { color: rgb(112, 112, 112); display: block; font-size: 1.2 em; padding-bottom: 20px; padding-top: 20px; text-align: center; } paper-checkbox { --paper-checkbox-checked-color: white; --paper-checkbox-checkmark-color: var(--paper-blue-700); --paper-checkbox-ink-size: 35px; --paper-checkbox-unchecked-color: white; } paper-item { cursor: pointer; font-size: 1.0em; line-height: 0; min-height: 0; padding: 12px 0; } paper-item:hover { background-color: rgb(238, 238, 238); border: 0; } paper-menu { -webkit-user-select: none; color: rgba(0, 0, 0, 0.87); overflow-x: hidden; overflow-y: auto; padding-bottom: 12px; padding-top: 4px; } #search-input-container { flex-grow: 1; } #search-results { overflow-x: hidden; overflow-y: auto; } #search-results-container { bottom: 0; left: 0; overflow-x: hidden; overflow-y: hidden; position: absolute; right: 0; top: 100%; } #searching-devices-spinner { height: 30px; width: 30px; } #share-screen-text { -webkit-padding-start: var(--dialog-padding-start); color: var(--paper-grey-600); cursor: default; font-weight: normal; padding-bottom: 4px; padding-top: 12px; } #share-screen-text::after { background-color: white; font-weight: normal; } .sink-content { display: flex; flex-direction: row; font-weight: normal; } .sink-domain { -webkit-padding-start: 6px; color: var(--paper-grey-600); /* TODO(crbug/589697): Handle overflow of very long domain names. */ } #sink-list-view { position: relative; } #sink-list { overflow-x: hidden; overflow-y: auto; } .sink-name { min-width: 10%; } #sink-search { padding-bottom: 0; padding-top: 4px; position: absolute; top: 100%; width: 100%; z-index: 1; } :host([search-use-bottom-padding]) #sink-search { padding-bottom: 16px; padding-top: 0; } /* Separate icon class is a consequence of box-sizing: border-box set by * paper-icon-button. This should achieve the same dimensions as .sink-icon. */ #sink-search-icon { -webkit-margin-start: 4px; -webkit-padding-end: 12px; -webkit-padding-start: 12px; } #sink-search-input { --paper-input-container: { margin: 8px 0; padding: 0; }; --paper-input-container-focus-color: rgb(33, 150, 243); --paper-input-container-input: { font-size: 12px; }; --paper-input-container-label: { font-size: 12px; }; -webkit-margin-end: 31px; box-sizing: border-box; } .sink-subtext { color: var(--paper-grey-600); padding-top: 8px; } .sink-text { flex-flow: row nowrap; line-height: normal; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; width: 275px; } // Copyright 2015 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. // This Polymer element contains the entire media router interface. It handles // hiding and showing specific components. Polymer({ is: 'media-router-container', properties: { /** * The list of available sinks. * @type {!Array} */ allSinks: { type: Array, value: [], observer: 'reindexSinksAndRebuildSinksToShow_', }, /** * The last promise in a chain that will be fulfilled when the current * animation has finished. It does not return a value; it is strictly a * synchronization mechanism. * @private {!Promise} */ animationPromise_: { type: Object, value: function() { return Promise.resolve(); }, }, /** * The list of CastModes to show. * @type {!Array|undefined} */ castModeList: { type: Array, observer: 'checkCurrentCastMode_', }, /** * The ID of the Sink currently being launched. * @private {string} * TODO(crbug.com/616604): Use per-sink route creation state. */ currentLaunchingSinkId_: { type: String, value: '', }, /** * The current route. * @private {?media_router.Route|undefined} */ currentRoute_: { type: Object, }, /** * The current view to be shown. * @private {?media_router.MediaRouterView|undefined} */ currentView_: { type: String, observer: 'currentViewChanged_', }, /** * The URL to open when the device missing link is clicked. * @type {string|undefined} */ deviceMissingUrl: { type: String, }, /** * The height of the dialog. * @private {number} */ dialogHeight_: { type: Number, value: 330, }, /** * The time |this| element calls ready(). * @private {number|undefined} */ elementReadyTimeMs_: { type: Number, }, /** * Animation player used for running filter transition animations. * @private {?Animation} */ filterTransitionPlayer_: { type: Object, value: null, }, /** * The URL to open when the cloud services pref learn more link is clicked. * @type {string|undefined} */ firstRunFlowCloudPrefLearnMoreUrl: { type: String, }, /** * The URL to open when the first run flow learn more link is clicked. * @type {string|undefined} */ firstRunFlowLearnMoreUrl: { type: String, }, /** * The header text for the sink list. * @type {string|undefined} */ headerText: { type: String, }, /** * The header text tooltip. This would be descriptive of the * source origin, whether a host name, tab URL, etc. * @type {string|undefined} */ headerTextTooltip: { type: String, }, /** * An animation player that is used for running dialog height adjustments. * @private {?Animation} */ heightAdjustmentPlayer_: { type: Object, value: null, }, /** * Whether the sink list is being hidden for animation purposes. * @private {boolean} */ hideSinkListForAnimation_: { type: Boolean, value: false, }, /** * Records whether the search input is focused when a window blur event is * received. This is used to handle search focus edge cases. See * |setSearchFocusHandlers_| for details. * @private {boolean} */ isSearchFocusedOnWindowBlur_: { type: Boolean, value: false, }, /** * Whether the search list is currently hidden. * @private {boolean} */ isSearchListHidden_: { type: Boolean, value: true, }, /** * The issue to show. * @type {?media_router.Issue} */ issue: { type: Object, value: null, observer: 'maybeShowIssueView_', }, /** * Whether the MR UI was just opened. * @private {boolean} */ justOpened_: { type: Boolean, value: true, }, /** * Whether the user's mouse is positioned over the dialog. * @private {boolean|undefined} */ mouseIsPositionedOverDialog_: { type: Boolean, }, /** * The ID of the route that is currently being created. This is set when * route creation is resolved but not ready for its controls to be * displayed. * @private {string|undefined} */ pendingCreatedRouteId_: { type: String, }, /** * The time the sink list was shown and populated with at least one sink. * This is reset whenever the user switches views or there are no sinks * available for display. * @private {number} */ populatedSinkListSeenTimeMs_: { type: Number, value: -1, }, /** * Pseudo sinks from MRPs that represent their ability to accept sink search * requests. * @private {!Array} */ pseudoSinks_: { type: Array, value: [], }, /** * Helps manage the state of creating a sink and a route from a pseudo sink. * @private {PseudoSinkSearchState|undefined} */ pseudoSinkSearchState_: { type: Object, }, /** * Whether the next character input should cause a filter action metric to * be sent. * @type {boolean} * @private */ reportFilterOnInput_: { type: Boolean, value: false, }, /** * The list of current routes. * @type {!Array|undefined} */ routeList: { type: Array, observer: 'rebuildRouteMaps_', }, /** * Maps media_router.Route.id to corresponding media_router.Route. * @private {!Object|undefined} */ routeMap_: { type: Object, }, /** * Whether the search feature is enabled and we should show the search * input. * @private {boolean} */ searchEnabled_: { type: Boolean, value: false, observer: 'searchEnabledChanged_', }, /** * Search text entered by the user into the sink search input. * @private {string} */ searchInputText_: { type: String, value: '', observer: 'searchInputTextChanged_', }, /** * Sinks to display that match |searchInputText_|. * @private {!Array>}>|undefined} */ searchResultsToShow_: { type: Array, }, /** * Whether the search input should be padded as if it were at the bottom of * the dialog. * @type {boolean} */ searchUseBottomPadding: { type: Boolean, reflectToAttribute: true, value: true, }, /** * Whether to show the user domain of sinks associated with identity. * @type {boolean|undefined} */ showDomain: { type: Boolean, }, /** * Whether to show the first run flow. * @type {boolean|undefined} */ showFirstRunFlow: { type: Boolean, observer: 'updateElementPositioning_', }, /** * Whether to show the cloud preference setting in the first run flow. * @type {boolean|undefined} */ showFirstRunFlowCloudPref: { type: Boolean, }, /** * The cast mode shown to the user. Initially set to auto mode. (See * media_router.CastMode documentation for details on auto mode.) * This value may be changed in one of the following ways: * 1) The user explicitly selected a cast mode. * 2) The user selected cast mode is no longer available for the associated * WebContents. In this case, the container will reset to auto mode. Note * that |userHasSelectedCastMode_| will switch back to false. * 3) The sink list changed, and the user had not explicitly selected a cast * mode. If the sinks support exactly 1 cast mode, the container will * switch to that cast mode. Otherwise, the container will reset to auto * mode. * @private {number} */ shownCastModeValue_: { type: Number, value: media_router.AUTO_CAST_MODE.type, }, /** * Max height for the sink list. * @private {number} */ sinkListMaxHeight_: { type: Number, value: 0, }, /** * Maps media_router.Sink.id to corresponding media_router.Sink. * @private {!Object|undefined} */ sinkMap_: { type: Object, }, /** * Maps media_router.Sink.id to corresponding media_router.Route. * @private {!Object} */ sinkToRouteMap_: { type: Object, value: {}, }, /** * Sinks to show for the currently selected cast mode. * @private {!Array|undefined} */ sinksToShow_: { type: Array, observer: 'updateElementPositioning_', }, /** * Whether the user has explicitly selected a cast mode. * @private {boolean} */ userHasSelectedCastMode_: { type: Boolean, value: false, }, /** * Whether the user has already taken an action. * @type {boolean} */ userHasTakenInitialAction_: { type: Boolean, value: false, }, }, behaviors: [ I18nBehavior, ], observers: [ 'maybeUpdateStartSinkDisplayStartTime_(currentView_, sinksToShow_)', ], ready: function() { this.elementReadyTimeMs_ = window.performance.now(); this.showSinkList_(); Polymer.RenderStatus.afterNextRender(this, function() { // Import the elements that aren't needed at startup. This reduces // initial load time. Delayed loading interferes with getting the // offsetHeight of the first-run-flow element in updateElementPositioning_ // though, so we also make sure it is called after the last load. var that = this; var loadsRemaining = 3; var onload = function() { loadsRemaining--; if (loadsRemaining > 0) { return; } that.updateElementPositioning_(); if (that.currentView_ == media_router.MediaRouterView.SINK_LIST) { that.putSearchAtBottom_(); } }; this.importHref('chrome://resources/polymer/v1_0/neon-animation/' + 'web-animations.html', onload); this.importHref(this.resolveUrl( '../issue_banner/issue_banner.html'), onload); this.importHref(this.resolveUrl( '../media_router_search_highlighter/' + 'media_router_search_highlighter.html'), onload); // If this is not on a Mac platform, remove the placeholder. See // onFocus_() for more details. ready() is only called once, so no need // to check if the placeholder exist before removing. if (!cr.isMac) this.$$('#focus-placeholder').remove(); document.addEventListener('keydown', this.onKeydown_.bind(this), true); this.listen(this, 'focus', 'onFocus_'); this.listen(this, 'header-height-changed', 'updateElementPositioning_'); this.listen(this, 'header-or-arrow-click', 'toggleCastModeHidden_'); this.listen(this, 'mouseleave', 'onMouseLeave_'); this.listen(this, 'mouseenter', 'onMouseEnter_'); // Turn off the spinner after 3 seconds, then report the current number of // sinks. this.async(function() { this.justOpened_ = false; this.fire('report-sink-count', { sinkCount: this.allSinks.length, }); }, 3000 /* 3 seconds */); // For Mac platforms, request data after a short delay after load. This // appears to speed up initial data load time on Mac. if (cr.isMac) { this.async(function() { this.fire('request-initial-data'); }, 25 /* 0.025 seconds */); } }); }, /** * Fires an acknowledge-first-run-flow event and hides the first run flow. * This is call when the first run flow button is clicked. * * @private */ acknowledgeFirstRunFlow_: function() { // Only set |userOptedIntoCloudServices| if the user was shown the cloud // services preferences option. var userOptedIntoCloudServices = this.showFirstRunFlowCloudPref ? this.$$('#first-run-cloud-checkbox').checked : undefined; this.fire('acknowledge-first-run-flow', { optedIntoCloudServices: userOptedIntoCloudServices, }); this.showFirstRunFlow = false; this.showFirstRunFlowCloudPref = false; }, /** * Fires a 'report-initial-action' event when the user takes their first * action after the dialog opens. Also fires a 'report-initial-action-close' * event if that initial action is to close the dialog. * @param {!media_router.MediaRouterUserAction} initialAction */ maybeReportUserFirstAction: function(initialAction) { if (this.userHasTakenInitialAction_) return; this.fire('report-initial-action', { action: initialAction, }); if (initialAction == media_router.MediaRouterUserAction.CLOSE) { var timeToClose = window.performance.now() - this.elementReadyTimeMs_; this.fire('report-initial-action-close', { timeMs: timeToClose, }); } this.userHasTakenInitialAction_ = true; }, get header() { return this.$['container-header']; }, /** * Checks that the currently selected cast mode is still in the * updated list of available cast modes. If not, then update the selected * cast mode to the first available cast mode on the list. */ checkCurrentCastMode_: function() { if (!this.castModeList.length) return; // If we are currently showing auto mode, then nothing needs to be done. // Otherwise, if the cast mode currently shown no longer exists (regardless // of whether it was selected by user), then switch back to auto cast mode. if (this.shownCastModeValue_ != media_router.CastModeType.AUTO && !this.findCastModeByType_(this.shownCastModeValue_)) { this.setShownCastMode_(media_router.AUTO_CAST_MODE); this.rebuildSinksToShow_(); } }, /** * Compares two search match objects for sorting. Earlier and longer matches * are prioritized. * * @param {!{sinkItem: !media_router.Sink, * substrings: Array>}} resultA * Parameters in |resultA|: * sinkItem - sink object. * substrings - start-end index pairs of substring matches. * @param {!{sinkItem: !media_router.Sink, * substrings: Array>}} resultB * Parameters in |resultB|: * sinkItem - sink object. * substrings - start-end index pairs of substring matches. * @return {number} -1 if |resultA| should come before |resultB|, 1 if * |resultB| should come before |resultA|, and 0 if they are considered * equal. */ compareSearchMatches_: function(resultA, resultB) { var substringsA = resultA.substrings; var substringsB = resultB.substrings; var numberSubstringsA = substringsA.length; var numberSubstringsB = substringsB.length; if (numberSubstringsA == 0 && numberSubstringsB == 0) { return 0; } else if (numberSubstringsA == 0) { return 1; } else if (numberSubstringsB == 0) { return -1; } var loopMax = Math.min(numberSubstringsA, numberSubstringsB); for (var i = 0; i < loopMax; ++i) { var [matchStartA, matchEndA] = substringsA[i]; var [matchStartB, matchEndB] = substringsB[i]; if (matchStartA < matchStartB) { return -1; } else if (matchStartA > matchStartB) { return 1; } if (matchEndA > matchEndB) { return -1; } else if (matchEndA < matchEndB) { return 1; } } if (numberSubstringsA > numberSubstringsB) { return -1; } else if (numberSubstringsA < numberSubstringsB) { return 1; } return 0; }, /** * Returns a duration in ms from a distance in pixels using a default speed of * 1000 pixels per second. * @param {number} distance Number of pixels that will be traveled. * @private */ computeAnimationDuration_: function(distance) { // The duration of the animation can be found by abs(distance)/speed, where // speed is fixed at 1000 pixels per second, or 1 pixel per millisecond. return Math.abs(distance); }, /** * If |allSinks| supports only a single cast mode, returns that cast mode. * Otherwise, returns AUTO_MODE. Only called if |userHasSelectedCastMode_| is * |false|. * @return {!media_router.CastMode} The single cast mode supported by * |allSinks|, or AUTO_MODE. */ computeCastMode_: function() { var allCastModes = this.allSinks.reduce(function(castModesSoFar, sink) { return castModesSoFar | sink.castModes; }, 0); // This checks whether |castModes| does not consist of exactly 1 cast mode. if (!allCastModes || allCastModes & (allCastModes - 1)) return media_router.AUTO_CAST_MODE; var castMode = this.findCastModeByType_(allCastModes); if (castMode) return castMode; console.error('Cast mode ' + allCastModes + ' not in castModeList'); return media_router.AUTO_CAST_MODE; }, /** * @param {?media_router.MediaRouterView} view The current view. * @return {boolean} Whether or not to hide the cast mode list. * @private */ computeCastModeListHidden_: function(view) { return view != media_router.MediaRouterView.CAST_MODE_LIST; }, /** * @param {!media_router.CastMode} castMode The cast mode to determine an * icon for. * @return {string} The icon to use. * @private */ computeCastModeIcon_: function(castMode) { switch (castMode.type) { case media_router.CastModeType.DEFAULT: return 'media-router:web'; case media_router.CastModeType.TAB_MIRROR: return 'media-router:tab'; case media_router.CastModeType.DESKTOP_MIRROR: return 'media-router:laptop'; default: return ''; } }, /** * @param {!Array} castModeList The current list of * cast modes. * @return {!Array} The list of default cast modes. * @private */ computeDefaultCastModeList_: function(castModeList) { return castModeList.filter(function(mode) { return mode.type == media_router.CastModeType.DEFAULT; }); }, /** * @param {!Array} sinksToShow The list of sinks. * @return {boolean} Whether or not to hide the 'devices missing' message. * @private */ computeDeviceMissingHidden_: function(sinksToShow) { return sinksToShow.length != 0; }, /** * @param {?Element} element Element to compute padding for. * @return {number} Computes the amount of vertical padding (top + bottom) on * |element|. * @private */ computeElementVerticalPadding_: function(element) { var paddingBottom, paddingTop; [paddingBottom, paddingTop] = this.getElementVerticalPadding_(element); return paddingBottom + paddingTop; }, /** * @param {?media_router.MediaRouterView} view The current view. * @param {?media_router.Issue} issue The current issue. * @return {boolean} Whether or not to hide the header. * @private */ computeHeaderHidden_: function(view, issue) { return view == media_router.MediaRouterView.ROUTE_DETAILS || (view == media_router.MediaRouterView.SINK_LIST && !!issue && issue.isBlocking); }, /** * @param {?media_router.MediaRouterView} view The current view. * @param {string} headerText The header text for the sink list. * @return {string|undefined} The text for the header. * @private */ computeHeaderText_: function(view, headerText) { switch (view) { case media_router.MediaRouterView.CAST_MODE_LIST: return this.i18n('selectCastModeHeaderText'); case media_router.MediaRouterView.ISSUE: return this.i18n('issueHeaderText'); case media_router.MediaRouterView.ROUTE_DETAILS: return this.currentRoute_ && this.sinkMap_[this.currentRoute_.sinkId] ? this.sinkMap_[this.currentRoute_.sinkId].name : ''; case media_router.MediaRouterView.SINK_LIST: case media_router.MediaRouterView.FILTER: return this.headerText; default: return ''; } }, /** * @param {?media_router.MediaRouterView} view The current view. * @param {string} headerTooltip The tooltip for the header for the sink * list. * @return {string} The tooltip for the header. * @private */ computeHeaderTooltip_: function(view, headerTooltip) { return view == media_router.MediaRouterView.SINK_LIST ? headerTooltip : ''; }, /** * @param {string} currentLaunchingSinkId ID of the sink that is currently * launching, or empty string if none exists. * @private */ computeIsLaunching_: function(currentLaunchingSinkId) { return currentLaunchingSinkId != ''; }, /** * @param {?media_router.Issue} issue The current issue. * @return {string} The class for the issue banner. * @private */ computeIssueBannerClass_: function(issue) { return !!issue && !issue.isBlocking ? 'non-blocking' : ''; }, /** * @param {?media_router.MediaRouterView} view The current view. * @param {?media_router.Issue} issue The current issue. * @return {boolean} Whether or not to show the issue banner. * @private */ computeIssueBannerShown_: function(view, issue) { return !!issue && (view == media_router.MediaRouterView.SINK_LIST || view == media_router.MediaRouterView.FILTER || view == media_router.MediaRouterView.ISSUE); }, /** * @param {!Array>}>} searchResultsToShow * The sinks currently matching the search text. * @param {boolean} isSearchListHidden Whether the search list is hidden. * @return {boolean} Whether or not the 'no matches' message is hidden. * @private */ computeNoMatchesHidden_: function(searchResultsToShow, isSearchListHidden) { return isSearchListHidden || this.searchInputText_.length == 0 || searchResultsToShow.length != 0; }, /** * @param {!Array} castModeList The current list of * cast modes. * @return {!Array} The list of non-default cast * modes. * @private */ computeNonDefaultCastModeList_: function(castModeList) { return castModeList.filter(function(mode) { return mode.type != media_router.CastModeType.DEFAULT; }); }, /** * @param {?media_router.MediaRouterView} view The current view. * @param {?media_router.Issue} issue The current issue. * @return {boolean} Whether or not to hide the route details. * @private */ computeRouteDetailsHidden_: function(view, issue) { return view != media_router.MediaRouterView.ROUTE_DETAILS || (!!issue && issue.isBlocking); }, /** * Computes an array of substring indices that mark where substrings of * |searchString| occur in |sinkName|. * * @param {string} searchString Search string entered by user. * @param {string} sinkName Sink name being filtered. * @return {Array>} Array of substring start-end (inclusive) * index pairs if every character in |searchString| was matched, in order, * in |sinkName|. Otherwise it returns null. * @private */ computeSearchMatches_: function(searchString, sinkName) { var i = 0; var matchStart = -1; var matchEnd = -1; var matchPairs = []; for (var j = 0; i < searchString.length && j < sinkName.length; ++j) { if (searchString[i].toLocaleLowerCase() == sinkName[j].toLocaleLowerCase()) { if (matchStart == -1) { matchStart = j; } ++i; } else if (matchStart != -1) { matchEnd = j - 1; matchPairs.push([matchStart, matchEnd]); matchStart = -1; } } if (matchStart != -1) { matchEnd = j - 1; matchPairs.push([matchStart, matchEnd]); } return (i == searchString.length) ? matchPairs : null; }, /** * Computes whether the search results list should be hidden. * @param {!Array>}>} searchResultsToShow * The sinks currently matching the search text. * @param {boolean} isSearchListHidden Whether the search list is hidden. * @return {boolean} Whether the search results list should be hidden. * @private */ computeSearchResultsHidden_: function(searchResultsToShow, isSearchListHidden) { return isSearchListHidden || searchResultsToShow.length == 0; }, /** * @param {!Array} castModeList The current list of * cast modes. * @return {boolean} Whether or not to hide the share screen subheading text. * @private */ computeShareScreenSubheadingHidden_: function(castModeList) { return this.computeNonDefaultCastModeList_(castModeList).length == 0; }, /** * @param {boolean} showFirstRunFlow Whether or not to show the first run * flow. * @param {?media_router.MediaRouterView} currentView The current view. * @private */ computeShowFirstRunFlow_: function(showFirstRunFlow, currentView) { return showFirstRunFlow && currentView == media_router.MediaRouterView.SINK_LIST; }, /** * @param {!media_router.Sink} sink The sink to determine an icon for. * @return {string} The icon to use. * @private */ computeSinkIcon_: function(sink) { switch (sink.iconType) { case media_router.SinkIconType.CAST: return 'media-router:chromecast'; case media_router.SinkIconType.CAST_AUDIO: return 'media-router:speaker'; case media_router.SinkIconType.CAST_AUDIO_GROUP: return 'media-router:speaker-group'; case media_router.SinkIconType.GENERIC: return 'media-router:tv'; case media_router.SinkIconType.HANGOUT: return 'media-router:hangout'; default: return 'media-router:tv'; } }, /** * @param {!string} sinkId A sink ID. * @param {!Object} sinkToRouteMap * Maps media_router.Sink.id to corresponding media_router.Route. * @return {string} The class for the sink icon. * @private */ computeSinkIconClass_: function(sinkId, sinkToRouteMap) { return sinkToRouteMap[sinkId] ? 'sink-icon active-sink' : 'sink-icon'; }, /** * @param {!string} currentLaunchingSinkId The ID of the sink that is * currently launching. * @param {!string} sinkId A sink ID. * @return {boolean} |true| if given sink is currently launching. * @private */ computeSinkIsLaunching_: function(currentLaunchingSinkId, sinkId) { return currentLaunchingSinkId == sinkId; }, /** * @param {!Array} sinksToShow The list of sinks. * @return {boolean} Whether or not to hide the sink list. * @private */ computeSinkListHidden_: function(sinksToShow) { return sinksToShow.length == 0; }, /** * @param {?media_router.MediaRouterView} view The current view. * @param {?media_router.Issue} issue The current issue. * @return {boolean} Whether or not to hide entire the sink list view. * @private */ computeSinkListViewHidden_: function(view, issue) { return (view != media_router.MediaRouterView.SINK_LIST && view != media_router.MediaRouterView.FILTER) || (!!issue && issue.isBlocking); }, /** * Returns whether the sink domain for |sink| should be hidden. * @param {!media_router.Sink} sink * @return {boolean} |true| if the domain should be hidden. * @private */ computeSinkDomainHidden_: function(sink) { return !this.showDomain || this.isEmptyOrWhitespace_(sink.domain); }, /** * Computes which portions of a sink name, if any, should be highlighted when * displayed in the filter view. Any substrings matching the search text * should be highlighted. * * The order the strings are combined is plainText[0] highlightedText[0] * plainText[1] highlightedText[1] etc. * * @param {!{sinkItem: !media_router.Sink, * substrings: !Array>}} matchedItem * Parameters in matchedItem: * sinkItem - Original !media_router.Sink from the sink list. * substrings - List of index pairs denoting substrings of sinkItem.name * that match |searchInputText_|. * @return {!{highlightedText: !Array, plainText: !Array}} * highlightedText - Array of strings that should be displayed highlighted. * plainText - Array of strings that should be displayed normally. * @private */ computeSinkMatchingText_: function(matchedItem) { if (!matchedItem.substrings) { return {highlightedText: [null], plainText: [matchedItem.sinkItem.name]}; } var lastMatchIndex = -1; var nameIndex = 0; var sinkName = matchedItem.sinkItem.name; var highlightedText = []; var plainText = []; for (var i = 0; i < matchedItem.substrings.length; ++i) { var [matchStart, matchEnd] = matchedItem.substrings[i]; if (lastMatchIndex + 1 < matchStart) { plainText.push(sinkName.substring(lastMatchIndex + 1, matchStart)); } else { plainText.push(null); } highlightedText.push(sinkName.substring(matchStart, matchEnd + 1)); lastMatchIndex = matchEnd; } if (lastMatchIndex + 1 < sinkName.length) { highlightedText.push(null); plainText.push(sinkName.substring(lastMatchIndex + 1)); } return {highlightedText: highlightedText, plainText: plainText}; }, /** * Returns the subtext to be shown for |sink|. Only called if * |computeSinkSubtextHidden_| returns false for the same |sink| and * |sinkToRouteMap|. * @param {!media_router.Sink} sink * @param {!Object} sinkToRouteMap * @return {?string} The subtext to be shown. * @private */ computeSinkSubtext_: function(sink, sinkToRouteMap) { var route = sinkToRouteMap[sink.id]; if (route && !this.isEmptyOrWhitespace_(route.description)) return route.description; return sink.description; }, /** * Returns whether the sink subtext for |sink| should be hidden. * @param {!media_router.Sink} sink * @param {!Object} sinkToRouteMap * @return {boolean} |true| if the subtext should be hidden. * @private */ computeSinkSubtextHidden_: function(sink, sinkToRouteMap) { if (!this.isEmptyOrWhitespace_(sink.description)) return false; var route = sinkToRouteMap[sink.id]; return !route || this.isEmptyOrWhitespace_(route.description); }, /** * @param {boolean} justOpened Whether the MR UI was just opened. * @return {boolean} Whether or not to hide the spinner. * @private */ computeSpinnerHidden_: function(justOpened) { return !justOpened; }, /** * Computes the height of the sink list view element when search results are * being shown. * * @param {?Element} deviceMissing No devices message element. * @param {?Element} noMatches No search matches element. * @param {?Element} results Search results list element. * @param {number} searchOffsetHeight Search input container element height. * @param {number} maxHeight Max height of the list elements. * @return {number} The height of the sink list view when search results are * being shown. * @private */ computeTotalSearchHeight_: function( deviceMissing, noMatches, results, searchOffsetHeight, maxHeight) { var contentHeight = deviceMissing.offsetHeight + ((noMatches.hasAttribute('hidden')) ? results.offsetHeight : noMatches.offsetHeight); return Math.min(contentHeight, maxHeight) + searchOffsetHeight; }, /** * Updates element positioning when the view changes and possibly triggers * reporting of a user filter action. If there is no filter text, it defers * the reporting until some text is entered, but otherwise it reports the * filter action here. * @param {?media_router.MediaRouterView} currentView The current view of the * dialog. * @private */ currentViewChanged_: function(currentView) { if (currentView == media_router.MediaRouterView.FILTER) { this.reportFilterOnInput_ = true; this.maybeReportFilter_(); } this.updateElementPositioning_(); }, /** * Filters all sinks based on fuzzy matching to the currently entered search * text. * @param {string} searchInputText The currently entered search text. * @private */ filterSinks_: function(searchInputText) { if (searchInputText.length == 0) { this.searchResultsToShow_ = this.sinksToShow_.map(function(item) { return {sinkItem: item, substrings: null}; }); return; } var searchResultsToShow = []; for (var i = 0; i < this.sinksToShow_.length; ++i) { var matchSubstrings = this.computeSearchMatches_( searchInputText, this.sinksToShow_[i].name); if (!matchSubstrings) { continue; } searchResultsToShow.push({sinkItem: this.sinksToShow_[i], substrings: matchSubstrings}); } searchResultsToShow.sort(this.compareSearchMatches_); var pendingPseudoSink = (this.pseudoSinkSearchState_) ? this.pseudoSinkSearchState_.getPseudoSink() : null; // We may need to add pseudo sinks to the filter results. A pseudo sink will // be shown if there is no real sink with the same icon and name exactly // matching the filter text. The map() call transforms any pseudo sink // objects that will be shown to the search result format, where we know // that the entire sink name will be a match. // // The exception to this is when there is a pending pseudo sink search. Then // the pseudo sink for the search will be treated like a real sink because // it will actually be in |sinksToShow_| until a real sink is returned by // search. So the filter here shouldn't treat it like a pseudo sink. searchResultsToShow = this.pseudoSinks_.filter(function(pseudoSink) { return (!pendingPseudoSink || pseudoSink.id != pendingPseudoSink.id) && !searchResultsToShow.find(function(searchResult) { return searchResult.sinkItem.name == searchInputText && searchResult.sinkItem.iconType == pseudoSink.iconType; }); }).map(function(pseudoSink) { pseudoSink.name = searchInputText; return {sinkItem: pseudoSink, substrings: [[0, searchInputText.length - 1]]}; }).concat(searchResultsToShow); this.searchResultsToShow_ = searchResultsToShow; }, /** * Helper function to locate the CastMode object with the given type in * castModeList. * * @param {number} castModeType Type of cast mode to look for. * @return {media_router.CastMode|undefined} CastMode object with the given * type in castModeList, or undefined if not found. */ findCastModeByType_: function(castModeType) { return this.castModeList.find(function(element, index, array) { return element.type == castModeType; }); }, /** * @param {?Element} element Element to compute padding for. * @return {!Array} Array containing the element's bottom padding * value and the element's top padding value, in that order. * @private */ getElementVerticalPadding_: function(element) { var style = window.getComputedStyle(element); return [parseInt(style.getPropertyValue('padding-bottom'), 10) || 0, parseInt(style.getPropertyValue('padding-top'), 10) || 0]; }, /** * Retrieves the first run flow cloud preferences text, if it exists. On * non-officially branded builds, the string is not defined. * * @return {string} Cloud preferences text. */ getFirstRunFlowCloudPrefText_: function() { return loadTimeData.valueExists('firstRunFlowCloudPrefText') ? this.i18n('firstRunFlowCloudPrefText') : ''; }, /** * @param {?media_router.Route} route Route to get the sink for. * @return {?media_router.Sink} Sink associated with |route| or * undefined if we don't have data for the sink. */ getSinkForRoute_: function(route) { return route ? this.sinkMap_[route.sinkId] : null; }, /** * @param {?Element} element Conditionally-templated element to check. * @return {boolean} Whether |element| is considered present in the document * as a conditionally-templated element. This does not check the |hidden| * attribute. */ hasConditionalElement_: function(element) { return !!element && (!element.style.display || element.style.display != 'none'); }, /** * Returns whether given string is undefined, null, empty, or whitespace only. * @param {?string} str String to be tested. * @return {boolean} |true| if the string is undefined, null, empty, or * whitespace. * @private */ isEmptyOrWhitespace_: function(str) { return str === undefined || str === null || (/^\s*$/).test(str); }, /** * Reports a user filter action if |searchInputText_| is not empty and the * filter action hasn't been reported since the view changed to the filter * view. * @private */ maybeReportFilter_: function() { if (this.reportFilterOnInput_ && this.searchInputText_.length != 0) { this.reportFilterOnInput_ = false; this.fire('report-filter'); } }, /** * Updates |currentView_| if the dialog had just opened and there's * only one local route. */ maybeShowRouteDetailsOnOpen: function() { var localRoute = null; for (var i = 0; i < this.routeList.length; i++) { var route = this.routeList[i]; if (!route.isLocal) continue; if (!localRoute) { localRoute = route; } else { // Don't show route details if there are more than one local route. localRoute = null; break; } } if (localRoute) this.showRouteDetails_(localRoute); this.fire('show-initial-state', {currentView: this.currentView_}); }, /** * Updates |currentView_| if there is a new blocking issue or a blocking * issue is resolved. Clears any pending route creation properties if the * issue corresponds with |pendingCreatedRouteId_|. * * @param {?media_router.Issue} issue The new issue, or null if the * blocking issue was resolved. * @private */ maybeShowIssueView_: function(issue) { if (!!issue) { if (issue.isBlocking) { this.currentView_ = media_router.MediaRouterView.ISSUE; } else if (this.currentView_ == media_router.MediaRouterView.SINK_LIST) { // Make space for the non-blocking issue in the sink list. this.updateElementPositioning_(); } } else { // Switch back to the sink list if the issue was cleared. If the previous // issue was non-blocking, this would be a no-op. It is expected that // the only way to clear an issue is by user action; the IssueManager // (C++ side) does not clear issues in the UI. this.showSinkList_(); } if (!!this.pendingCreatedRouteId_ && !!issue && issue.routeId == this.pendingCreatedRouteId_) { this.resetRouteCreationProperties_(false); } }, /** * If an element in the search results list has keyboard focus when we are * transitioning from the filter view to the sink list view, give focus to the * same sink in the sink list. Otherwise we leave the keyboard focus where it * is. * @private */ maybeUpdateFocusOnFilterViewExit_: function() { var searchSinks = this.$$('#search-results').querySelectorAll('paper-item'); var focusedElem = Array.prototype.find.call(searchSinks, function(sink) { return sink.focused; }); if (!focusedElem) { return; } var focusedSink = this.$$('#searchResults').itemForElement(focusedElem).sinkItem; setTimeout(function() { var sinkListPaperMenu = this.$$('#sink-list-paper-menu'); var sinks = sinkListPaperMenu.children; var sinkList = this.$$('#sinkList'); for (var i = 0; i < sinks.length; i++) { if (sinkList.itemForElement(sinks[i]).id == focusedSink.id) { sinkListPaperMenu.selectIndex(i); break; } } }.bind(this)); }, /** * May update |populatedSinkListSeenTimeMs_| depending on |currentView| and * |sinksToShow|. * Called when |currentView_| or |sinksToShow_| is updated. * * @param {?media_router.MediaRouterView} currentView The current view of the * dialog. * @param {!Array} sinksToShow The sinks to display. * @private */ maybeUpdateStartSinkDisplayStartTime_: function(currentView, sinksToShow) { if (currentView == media_router.MediaRouterView.SINK_LIST && sinksToShow.length != 0) { // Only set |populatedSinkListSeenTimeMs_| if it has not already been set. if (this.populatedSinkListSeenTimeMs_ == -1) this.populatedSinkListSeenTimeMs_ = window.performance.now(); } else { // Reset |populatedSinkListLastSeen_| if the sink list isn't being shown // or if there aren't any sinks available for display. this.populatedSinkListSeenTimeMs_ = -1; } }, /** * Animates the transition from the filter view, where the search field is at * the top of the list, to the sink list view, where the search field is at * the bottom of the list. * * If this is called while another animation is in progress, it queues itself * to be run at the end of the current animation. * * @param {!function()} resolve Resolves the animation promise that is waiting * on this animation. * @private */ moveSearchToBottom_: function(resolve) { var deviceMissing = this.$['device-missing']; var list = this.$$('#sink-list'); var resultsContainer = this.$$('#search-results-container'); var search = this.$$('#sink-search'); var view = this.$['sink-list-view']; var hasList = this.hasConditionalElement_(list); var initialHeight = view.offsetHeight; // Force the view height to be max dialog height. view.style['overflow'] = 'hidden'; var searchInitialOffsetHeight = search.offsetHeight; var searchInitialPaddingBottom, searchInitialPaddingTop; [searchInitialPaddingBottom, searchInitialPaddingTop] = this.getElementVerticalPadding_(search); var searchPadding = searchInitialPaddingBottom + searchInitialPaddingTop; var searchHeight = search.offsetHeight - searchPadding; this.searchUseBottomPadding = true; var searchFinalPaddingBottom, searchFinalPaddingTop; [searchFinalPaddingBottom, searchFinalPaddingTop] = this.getElementVerticalPadding_(search); var searchFinalOffsetHeight = searchHeight + searchFinalPaddingBottom + searchFinalPaddingTop; var resultsInitialTop = 0; var finalHeight = 0; // Get final view height ahead of animation. if (hasList) { list.style['position'] = 'absolute'; list.style['opacity'] = '0'; this.hideSinkListForAnimation_ = false; finalHeight += list.offsetHeight; list.style['position'] = 'relative'; } else { resultsInitialTop += deviceMissing.offsetHeight + searchInitialOffsetHeight; finalHeight += deviceMissing.offsetHeight; } var searchInitialTop = hasList ? 0 : deviceMissing.offsetHeight; var searchFinalTop = hasList ? list.offsetHeight - search.offsetHeight : deviceMissing.offsetHeight; resultsContainer.style['position'] = 'absolute'; var duration = this.computeAnimationDuration_(searchFinalTop - searchInitialTop); var timing = {duration: duration, easing: 'ease-in-out', fill: 'forwards'}; // This GroupEffect does the reverse of |moveSearchToTop_|. It fades the // sink list in while sliding the search input and search results list down. // The dialog height is also adjusted smoothly to the sink list height. var deviceMissingEffect = new KeyframeEffect(deviceMissing, [{'marginBottom': searchInitialOffsetHeight}, {'marginBottom': searchFinalOffsetHeight}], timing); var listEffect = new KeyframeEffect(list, [{'opacity': '0'}, {'opacity': '1'}], timing); var resultsEffect = new KeyframeEffect(resultsContainer, [{'top': resultsInitialTop + 'px', 'paddingTop': resultsContainer.style['padding-top']}, {'top': '100%', 'paddingTop': '0px'}], timing); var searchEffect = new KeyframeEffect(search, [{'top': searchInitialTop + 'px', 'marginTop': '0px', 'paddingBottom': searchInitialPaddingBottom + 'px', 'paddingTop': searchInitialPaddingTop + 'px'}, {'top': '100%', 'marginTop': '-' + searchFinalOffsetHeight + 'px', 'paddingBottom': searchFinalPaddingBottom + 'px', 'paddingTop': searchFinalPaddingTop + 'px'}], timing); var viewEffect = new KeyframeEffect(view, [{'height': initialHeight + 'px', 'paddingBottom': '0px'}, {'height': finalHeight + 'px', 'paddingBottom': searchFinalOffsetHeight + 'px'}], timing); var player = document.timeline.play(new GroupEffect(hasList ? [listEffect, resultsEffect, searchEffect, viewEffect] : [deviceMissingEffect, resultsEffect, searchEffect, viewEffect])); var that = this; var finalizeAnimation = function() { view.style['overflow'] = ''; that.putSearchAtBottom_(); that.filterTransitionPlayer_.cancel(); that.filterTransitionPlayer_ = null; that.isSearchListHidden_ = true; resolve(); }; player.finished.then(finalizeAnimation); this.filterTransitionPlayer_ = player; }, /** * Animates the transition from the sink list view, where the search field is * at the bottom of the list, to the filter view, where the search field is at * the top of the list. * * If this is called while another animation is in progress, it queues itself * to be run at the end of the current animation. * * @param {!function()} resolve Resolves the animation promise that is waiting * on this animation. * @private */ moveSearchToTop_: function(resolve) { var deviceMissing = this.$['device-missing']; var list = this.$$('#sink-list'); var noMatches = this.$$('#no-search-matches'); var results = this.$$('#search-results'); var resultsContainer = this.$$('#search-results-container'); var search = this.$$('#sink-search'); var view = this.$['sink-list-view']; // Saves current search container |offsetHeight| which includes bottom // padding. var searchInitialOffsetHeight = search.offsetHeight; var hasList = this.hasConditionalElement_(list); var searchInitialTop = hasList ? list.offsetHeight - searchInitialOffsetHeight : deviceMissing.offsetHeight; var searchFinalTop = hasList ? 0 : deviceMissing.offsetHeight; var searchInitialPaddingBottom, searchInitialPaddingTop; [searchInitialPaddingBottom, searchInitialPaddingTop] = this.getElementVerticalPadding_(search); var searchPadding = searchInitialPaddingBottom + searchInitialPaddingTop; var searchHeight = search.offsetHeight - searchPadding; this.searchUseBottomPadding = this.shouldSearchUseBottomPadding_(deviceMissing); var searchFinalPaddingBottom, searchFinalPaddingTop; [searchFinalPaddingBottom, searchFinalPaddingTop] = this.getElementVerticalPadding_(search); var searchFinalOffsetHeight = searchHeight + searchFinalPaddingBottom + searchFinalPaddingTop; // Omitting |search.offsetHeight| because it is handled by view animation // separately. var initialHeight = hasList ? list.offsetHeight : deviceMissing.offsetHeight; view.style['overflow'] = 'hidden'; var resultsPadding = this.computeElementVerticalPadding_(results); var finalHeight = this.computeTotalSearchHeight_( deviceMissing, noMatches, results, searchFinalOffsetHeight, this.sinkListMaxHeight_ + resultsPadding); var duration = this.computeAnimationDuration_(searchFinalTop - searchInitialTop); var timing = {duration: duration, easing: 'ease-in-out', fill: 'forwards'}; // This GroupEffect will cause the sink list to fade out while the search // input and search results list slide up. The dialog will also resize // smoothly to the new search result list height. var deviceMissingEffect = new KeyframeEffect(deviceMissing, [{'marginBottom': searchInitialOffsetHeight}, {'marginBottom': searchFinalOffsetHeight}], timing); var listEffect = new KeyframeEffect(list, [{'opacity': '1'}, {'opacity': '0'}], timing); var resultsEffect = new KeyframeEffect(resultsContainer, [{'top': '100%', 'paddingTop': '0px'}, {'top': searchFinalTop + 'px', 'paddingTop': searchFinalOffsetHeight + 'px'}], timing); var searchEffect = new KeyframeEffect(search, [{'top': '100%', 'marginTop': '-' + searchInitialOffsetHeight + 'px', 'paddingBottom': searchInitialPaddingBottom + 'px', 'paddingTop': searchInitialPaddingTop + 'px'}, {'top': searchFinalTop + 'px', 'marginTop': '0px', 'paddingBottom': searchFinalPaddingBottom + 'px', 'paddingTop': searchFinalPaddingTop + 'px'}], timing); var viewEffect = new KeyframeEffect(view, [{'height': initialHeight + 'px', 'paddingBottom': searchInitialOffsetHeight + 'px'}, {'height': finalHeight + 'px', 'paddingBottom': '0px'}], timing); var player = document.timeline.play(new GroupEffect(hasList ? [listEffect, resultsEffect, searchEffect, viewEffect] : [deviceMissingEffect, resultsEffect, searchEffect, viewEffect])); var that = this; var finalizeAnimation = function() { // When we are moving the search results up into view, the user may type // more text or delete text which may change the height of the search // results list. In this case, the dialog height that the animation ends // on will now be wrong. In order to correct this smoothly, // |putSearchAtTop_| will queue another animation just to adjust the // dialog height. // // The |filterTransitionPlayer_| will hold all of the animated elements in // their final keyframe state until it is canceled or another player // overrides it because we used |fill: 'forwards'| in all of the effects. // So unlike |moveSearchToBottom_|, we don't know for sure whether we want // to cancel |filterTransitionPlayer_| after |putSearchAtTop_| because // another animation may have been run to correct the dialog height. // // If |putSearchAtTop_| has to adjust the dialog height, it also queues // itself to run again when that animation is finished. When the height is // finally correct at the end of an animation, it will cancel // |filterTransitionPlayer_| itself. that.putSearchAtTop_(resolve); }; player.finished.then(finalizeAnimation); this.filterTransitionPlayer_ = player; }, /** * Handles a cast mode selection. Updates |headerText|, |headerTextTooltip|, * and |shownCastModeValue_|. * * @param {!Event} event The event object. * @private */ onCastModeClick_: function(event) { // The clicked cast mode can come from one of two lists, // defaultCastModeList and nonDefaultCastModeList. var clickedMode = this.$$('#defaultCastModeList').itemForElement(event.target) || this.$$('#nonDefaultCastModeList').itemForElement(event.target); if (!clickedMode) return; this.selectCastMode(clickedMode.type); this.fire('cast-mode-selected', {castModeType: clickedMode.type}); this.showSinkList_(); this.maybeReportUserFirstAction( media_router.MediaRouterUserAction.CHANGE_MODE); }, /** * Handles a change-route-source-click event. Sets the currently launching * sink to be the current route's sink and shows the sink list. * * @param {!Event} event The event object. * Parameters in |event|.detail: * route - route to modify. * selectedCastMode - cast mode to use for the new source. * @private */ onChangeRouteSourceClick_: function(event) { /** @type {{route: !media_router.Route, selectedCastMode: number}} */ var detail = event.detail; this.currentLaunchingSinkId_ = detail.route.sinkId; var sink = this.sinkMap_[detail.route.sinkId]; this.showSinkList_(); this.maybeReportUserFirstAction( media_router.MediaRouterUserAction.REPLACE_LOCAL_ROUTE); }, /** * Handles a close-route event. Shows the sink list and starts a timer to * close the dialog if there is no click within three seconds. * * @param {!Event} event The event object. * Parameters in |event|.detail: * route - route to close. * @private */ onCloseRoute_: function(event) { /** @type {{route: media_router.Route}} */ var detail = event.detail; this.showSinkList_(); this.startTapTimer_(); if (detail.route.isLocal) { this.maybeReportUserFirstAction( media_router.MediaRouterUserAction.STOP_LOCAL); } }, /** * Handles response of previous create route attempt. * * @param {string} sinkId The ID of the sink to which the Media Route was * creating a route. * @param {?media_router.Route} route The newly created route that * corresponds to the sink if route creation succeeded; null otherwise. * @param {boolean} isForDisplay Whether or not |route| is for display. */ onCreateRouteResponseReceived: function(sinkId, route, isForDisplay) { // The provider will handle sending an issue for a failed route request. if (!route) { this.resetRouteCreationProperties_(false); this.fire('report-resolved-route', { outcome: media_router.MediaRouterRouteCreationOutcome.FAILURE_NO_ROUTE }); return; } // Check that |sinkId| exists and corresponds to |currentLaunchingSinkId_|. if (!this.sinkMap_[sinkId] || this.currentLaunchingSinkId_ != sinkId) { this.fire('report-resolved-route', { outcome: media_router.MediaRouterRouteCreationOutcome.FAILURE_INVALID_SINK }); return; } // Regardless of whether the route is for display, it was resolved // successfully. this.fire('report-resolved-route', { outcome: media_router.MediaRouterRouteCreationOutcome.SUCCESS }); if (isForDisplay) { this.showRouteDetails_(route); this.startTapTimer_(); this.resetRouteCreationProperties_(true); } else { this.pendingCreatedRouteId_ = route.id; } }, /** * Called when a focus event is triggered. * * @param {!Event} event The event object. * @private */ onFocus_: function(event) { // If the focus event was automatically fired by Polymer, remove focus from // the element. This prevents unexpected focusing when the dialog is // initially loaded. This only happens on mac. if (cr.isMac && !event.sourceCapabilities) { // Adding a focus placeholder element is part of the workaround for // handling unexpected focusing, which only happens once on dialog open. // Since the placeholder is focus-enabled as denoted by its tabindex // value, the focus will not appear in other elements. var placeholder = this.$$('#focus-placeholder'); // Check that the placeholder is the currently focused element. In some // tests, other elements are non-user-triggered focused. if (placeholder && this.shadowRoot.activeElement == placeholder) { event.path[0].blur(); // Remove the placeholder since we have no more use for it. placeholder.remove(); } } }, /** * Called when a keydown event is fired. * @param {!Event} e Keydown event object for the event. */ onKeydown_: function(e) { // The ESC key may be pressed with a combination of other keys. It is // handled on the C++ side instead of the JS side on non-mac platforms, // which uses toolkit-views. Handle the expected behavior on all platforms // here. if (e.key == media_router.KEY_ESC && !e.shiftKey && !e.ctrlKey && !e.altKey && !e.metaKey) { // When searching, allow ESC as a mechanism to leave the filter view. if (this.currentView_ == media_router.MediaRouterView.FILTER) { // If the user tabbed to an item in the search results, or otherwise has // an item in the list focused, focus will seem to vanish when we // transition back to the sink list. Instead we should move focus to the // appropriate item in the sink list. this.maybeUpdateFocusOnFilterViewExit_(); this.showSinkList_(); e.preventDefault(); } else { this.fire('close-dialog', { pressEscToClose: true, }); } } }, /** * Called when a mouseleave event is triggered. * * @private */ onMouseLeave_: function() { this.mouseIsPositionedOverDialog_ = false; }, /** * Called when a mouseenter event is triggered. * * @private */ onMouseEnter_: function() { this.mouseIsPositionedOverDialog_ = true; }, /** * Called when a search has completed up to route creation. |sinkId| * identifies the sink that should be in |allSinks|, if a sink was found. * * @param {string} sinkId The ID of the sink that is the result of the * currently pending search. */ onReceiveSearchResult: function(sinkId) { this.pseudoSinkSearchState_.receiveSinkResponse(sinkId); this.currentLaunchingSinkId_ = this.pseudoSinkSearchState_.checkForRealSink(this.allSinks); this.rebuildSinksToShow_(); // If we're in filter view, make sure the |sinksToShow_| change is picked // up. if (this.currentView_ == media_router.MediaRouterView.FILTER) { this.filterSinks_(this.searchInputText_); } }, /** * Called when a sink is clicked. * * @param {!Event} event The event object. * @private */ onSinkClick_: function(event) { var clickedSink = (this.currentView_ == media_router.MediaRouterView.FILTER) ? this.$$('#searchResults').itemForElement(event.target).sinkItem : this.$$('#sinkList').itemForElement(event.target); this.showOrCreateRoute_(clickedSink); this.fire('sink-click', {index: event['model'].index}); }, /** * Sets the positioning of the sink list, search input, and search results so * that everything is in the correct state for the sink list view. * * @private */ putSearchAtBottom_: function() { var search = this.$$('#sink-search'); if (!this.hasConditionalElement_(search)) { return; } var deviceMissing = this.$['device-missing']; var list = this.$$('#sink-list'); var resultsContainer = this.$$('#search-results-container'); var view = this.$['sink-list-view']; this.searchUseBottomPadding = true; search.style['top'] = ''; if (resultsContainer) { resultsContainer.style['position'] = ''; resultsContainer.style['padding-top'] = ''; resultsContainer.style['top'] = ''; } this.hideSinkListForAnimation_ = false; var hasList = this.hasConditionalElement_(list); if (hasList) { search.style['margin-top'] = '-' + search.offsetHeight + 'px'; view.style['padding-bottom'] = search.offsetHeight + 'px'; list.style['opacity'] = ''; } else { deviceMissing.style['margin-bottom'] = search.offsetHeight + 'px'; search.style['margin-top'] = ''; view.style['padding-bottom'] = ''; } }, /** * Sets the positioning of the sink list, search input, and search results so * that everything is in the correct state for the filter view. * * If the user was searching while the |moveSearchToTop_| animation was * happening then the dialog height that animation ends at could be different * than the current height of the search results. If this is the case, this * function first spawns a new animation that smoothly corrects the height * problem. This is iterative, but once we enter a call where the heights * match up, the elements will become static again. * * @param {!function()} resolve Resolves the animation promise that is waiting * on this animation. * @private */ putSearchAtTop_: function(resolve) { var deviceMissing = this.$['device-missing']; var list = this.$$('#sink-list'); var noMatches = this.$$('#no-search-matches'); var results = this.$$('#search-results'); var resultsContainer = this.$$('#search-results-container'); var search = this.$$('#sink-search'); var view = this.$['sink-list-view']; // If there is a height mismatch between where the animation calculated the // height should be and where it is now because the search results changed // during the animation, correct it with... another animation. this.searchUseBottomPadding = this.shouldSearchUseBottomPadding_(deviceMissing); var resultsPadding = this.computeElementVerticalPadding_(results); var finalHeight = this.computeTotalSearchHeight_(deviceMissing, noMatches, results, search.offsetHeight, this.sinkListMaxHeight_ + resultsPadding); if (finalHeight != view.offsetHeight) { var viewEffect = new KeyframeEffect(view, [{'height': view.offsetHeight + 'px'}, {'height': finalHeight + 'px'}], {duration: this.computeAnimationDuration_(finalHeight - view.offsetHeight), easing: 'ease-in-out', fill: 'forwards'}); var player = document.timeline.play(viewEffect); if (this.heightAdjustmentPlayer_) { this.heightAdjustmentPlayer_.cancel(); } this.heightAdjustmentPlayer_ = player; player.finished.then(this.putSearchAtTop_.bind(this, resolve)); return; } var hasList = this.hasConditionalElement_(list); search.style['margin-top'] = ''; deviceMissing.style['margin-bottom'] = search.offsetHeight + 'px'; var searchFinalTop = hasList ? 0 : deviceMissing.offsetHeight; var resultsPaddingTop = hasList ? search.offsetHeight + 'px' : '0px'; search.style['top'] = searchFinalTop + 'px'; this.hideSinkListForAnimation_ = true; resultsContainer.style['position'] = 'relative'; resultsContainer.style['padding-top'] = resultsPaddingTop; resultsContainer.style['top'] = ''; view.style['overflow'] = ''; view.style['padding-bottom'] = ''; if (this.filterTransitionPlayer_) { this.filterTransitionPlayer_.cancel(); this.filterTransitionPlayer_ = null; } if (this.heightAdjustmentPlayer_) { this.heightAdjustmentPlayer_.cancel(); this.heightAdjustmentPlayer_ = null; } resolve(); }, /** * Queues a call to |moveSearchToBottom_| by adding it as a continuation to * |animationPromise_| and updating |animationPromise_|. */ queueMoveSearchToBottom_: function() { var oldPromise = this.animationPromise_; var that = this; this.animationPromise_ = new Promise(function(resolve) { oldPromise.then(that.moveSearchToBottom_.bind(that, resolve)); }); }, /** * Queues a call to |moveSearchToTop_| by adding it as a continuation to * |animationPromise_| and updating |animationPromise_|. The new promise will * not resolve until |putSearchAtTop_| is finished, including any potential * dialog height adjustment animations. */ queueMoveSearchToTop_: function() { var oldPromise = this.animationPromise_; var that = this; this.animationPromise_ = new Promise(function(resolve) { oldPromise.then(function() { that.isSearchListHidden_ = false; setTimeout(that.moveSearchToTop_.bind(that, resolve)); }); }); }, /** * Queues a call to |putSearchAtTop_| by adding it as a continuation to * |animationPromise_| and updating |animationPromise_|. */ queuePutSearchAtTop_: function() { var that = this; var oldPromise = this.animationPromise_; this.animationPromise_ = new Promise(function(resolve) { oldPromise.then(that.putSearchAtTop_.bind(that, resolve)); }); }, /** * Called when |routeList| is updated. Rebuilds |routeMap_| and * |sinkToRouteMap_|. * * @private */ rebuildRouteMaps_: function() { this.routeMap_ = {}; // Rebuild |sinkToRouteMap_| with a temporary map to avoid firing the // computed functions prematurely. var tempSinkToRouteMap = {}; // We expect that each route in |routeList| maps to a unique sink. this.routeList.forEach(function(route) { this.routeMap_[route.id] = route; tempSinkToRouteMap[route.sinkId] = route; }, this); // If there is route creation in progress, check if any of the route ids // correspond to |pendingCreatedRouteId_|. If so, the newly created route // is ready to be displayed; switch to route details view. if (this.currentLaunchingSinkId_ != '' && this.pendingCreatedRouteId_ != '') { var route = tempSinkToRouteMap[this.currentLaunchingSinkId_]; if (route && this.pendingCreatedRouteId_ == route.id) { this.showRouteDetails_(route); this.startTapTimer_(); this.resetRouteCreationProperties_(true); } } else { // If |currentRoute_| is no longer active, clear |currentRoute_|. Also // switch back to the SINK_PICKER view if the user is currently in the // ROUTE_DETAILS view. if (this.currentRoute_) { this.currentRoute_ = this.routeMap_[this.currentRoute_.id] || null; } if (!this.currentRoute_ && this.currentView_ == media_router.MediaRouterView.ROUTE_DETAILS) { this.showSinkList_(); } } this.sinkToRouteMap_ = tempSinkToRouteMap; this.rebuildSinksToShow_(); }, /** * Rebuilds the list of sinks to be shown for the current cast mode. * A sink should be shown if it is compatible with the current cast mode, or * if the sink is associated with a route. The resulting list is sorted by * name. */ rebuildSinksToShow_: function() { var updatedSinkList = this.allSinks.filter(function(sink) { return !sink.isPseudoSink; }, this); if (this.pseudoSinkSearchState_) { var pendingPseudoSink = this.pseudoSinkSearchState_.getPseudoSink(); // Here we will treat the pseudo sink that launched the search as a real // sink until one is returned by search. This way it isn't possible to // ever reach a UI state where there is no spinner being shown in the sink // list but |currentLaunchingSinkId_| is non-empty (thereby preventing any // other sink from launching). if (pendingPseudoSink.id == this.currentLaunchingSinkId_) { updatedSinkList.unshift(pendingPseudoSink); } } if (this.userHasSelectedCastMode_) { // If user explicitly selected a cast mode, then we show only sinks that // are compatible with current cast mode or sinks that are active. updatedSinkList = updatedSinkList.filter(function(element) { return (element.castModes & this.shownCastModeValue_) || this.sinkToRouteMap_[element.id]; }, this); } else { // If user did not select a cast mode, then: // - If all sinks support only a single cast mode, then the cast mode is // switched to that mode. // - Otherwise, the cast mode becomes auto mode. // Either way, all sinks will be shown. this.setShownCastMode_(this.computeCastMode_()); } // When there's an updated list of sinks, append any new sinks to the end // of the existing list. This prevents sinks randomly jumping around the // dialog, which can surprise users / lead to inadvertently casting to the // wrong sink. if (this.sinksToShow_) { for (var i = this.sinksToShow_.length - 1; i >= 0; i--) { var index = updatedSinkList.findIndex(function(updatedSink) { return this.sinksToShow_[i].id == updatedSink.id; }.bind(this)); if (index < 0) { // Remove any sinks that are no longer discovered. this.sinksToShow_.splice(i, 1); } else { // If the sink exists, move it from |updatedSinkList| to // |sinksToShow_| in the same position, as the cast modes or other // fields may have been updated. this.sinksToShow_[i] = updatedSinkList[index]; updatedSinkList.splice(index, 1); } } updatedSinkList = this.sinksToShow_.concat(updatedSinkList); } this.sinksToShow_ = updatedSinkList; }, /** * Called when |allSinks| is updated. * * @private */ reindexSinksAndRebuildSinksToShow_: function() { this.sinkMap_ = {}; this.allSinks.forEach(function(sink) { if (!sink.isPseudoSink) { this.sinkMap_[sink.id] = sink; } }, this); if (this.pseudoSinkSearchState_) { this.currentLaunchingSinkId_ = this.pseudoSinkSearchState_.checkForRealSink(this.allSinks); } this.pseudoSinks_ = this.allSinks.filter(function(sink) { return sink.isPseudoSink && !!sink.domain; }); this.rebuildSinksToShow_(); this.searchEnabled_ = this.searchEnabled_ || this.pseudoSinks_.length > 0 || this.sinksToShow_.length >= media_router.MINIMUM_SINKS_FOR_SEARCH; this.filterSinks_(this.searchInputText_ || ''); if (this.currentView_ != media_router.MediaRouterView.FILTER) { // This code is in the unique position of seeing |animationPromise_| as // null on startup. |allSinks| is initialized before |animationPromise_| // and this listener runs when |allSinks| is initialized. if (this.animationPromise_) { this.animationPromise_ = this.animationPromise_.then(this.putSearchAtBottom_.bind(this)); } else { this.putSearchAtBottom_(); } } else { this.queuePutSearchAtTop_(); } }, /** * Resets the properties relevant to creating a new route. Fires an event * indicating whether or not route creation was successful. * Clearing |currentLaunchingSinkId_| hides the spinner indicating there is * a route creation in progress and show the device icon instead. * * @private */ resetRouteCreationProperties_: function(creationSuccess) { this.pseudoSinkSearchState_ = null; this.currentLaunchingSinkId_ = ''; this.pendingCreatedRouteId_ = ''; this.fire('report-route-creation', {success: creationSuccess}); }, /** * Responds to a click on the search button by toggling sink filtering. */ searchButtonClick_: function() { // Redundancy needed because focus() only fires event if input is not // already focused. In the case that user typed text, hit escape, then // clicks the search button, a focus event will not fire and so its event // handler from ready() will not run. this.showSearchResults_(); this.$$('#sink-search-input').focus(); }, /** * Initializes the position of the search input if search becomes enabled. * @param {boolean} searchEnabled The new value of |searchEnabled_|. * @private */ searchEnabledChanged_: function(searchEnabled) { if (searchEnabled) { this.async(function() { this.setSearchFocusHandlers_(); this.putSearchAtBottom_(); }); } }, /** * Filters the sink list when the input text changes and shows the search * results if |searchInputText| is not empty. * @param {string} searchInputText The currently entered search text. * @private */ searchInputTextChanged_: function(searchInputText) { this.filterSinks_(searchInputText); if (searchInputText.length != 0) { this.showSearchResults_(); this.maybeReportFilter_(); } }, /** * Sets the selected cast mode to the one associated with |castModeType|, * and rebuilds sinks to reflect the change. * @param {number} castModeType The type of the selected cast mode. */ selectCastMode: function(castModeType) { var castMode = this.findCastModeByType_(castModeType); if (castMode && castModeType != this.shownCastModeValue_) { this.setShownCastMode_(castMode); this.userHasSelectedCastMode_ = true; this.rebuildSinksToShow_(); } }, /** * Sets various focus and blur event handlers to handle showing search results * when the search input is focused. * @private */ setSearchFocusHandlers_: function() { var searchInput = this.$$('#sink-search-input'); var that = this; // The window can see a blur event for two important cases: the window is // actually losing focus or keyboard focus is wrapping from the end of the // document to the beginning. To handle both cases, we save whether the // search input was focused during the window blur event. // // When the search input receives focus, it could be as part of window // focus. If the search input was also focused on window blur, it shouldn't // show search results if they aren't already being shown. Otherwise, // focusing the search input should activate the FILTER view by calling // |showSearchResults_()|. window.addEventListener('blur', function() { that.isSearchFocusedOnWindowBlur_ = that.shadowRoot.activeElement == searchInput; }); searchInput.addEventListener('focus', function() { if (!that.isSearchFocusedOnWindowBlur_) { that.showSearchResults_(); } }); }, /** * Updates the shown cast mode, and updates the header text fields * according to the cast mode. If |castMode| type is AUTO, then set * |userHasSelectedCastMode_| to false. * * @param {!media_router.CastMode} castMode */ setShownCastMode_: function(castMode) { if (this.shownCastModeValue_ == castMode.type) return; this.shownCastModeValue_ = castMode.type; this.headerText = castMode.description; this.headerTextTooltip = castMode.host || ''; if (castMode.type == media_router.CastModeType.AUTO) this.userHasSelectedCastMode_ = false; }, /** * @param {?Element} deviceMissing Device missing message element. * @return {boolean} Whether the search input should use vertical padding as * if it were the lowest (at the very bottom) item in the dialog. * @private */ shouldSearchUseBottomPadding_: function(deviceMissing) { return !deviceMissing.hasAttribute('hidden'); }, /** * Shows the cast mode list. * * @private */ showCastModeList_: function() { this.currentView_ = media_router.MediaRouterView.CAST_MODE_LIST; }, /** * Creates a new route if there is no route to the |sink| . Otherwise, * shows the route details. * * @param {!media_router.Sink} sink The sink to use. * @private */ showOrCreateRoute_: function(sink) { var route = this.sinkToRouteMap_[sink.id]; if (route) { this.showRouteDetails_(route); this.fire('navigate-sink-list-to-details'); this.maybeReportUserFirstAction( media_router.MediaRouterUserAction.STATUS_REMOTE); } else if (this.currentLaunchingSinkId_ == '') { // Allow one launch at a time. var selectedCastModeValue = this.shownCastModeValue_ == media_router.CastModeType.AUTO ? sink.castModes & -sink.castModes : this.shownCastModeValue_; if (sink.isPseudoSink) { this.pseudoSinkSearchState_ = new PseudoSinkSearchState(sink); this.fire('search-sinks-and-create-route', { id: sink.id, name: sink.name, domain: sink.domain, selectedCastMode: selectedCastModeValue }); } else { this.fire('create-route', { sinkId: sink.id, // If user selected a cast mode, then we will create a route using // that cast mode. Otherwise, the UI is in "auto" cast mode and will // use the preferred cast mode compatible with the sink. The preferred // cast mode value is the least significant bit on the bitset. selectedCastModeValue: selectedCastModeValue }); var timeToSelectSink = window.performance.now() - this.populatedSinkListSeenTimeMs_; this.fire('report-sink-click-time', {timeMs: timeToSelectSink}); } this.currentLaunchingSinkId_ = sink.id; if (sink.isPseudoSink) { this.rebuildSinksToShow_(); } this.maybeReportUserFirstAction( media_router.MediaRouterUserAction.START_LOCAL); } }, /** * Shows the route details. * * @param {!media_router.Route} route The route to show. * @private */ showRouteDetails_: function(route) { this.currentRoute_ = route; this.currentView_ = media_router.MediaRouterView.ROUTE_DETAILS; }, /** * Shows the search results. * * @private */ showSearchResults_: function() { if (this.currentView_ != media_router.MediaRouterView.FILTER) { this.currentView_ = media_router.MediaRouterView.FILTER; this.queueMoveSearchToTop_(); } }, /** * Shows the sink list. * * @private */ showSinkList_: function() { if (this.currentView_ == media_router.MediaRouterView.FILTER) { this.queueMoveSearchToBottom_(); this.currentView_ = media_router.MediaRouterView.SINK_LIST; } else { this.currentView_ = media_router.MediaRouterView.SINK_LIST; this.putSearchAtBottom_(); } }, /** * Starts a timer which fires a close-dialog event if the user's mouse is * not positioned over the dialog after three seconds. * * @private */ startTapTimer_: function() { var id = setTimeout(function() { if (!this.mouseIsPositionedOverDialog_) this.fire('close-dialog', { pressEscToClose: false, }); }.bind(this), 3000 /* 3 seconds */); }, /** * Toggles |currentView_| between CAST_MODE_LIST and SINK_LIST. * * @private */ toggleCastModeHidden_: function() { if (this.currentView_ == media_router.MediaRouterView.CAST_MODE_LIST) { this.showSinkList_(); } else if (this.currentView_ == media_router.MediaRouterView.SINK_LIST) { this.showCastModeList_(); this.fire('navigate-to-cast-mode-list'); } }, /** * Update the position-related styling of some elements. * * @private */ updateElementPositioning_: function() { // Ensures that conditionally templated elements have finished stamping. this.async(function() { var headerHeight = this.header.offsetHeight; // Unlike the other elements whose heights are fixed, the first-run-flow // element can have a fractional height. So we use getBoundingClientRect() // to avoid rounding errors. var firstRunFlowHeight = this.$$('#first-run-flow') && this.$$('#first-run-flow').style.display != 'none' ? this.$$('#first-run-flow').getBoundingClientRect().height : 0; var issueHeight = this.$$('#issue-banner') && this.$$('#issue-banner').style.display != 'none' ? this.$$('#issue-banner').offsetHeight : 0; var search = this.$$('#sink-search'); var hasSearch = this.hasConditionalElement_(search); var searchHeight = hasSearch ? search.offsetHeight : 0; var searchPadding = hasSearch ? this.computeElementVerticalPadding_(search) : 0; this.header.style.marginTop = firstRunFlowHeight + 'px'; this.$['content'].style.marginTop = firstRunFlowHeight + headerHeight + 'px'; var sinkList = this.$$('#sink-list'); if (hasSearch && sinkList) { // This would need to be reset to '' if search could be disabled again, // but once it's enabled it can't be disabled again. this.$$('#sink-list-paper-menu').style.paddingBottom = '0'; } var sinkListPadding = sinkList ? this.computeElementVerticalPadding_(sinkList) : 0; this.sinkListMaxHeight_ = this.dialogHeight_ - headerHeight - firstRunFlowHeight - issueHeight - searchHeight + searchPadding - sinkListPadding; if (sinkList) { sinkList.style.maxHeight = this.sinkListMaxHeight_ + 'px'; var searchResults = this.$$('#search-results'); if (searchResults) searchResults.style.maxHeight = this.sinkListMaxHeight_ + 'px'; } }); }, /** * Update the max dialog height and update the positioning of the elements. * * @param {number} height The max height of the Media Router dialog. */ updateMaxDialogHeight: function(height) { this.dialogHeight_ = height; this.updateElementPositioning_(); }, }); /* Copyright 2015 The Chromium Authors. All rights reserved. * Use of this source code is governed by a BSD-style license that can be * found in the LICENSE file. */ #arrow-drop-container { flex-grow: 1; } #arrow-drop-icon { height: var(--navigation-icon-button-size); width: var(--navigation-icon-button-size); } #back-button { height: var(--navigation-icon-button-size); width: var(--navigation-icon-button-size); } #back-button-container { -webkit-padding-end: 4px; } #close-button { -webkit-margin-start: auto; height: 31px; width: 31px; } #close-button-container { -webkit-margin-start: auto; -webkit-padding-end: 16px; -webkit-padding-start: 24px; } #header { -webkit-padding-start: 8px; align-items: center; color: white; } #header-and-arrow-container { display: flex; overflow: hidden; white-space: nowrap; } #header-text { -webkit-padding-end: 4px; font-size: 1.175em; margin: 8px; overflow: hidden; text-overflow: ellipsis; } .issue { background-color: var(--paper-red-700); } paper-icon-button { display: inline-block; } #main-container { display: flex; padding-top: 10px; } .cast-mode-list, .filter, .route-details, .sink-list { background-color: var(--paper-blue-700); } #user-email-container { -webkit-padding-start: 8px; bottom: 0; font-size: 0.917em; left: auto; padding-bottom: 12px; position: absolute; } // Copyright 2015 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. // This Polymer element is used as a header for the media router interface. Polymer({ is: 'media-router-header', properties: { /** * The name of the icon used as the back button. This is set once, when * the |this| is ready. * @private {string|undefined} */ arrowDropIcon_: { type: String, }, /** * Whether or not the arrow drop icon should be disabled. * @type {boolean} */ arrowDropIconDisabled: { type: Boolean, value: false, }, /** * The header text to show. * @type {string|undefined} */ headingText: { type: String, }, /** * The height of the header when it shows the user email. * @private {number} */ headerWithEmailHeight_: { type: Number, readOnly: true, value: 62, }, /** * The height of the header when it doesn't show the user email. * @private {number} */ headerWithoutEmailHeight_: { type: Number, readOnly: true, value: 52, }, /** * Whether to show the user email in the header. * @type {boolean|undefined} */ showEmail: { type: Boolean, observer: 'maybeChangeHeaderHeight_', }, /** * The text to show in the tooltip. * @type {string|undefined} */ tooltip: { type: String, }, /** * The user email if they are signed in. * @type {string|undefined} */ userEmail: { type: String, }, /** * The current view that this header should reflect. * @type {?media_router.MediaRouterView|undefined} */ view: { type: String, observer: 'updateHeaderCursorStyle_', }, }, behaviors: [ I18nBehavior, ], ready: function() { this.$$('#header').style.height = this.headerWithoutEmailHeight_ + 'px'; }, attached: function() { // isRTL() only works after i18n_template.js runs to set . // Set the back button icon based on text direction. this.arrowDropIcon_ = isRTL() ? 'media-router:arrow-forward' : 'media-router:arrow-back'; }, /** * @param {?media_router.MediaRouterView} view The current view. * @return {string} The icon to use. * @private */ computeArrowDropIcon_: function(view) { return view == media_router.MediaRouterView.CAST_MODE_LIST ? 'media-router:arrow-drop-up' : 'media-router:arrow-drop-down'; }, /** * @param {?media_router.MediaRouterView} view The current view. * @return {boolean} Whether or not the arrow drop icon should be hidden. * @private */ computeArrowDropIconHidden_: function(view) { return view != media_router.MediaRouterView.SINK_LIST && view != media_router.MediaRouterView.CAST_MODE_LIST; }, /** * @param {?media_router.MediaRouterView} view The current view. * @return {string} The title text for the arrow drop button. * @private */ computeArrowDropTitle_: function(view) { return view == media_router.MediaRouterView.CAST_MODE_LIST ? this.i18n('viewDeviceListButtonTitle') : this.i18n('viewCastModeListButtonTitle'); }, /** * @param {?media_router.MediaRouterView} view The current view. * @return {boolean} Whether or not the back button should be shown. * @private */ computeBackButtonShown_: function(view) { return view == media_router.MediaRouterView.ROUTE_DETAILS || view == media_router.MediaRouterView.FILTER; }, /** * Returns whether given string is undefined, null, empty, or whitespace only. * @param {?string} str String to be tested. * @return {boolean} |true| if the string is undefined, null, empty, or * whitespace. * @private */ isEmptyOrWhitespace_: function(str) { return str === undefined || str === null || (/^\s*$/).test(str); }, /** * Handles a click on the back button by firing a back-click event. * * @private */ onBackButtonClick_: function() { this.fire('back-click'); }, /** * Handles a click on the close button by firing a close-button-click event. * * @private */ onCloseButtonClick_: function() { this.fire('close-dialog', { pressEscToClose: false, }); }, /** * Handles a click on the arrow button by firing an arrow-click event. * * @private */ onHeaderOrArrowClick_: function() { if (this.view == media_router.MediaRouterView.SINK_LIST || this.view == media_router.MediaRouterView.CAST_MODE_LIST) { this.fire('header-or-arrow-click'); } }, /** * Updates header height to accomodate email text. This is called on changes * to |showEmail| and will return early if the value has not changed. * * @param {boolean} newValue The new value of |showEmail|. * @param {boolean} oldValue The previous value of |showEmail|. * @private */ maybeChangeHeaderHeight_: function(newValue, oldValue) { if (oldValue == newValue) return; // Ensures conditional templates are stamped. this.async(function() { var currentHeight = this.offsetHeight; this.$$('#header').style.height = this.showEmail && !this.isEmptyOrWhitespace_(this.userEmail) ? this.headerWithEmailHeight_ + 'px' : this.headerWithoutEmailHeight_ + 'px'; // Only fire if height actually changed. if (currentHeight != this.offsetHeight) { this.fire('header-height-changed'); } }); }, /** * Updates the cursor style for the header text when the view changes. When * the drop arrow is also shown, the header text is also clickable. * * @param {?media_router.MediaRouterView} view The current view. * @private */ updateHeaderCursorStyle_: function(view) { this.$$('#header-text').style.cursor = view == media_router.MediaRouterView.SINK_LIST || view == media_router.MediaRouterView.CAST_MODE_LIST ? 'pointer' : 'auto'; }, }); /* Copyright 2016 The Chromium Authors. All rights reserved. * Use of this source code is governed by a BSD-style license that can be * found in the LICENSE file. */ .highlight { font-weight: bold; } // Copyright 2016 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. // This Polymer element displays text that needs sections of it highlighted. // This is useful, for example, for displaying which portions of a string were // matched by some filter text. Polymer({ is: 'media-router-search-highlighter', properties: { /** * The text that this element should display, split it into highlighted and * normal text. The displayed text will alternate between plainText and * highlightedText. * * Example: You have a sink with the name 'living room'. * When your seach text is 'living', the resulting arrays will be: * plainText: [null, ' room'], highlightedText: ['living', null] * * When your search text is 'room', the resulting arrays will be: * plainText: ['living ', null], highlightedText: [null, 'room'] * * null corresponds to an empty string when the arrays are being combined. * So both examples reproduce the text 'living room', but with different * words highlighted. * @type {{highlightedText: !Array, * plainText: !Array}|undefined} */ data: { type: Object, observer: 'dataChanged_', }, /** * The text that this element is displaying as a plain string. The primary * purpose for this property is to make getting this element's textContent * easy for testing. * @type {string|undefined} */ text: { type: String, readOnly: true, notify: false, }, }, /** * Update the element text if |data| changes. * * The order the strings are combined is plainText[0] highlightedText[0] * plainText[1] highlightedText[1] etc. * * @param {{highlightedText: !Array, plainText: !Array}} * data * Parameters in |data|: * highlightedText - Array of strings that should be displayed highlighted. * plainText - Array of strings that should be displayed normally. */ dataChanged_: function(data) { if (!data || !data.highlightedText || !data.plainText) { return; } var text = ''; for (var i = 0; i < data.highlightedText.length; ++i) { if (data.plainText[i]) { text += HTMLEscape(/** @type {!string} */ (data.plainText[i])); } if (data.highlightedText[i]) { text += '' + HTMLEscape(/** @type {!string} */ (data.highlightedText[i])) + ''; } } this.$.text.innerHTML = text; this._setText(this.$.text.textContent); }, }); /* Copyright 2015 The Chromium Authors. All rights reserved. * Use of this source code is governed by a BSD-style license that can be * found in the LICENSE file. */ #custom-controller { display: inline-block; height: 142px; width: 100%; } #route-action-buttons { @apply(--layout-horizontal); @apply(--layout-end-justified); margin: 0; padding: 0; white-space: nowrap; } .route-button { -webkit-padding-end: 24px; -webkit-padding-start: 0; background-color: white; line-height: 12px; margin: 12px 0; text-align: end; } #route-information { -webkit-padding-end: var(--dialog-padding-end); -webkit-padding-start: 44px; background-color: white; font-size: 1.2em; line-height: 1.5em; margin-top: 16px; overflow: hidden; } // Copyright 2015 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. // This Polymer element shows information from media that is currently cast // to a device. Polymer({ is: 'route-details', properties: { /** * The text for the current casting activity status. * @private {string|undefined} */ activityStatus_: { type: String, }, /** * Whether the external container will accept change-route-source-click * events. * @private {boolean} */ changeRouteSourceAvailable_: { type: Boolean, computed: 'computeChangeRouteSourceAvailable_(route, sink,' + 'isAnySinkCurrentlyLaunching, shownCastModeValue)', }, /** * Whether a sink is currently launching in the container. * @type {boolean} */ isAnySinkCurrentlyLaunching: { type: Boolean, value: false, }, /** * The route to show. * @type {?media_router.Route|undefined} */ route: { type: Object, observer: 'maybeLoadCustomController_', }, /** * The cast mode shown to the user. Initially set to auto mode. (See * media_router.CastMode documentation for details on auto mode.) * @type {number} */ shownCastModeValue: { type: Number, value: media_router.AUTO_CAST_MODE.type, }, /** * Sink associated with |route|. * @type {?media_router.Sink} */ sink: { type: Object, value: null, }, /** * Whether the custom controller should be hidden. * A custom controller is shown iff |route| specifies customControllerPath * and the view can be loaded. * @private {boolean} */ isCustomControllerHidden_: { type: Boolean, value: true, }, }, behaviors: [ I18nBehavior, ], /** * Fires a close-route event. This is called when the button to close * the current route is clicked. * * @private */ closeRoute_: function() { this.fire('close-route', {route: this.route}); }, /** * @param {?media_router.Route|undefined} route * @param {boolean} changeRouteSourceAvailable * @return {boolean} Whether to show the button that allows casting to the * current route or the current route's sink. */ computeCastButtonHidden_: function(route, changeRouteSourceAvailable) { return !((route && route.canJoin) || changeRouteSourceAvailable); }, /** * @param {?media_router.Route|undefined} route The current route for the * route details view. * @param {?media_router.Sink|undefined} sink Sink associated with |route|. * @param {boolean} isAnySinkCurrentlyLaunching Whether a sink is launching * now. * @param {number} shownCastModeValue Currently selected cast mode value or * AUTO if no value has been explicitly selected. * @return {boolean} Whether the change route source function should be * available when displaying |currentRoute| in the route details view. * Changing the route source should not be available when the currently * selected source that would be cast is the same as the route's current * source. * @private */ computeChangeRouteSourceAvailable_: function( route, sink, isAnySinkCurrentlyLaunching, shownCastModeValue) { if (isAnySinkCurrentlyLaunching || !route || !sink) { return false; } if (!route.currentCastMode) { return true; } var selectedCastMode = this.computeSelectedCastMode_(shownCastModeValue, sink); return (selectedCastMode != 0) && (selectedCastMode != route.currentCastMode); }, /** * @param {number} castMode User selected cast mode or AUTO. * @param {?media_router.Sink} sink Sink to which we will cast. * @return {number} The selected cast mode when |castMode| is selected in the * dialog and casting to |sink|. Returning 0 means there is no cast mode * available to |sink| and therefore the start-casting-to-route button * will not be shown. */ computeSelectedCastMode_: function(castMode, sink) { // |sink| can be null when there is a local route, which is shown in the // dialog, but the sink to which it is connected isn't in the current set of // sinks known to the dialog. This can happen, for example, with DIAL // devices. A route is created to a DIAL device, but opening the dialog on // a tab that only supports mirroring will not show the DIAL device. The // route will be shown in route details if it is the only local route, so // you arrive at this function with a null |sink|. if (!sink) { return 0; } if (castMode == media_router.CastModeType.AUTO) { return sink.castModes & -sink.castModes; } return castMode & sink.castModes; }, /** * Fires a join-route-click event if the current route is joinable, otherwise * it fires a change-route-source-click event, which changes the source of the * current route. This may cause the current route to be closed and a new * route to be started. This is called when the button to start casting to the * current route is clicked. * * @private */ startCastingToRoute_: function() { if (this.route.canJoin) { this.fire('join-route-click', {route: this.route}); } else { this.fire('change-route-source-click', { route: this.route, selectedCastMode: this.computeSelectedCastMode_(this.shownCastModeValue, this.sink) }); } }, /** * Loads the custom controller if |route.customControllerPath| exists. * Falls back to the default route details view otherwise, or if load fails. * Updates |activityStatus_| for the default view. * * @private */ maybeLoadCustomController_: function() { this.activityStatus_ = this.route ? loadTimeData.getStringF('castingActivityStatus', this.route.description) : ''; if (!this.route || !this.route.customControllerPath) { this.isCustomControllerHidden_ = true; return; } // Show custom controller var extensionview = this.$['custom-controller']; // Do nothing if the url is the same and the view is not hidden. if (this.route.customControllerPath == extensionview.src && !this.isCustomControllerHidden_) return; var that = this; extensionview.load(this.route.customControllerPath) .then(function() { // Load was successful; show the custom controller. that.isCustomControllerHidden_ = false; }, function() { // Load was unsuccessful; fall back to default view. that.isCustomControllerHidden_ = true; }); }, }); // Copyright 2016 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. /** * This class holds state that is relevant to the search process from the UI's * perspective. It primarily handles the spinner logic while waiting for a * search to complete. The spinner first needs to start on the pseudo sink, but * when a real sink arrives to replace it the spinner should transfer to the * real sink. * * Additionally, this class provides a method for * onCreateRouteResponseReceived() that maps the pseudo sink ID that started the * search to the real sink that was produced by the search. This helps check * whether a received route is valid. * * @param {!media_router.Sink} pseudoSink Pseudo sink that started the search. * @constructor */ var PseudoSinkSearchState = function(pseudoSink) { /** * Pseudo sink that started the search. * @private {!media_router.Sink} */ this.pseudoSink_ = pseudoSink; /** * The ID of the sink that is found by search. * @private {string} */ this.realSinkId_ = ''; /** * Whether we have received a sink in the sink list with ID |realSinkId_|. * @private {boolean} */ this.hasRealSink_ = false; }; /** * Record the real sink ID returned from the Media Router. * @param {string} sinkId Real sink ID that is the result of the search. */ PseudoSinkSearchState.prototype.receiveSinkResponse = function(sinkId) { this.realSinkId_ = sinkId; }; /** * Checks whether we have a sink in |sinkList| that is our search result then * computes the value for |currentLaunchingSinkId_| based on the state of the * search. It should be the pseudo sink ID until the real sink arrives, then the * real sink ID. * @param {!Array} sinkList List of all sinks to check. * @return {string} New value for |currentLaunchingSinkId_|. */ PseudoSinkSearchState.prototype.checkForRealSink = function(sinkList) { if (!this.hasRealSink_) { this.hasRealSink_ = !!this.realSinkId_ && sinkList.some(function(sink) { return (sink.id == this.realSinkId_); }, this); return !this.hasRealSink_ ? this.pseudoSink_.id : this.realSinkId_; } return this.realSinkId_; }; /** * Returns the pseudo sink for the current search. This is used to enforce * freezing its name in filter view and displaying it in the sink list view. * @return {!media_router.Sink} */ PseudoSinkSearchState.prototype.getPseudoSink = function() { return this.pseudoSink_; }; Google Cast /* Copyright 2016 The Chromium Authors. All rights reserved. * Use of this source code is governed by a BSD-style license that can be * found in the LICENSE file. */ body, extensionview, html { border: 0; height: 100%; margin: 0; padding: 0; width: 100%; } extensionview { overflow: hidden; position: absolute; } // Copyright 2016 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. window.addEventListener('load', function init() { var extensionView = document.querySelector('extensionview'); /** * @param {string} str * @return {!Array} */ var splitUrlOnHash = function(str) { str = str || ''; var pos = str.indexOf('#'); return (pos !== -1) ? [str.substr(0, pos), str.substr(pos + 1)] : [str, '']; }; new MutationObserver(function() { var newHash = splitUrlOnHash(extensionView.getAttribute('src'))[1]; var oldHash = window.location.hash.substr(1); if (newHash !== oldHash) { window.location.hash = newHash; } }).observe(extensionView, { attributes: true }); window.addEventListener('hashchange', function() { var newHash = window.location.hash.substr(1); var extensionViewSrcParts = splitUrlOnHash( extensionView.getAttribute('src')); if (newHash !== extensionViewSrcParts[1]) { extensionView.load(extensionViewSrcParts[0] + '#' + newHash); } }); extensionView.load( 'chrome-extension://' + loadTimeData.getString('extensionId') + '/cast_setup/index.html#' + window.location.hash.substr(1) || 'devices'); }); @@ (@F  (n@ ( P (Y(@  ۶yΆ$̀ـٳfٳ٦Yٌ ̀ ٙ3̌̌3   ٙ ٌ ٙ ٙ        Հ ժ3Pacbbbcc⹀bؓ'cՂbۏbmbbbbbbbbخbXbb݊ bҌ#Ђ|π ӎ*٤V῎Ń<  2x1~}uܫbأSՙ?ю*΄ }̀ϊ%ԚGگtǤȍH 2Ѝ$1ۑ(ݔ()ߓ%щχ΃ ~ ͂Ћ(ԙF٭mտ̔O  2x1~ׁ݆څ ΁΅Љ"ю-ԗ>֢U۰tӻ̔R  2ˇ$1ב*ܓ-2ߛ9ԘA՞N٧aܲx࿐̩ˑQ   2ձx1廁ĊƏƟίܿȈM  21װwC   21Мh5 21֮ƀP*  21ط̐d;   21ذˎhD&  21ۼңɆeE)  21ۼլϘǂlT<'  2Ь1߱КʏƄwdO:+   2S1TSVRIC=6*  22  32 32  2ݟ-}  ~ -ޜ!!f  fے$#B""#"!!  A +&P##$$$""""N$ا'ޥ&6(R'a'b'a%a%a%a%a%a%a%a%a%a%a%a%a"a"a"a"a a a a a a a a a a aaaaaaaaaaaaaaaaaaaaaaaabaR١6ߪ ۶$ժ@ 636666666666666((((((((((((666666(((((((((((((((((((۞$ס(ժ+ ( @ ()ڢK) ̟((((Ɔ( f((((߿( PȶݤH ΦĂ mڶ PAܜ/ ٻs }ڮ PĞڈS ɗϕ Nٖ'ۉŽ  Nͬۙ.؄ݧT˕ݎC ޷rNpۢJ׉ وݥO<܋Ǖ ՀNڋ ژ/Ểۉޠ8ņ ⺀Nʎˢѫېٌ ͥ= NҮڕ#׆uʕ NŎےׅ߭b/ NЪߧOׇ ׉qb ѱN۶řݳtԗ<΃zѐ2ÛNj yNق| ΂Ғ7ݸ͙ ՝KNW٨cݷϳʐ  Nm NӦ= NҟQ N׵̔a7 P P P P(0 ;YYΩYׁYԲYYYdYߒYYYY(YY Y Y YYYY<HΣa) IݤMݗ#@9 |ւ佁ߔo۹ {zܺګ^ׄިRÃ܍ָɈ {Ӆ{؏wݒߧIF {ŗ{ۯߤDً׶ {{ߦJׄǚL {{Ǘړ#׈ ƘΓ {ཇ{Ljڨ]ъ#}ԘBսٷ  {~{ߍҐ1٧`˪ڻ! {{Ϛ {{[  {{ҡ[ {K{N<%  {  "L !  J->(Y(Y(Y%Y%Y%Y"Y"Y Y Y YYYYYYYYYYޣ=(  .ۙ.ظ@͞c .*=c ٚ3n'; ڝ=.iYa ̧‹۔$p dٓ$ ٤VNҏ.٦[0 ߻Ԫ3 ~ ~P   %0$" /PNG  IHDRw= pHYs  tEXtSoftwareAdobe ImageReadyqe<IDATxb?-d[ K!xbAH-7s)5 ~ T )] Yd8X-Pb}%Ĩ!8 pAP) '*bXÉh0k801 l4iLiUTЮцgկ IENDB`PNG  IHDRw= pHYs  tEXtSoftwareAdobe ImageReadyqe<IDATx E C &; p 11Q# ?hSJk퀊hR0RX(z@s4g fΝ 6R)ďE4$5M9%$T ĠϘdE0|q=- WHh\\+b^n.ђvShkIENDB`PNG  IHDRw= pHYs  tEXtSoftwareAdobe ImageReadyqe<IDATx E C &; p 11Q# ?hSJk퀊hR0RX(z@s4g fΝ 6R)ďE4$5M9%$T ĠϘdE0|q=- WHh\\+b^n.ђvShkIENDB`{ "services": [ { "display_name": "Image Decoder Service", "name": "image_decoder", "interface_provider_specs": { "service_manager:connector": { "requires": {}, "provides": { "decode": [ "image_decoder::mojom::ImageDecoder" ] } } } }, { "display_name": "Preferences", "name": "preferences", "interface_provider_specs": { "service_manager:connector": { "requires": {}, "provides": { "preferences_manager": [ "prefs::mojom::PreferencesFactory" ] } } } } ], "display_name": "Chrome", "name": "content_browser", "interface_provider_specs": { "service_manager:connector": { "requires": { "nacl_loader": [ "browser" ], "preferences": [ "preferences_manager" ], "image_decoder": [ "decode" ], "nacl_broker": [ "browser" ], "ui": [ "ime_registrar" ], "ash": [ "ash" ], "accessibility_autoclick": [ "ash:autoclick" ] }, "provides": { "gpu": [ "metrics::mojom::CallStackProfileCollector" ], "mash:launchable": [ "mash::mojom::Launchable" ], "ash": [ "app_list::mojom::AppList", "app_list::mojom::AppListPresenter", "ash::mojom::AcceleratorController", "ash::mojom::CastConfig", "ash::mojom::LocaleNotificationController", "ash::mojom::MediaController", "ash::mojom::NewWindowController", "ash::mojom::SessionController", "ash::mojom::ShelfController", "ash::mojom::ShutdownController", "ash::mojom::SystemTray", "ash::mojom::TouchViewManager", "ash::mojom::WallpaperController", "ash::mojom::VpnList", "keyboard::mojom::Keyboard" ], "renderer": [ "autofill::mojom::AutofillDriver", "autofill::mojom::PasswordManagerDriver", "chrome::mojom::NetBenchmarking", "chrome::mojom::FieldTrialRecorder", "extensions::StashService", "metrics::mojom::LeakDetector", "mojom::ModuleEventSink", "rappor::mojom::RapporRecorder", "startup_metric_utils::mojom::StartupMetricHost", "translate::mojom::ContentTranslateDriver" ], "ime:ime_driver": [] } }, "navigation:frame": { "provides": { "renderer": [ "autofill::mojom::AutofillDriver", "autofill::mojom::PasswordManagerDriver", "blink::mojom::ShareService", "bluetooth::mojom::AdapterFactory", "chrome::mojom::PrerenderCanceler", "device::usb::ChooserService", "device::usb::DeviceManager", "contextual_search::mojom::ContextualSearchJsApiService", "dom_distiller::mojom::DistillabilityService", "dom_distiller::mojom::DistillerJavaScriptService", "extensions::KeepAlive", "extensions::mime_handler::MimeHandlerService", "media_router::mojom::MediaRouter", "mojom::OmniboxPageHandler", "password_manager::mojom::CredentialManager", "shape_detection::mojom::BarcodeDetection", "shape_detection::mojom::TextDetection", "translate::mojom::ContentTranslateDriver", "mojom::OmniboxPageHandler", "mojom::PluginsPageHandler", "mojom::SiteEngagementUIHandler", "mojom::UsbInternalsPageHandler" ] } } } }{ "name": "content_gpu", "interface_provider_specs": { "service_manager:connector": { "provides": { "browser": [ "arc::mojom::VideoAcceleratorService", "arc::mojom::VideoAcceleratorServiceClient", "chrome::mojom::ResourceUsageReporter" ] } } } } { "name": "content_plugin", "interface_provider_specs": { "service_manager:connector": { "provides": { "browser": [ "chrome::mojom::ResourceUsageReporter" ] } } } } { "display_name": "Chrome Render Process", "interface_provider_specs": { "service_manager:connector": { "provides": { "browser": [ "chrome::mojom::ResourceUsageReporter" ] } }, "navigation:frame": { "provides": { "browser": [ "autofill::mojom::AutofillAgent", "autofill::mojom::PasswordAutofillAgent", "autofill::mojom::PasswordGenerationAgent", "contextual_search::mojom::OverlayPageNotifierService", "dom_distiller::mojom::DistillerPageNotifierService" ] } } } } { "name": "content_utility", "interface_provider_specs": { "service_manager:connector": { "provides": { "browser": [ "chrome::mojom::ProfileImport", "chrome::mojom::ResourceUsageReporter", "chrome::mojom::ShellHandler", "extensions::mojom::MediaParser", "extensions::mojom::WiFiCredentialsGetter", "net::interfaces::ProxyResolverFactory", "safe_json::mojom::SafeJsonParser" ] } } } } { "name": "nacl_loader", "display_name": "NaCl loader", "interface_provider_specs": { "service_manager:connector": { "provides": { "browser": [ "IPC::mojom::ChannelBootstrap" ] } } } } { "name": "nacl_broker", "display_name": "NaCl broker", "interface_provider_specs": { "service_manager:connector": { "provides": { "browser": [ "IPC::mojom::ChannelBootstrap" ] } } } } RIFF:WEBPVP8 .p*p>RP1(' j in2YNV-9~<3AKO}[$;O3+eM2]#;q.0A? {ml5z0` bq.0%UYNg"L%69_>EwSP1L\`.:._ 1j8OCu&)Xc+i[i1qLSc6׸J; bq.0` bq90` bq._1qLS 1r. gu51L\`.:Qm!gu51L\`.:ȷtn~?s9kqo5=Xb\u&)XBE-PG׏c^=Xb2#Em3.0` b\ 4~S*Zgbu1nڡ~gS 1qLS `>ň bsըJ[iRݬP_;8eK Xb\u&_Vpb\u&)F>:L &3 J41qLS @9g*\u&)$*|"~Izm.0` bq&!%TXG$j; bpgͅ~ϭQ!nNq.0` c~k/viU=\L=<1L\`.:xUz?%w *jb\u&)XcF3Veˏk4sO[K^&P;.?ۨv\.?P\n Dє;.?ۨv\/`?.?P\nqCrI;JP\nq]Sq.0` bvA\':Quˏ%t~\nqCqeC}p.oJ1~uˏeOe6ćqCqeuA6seuˏ`;٣HeP@1L\`.:#* bqʡoz^uXb\u&)`_P\nqf߫U][kq.0` bvd/*cAGxˏ;.?ۨȪ`} R TS打nXۨv\.?P\nqN[LH*ˏ;.?%47hRq19Eb+ۨv\.?P\nqF w Ə FKˏـE Ÿ;.?ۨv\.?P\ڹqeu(@nMl:E)j Mo鋌Xb\u&)khquˏ's3==4X9S.0` bq144nKˏퟴ\(739t?43qeuˏ/`i{K㹞Fm9EQCr=үˏ;.?ۨv\.?P\#xzy%3.0<~P\nqCq 4?ۨv\.?Pn^evuM2QF^H%t1L\`.: TJF.0` NG; qeuϜsLCN=mL8ZvPo h~?~?~>گ㴾 \%q~?Vc BW $Yz!h EVIUbV=VH>}ryN/B2(VTh7='~ofhCHk(*B'!#Ќgkk6 CV"H2IP?2哪eE&>znDQ : SZa¥* ;V!#nZȺT@fSr 4N"ʹj0l xJ $pZߜj}DPn*կL~: ǭf3=! 1SF`쇖`!Xt YDc=뿹E<pqs[*l> }BLL},H:٬**Mt= ;/Bl(*P45XǘtkLs@5N k^\><.$ ۩GK4ړ2*I)*AETY+Nv V'SpW(GiK0POnP@2}2uB eOEQFn? ˩u&-O&zm%Bf<}r6gE!p։iBz G_kYᶟ+_+wwLz)h`2?:26AkuMB|v*1iS\˳@]mW"B^ uF_4;ڿJt[@I,|4rk^ Af|[Lc8HHuy0>u8 u-o?5<`_څf=\)N%Yb H§òT%RzS$ωuEU.}[9/RR)s<K@L:So@ҬV@ HEtɌI Ymو)H-b8/ɷ$a7nV ![D6kM-V:6 J9٢Rjk8Aĸ]  \)Hڭmes? a>XSr]tdhRIFF WEBPVP8 * X>TP'$'Q in=3>ǢTvncDG;^Вt20a77:@sw:NOr:c a BBmLWĸ\Kq)' J[,ĸ$Ε_^itǏ:TjQ5FTjQ|qqtYȍ#TjQ5FeOSd9t.2Pcd7sZ[摑jQ5F#TAIw?ˉq.%)pF>.%ĸ\KrAJG~²cu:Xcu:Z0i_ ċYpF$Qd 2Q5F$7"̜?TG 6dcu:Xe5qvn8^#+HHĸ\KJr,߂5F zO-W3{q`j(FTj%H_3 6FPA J-ӧ5F"m]_e#du9yjKGOɱBA! SX<цTew]Ji:Xcu_@UOCB lr@t:;(Tf7 ةW(nٻ7fݛ`CJ]PVk5LxTjIF]S|ĸZmvrK30ñq,?W]Z.^*TIY%rvAB;q.%ĸ|H>PPPPPPOP>d*]e%jQ5FTd  Z!Tۂ5Gk vnٻ7fݛb\h7f{J% MrD Q5FTj aiQq ?2NL#Hb<#z^yڗNZh;60.%ĸAƏ e5`k \o;DRh10c 2CL}]]A,:O;b͌\Kq.(nyA$V$E[%'wz8l F|ʼWhnjי(?-Wc3s* |m=Wc3~Z6ܦT!2 X'0A8h{BS =Sds(˳QґgH7| m~{Pv<*sƭP5rQQ?qs&s!50u?e4>]+xv0tgH?l>1߿oGVCL)ma*XˮK>TKɨ=9; H,>v_'$RianBQlG-^}/!$@,q?Do.靮v&bཊ1q) -? 5s{JnY+#% -fUuAȓm(){pXp1gn(R:zl 'Ys@D 5A 9іKb!9ަ aM̧ A^ 8Fi2Vc&4Ԓ0pt/$%\]_cyy3pA+)MMPmY{w/$}H?x#QMn~k.a7<::9E???# eKN,i&um\TNY] ˾m&KӲ \nW.>OO%{g\U \1]O BX0({y$S+ɏ"@W/5$Ҋ <ȚqlҶ] .;j܎VT%PEb4Q $!vX'4۽|bN:'ˣlx1?N9 bWXƸ 8e5*@=NwߦO&R?~J6O1{g+x ^ 0Ν5y 1Mc}T:GlԧS@E=; H>J?1G (@v7Ֆ;L3y/졈#Q}X6(w'Tnv#;`G, ṣFP4MVWF{i,UM11@]=?qwu(E SCY ֘Uyu0U,\f; IJ]c*О^g"+"3N/6EÑqHw8ODDWX)hMZLZL5 g  M%` ~!*91@Y4 Q1bw]kv+E?L:v/lisA-;7 RIFF:WEBPVP8 :q*p>1D"! ` inc4“u>i&OϤ-ǸG3?<込W?L7%t +|{pC_}pܣO/|? }_ٸaU}YxxS-(@ߢ%}x[–#{xxRFcY1;ljSFY<K_Gruj6  X dt 0*:#\b-ΰhm 0A@I, ! ^6jL3u{бڢBH]fyZ.+$&ॼvVc87/]0Iٹ8|y}Ek.`v׌)6)!%cH4|2ꍸ#'6+7#D>i-ρz|1և|[<̾!!hagA cvBd q'.qP QNN_p !̏c^_ޗ8fƚA47#T)=>e4QEvbE=ubQZ\)pCKn.ݮWʜ}xLЌqCɥFN#oÈq4R'G9I2cbL}F!(2 x"C"wi+$C}t 6kp  H5%GFHV?10dSs|69{a}AW\Z],q`]̫( >W%'Fc|AxP3vQN|[yes$EF0J$s|cP<Ì 5-7-U'l{B/qFd@]ڗɩ5:`DnZK]3R≋~&fI.FԗX>9+[戠@1aUaFcF'쒁췪bF8-q=:)ӌfB>/@ĺ4ܺOUvu'R35 zRnLo;+>6߷12ߔ.Q5+uY^ g oē k7L+f -b;w*}4 +0~R0܇5!YQ7?-tAPKD=~*!G_o9qyng㝾ƶ̷ h֭\"i0sVZk'CSֲkMEr\g" |\0?П]nHy4\e]ϳlˀ>ZclH/neG=ub|C$#~"G{]4÷n)+{U,<ȇ*3mivApgs0] ;ƼLCVskg)ݘrUͅ ', Vڎ,b,QϨpsd`c7h}Fd2$y@`-UI+5By#X:RؘpiMZ҂a/ˮYnkp-!yqz @<폍B.\~)C-W s ug!Yx%FebU gJ żvƲ9sUUOzoXh M̝Ra'lD˶5fgDXPH9oN\]2+~Md< 4[UZ|>9'QY}}J0[nn㬮9?$B,4R= kr]Gq!^sk.u ^95[pE:RI7#u=Fv5jKl-KxRȰ@ νocRxbG:[6|,t &hdœ2M*C)}A-KxRQӨWz辱W8ߍ\x֢5`O'vS,49򮈈+–)oi >$bj,+;^kzݵ:[fIhs/q-L}h0-uܚCCk콾^,&X*erCpo&{DBB ҮWS">/o?x^gۙFv}(݁xIZ;4beJ%1`S$;,%!'1^77.܉[K_x1, ‰ x 5_ ę8 owR ZlA%2фaPyK2>do}W ؋t/rz.65DF `u {a~GjV8#wY* jIg2O$]v<|GjdK [dA\n4Pzu Nc^j/R tM[DŽS`#3#11K&2zD9pOْJA~k>/2T}xROD6%j(#~^-%sn7u4F,O'SjlE.xSFScN *q?~jk!V_^*@`R ];%Lb!`rD4% KxM"t $9Q&c.Ε0_HN+ J|βDWN1py)<Ӧy^%ibR@,$VAh:'nv4=*ؒݗ{/ 뤏̇&ɞ`߭2,j2L65FQecX5cX5cQ5oc0X1u%ѫZRyG]DuNaQi%t@Ro0~s>Ϸ3̨zVJOМPTZ& I1w|?$t8}HVH!s>Ϸ2P9०Țfwh,VǤƎ&uT u:٬j2#}cX5cƱkƱkƱkm Ov8u+W9qtS\LY(Իiu_4n؟i \59?b*'k8!|6jBm ,f!^gY޾O~wQay{6WVk> 1?nfvJi,D"ȶ[–d5hV-<_&ŏ~ܚeVx&6cy–Wԃ$ ou@9mD?)[v8\A{ew 82pb9oLlYM H#364* a*8ȀVU6ל+QEKΝR\B~&Q;>G,7%.>(l20G"p@L++klN<QAKHve& y8$WDӱ)in}mP= w'NP:eVsIvSeCiqp8($gqNnR &^gN 0j%Oca̽b)ъ*) yhX%n?6iR1(T2@#JG)o .EBdb>g]+ddIM@v!#80Q{NBǼYXġVT<'1# k> >9<^ v뤽 Ehл|/XʈHk~ۤm1pOn~̒6[ BJEՊŐ8rABQu4EeU;ljGm\|nAn)( .9[1o76K098*"5jglL:4"u)fD|KT~DKcn*i}(Sƒ+,5kpKiăogLҫʕo蝬Xt׍0u `mqjm >" 8[g>@?^=U5|}w cVY1dڨ \La4ڻtPZb YC1ڜ&Hii^p=><T&n+XO]52\-E6<>|/jW[gAjkmB GFphM rL2bcBoGHgxp$tsx0$ojb }H-! [o8nT5IJJXڐhIF}1OFڻx=VU/zWf{{>ynht ZMA?;?\S߬:SEH',dD%oK]#MJ]"sk:JrOPME;tx-SVsAFO5.f`/|k‚{^38)q,b_!"AĖ꫊a{؞jۃ:R54S#C֯-J^7pɾ:ޖy>NZqڿ)$ 6<7않#**wTj7% x)uՕ'.ndҢdj$bیY{4Nᶮ{J+65͚&0J \d@4?f$Ŋ<UشO=R)Q//Ӕ?gXQ%YRiҪw) iHqX o B mC|s ˠ0q1g=}%+*dF}E` g V\-/ސO36*.LIrH[  ~TN()bz ? LIXO@89QJ4ȝK(QNJZ!W*<{Mf[ ]Z7rέ ]!`s<Ԃ둲eSdQ?vέ&qw?%1bQm3uՄSӦ-vL-4v1; *'Y{Bj~Af8PSFDN2NQ -02⭧wBm"TCyvțpSso;G.(XrD?C@ťٟttV+pe񜌤>v[R]VhT*~Ų$TnFu8 @3k of(m=|BhdO7 ߩG Er'y4@sS &وf FrmI hߡt1jŃddWo 05]w075X.0=c4[4M.>#0>41sx&@1 V4FAFdzUK,$hqKDEb3$H, CS4o=GeiWFs 5zVe=HFaɎoWH$mzkVWnN 8}@z>P~55[q"Q&ԩs,O[Fݨ`ifg;D&5vXK|'\lx)_yY?S֖u* [.Ncen=uBz~ΑۍC$hunq݇%!dSgZNZ+kG膑P12.¢@ud!}XwlץϨ+51iog0\lp +:65vbЂƒN*e汄\*KSC>J=Hz1JC4!(&ޓ3ip j XAv6JQpӘ/f %ЫaduP&{(IΊ{l.2[b?HO⮮ C0t9Vfc2WQ} N{o)tQXB/U]CtrŸi"B= QA~5$pnɸI| Zq%cF{4B:Ў.;]S4CSEiN!kEDKoPsp;m7Cb͚XK@?{y܁jL,I{(6k쥠h E{07.aӦ9%'=gͯ$!nQj:[# (H~["#;@жtDa}~rm8vX[s@J;R5Az+Qp23cS2yS㈖QHlm(x3QZ3I)97{0u=Q"K[5䁶Ê%s=N,ab:4G>)i ч3@H3SxKk+>5 hJ9بikj)G"v{d$bQ " 0^hVT#bMY>8oq@2*9T\TL\r$" |%qv.̬-EY4ʿ֤! c,b_Do]M3O%ÏzNkh3xeA&iOMhߖ {G뮬lcb ,,Y+&S͉Z#XnAGb*JAYLI6mZU5UGm C aOh͕k+AQYlB{ /`.R {2 !#{Ӝ p2'ܚfX I2ٮ͘ɔC^c_|4^p40T=m*Tŵrd` _mE&`q,-LHƏ<|s 'q-jieaɴeU -^r`䎈jfwvXCsKkT}NyVa=b{^{_5#㨬b PᲴ1qK`IaNgŸB%qkA",X͎H7䴍|ۘQJ'7r2KwKc5ú4*KM:{sT^HY7W4u:B%r&& 9m((&GkKC%f?#پΊ;;>D"x-65[@+~F=ƾ0;g>3&} +0S24w=~ uYJP|O'$'T)$4Pٞ:NUIBSD NW6,mp..`LN/K<(Ǯo/._h{vb:8qض=+8 m, 槈11TMoht )},] Gqjm^s&mۜo;}{ j<`\0Cw->}4DlN'—>;3VA3< 2aգrn1,A.XU xLԸ[D# $Ͳ1U_vϻ\Gp{WR@\cV1i)kئTޞA/ꚕ9Eq>1ęKjY,vDÀѧQ;5W쫹X82`d|[;Y$ٱD)nՐI]A?N55X^}EYdCq_2K0ji85rh4|WB>Ur_mB$Z:^MkUyFķ_KFDN3: 4QHsb#6_qpg[W1TD 8<tN5ϠeoRFx(tc }W E^!:n1aY֣ V@Q (P2kQ TiP3?}AkA?ĩ Q6ܯgfiFE[;3L m1/R/Ar2-@]{qn[\vف\B3a6xM)*|S4 h,`Ժ!96|MV"D94 @2(JiNwQ+yTJT73ZW&Yv^aqU9UC"Ч `ƈyamp"}@}EOOKWhg4jFX U'ȏjDvMpq\1⍽@~= *,U^tSBU:w#ۺ/>\ |.]? doqJ}OX>??1O2M@œ1*[jE>y~,nc\6u͖XM ptT~gnU4? %2u3X<-wغaia΍iƦ t;`'G%jz /HhYr:%/t|9vGuzj7D8f$-Hrr)$l43lSRx|{^[fQը)tfџ渀bg Jfax^ăb'J|O Jph L\UmdY#<%mOp#c#_<|ǯ@x_vg"~B[4C_/6~j>]'NssukJi1 ?LBh9z VkQHpEჲy/EtN/ G'W=!ҋ "ǧvVaxTIVA"hnIsA̧;goGi!~_lQ%T|2R*OMy3CO I[wDD-d-CG\45bf(n }"%I )[D cI"!AMoACn& YRemVAHVҴ[R&]oG~* 7(/!cՑ')m0⧚ic(x%\qвA~+؂n)8(MmPUdzEj'Ķ /MmoRIEYZe`Q6SjeqZ{ "w]ծ%\N/ǑaJpJ?ISB~DG?@27TeS~լȾk/'@@g3ZDrA1CW ڑq"ޑ$}T2 (J9} D@=Unx:=43yP-b\:#|Xsa*ZQ41>00o< Z`X1W͑gU G EK-Qv'pK6`C(<)η=%,ҶӖw]R$O)!{5X&,kh|$xFaIųN4;cVpfb*S(J(z4]E FCinz  $PG>Pן8ht`$~2j~k"m0I"!p9 inPZ3g^t{m7>F꺜U4/<֝Jye";۲5y.kR Uua9;fˍ)_ѥëE2/<Z{iXb C~$s [rTHեaJ!ԜD+iNit%cp76Ut 94q:+íIXI Pf*tN|E)Nfu< cvta|N?ьV_+ R5EZz$,5 iK %O4sK\8̄^ ,?XuyX÷K;ۿ74~t|φ]A_;U &=oqx',S{zoɷzrsvjه=1IR;sҚ=נu J|u4%ד,1w{UbƐ 7W$y6Ob[z%BuHآ !qS IAh74|d(&9I_9Pj1*e֗UL ]z{9zG8QN&d86ojb+G1aG9h&I$#k4>uŅi=5´jBa#~ ,GO^,ŴR=23Y$Ĵ`#2G%#&͚Ņڌ e%,QH=OX?EPh|[@܍df,(a#9:M0gEs(;?4x_o|x.K[g%!;+ۧ_o3C:jE.uY1ֻ ~IȪ7]"h٥S?UQ]ywAn&,t_w_N{?:cD\c&KX76B Q1}v ( U؞QeQ[5h2ξ|yKNY]9|7b`9Sx3gB%!a>k\t1:#x&dgb0LJ0 Z{mLx}4{ (ՆqZ> kOQ5(MbШ/u}"od ;!o c? cn\^u^mgc Jz.zD;-Xf'LPk5i:&l/6qR,O@IH \+G1aFY{(b&vnvZ_^jdC-=؇Roej4(Zum3i٠i- kD)hf Z!KC0f Z6ֈRлD%ajSsZG65 ?'q/{PtA҃.pCvoN|pw~loѻqI EՆ$qCd}}9;R@5Ӗ,A3S!(zKg)v=Ȃ,~ 6qK8Vl|c8 E.wHPvy) %ַ68q/hSan WƖѺ/sf̵ZHMbAD?]d ,X1 UGZ;]eMt IT[olςY~{>C'FF a`=+ u&}}?w$M'4q5Ն14!+#$_#gWA̕I>\]?ŅuH2P6V.(4hcGzek.NOu,ƼP}?;CTa><+vAF^EPV Td-dlHm3+| D"!hYA-N_!( #SGSy 9>1ӷtSVWAM¢?ff[ĢQ };(@SZ] e:$#Np3o)tvcf Q$,is$87 u3cdCML7i4o3u.=> `i 3Am[I?-.lgIZvVyn.ZU#(UR"rQ_9wWhQh86m%2M4 d'\;`oCOǿ˝ٟ/W$ʌik-{}Bᥱaxsa<2UCΝB"dӑ4>:~r[TfIT`9H3,B \3$omnt[% ~sw֓6&4VOtC}YfYD)t .k9b0癮x${ޯK)absorQ0I]eɓ"SbvZIAC1zP3~ױlUF׷$ЂI{cH'l?Aܖ>Y-`Cʍ4PlOIP#-E"2T'W07o'm82[nc USP0r"ΐfXsQ1"ժJ9We׬eHftrn"yМRB31E)`inO{T aܽ-#2)h|GNQ>W^OSwWLPQ'+"o]n4 \`pVf<Ҷ'̈́=Cw{ 8jů d~c}W٠ Jݹ s!X#Y%3uedQړƍq墪&ފC^`o=V~&1V"j#eviaR$j+Zm-ޥ0%`ͧ TՓNz˿k?QJ֯0D'qj.] d !f5p'eWd[y*< *І5ˊT((z!(]C"ڋv:4JM mE25-8\k_%Z@fP*5$%\wX( Ur,;ܿH A?;9 LᬣT&f1iS[P7ǚ$v{Hv0LimSU~~f>"x{SL J7J2`CecOy)SeupqT$~O*cy!2y|Tu.i+DiՅeJG#|VUtXɗfހU)2JOKpB-A>!J? :`?? KKᑨ.'&Æ|Kd)w,NH__a\f+갬I__&P2&.[5쮖 {hT ʑ0nbo@\i]f)#z4f \ z 7y \6*)Mn3 ~V~I2""3"8-dKjX`&6()C] ²BB"V>ɻPɻ`Roa|/a@~Zlhs oi'YP%QWkI6FJfoT @/E rjsyT{3?{<vj+4A9j.z LT$Mw<IH@b~)sj5}{1~]G !^ft-) ^FzE>6gA>cn6UHgk3 Ǹրx?.AH/Ri1-!ITe gM웨`ؙeC'NM0Tó Br`)1(iouԧ2O{PPc?2 Uj8/Y@w($bA\2 #6oK0Ϲc? yN`r=q$|Ѯ\
    $i18n{loading}
    • $i18n{extensionSettingsExtensionId}
  • $i18n{title} /* Copyright (c) 2012 The Chromium Authors. All rights reserved. * Use of this source code is governed by a BSD-style license that can be * found in the LICENSE file. */ body { -webkit-user-select: text; } $i18n{aboutTitle}

    $i18n{aboutTitle}

    $i18n{aboutProductTitle}

    $i18n{aboutProductDescription}
    $i18n{browserVersion}
    $i18n{productName}
    $i18n{productCopyright}
    /* Copyright 2014 The Chromium Authors. All rights reserved. * Use of this source code is governed by a BSD-style license that can be * found in the LICENSE file. */ #about-container { align-items: center; display: flex; } #page-container #about-container { margin-top: 10px; } #product-description { -webkit-margin-start: 10px; } #version-container { margin-top: 30px; } #update-buttons-container { margin-top: 5px; } #update-status-container { margin-bottom: 12px; margin-top: 10px; } #update-status { vertical-align: middle; } #update-percentage { font-size: 90%; } #get-help, #promote { -webkit-margin-end: 4px; } #help-container { margin-top: 16px; } #product-container { line-height: 1.8em; margin-top: 200px; } .overlay #product-container { margin-top: 30px; } .help-page-icon { background-position: center; background-repeat: no-repeat; display: inline-block; height: 18px; vertical-align: middle; width: 18px; } #update-status-icon.up-to-date { background-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxOHB4IiBoZWlnaHQ9IjE4cHgiIHZpZXdCb3g9IjAgMCA0OCA0OCIgZmlsbD0iIzQyODVGNCI+CiAgICA8cGF0aCBkPSJNMCAwaDQ4djQ4SDB6IiBmaWxsPSJub25lIi8+CiAgICA8cGF0aCBkPSJNMjQgNEMxMi45NSA0IDQgMTIuOTUgNCAyNGMwIDExLjA0IDguOTUgMjAgMjAgMjAgMTEuMDQgMCAyMC04Ljk2IDIwLTIwIDAtMTEuMDUtOC45Ni0yMC0yMC0yMHptLTQgMzBMMTAgMjRsMi44My0yLjgzTDIwIDI4LjM0bDE1LjE3LTE1LjE3TDM4IDE2IDIwIDM0eiIvPgo8L3N2Zz4K); background-size: 18px; } #update-status-icon.working { background-image: url(chrome://resources/images/throbber_small.svg); background-size: 16px; } #update-status-icon.failed { background-image: url(chrome://resources/images/error.svg); background-size: 18px; } #update-status-icon.disabled-by-admin { background-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI0OHB4IiBoZWlnaHQ9IjQ4cHgiIHZpZXdCb3g9IjAgMCA0OCA0OCIgZmlsbD0iIzYyNjI2MiI+CiAgICA8cGF0aCBkPSJNMCAwaDQ4djQ4SDB6IiBmaWxsPSJub25lIi8+CiAgICA8cGF0aCBkPSJNMjQgMTRWNkg0djM2aDQwVjE0SDI0ek0xMiAzOEg4di00aDR2NHptMC04SDh2LTRoNHY0em0wLThIOHYtNGg0djR6bTAtOEg4di00aDR2NHptOCAyNGgtNHYtNGg0djR6bTAtOGgtNHYtNGg0djR6bTAtOGgtNHYtNGg0djR6bTAtOGgtNHYtNGg0djR6bTIwIDI0SDI0di00aDR2LTRoLTR2LTRoNHYtNGgtNHYtNGgxNnYyMHptLTQtMTZoLTR2NGg0di00em0wIDhoLTR2NGg0di00eiIvPgo8L3N2Zz4K); background-size: 18px; } #controlled-feature-icon { background-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI0OHB4IiBoZWlnaHQ9IjQ4cHgiIHZpZXdCb3g9IjAgMCA0OCA0OCIgZmlsbD0iIzYyNjI2MiI+CiAgICA8cGF0aCBkPSJNMCAwaDQ4djQ4SDB6IiBmaWxsPSJub25lIi8+CiAgICA8cGF0aCBkPSJNMjQgMTRWNkg0djM2aDQwVjE0SDI0ek0xMiAzOEg4di00aDR2NHptMC04SDh2LTRoNHY0em0wLThIOHYtNGg0djR6bTAtOEg4di00aDR2NHptOCAyNGgtNHYtNGg0djR6bTAtOGgtNHYtNGg0djR6bTAtOGgtNHYtNGg0djR6bTAtOGgtNHYtNGg0djR6bTIwIDI0SDI0di00aDR2LTRoLTR2LTRoNHYtNGgtNHYtNGgxNnYyMHptLTQtMTZoLTR2NGg0di00em0wIDhoLTR2NGg0di00eiIvPgo8L3N2Zz4K); background-size: 18px; } #eol-status-icon { background-image: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjBweCIgaGVpZ2h0PSIyMHB4IiB2aWV3Qm94PSIwIDAgMjAgMjAiIHZlcnNpb249IjEuMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayI+CiAgICA8ZyBpZD0iTm90aWZpY2F0aW9uLUljb24iIHN0cm9rZT0ibm9uZSIgc3Ryb2tlLXdpZHRoPSIxIiBmaWxsPSJub25lIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiPgogICAgICAgIDxnIGlkPSJBcnRib2FyZC00LUNvcHktMyIgZmlsbD0iI0RCNDQzNyI+CiAgICAgICAgICAgIDxwYXRoIGQ9Ik00LjU3NDAzNjYxLDIuOTE2Mzg1MTcgTDQuNTUwMzE2OTcsMi45MTYzODUxNyBMNC41NjA1NzQxMSwyLjkyNjY0NzI4IEM0LjU2NTA2MTYxLDIuOTIzNDQwMzcgNC41Njk1NDkxMSwyLjkxOTU5MjA4IDQuNTc0MDM2NjEsMi45MTYzODUxNyBaIE0yLjUwMDQyNzM4LDQuOTY3NjUyOTcgTDIuNTAwNDI3MzgsNC45OTA3NDI3MiBDMi41MDM2MzI3NCw0Ljk4NjI1MzA1IDIuNTA2ODM4MDksNC45ODI0MDQ3NSAyLjUxMDY4NDUyLDQuOTc3OTE1MDggTDIuNTAwNDI3MzgsNC45Njc2NTI5NyBaIE0xNy4wODQ3OTk1LDE1LjQ1NzI2MjYgTDE3LjA4NDc5OTUsMTUuNDMzNTMxNCBDMTcuMDgxNTk0MSwxNS40MzgwMjExIDE3LjA3Nzc0NzcsMTUuNDQyNTEwOCAxNy4wNzQ1NDIzLDE1LjQ0NzAwMDUgTDE3LjA4NDc5OTUsMTUuNDU3MjYyNiBaIE0xNy4zMDc1NzE4LDE3LjI3NTMyMzkgTDE2LjM3NTQ1NDEsMTYuMzI0MTU0NCBMMTIuMjY4NzUxMywxMi4yMTU0NjE2IEw4LjAyNjE0MTQ2LDcuOTg5Mzk1ODIgTDMuNjc0NTQ5NDcsMy42MzUwNTM2NiBMMi43MzI4MTU3MywyLjY5MzUwNDk0IEwyLjAyMDU4NTUxLDEuOTgwOTI5NTggTDEsMy4wMDIwMDk2NiBMMi43MzI4MTU3Myw0LjczNTAyMzczIEMxLjY3NzYxMjM3LDYuMjMwNzI2NDcgMS4wNTAwMDM1Niw4LjAyOTgwMjg4IDEuMDUwMDAzNTYsOS45Nzk2MDQwNSBDMS4wNTAwMDM1NiwxMS45Mjk0MDUyIDEuNjc3NjEyMzcsMTMuNzI4NDgxNiAyLjczMjgxNTczLDE1LjIwMDQ1MzIgQzMuMzA2NTc0NTQsMTYuMDAwODk3OSA0LjAwNjYyNDQsMTYuNzAxMjg3IDQuODA2NjgxMzksMTcuMjc1MzIzOSBDNi4yNzc5NDAwMiwxOC4zMzEwMzg2IDguMDc2MTQ1MDIsMTguOTU4OTUxNiAxMC4wMjUwMDE4LDE4Ljk1ODk1MTYgQzExLjk3Mzg1ODUsMTguOTU4OTUxNiAxMy43NzIwNjM1LDE4LjMzMTAzODYgMTUuMjQzMzIyMiwxNy4yNzUzMjM5IEwxNi45OTA4ODI1LDE5IEwxOC4wMTE0NjgxLDE3Ljk3ODkxOTkgTDE3LjMwNzU3MTgsMTcuMjc1MzIzOSBaIE0xNS4yNDMzMjIyLDIuNjgzNjI3NjYgQzEzLjc3MjA2MzUsMS42Mjg1NTQzMyAxMS45NzM4NTg1LDEgMTAuMDI1MDAxOCwxIEM4LjA3NjE0NTAyLDEgNi4yNzc5NDAwMiwxLjYyODU1NDMzIDQuODA2NjgxMzksMi42ODM2Mjc2NiBDNC44MDE1NTI4MiwyLjY4NzQ3NTk1IDguOTQxNTkxMjgsNi44NDQ5MTM4NCA4Ljk0MTU5MTI4LDYuODQ0OTEzODQgTDEwLjAyNTAwMTgsNS4wMzAwMjkzIEwxNS4wNDE1NjQ3LDExIEwxMy4xOTYzODE1LDExIEwxNy4zMDY5MzA3LDE1LjIxNDMwNzEgQzE4LjM3MjM5MTIsMTMuNzI4ODY2NSAxOSwxMS45MjkxNDg3IDE5LDkuOTc5MzQ3NSBDMTksOC4wMzAxODc3MSAxOC4zNzIzOTEyLDYuMjMwNDY5OTIgMTcuMzE3MTg3OCw0Ljc1ODQ5ODMxIEMxNi43NDM0MjksMy45NTgwNTM2MiAxNi4wNDMzNzkyLDMuMjU3NjY0NTEgMTUuMjQzMzIyMiwyLjY4MzYyNzY2IFogTTEyLDE1IEw3LjAxMjE0NiwxNSBMNy4wMTIxNDYwNywxMS4wMTExMDgzIEw0LjUsMTEgTDYuNzI2NzY3OSw5LjEyNDAwMDUxIEwxMiwxNC42MDAwMDA0IEwxMiwxNSBaIiBpZD0iQ29tYmluZWQtU2hhcGUiPjwvcGF0aD4KICAgICAgICA8L2c+CiAgICA8L2c+Cjwvc3ZnPgo=); background-size: 18px; } #eol-status-container { margin-bottom: 12px; margin-top: 10px; } #eol-status-message-container { -webkit-margin-start: 8px; display: inline-block; vertical-align: middle; } #update-status-message-container { -webkit-margin-start: 8px; display: inline-block; vertical-align: middle; } #more-info-expander { -webkit-padding-start: 0; margin-top: 10px; } #more-info-container.visible { margin-bottom: 10px; } #more-info-container { -webkit-transition: all 200ms; height: 0; margin-bottom: 0; overflow: hidden; } #build-date-container.empty { visibility: hidden; } #channel-change-confirmation { margin-top: 5px; } #change-channel { margin-top: 8px; } #channel-change-disallowed-icon, .channel-change-error-icon { -webkit-margin-start: 4px; background-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI0OHB4IiBoZWlnaHQ9IjQ4cHgiIHZpZXdCb3g9IjAgMCA0OCA0OCIgZmlsbD0iIzYyNjI2MiI+CiAgICA8cGF0aCBkPSJNMCAwaDQ4djQ4SDB6IiBmaWxsPSJub25lIi8+CiAgICA8cGF0aCBkPSJNMjQgMTRWNkg0djM2aDQwVjE0SDI0ek0xMiAzOEg4di00aDR2NHptMC04SDh2LTRoNHY0em0wLThIOHYtNGg0djR6bTAtOEg4di00aDR2NHptOCAyNGgtNHYtNGg0djR6bTAtOGgtNHYtNGg0djR6bTAtOGgtNHYtNGg0djR6bTAtOGgtNHYtNGg0djR6bTIwIDI0SDI0di00aDR2LTRoLTR2LTRoNHYtNGgtNHYtNGgxNnYyMHptLTQtMTZoLTR2NGg0di00em0wIDhoLTR2NGg0di00eiIvPgo8L3N2Zz4K); background-size: 18px; } .channel-change-error-bubble { display: flex; } .channel-change-error-bubble .channel-change-error-icon { vertical-align: top; } .channel-change-error-text { -webkit-margin-start: 4px; display: block; vertical-align: top; width: 240px; } #regulatory-label-container { padding-top: 32px; } #regulatory-label { display: block; width: 330px; }

    $i18n{aboutProductTitle}

    $i18n{aboutProductDescription}
    $i18n{browserVersion}
    $i18n{productName}
    $i18n{productCopyright}
    /* Copyright 2013 The Chromium Authors. All rights reserved. * Use of this source code is governed by a BSD-style license that can be * found in the LICENSE file. */ #channel-change-page { min-height: 200px; width: 500px; } .channel-change-page-channel label { margin-left: 10px; } .channel-change-page-channel { display: block; margin: 0 25px; } .show-when-selected-channel-requires-powerwash, .show-when-selected-channel-requires-delayed-update, .show-when-selected-channel-good, .show-when-selected-channel-unstable { display: none !important; } .selected-channel-requires-powerwash .show-when-selected-channel-requires-powerwash, .selected-channel-requires-delayed-update .show-when-selected-channel-requires-delayed-update, .selected-channel-good .show-when-selected-channel-good, .selected-channel-unstable .show-when-selected-channel-unstable { display: block !important; }

    /* Copyright 2013 The Chromium Authors. All rights reserved. * Use of this source code is governed by a BSD-style license that can be * found in the LICENSE file. */ html { background-attachment: fixed; background-color: $i18n{colorBackground}; background-position: $i18n{backgroundBarDetached}; background-repeat: $i18n{backgroundTiling}; height: 100%; overflow: auto; } html[hascustombackground='true'] { background-image: url(chrome://theme/IDR_THEME_NTP_BACKGROUND?$i18n{themeId}); } html[bookmarkbarattached='true'] { background-position: $i18n{backgroundBarAttached}; } #attribution-img { content: url(chrome://theme/IDR_THEME_NTP_ATTRIBUTION?$i18n{themeId}); }
    /* Copyright 2012 The Chromium Authors. All rights reserved. * Use of this source code is governed by a BSD-style license that can be * found in the LICENSE file. */ html { background-attachment: fixed; background-color: $i18n{colorBackground}; background-image: url(chrome://theme/IDR_THEME_NTP_BACKGROUND?$i18n{themeId}); background-position: $i18n{backgroundBarDetached}; background-repeat: $i18n{backgroundTiling}; } #attribution { left: $i18n{leftAlignAttribution}; right: $i18n{rightAlignAttribution}; text-align: $i18n{textAlignAttribution}; display: $i18n{displayAttribution}; } #attribution-img { content: url(chrome://theme/IDR_THEME_NTP_ATTRIBUTION?$i18n{themeId}); } html[bookmarkbarattached='true'] { background-position: $i18n{backgroundBarAttached}; } body { color: $i18n{colorTextRgba}; height: 100%; overflow: auto; } #attribution, [is='action-link'] { color: $i18n{colorTextLight}; } [is='action-link']:active { color: $i18n{colorTextRgba}; } .page-switcher { color: rgba($i18n{colorText}, 0.5); } .page-switcher:hover, .page-switcher:focus, .page-switcher.drag-target { background-color: rgba($i18n{colorText}, 0.06); } /* Only change the background to a gradient when a promo is showing. */ .showing-login-area #page-switcher-end:hover, .showing-login-area #page-switcher-end:focus, .showing-login-area #page-switcher-end.drag-target { background: linear-gradient( rgba($i18n{colorText}, 0) 0, rgba($i18n{colorText}, .01) 60px, rgba($i18n{colorText}, .06) 183px); } .tile-page-scrollbar { background-color: $i18n{colorTextLight}; } /* Footer *********************************************************************/ #footer-border { background: linear-gradient(to left, rgba($i18n{colorSectionBorder}, 0.2), rgba($i18n{colorSectionBorder}, 0.3) 20%, rgba($i18n{colorSectionBorder}, 0.3) 80%, rgba($i18n{colorSectionBorder}, 0.2)); } .dot input:focus { background-color: $i18n{colorBackground}; } .filler .thumbnail { border-color: $i18n{colorBackground}; } /* Copyright 2016 The Chromium Authors. All rights reserved. * Use of this source code is governed by a BSD-style license that can be * found in the LICENSE file. */ body { align-items: center; color: var(--paper-grey-900); display: flex; flex-direction: column; font-size: 100%; justify-content: center; min-height: 100vh; overflow-y: hidden; } @keyframes slideUpContent { from { transform: translateY(186px); } } @keyframes fadeIn { from { opacity: 0; } } @keyframes fadeOut { to { opacity: 0; } } @keyframes fadeInAndSlideUp { from { opacity: 0; transform: translateY(8px); } } @keyframes spin { from { transform: rotate(1440deg) scale(0.8); } } @keyframes fadeInAndSlideDownShadow { from { opacity: .6; top: 0; } } @keyframes scaleUp { 0% { transform: scale(.8); } } @keyframes colorize { from { -webkit-filter: grayscale(100%); opacity: .6; } } @keyframes bounce { 0% { transform: matrix3d(0.8, 0, 0, 0, 0, 0.8, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); } 7.61% { transform: matrix3d(0.907, 0, 0, 0, 0, 0.907, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); } 11.41% { transform: matrix3d(0.948, 0, 0, 0, 0, 0.948, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); } 15.12% { transform: matrix3d(0.976, 0, 0, 0, 0, 0.976, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); } 18.92% { transform: matrix3d(0.996, 0, 0, 0, 0, 0.996, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); } 22.72% { transform: matrix3d(1.008, 0, 0, 0, 0, 1.008, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); } 30.23% { transform: matrix3d(1.014, 0, 0, 0, 0, 1.014, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); } 50.25% { transform: matrix3d(1.003, 0, 0, 0, 0, 1.003, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); } 70.27% { transform: matrix3d(0.999, 0, 0, 0, 0, 0.999, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); } 100% { transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); } } .content { align-items: center; animation: slideUpContent 600ms 1.8s cubic-bezier(.4, .2, 0, 1) both; display: flex; flex: 1; flex-direction: column; justify-content: center; max-width: 500px; } .heading { animation: fadeInAndSlideUp 600ms 1.9s cubic-bezier(.4, .2, 0, 1) both; font-size: 2.125em; margin-bottom: .25em; margin-top: 1.5em; text-align: center; } .subheading { animation: fadeInAndSlideUp 600ms 1.9s cubic-bezier(.4, .2, 0, 1) both; color: #939393; font-size: 1em; font-weight: 500; margin-top: .25em; text-align: center; } .logo { animation: fadeIn 600ms both, bounce 1s 600ms linear both; height: 96px; position: relative; width: 96px; } .logo-icon { animation: spin 2.4s cubic-bezier(.4, .2, 0, 1) both, colorize 300ms 700ms linear both; background-image: -webkit-image-set(url(chrome://welcome/logo.png) 1x, url(chrome://welcome/logo2x.png) 2x); background-size: 100%; height: 96px; width: 96px; } .logo-shadow { -webkit-filter: blur(16px); animation: fadeInAndSlideDownShadow 300ms 600ms both; background: rgba(0, 0, 0, .2); border-radius: 50%; height: 96px; position: absolute; top: 16px; width: 96px; z-index: -1; } .signin { animation: fadeInAndSlideUp 600ms 2s cubic-bezier(.4, .2, 0, 1) both; margin-top: 3em; text-align: left; } .signin-description { font-size: .875em; line-height: 1.725em; max-width: 344px; } .signin-buttons { align-items: center; display: flex; flex-direction: column; margin-top: 2em; } .action { background: var(--google-blue-500); border-radius: 2px; color: white; font-size: .8125em; font-weight: 500; line-height: 2.25rem; padding: 0 1.5em; } .action:active { background: var(--google-blue-500); } .action:focus { background: var(--google-blue-700); } .link { color: var(--google-blue-700); display: inline-block; font-size: .8125em; margin: 1.5em; text-align: center; text-decoration: none; } .watermark { -webkit-mask-image: url(chrome://welcome/watermark.svg); -webkit-mask-repeat: no-repeat; -webkit-mask-size: 100%; animation: fadeIn 1s cubic-bezier(0, 0, .2, 1) both; background: var(--paper-grey-400); bottom: 24px; height: 24px; position: absolute; width: 74px; } @media(max-height: 608px) { .watermark { display: none; } } $i18n{headerText}
    $i18n{headerText}
    // Copyright 2016 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. cr.define('welcome', function() { 'use strict'; function onAccept(e) { chrome.send('handleActivateSignIn'); } function onDecline(e) { chrome.send('handleUserDecline'); e.preventDefault(); } function initialize() { $('accept-button').addEventListener('click', onAccept); $('decline-button').addEventListener('click', onDecline); var logo = document.querySelector('.logo-icon'); logo.onclick = function(e) { logo.animate({ transform: ['none', 'rotate(-10turn)'], }, /** @type {!KeyframeEffectOptions} */({ duration: 500, easing: 'cubic-bezier(1, 0, 0, 1)', })); }; } return { initialize: initialize }; }); document.addEventListener('DOMContentLoaded', welcome.initialize); /* Copyright 2016 The Chromium Authors. All rights reserved. * Use of this source code is governed by a BSD-style license that can be * found in the LICENSE file. */ body { box-sizing: border-box; color: var(--paper-grey-900); display: flex; flex-direction: column; font-size: 100%; justify-content: center; margin: 0; min-height: 100vh; } a { color: var(--google-blue-500); text-decoration: none; } ol { margin: 0; padding: 0; } strong { font-weight: 500; } .content { margin: 0 auto; padding: 4em 1.5em 1.5em 1.5em; } .header-logo { content: url(chrome://welcome-win10/logo-large.png); height: 4em; } .heading { font-size: 2.125em; line-height: 1.6em; margin-bottom: 0.5em; margin-top: 1.2em; } .sections { margin-bottom: 2em; } .section.expandable { border-top: 1px solid var(--google-grey-300); } .section.expandable:last-child { border-bottom: 1px solid var(--google-grey-300); } .section.expandable .section-heading { color: var(--google-blue-500); cursor: pointer; } .section-heading { align-items: center; display: flex; padding: 1.5em 0; } .section-heading-text { flex: 1; font-weight: 500; } .section.expandable .section-heading-text { font-weight: normal; } .section.expandable.expanded .section-heading-text { font-weight: 500; } .section-heading-expand { height: 1.25em; opacity: 0.54; transition: transform 150ms cubic-bezier(.4, .2, 0, 1) 50ms; width: 1.25em; } .section.expandable.expanded .section-heading-expand { transform: rotate(180deg); transition-delay: 150ms; } .section-steps { overflow: hidden; } .section-steps li { -webkit-margin-start: 1.25em; -webkit-padding-start: 1em; margin-bottom: 1em; } .section-steps li:last-child { margin-bottom: 1em; } .section.expandable .section-steps { max-height: 0; opacity: 0; transition: max-height 300ms cubic-bezier(.4, .2, 0, 1) 50ms, opacity 150ms; } .section.expandable.expanded .section-steps { max-height: 28.75em; opacity: 1; transition: max-height 300ms cubic-bezier(.4, .2, 0, 1) 50ms, opacity 150ms 250ms; } .button { -webkit-font-smoothing: antialiased; background: var(--google-blue-500); border-radius: 2px; box-shadow: inset 0 0 0 1px rgba(0, 0, 0, .1); color: #fff; display: inline-block; font-size: .8125em; font-weight: 500; line-height: 2.25rem; padding: 0 1em; text-align: center; transition: 300ms cubic-bezier(.4, .2, 0, 1); will-change: box-shadow; } .button:hover { background: var(--paper-blue-a400); box-shadow: inset 0 0 0 1px rgba(0, 0, 0, .1), 0 1px 2px rgba(0, 0, 0, .24) } .logo-small { content: url(chrome://welcome-win10/logo-small.png); display: inline; height: 1.25em; vertical-align: top; width: 1.25em; } .screenshot { display: block; height: 440px; margin: 0 auto; max-width: 100%; position: relative; top: -96px; width: 720px; } .screenshot-image { box-shadow: 0 0 0 1px rgba(0, 0, 0, .12), 0 1px 2px rgba(0, 0, 0, .24); height: 48vw; margin: 1em 0; max-height: 300px; max-width: 400px; min-height: 150px; min-width: 200px; position: relative; width: 64vw; } #default-image { background: url(chrome://welcome-win10/default.webp); background-repeat: no-repeat; background-size: cover; } #taskbar-image { background: url(chrome://welcome-win10/pin.webp); background-repeat: no-repeat; background-size: cover; } .screenshot-overlay { box-sizing: border-box; line-height: 0; position: absolute; } #browser-overlay { left: 41%; top: 81%; } #edge-overlay { left: 49%; top: 88%; } #taskbar-overlay { left: 31%; top: 73%; } #taskbar-overlay div { color: #ccc; font-family: Tahoma, Verdana, Segoe, sans-serif; font-weight: 500; } #icon-overlay { background-image: url(chrome://welcome-win10/logo-small.png); background-size: cover; height: 8%; left: 46%; top: 90%; width: 6%; } /* These values are precisely set so that the text over the screenshot starts * scaling at the same time the image starts scaling too. */ @media (max-width: 626px) { #browser-overlay { font-size: 1.28vw; } #edge-overlay { font-size: 1.44vw; } #taskbar-overlay { font-size: 1.95vw; } } /* Font-sizes used when the screenshot exactly reaches its max size. */ @media (min-width: 626px) { #browser-overlay { font-size: 8px; } #edge-overlay { font-size: 9px; } #taskbar-overlay { font-size: 12.2px; } } $i18n{headerText} // Copyright 2016 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. cr.define('inline', function() { 'use strict'; function computeClasses(isCombined) { if (isCombined) return 'section expandable expanded'; return 'section'; } function onContinue() { chrome.send('handleContinue'); } function onOpenSettings() { chrome.send('handleSetDefaultBrowser'); } function onToggle(app) { if (app.isCombined) { var sections = document.querySelectorAll('.section.expandable'); sections.forEach(function(section) { section.classList.toggle('expanded'); }); } } function initialize() { var app = $('inline-app'); // Set variables. // Determines if the combined variant should be displayed. The combined // variant includes instructions on how to pin Chrome to the taskbar. app.isCombined = false; // Set handlers. app.computeClasses = computeClasses; app.onContinue = onContinue; app.onOpenSettings = onOpenSettings; app.onToggle = onToggle.bind(this, app); // Asynchronously check if Chrome is pinned to the taskbar. cr.sendWithPromise('getPinnedToTaskbarState').then( function(isPinnedToTaskbar) { // Allow overriding of the result via a query parameter. // TODO(pmonette): Remove these checks when they are no longer needed. /** @const */ var VARIANT_KEY = 'variant'; var VariantType = { DEFAULT_ONLY: 'defaultonly', COMBINED: 'combined' }; var params = new URLSearchParams(location.search.slice(1)); if (params.has(VARIANT_KEY)) { if (params.get(VARIANT_KEY) === VariantType.DEFAULT_ONLY) app.isCombined = false; else if (params.get(VARIANT_KEY) === VariantType.COMBINED) app.isCombined = true; } else { app.isCombined = !isPinnedToTaskbar; } }); } return { initialize: initialize }; }); document.addEventListener('DOMContentLoaded', inline.initialize); /* Copyright 2016 The Chromium Authors. All rights reserved. * Use of this source code is governed by a BSD-style license that can be * found in the LICENSE file. */ body { box-sizing: border-box; color: var(--paper-grey-900); display: flex; flex-direction: column; font-size: 100%; justify-content: center; margin: 0; min-height: 100vh; } a { color: var(--google-blue-500); text-decoration: none; } ol { margin: 0; padding: 0; } strong { font-weight: 500; } .content { padding: 64px 24px 24px 24px; } .header-logo { content: url(chrome://welcome-win10/logo-large.png); height: 4em; } .text { margin: 0 auto; max-width: 45em; } .heading { font-size: 2.125em; margin-bottom: .25em; margin-top: 1.5em; } .subheading { color: var(--google-grey-500); font-size: 1em; margin-bottom: 1.5em; margin-top: .25em; text-align: center; } .sections { margin-bottom: 2em; } .section.expandable { border-top: 1px solid var(--google-grey-300); } .section.expandable:last-child { border-bottom: 1px solid var(--google-grey-300); } .section.expandable .section-heading { color: var(--google-blue-500); cursor: pointer; } .section-heading { align-items: center; display: flex; padding: 1.5em 0; } .section-heading-text { flex: 1; font-weight: 500; } .section.expandable .section-heading-text { font-weight: normal; } .section.expandable.expanded .section-heading-text { font-weight: 500; } .section-heading-expand { height: 1.25em; opacity: 0.54; transition: transform 150ms cubic-bezier(.4, .2, 0, 1) 50ms; width: 1.25em; } .section.expandable.expanded .section-heading-expand { transform: rotate(180deg); transition-delay: 150ms; } .section-steps { overflow: hidden; } .section-steps li { -webkit-margin-start: 1.25em; -webkit-padding-start: 1em; margin-bottom: 1em; } .section-steps li:last-child { margin-bottom: 1em; } .section.expandable .section-steps { max-height: 0; opacity: 0; transition: max-height 300ms cubic-bezier(.4, .2, 0, 1) 50ms, opacity 150ms; } .section.expandable.expanded .section-steps { max-height: 8.75em; opacity: 1; transition: max-height 300ms cubic-bezier(.4, .2, 0, 1) 50ms, opacity 150ms 250ms; } .button { -webkit-font-smoothing: antialiased; background: var(--google-blue-500); border-radius: 2px; box-shadow: inset 0 0 0 1px rgba(0, 0, 0, .1); color: #fff; display: inline-block; font-size: .8125em; font-weight: 500; line-height: 2.25rem; padding: 0 1em; text-align: center; transition: 300ms cubic-bezier(.4, .2, 0, 1); will-change: box-shadow; } .button:hover { background: var(--paper-blue-a400); box-shadow: inset 0 0 0 1px rgba(0, 0, 0, .1), 0 1px 2px rgba(0, 0, 0, .24); } .bg { background: var(--paper-light-blue-700); flex: 1; margin-top: 96px; padding: 24px; } .logo-small { content: url(chrome://welcome-win10/logo-small.png); display: inline; height: 1.25em; vertical-align: top; width: 1.25em; } .screenshots { display: block; height: 440px; margin: 0 auto; max-width: 100%; position: relative; top: -96px; width: 720px; } .screenshot-image { box-shadow: 0 0 8px rgba(0, 0, 0, .12), 0 4px 16px rgba(0, 0, 0, .24); height: 56vw; max-height: 440px; max-width: 720px; min-height: 294px; min-width: 480px; position: absolute; transition: opacity 150ms; width: 92vw; } .screenshot-image.hidden { opacity: 0; } #default-image { background: url(chrome://welcome-win10/default.webp); background-repeat: no-repeat; background-size: cover; } #taskbar-image { background: url(chrome://welcome-win10/pin.webp); background-repeat: no-repeat; background-size: cover; } .screenshot-overlay { box-sizing: border-box; line-height: 0; position: absolute; } #browser-overlay { left: 42.5%; top: 76%; } #edge-overlay { left: 50%; top: 84%; } #taskbar-overlay { left: 62.2%; top: 81.5%; } #taskbar-overlay div { color: #ccc; font-family: Tahoma, Verdana, Segoe, sans-serif; font-weight: 500; } #icon-overlay { background-image: url(chrome://welcome-win10/logo-small.png); background-size: cover; height: 5.8%; left: 70.60%; top: 93.1%; width: 3.5%; } /* These values are precisely set so that the text over the screenshot starts * scaling at the same time the image starts scaling too. */ @media (min-width: 520px) { #browser-overlay { font-size: 1.8vw; } #edge-overlay { font-size: 2.05vw; } #taskbar-overlay { font-size: 1.35vw; } } /* Font-sizes used when the screenshot exactly reaches its max size. */ @media (min-width: 780px) { #browser-overlay { font-size: 14px; } #edge-overlay { font-size: 16px; } #taskbar-overlay { font-size: 10.5px; } } /* Font-sizes used when the screenshot exactly reaches its min size. */ @media(max-width: 520px) { #browser-overlay { font-size: 9px; } #edge-overlay { font-size: 10.5px; } #taskbar-overlay { font-size: 7px; } } @media (min-width: 1280px) { body { flex-direction: row; } .content { align-items: center; display: flex; flex: 1; justify-content: flex-end; padding: 96px; } .text { margin: 0 180px; max-width: none; width: 400px; } .bg { align-items: center; display: flex; flex: 1; margin: 0; max-width: 42%; padding: 0; } .screenshots { margin-left: -180px; max-width: none; top: 0; } } $i18n{headerText} // Copyright 2016 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. cr.define('sectioned', function() { 'use strict'; function computeClasses(isCombined) { if (isCombined) return 'section expandable expanded'; return 'section'; } function onContinue() { chrome.send('handleContinue'); } function onOpenSettings() { chrome.send('handleSetDefaultBrowser'); } function onToggle(app) { if (app.isCombined) { // Toggle sections. var sections = document.querySelectorAll('.section.expandable'); sections.forEach(function(section) { section.classList.toggle('expanded'); }); // Toggle screenshots. var screenshots = document.querySelectorAll('.screenshot-image'); screenshots.forEach(function(screenshot) { screenshot.classList.toggle('hidden'); }); } } function initialize() { var app = $('sectioned-app'); // Set variables. // Determines if the combined variant should be displayed. The combined // variant includes instructions on how to pin Chrome to the taskbar. app.isCombined = false; // Set handlers. app.computeClasses = computeClasses; app.onContinue = onContinue; app.onOpenSettings = onOpenSettings; app.onToggle = onToggle.bind(this, app); // Asynchronously check if Chrome is pinned to the taskbar. cr.sendWithPromise('getPinnedToTaskbarState').then( function(isPinnedToTaskbar) { // Allow overriding of the result via a query parameter. // TODO(pmonette): Remove these checks when they are no longer needed. /** @const */ var VARIANT_KEY = 'variant'; var VariantType = { DEFAULT_ONLY: 'defaultonly', COMBINED: 'combined' }; var params = new URLSearchParams(location.search.slice(1)); if (params.has(VARIANT_KEY)) { if (params.get(VARIANT_KEY) === VariantType.DEFAULT_ONLY) app.isCombined = false; else if (params.get(VARIANT_KEY) === VariantType.COMBINED) app.isCombined = true; } else { app.isCombined = !isPinnedToTaskbar; } }); } return { initialize: initialize }; }); document.addEventListener('DOMContentLoaded', sectioned.initialize); }W8Js醐0= w3y{Vw{p aՇd˶&swCmTJR߯۟? 7f<'qY.mb_Δ8l ޅHU{6>gJςLd"b_ x*/ƏB7, OEP-\x2c1/^*LLP 6G3%}~\ o&LǽE>ӟC46s"2SF^8 b: C1 |_EoRu xi TL;jEHI~(՜x8He<"*+uATMMUs-RZ`_*~/j#5 >S DqҐ00//0sarH8=}ėIѡc-?K^(g7 (X› ^4ίZzya<3aRRL tORE'Tq w()g,#yܫЬt 0 B^A<3nyN 2,1JYQ}M ;Bi) BYGVoKzqͰ|D>Aҝ*ً//~&AS&^8:548ψ``\ L49 -qOz%ٚ*EejY 6][+Kok|IP~3o#˔x(_>‚.0ZtRp&>jH¦{3kjJw%ʕ>08 cye,=/RTXwuv8IED Ɗk۔_41CƅR -:MQqv_p5ȃܐem/H`g sEŒ5BYpeS8%sD s|'%G$i 0؛4dwA_?q3SaCƜB&0S-285n?>~`3}yg^$;_̂=M25<~Pm bUB_JΝZGXՉuUzGk<3Qtrpy<6|}&IU! =ip.$t{SŕP_1v4FGĊޟYKltSrK KD;T1V,|"޵f뎗Uk8NT\Xxɿs-INYYhȈ~!iygC\eД@7X%v% r[gSR6=[Y!퐨"߼yco 43N.PɌ b#Cx\J5b hLdV&cYQj/UE[,c&Lc,2W4#=c-e;͏if gj#BڏRϹg{ȃp{BE*ܹ>P#/sF>e$C~*~b]йF]pDPiQNU^947^nPaMu D-CA \JSE)Vڸ1j:N?u7hҴ{*tܳ!CEoټ6jpW]bH h694T蠎MXm걒i›ģ]YJ\n1;_SA-M͋|ALLEBIMMp[#kGȆdoCe~(atӄ $Gij:^ u} o]~Vi9^Lqa֊*XX OwABi.pIn(ZެR&̇7J+@%eO.밠|d8 ;i[ I+ȼ'"y9W/8y1öZe BGo_od90]$ 8 V9F5}~:Nk{ͬw fTUukMVLHlA^2鈷1D ]+ 0 Ez5Щ|yGGq/CL9 zgg擠&ҥ0 )4ņ68A6_A&֔38BzS9`-vzqhx/.oAZ3R, HK:1VgVb > >y{ilA3~bXC1 7dz6(uz8&c 3| :/BDYN^ uS  a.ޫ ei]X@^P7玝.a&JܽE"ʨc9oa}I4VQ\ЍF8q挳!N$*5<>L|'UC%+]]H8saƳ^܅Q1+o\ u䓎 byb!p(o J'ъVhޙXkEϑQek[3 tݚa!꒡.j˵lL:C#kDiaVM\jqmmmq(676%SC6S%t$&X9W?wG߫ +E>2zMK=|#;UY?V2ӵ겣UWcLFQ,Nu % `Ga\]jd֫#fX#IڕFveTbݝiV_E, jV6'Kgϰ ce{(G,:"q|ź<B->ap6?ŋ@w ϋ00)AQIlP%;< -M ?ң=ǐkWJ[` P}.6¨$p(nphK)I6?s*.'SeVaFQQ<X$aq om#|mc'W8_-EF,M{3AT̶LJ t7a]C{F[miܯ!wh8g6VP"PnVOmkC*ts`2ڃE cNDAXlRora m80@J'0W:GΩCyPttouõg4dF?FrZ6AYfS u#+Mg[+hCҞfe]\e׆_nZ-fyb +{me,VMP)_&sv|>hF*iD1q6& Ms]Cu BϔթlP5`2{Zh[fWt~e=Syŏ3`BU:Z(IUXWXQn ]zﵣ z?>S||gߗ9qE[, _ ܪWb[hلF%;< 0Ъc.ߵ#1W6rJV\eSs4kE&u~Uh@jYkW PxRk[t= 6XDTe+]g4q1A,G S+VjB^Qt䥺o0V)CQdXv{{p<1_ `5W@bV=[WtW3aCk$(#I\)RSv&c"P&8#ZNh*PVt4h3c]~GR[A8 9W/R;O; t ~d%GVοeA!9 }-aEIPl.\IVj౨td<يV3dJ:gr_GsŲS2$V['X l2|dudԇp|Cv5 J*wQ13 ەpk~fB"*SgL om<u'0 :N˟I)2n[{U^}#LK[t)Tk6G<1& Q;Ot ~W$T&c522|,|Lo`̭6|>ιo_nB79rGA'b=WeǵTruvN;:>\58L9<oϯvCqi 9FAhO^D0SJ#3hxt\Ɉ.[sIȸͪäk*I{wzVqPL$$󐢂6nR':eAhzCJtj"u@W"Lu!MWSkޫ,2LEc1 Rг^}.+a͗k+txO >|EQ١h-$Q%+f#HCy]V(Lx  }ݕN;9鮯wat,ˆ HqzWD:xYGdy8g8iEyNM5WdtBf, ml̈́ s:\@٤$1IR|yLpddu3)]G5P *s8)#@LX(LCH|s_-" cЌ#hٽ mΉH.$V0@G 0M"T (,)A;zETë+j"?T @S [cyp@E0^zGMձ]Nrhj1AS1U)t!M)h+k@6̮ԴllT.GV[|HK@}b+qwa//)% ._3[bp t09* V#3+0k0o72?qd?M~o/͇={wz_/~ |ٴh6kNaNI/S]:VKe24JY{-d!Sa>rȢ'ӆʔL끨+ 1xDbenJ|Αi p5gKꛟVdގ.a "]&лAu 'YlF' [/_|tcxK&fO09)A3xbW v F@[M^}o[qp%Bqe t-.L wj> .vlw'dq.~C;%X&#@eFhPkO^ эjPgˎ/iX>kɵ tf8ޣ'醧z]5;)uo }t!uVqE6z!ny,7|CIVՊ>3wAUijE <I-lt s+(16 M4P`1" j3[H3`d[C$q> >ݚJ*q̬- m4",%]ІvħGE({Cq mu9o_FL@V4&nHz~="Ýlqum |¡6̜K_ox|͏{?dmޟ8EZsHˊ*.W#~s#< 7oO{ㄩTX'lZxnQu巟L8ŧzK6!uHkN_\I -+7/^(1_yž7[mtNϞa*Bf^D7ʷ>ݖg7NN.O/::.$_5?X=wQץˋwx:OOcKS$>?*۰p{P%jZ81qEe`g#]oҧHouT -n|S&2s9Y?Z!bOĸ]`#VEtRP@憞jm6WRv^Q#l\~4$+tRf(W޵3-0$'Rti ٌn%`W1eoJ^u5 W:joVF]os )./NtPh!: {՝ &zt\0+b +Tk0f d]LQ&)"(a UӦH! R-;xXM \= {xKzVvmlU=ʦxA 2{(fdWs^HBN kCGui*P!R ]T_^`Cңx DeiCkP .}z&AImHU>Qae=S#đ/k6 Y$Լ )<$A~⇕e֊uV fw^MΑl7xS4'U+ ~Lq= YXPFmw7(ݿgHmgvʒkF4l׏NlI=&9MɚMR~doΞ Pz .7U~q$ ,پY%[沬Q5&԰NΪl2AΒjgq8H'{ڋx>s;Yϩ`o@`=-D¼s-CSSp>x,ٹAE[SΥkN+NآClnӵvHyqOx풛>U\u~OKu+H4Kpw^0@~` ԗ5ME`'Fcp=vA7^/Lzλ&[I ˥a60笠nDoYrۤ/Yvۅ ᄈuYmkel_qEDْ̽fQvS<Ӳ|Φ3i"śi9[9'@|o--WѹY75vXCo-B00>ٝoO^b[ҁGL^\\/h#_ 4 ޵+ B2te٘;+'yΒlq6NS? @ vb j"-ē;[1;[cvAfkm؇3Z'pJo=uCnilu30,:vW4pa%*U>û q9O&;wY`3!uYpC`;s7u'9Rgm. J h,f-?;4>vrji1fs3ac?1-gd.ˠRk؇/^d|#!B1A xABm9sp܎Vლϲ:_B` `:2~3ѻ)NAS-w$s8}h6+(yݻ~zn8?gB|Q_"0J/y ",qrx#4 A1ڈP4~$"E?a-sŘy {)/"5oT7xn47q6.ׄ~#٭и;lE+ 5'L^-ly6LW 5 \@LӺFV.79q\"&L)+ oa/;ƽf`yWun^X.5btLP5xc02MVQBGiM'tj\V 4VUY7H- 1?Hދ;% zqQcs .>4>٬ _7AћOP-{'"l9ɥ=pf/,ɋ}"Wuzm,Qy Ľ?38 Lݬ8?gk6g$h:8J%uHУfBCf>9eAQDV4 ¨2`ƖY &5s4n8vH 9  )'7hɔ%?9ɛiE %6ɀEG(ue"; ĬUNt"N4%Bk8 iy1g؏!$t%L 5ˊQ#mg:xI7^yYT" 0HC$H_chwD1GSYȏ@bpwB2~()eucxR L ["OS9 j89%&%?Ѽ*,OdMDjv6Mw2;Qm,GRHVhd$tx'wvI3`2S7G$A:%؎Y?C{[(؋NXێHLg:9E34o9ЀGeH@~t7 ;рMټ1WR[:6$3˹"X%h$aλ˰M?u;H7ÚV-qy#OV MG$W2{o0ls(y>>}Ǚ){^w5zf]p)ڨTك0RC1 5 Zf+kaT!2i?\S!DGf 8KYIs0xaրoԋjT=ak`Ab *" Ϋ .1l5*>]e-VFV#&E*dZ^g ZtO˙sońMM$4\Eŗ\\ט{"|{̆Npf a%2~LZ*lJۉ,) x}$x6JJFZَe M!%1F0DT8-w޷<Q>Q`JlBB 7MvوMBCr>TibEk^Xs-kh؍b /[$94cX([缞?z"P^*\?Q6z] 2ePA"ǸGč(?Go<ôctWFiQۻOj?$O#R\e968㷚U >اک,!汐BKnL"<x`'n-.>XhO5ǻtۀZ#$s`I!h) uO%8tj҃g(y!B*="# c@{Һsr4K#LK:/#T7q4/ޅ#,tv@ܫ\f1pUr ݎW^.L#ik&xy܂r/Կt]V9YCWEbZpE\A%׹G٬4MV:vet%],;n>No]4+7R2%'4 yUxUÀRۖɾJ-!{ҫfph~=3W,{XwnȘiLQl>Bf3[IUn'r-uhl!ԧ<4E}'*+z|ÀߣST ; d пʳoa [$=7fvA=5"s@p $+lm}``ƀSjEی}xuo)+gYS( (LLV`fV晶)Gy?SF}b6wu8ȭ$(v򃀭?Q!'>FMQ]TfZ d5냌PmPvF*n-A`$\C4e@k_4x;\Qץ%QݫMۉV HdRVN2 P>r &u-`?'7饽᰸b^qnu09فE df.!.a328xy}>o+isgcRri+$}d*fgQLuؔ:Q<^M&v,}}FџW{d("dV7ԍ;B7ARx { |>8H]%'6NŴr<Ŵ1+kbgܔFy-]{Zbw" k ( q|ꭙ3":nݛQ,c HsGDY5Z 3!D4 ұt%;_ t+szjaMH-@ᘶ|JF r,mbqo 9!5;2b<1y˩4ӹ DAX!r֟o?3p?xdp" 7l.ta,qr_OyrpGp>δ=]nx,zcwډ`!h<Z{H ;a}cȇy"jSa9bd:r9p6x ָ9hRe4'Spd"4d)AO-o=o9~ ̎nFK_dIyh.E(x>͛Ϡ9F-}3\j_*ȟQD4F/ g ;sf[{@,ڒĂ1N ֒l|a=؛[im)#xb>nulǍBݿKt{z"V--nC}]<ܞҞHwXN[ Yl >t?iF0w}##sYlYvB|wfmɲGҼɭļ?]Vzk=vp6 dzcVl$L> {:^T5'J +AhM\qr NSLh&2t*!4tЌg/J 茂tFoT/UEZr ̄D`PCRx/NwkqKcc)H\{[p<7g}̅~,WaKeaGWkY4!EpkJ5ܥj/k٭+0 F$M6Fߴp"..*4 URdѺ&ӧ!6EuFr9D7_A<[s x0~:<ύIGDWHŶmtZ. >enOv  t2Z3 |]ӿqga~ K0|U(|RI3'Y]}%M`! V)2?–*:x5{m$fBF be2er=`^xҩI7Fx1Wd1dhSJs6Y8K85ح-w?35W gycQfԟȿU#xK,&H}n2&qidS%JR[ڧt;E@ƹ,oe,÷];x>lRVSt,VG|q&\0SjYeЧƵwgs(J<:X]v\IV P_R&bnq < N6Ghd! 0"-|k2ӌCEO_YS/a@"ZVv5"eڥTMMfix̢6Wct՛LK_Z\ Ld2bb][$AjeL ?3u_2(xxH\acP4Xx/qgnLqqcO<ɩw.v{Lim%+'طx> *HN%P c|›m#Wn{DGM4#k16EΙ;s fa\nvt9I}~LK :"dTeNƈV&{R}7VykfHTzu`"ՐXL[wۛa^}o0ǂЧ3],oGA)ϑohI4H2E*F]Z0tO:($*U =c 2z4Ѵz4TΤ3,n(skiLB;#X<;6=/`c~N 9As: 17F{Ν}j~ֺ``L4nҲvZ M,jdl:>=/CpBg:.;?2ӝTE&eO:upVӾ̪u#Tvǟ+Aӗ[/v^g[ϱſX |evh6e ))]S#: ř;a?>htӧ1hg(Et70#`$9!m/e峃WǻG/^<9ZJf\ނx័vF9t^ˆ{ ;'o:X. Xw4.Y9ǃ#/  GT$wODC&Lsn"Z;X*;ⴢ)W$ ė W>DdMmf0"  #g@c!ZƇ]|$`QxԮ3ġ/I3Êmr%@b\:QZ-MҪJo$1 nrpIRZVəڟya=y M¢Ϛ0ip?OϳjmjcrNb4㝃Lqh0B`S&:SvD>f1t\hU*H$}^po35˦o>x0# q=ӿ0ArIǴi$jy7q _cb6&I7@uƣV?"?&>5Z׏T!prz0–DJ)y|pl-IRQ IHUWB1nw [1,D1 X4~Fe^5R='yVvM0#:1pvdm|_|cFG6/e3' F^0iv 1cxflƮt@&䂡σ+ 4\m"wĮ~8҇1 HxyBIZK;G V5XGfjH iTxmUK aaxr'^á]Ƣ&ao;2Ԧ$!cN@T:sR^.+37r:dIn˛OO:99x -%m E ]Z]RK\Ncԃ/6>Z*#REwKGlZnt>Fu64{!D#+띫,`#:N"[䨵$8#*dh%t=@' 62U,@13yv&3/Ε)yJo 3^38gYӨ D{h?Zֵz~tʊ̝cbV͠ŀCT3dx-ݚ3Ԙ0Eu"k\=eLא4i\ t(:Ƙk^1-F*J7?3|EWmޱb +pK{Zq,&u^6+ [bB- ,^q117:2&'y|/ +Vg9ZўhK[j`sSH9;h/'G{qU%9 :jSxD?f wx.{:-48{2cГ9ɬ2x\>⫸-n|) uڣc^"`wB91R8h(qZr6-ʦY0Ȋ}(Q#a[-2\Ӻy _+p?#=a{ctupj cezrICTIhng T@)q{OJ/ɓzHg #GjxE Dnaww^~C7uIhB6jC| Z0f [G/qfJ"W-okjt>EaotF0uYMj~ͭaf+\G;w6}%Ը,)tF3&_pk2$"`*g4TKRqRy0$T9=H|M!:xBaOSV7 YF#..!Ay+\ g^pae|E/TP¦R橴 m 0:Kj'ӸxD]}38lү~839 &<ÅR&-6[˔o!<;CPZa>cvĴ$';K.xn7_ZJHm2>?ڢGBܜ8>Ll ߘoɯ(Kn&rd`t8D_|! Z uYg8QEj 3x: Tvme]$k9M?XI[wHƜb 4B낛*£ߙqA4QN{{Kw%t܊ Ld{1^AM;;eqLoM<--(*>aBe|˜C/g`XSQ538p?k1mg"x-h.A=;}/kݼ+,rˉi7Yӫ, P:՞t꘡tM'C~7"D)FP'CǺFǝk.KNveY_A$pt;v /@~h)+цy7Z=%"L|ez2_YEh \IW`^ [+S߫l.w鷸6?zej[:,#JȢfZ| Ij0K1dԒlXh$H2dt+E3.dK&CY1j‰ɗ#gnG %Ig?~9>a7Z:6q!?U~ІCb|ˈ*_TL+s  9$D>= p ]~2Hgx0_{/nDž8]ذd qϡ_wMp,77z|=Xӷ-]*>!d0|ܿ1<L20oUQ)0aOMxI{7P8Ke?+'Wq`! gaU[,E)b!wlϲY2͹$hM54F||%ulK]ypK`#6bʷ& _eAq|xӢ]@_Il u̠A[˼J!|5H~ m䔶}wsXuXv ld!?1ଷ|pՐq.{/Ov^o퟾8Br׷xY0uP֑} {Yf9W6K7L9\$KڃIU/0ꬻe Kv~y":s>6j9[! k\&lmK΋zt1VͩdQO@;pzҔx\bEI*-<*@jzs$ð b(YؚܱLoTyvIhkitdeFu%ۣ;d1ēLBϐIzI4\gΰ4Wh™ y' PSvò:o|>- &$ߟ0t__*>,[䯏Xd|'}ʩ1r"3DRS B,qjq)]:M iu4s OY(|ڟ>瑞˧ў+j s+Y`go Xexy*Ŵk=.yyM _+h.xM2|ŏ0FGD7k'w9gl![B!5Nm^]0'bC>$tݤNRSYRCJVfAUSBp"ELUȁ &>&DD/X_F*-(S!d* !R )0ʔ7>/6DmEMWNn-ÉA;cW6ՓDD}X9;Z#bQ*NWHqm\ @I/UIWE -+8xEi, ŹoiUaP~(.\$\ 1sYM dQJa8&URQe5D־XF{@o\V#ynXAVaA#t6h3\[J=Rn+4Je6`"!!9 HD'*[svӺ}M217 MvR$0Qzy"YA(ZN1a)"G!:MI ˤt#t,{v>W;67-oD^W.d*dUP~NZ%9d-^3gl}кC'ԭt b3G>Ftd5.tãcH /Gv#Hn$IӞ}D0f;&q[cSҢ j4K료/Y\]u/zq1QtbW޽TRA31Ϧ)V<0 kOɘkwkwlz9qu+C-<\FTj$ kmHqט2ڵui=*# M Y¾۩UK2}!R'aV ܸ*?b&\<-G%YX( IL CɍT;,J]`kbȄ#iU$' l^oaWܔ'/;qBufI͜YNM {~WbyϸvZ}bZ]4>[=9sXYl,V۾1v6CZ'ƻS[ST̚R/2zhWi XCլ򂮖9:|Xo/XdRueyBm(]i^ t1;dVu u,ty6B"m<͘N1yN% KJ!&Әm1yDƌ iÕ\c ++(n\[p-ܱWņQn=R&Al3í3X?39E=P)V9:fӖyW?,#q5އI,M(l8K}"P]p쏟me!1kssjS 8Y~OY#1}F~|于8*Z]kL6'1sqa :[N<}MfEC:mN!<̾!H۲T_{ ˼-dY?YM%u)y#G'sZ9wfk2Øa5"N(5[5i20Q%\Gi`qq)%Љ}mvtU;:R,b[?H0L"/IA f@<2CX~>F ?2jX[[3o"9PRlL :`t ]4b0]u5d0 R3")RM9bÐPPR?Ko0$o9'Ҿɚ4 *-;za!WOy+6rr6s=Z)Xyӓ C @R䩋k0bK GC˻[t}bP$iUQI $ V!B+롐M7 -tQ %9$ys48[^mk;nv׸O Ɂ*ce\Qkjt?)F}0A4DQcYzVY=^ӈz|"q9ِ™dݍ?\{ }ⵞƖ'M &сE6[kR!r҈Vx&3˘̴Zb+!SIz˗@UEɀ&)bq?ؾ4EJoN)q _bw:\~6l?ʗ#Hl7iڊ+EV3c}"LyAp-RT:5K$ucUi#,:Yz a!:qM S4aiV.zVc?iXa{9 eWl`TPSb|eO|[ivoe~kƴO W{+7Qv=1M`[:ρB{`-Wg~:J]ОT`[qޮ=KfӋ!Tz&a=z쩹vQsz)p|es HKM2lT:ANS+;ρb+]Q^Ydee;Dw ȏh푺Ϥ9\ŬEۿz_xq< ]+TȚdV:FVeG|`|V]VVAQU1]`x%ץMFM H%龿RZ$Ё}wZOjܵz0yQCĻ >\?'BrX!Eәxvi0xt1fH%B.; +|m@>d輍7UO&6-y,[Ȑ.L")jg]xOhTE @v"ckSE Xb 7.xÿ2@O NtK'iZ`VXz'y |=Ğֵi\7)2:xuq֑WC-"UW~u<awOť@څHdl2=])!A3w \0p RV)qN'+6廻LЎdv \)QUT!Z%HcC*i"p|hZVt9hQTzU⤬4NycN@})g1j %"KRT|ѕ&Pp'r? ^uxSʘonlxWYaϴNyT $K#JH^f~iUG UM k+׹2SM5G0:5٧0]4YL %8CtDF3[) &$pa n9^1WL9ټUo4 ބ zpG@@BS.ÖkE0%&ier_PU(hmIsPF?SK{# G.TsQ fၒm1K =GC,o\\VF9p~J+@JܡNx7AwJS)VhpUxB(8V&  '\߬`Vٯ Gr s-3;X٬qs8%M:M!k"Pu,jzMXC\-F((\ɴ6{b\'XV$Щ.Xl SN਷&޶2{ڗn hH. &oX M$>qgm\/K^p'%SϥѴ,qYlJ/s;h׶ 38SbҟD6Z"\jRA,CL1Y);]d@3l;8sYޠJڃoZ&/c0;F؟QonbYk^P Zp/Pg|,cAhzCĐ߯_X⫽o@ͪ ^=U%{Ru$ov[Ȫ ͓3I') ǩjA~Bېh6IҔ %I}Y.rz/6b&Q l@.iXVN/%AO7B.W{AHtj53 3?XXa e9BC_0{U2![;c:(|b- ѷ6Bo8G=p ou`Al6l zYOa JD@ Ӿ KGݴo]k^2psb5m~m>Λ׈i՜xfk6!IE2_A}p vdwu B>l?W Nv 5ӵ7)zaU( \cqs݃E^%M*H>x8䋽[/wO9y顁3:q8ӯ^̳j̀Oo%\ns^glСm$gA^!9ws$jchq3Ps?tzY ;.EҔeY&0Xp˘C@6aB5dn!P3v` xkAЃo , tn Ǫ#?-n6o?åD^YK^V/ o774/ NetTq3wvADmd;[̻@ڲY qNH>;ˊIjtzФr&%1dOlM=8!NO&"9)ȃx+& l`Y5tNL[7V6vjS!l+'RsAl^8ٍpptȜ1ɑ0֊t8+CUH,czy޿e_?H57?xj@WcSktV ? G`DOvQBKX]PyI-f3LqDa$31}h!9![ 5(|E;@L[6n>S/:Zta7 ٦1܂#8&fbK}Y^k+rZW\СJ#sI3Xn~iH,Œ#\N2`gxH+*0lP r% 1VGż*9NDκ9vSLr6'O#}llLZK:OM@Wؕ, 7,=ki谧CYZB r23  PL@j %7 nŹ%X}{lϧXC6>M%h{,ܜZ¬K84YYf:+7 x慨qchì aE=H~u  ]ڭ2ʋwf/I.x,{is.Ӱ>^>&sk >/Û% HhOLizg6$: iw'ffrR21Yv C1Pt ȿV츲01smaDݨ'?gs ͢h`UkW mht.$/U]%foLgLv7 НpQJ4$'( (~ܷ6i>l2kv#XzT7}6M/Qxr'lulM&XfY:˧7/|Xpg۬utڙ`PIFi:^WBT.E>W\DKD.OmU!<:ʒUhS`~.\~: ԦtI dx$F+Ei i'vn_yqHh O fp_}*ͤz CJb%+"f- 9c\>I9^lyVNnFEt{*ΆSE%g?ץ T9ӧL/@c"N(31G;ec8bZq&UꪅM99 -DOՠ_wMpXM J#uV>r `_N`N5¶1ƕQ=4Nͺ]Ŧo÷Kx߁ʦR$;ǰf+ , I 0C5}MkJQT(Ζ9(~Ԍl4wTR'WmӪm  fz,[sj!fq8  /ֈ,hKO˗Kvӱ%W9>2l͊Szz.%g ctɫQרޕ}0W$q/&i8rDy2{<^~č'.f;q|J|{??&p.qbUrc Ʀ7D#w/愯} $s!կy}CiEzo:n 7= I0?#|aفR s$QKBbAc2J9#]Ε{"wظ}PCaXa&1:+r~trR}g#}#+̄à.^U>ywx( $p51eܓ8B,273ryB^cr|[ajBL&T0FQ>$9ū [GU{[L9 &24no )栴M{]y^🷶o ,@i ;Z\H{\fYZq ;9x@imҾ c4$>5wU{^aog8wQ_D;hpOwNv\OCg6@j?|;_Чθa+C<Ȅ-:4mT~v1s%:c۫lIl(V,y)6D[uJܦ]s.i;w6ͱA1ju]!⩝՘n~) ~uYP0@ wSr-B\c)^oO:=aZ * *4!%&G񒐜_RCK,z0OR4KUl-4`R'VFC pqw2Hma?OQGtVΝݵwѳvfvrM<=#kxI4\2FHd6͂M)&شNJfi`Tw|>÷GG=}yprz{|zw[d>Xl̀s1RY麝T 䲿f7?L9þy`%Bb'}c'{/vwN^ıdjo3=GRLYa@+-"obgnnmo?/[;ݨ ӣ4nXMJ͇-H0Ze3&9Q([U[%[5 )|4Ps!9ބ ,Ӗxjf.֋_NqG{!rZ9yV4ǢnRj:&ȁ+tIl-fgw`g/@$Ixg9 ~C5W#!PבֿaTy'{,(P~AъYS=R$OdWq*F\NS9U=Lԟ+/K-QrzL*G*J449R(ab|c[R)/F=r\ΰ)}":$ F9z~ܒ~_Hqe\C OJ,j{&³t P:"YUӾi6<&\a[nRmp=5D{6yhy1My_"$f's0^d5rHj,X`+D-Ь[v[sx=Ӫ3ӐϜҒw`|ز:2i<) HW }A\PZwLVxbRoqv>*d?P7錺"kFVx8}>x7;~,i !f,}zͦ'M.,ts1_nڦ$h=^11wҶq4;>F qqf«-hoXyVC3qcԔwvMzfR9aJ*rxp: ̘a(T&e}dR,Qmպ!~e{CQ\c/M 4M%ԑo]((d,%5v+N:P7lY;ȑrK 8:Ӯy ľ ]@1F//Uo9ʗ6 $T”[|`Ƽ*1g)|f$la"*k|ȹ)-2“2A& , cZލknz. C&¯sKHONX9  oHɱoˍ\j~zy]c@B|}l.Q1FFVypٗ0&op@{Pg2-BƘˮ"T)~iMBsgOY=$M:P~vM9]UoS|ᓬwAFoӠ}K]ڈOM6,; Rye"+$8K56"Q+̏~95# Ƙg{ Q_˙)Ƈ?I])9)f`1A%-C)fUDIx1v7a瑦kb-Ql#Oh?@Z?}҆:lwgyMpF.vMd8`}&5d|YG̚TjGw;_HC:\|p[T k>@&5`UcZ'PD:]1a,\"J4𫚍(mthj>Ghfnu [mns`_cYvJ"c,uILw%S ݆Lgabl"˽N@u9.naŎQ 80?q"&KV&N8//>_M\%kAiV/ JR>Hxզ56+ |xnlÑbȨR7nޞHOIf K*INV-52]:NoJ+NOx.9Óf9I0/6f '3b@E v ,C%Pء)3#;8 \{2t|?MU2IAzB]AF25pQiGMA$u9 ISGNPL" 0ޏ\RGNaDNul.}S|06!9?w(VKkf[^͞{02 [d"VKm@+j].S ԗqs],$!%!?X*Re'!*4)HI!X#)mdr:R׋"XQ:pj$䂠x{rO&Ya.VxY^B35\~,#*a93AXuV}1%NT}ÖP*/r="O؝r. *IQD(YwxT$u%Zfח'ה9 <GBaSj8IҊE#[QLY=dd)o\ĪOhT $p ^Gpn8K( i5R貰 ]aUA: &^rTB+HicdgG1Io`uRgiE ݊  5KC.rb44^E&uPp re#^ΐ3_H匩QljrNuqr4X>lLgP4s̼yXKvn)&Y* ,3ed'ܷkH= :q=>on%Vw -iLh,j'ao.r8|˚jܰW#W4 nIfz]W5i ~9 \5BtW]k{0;6 vgO7.xCxKWSY)Lo#yuP{zk;:QAIvZ7YSǽMR$$1K A( ΐc,]߮GwWu @vdﹱ~wWԊ&Y}k/srI> QQ1}+l>] > {T`F4~ i~e(b]8pn˥CrB4"vIط mZC'ҳ]Q^z ZQg`f&Lݛqy3@΢X03gѨrbcD6x}v!yI];雓hvpx:9<8$"]$+':M7B{(n }!wY"sSѣ=G\qg`㽣 N!T7n4H0!_>oV2 9X﬜D\q8}!ֹ$rƞ 魶:W:v5- CsJBq}{u F)7K7uf]I4:5ir}/k%( u?,/g['qX>.{U_Re8mW/[[K6L]~'\GǣoNO) yf?Dـ&`l z2NMmcCE[3D'5V,^E2rH,'*v Qڳfmh|+Ld]~uz]J9fj.rki($ӿ,UA|a ԗΪyC볆a+8ðx2T qy}QMPPDr ܕaӫv~y RQD,(ݶG/k!US+3Q $ N'^jJ{XT8:SIeIq}+ה<ʥ`.3* ;e27ZXۗnN/.'Tĸ$#ʢ3#*srQDSJQ!-ʑ%eUxDCB4r;3.ҹy~GU'ET=qY9I;Shzt4ftgxɫ1? P ?r]P]TE~#d}!rU`T w0'8;/'2얾bcDž̜ Bѣbo7tA^|E_t1rC9p3 4ݏ 4\@b6~v=Ng\OVH%n j -=@0ira}3tOCgfXլL@.{9M 縶cAnb\%". r5P⿍y59z£G3o5؉Cλ 8X3I7`c&Ls_uw{#퓔:7vxwDoxǙTx;N`s2}5 'ÖIZWfiEu8|I?dp+ۡ*9esɴٷgg=+Y/ 7H*i0Y#(@#Y:5b+NwFohxsR6 u(9 ZF,;AO/zLIzr|ǝܠ{ʞ%,޳no ՈQ zR"T)400P%1!iT/ΖFK MR:1_#SAMmvDB&uo{cstuQx!quy JԈR _d2Uhs٢㶜bCL\z7>bQ;HR,_0|czz"^5(Ι%Iyɦ(E#>l[rTcnP򿼤XTc]MtLn=-hA\XK\m +~86I+'.q%$Ht4k~"'.HHoݤ1+؊'W++o*ۮ Tw *,P|, YكY;N"h:ݚAM6L@a9[֮q}r~}( C[ i6f &J:Vin@H&_m \uٵzb7KVs,''凜?ZaYge4b7Nk=|xL6C1unZ.6p*`*1x`ZR%UAxQa&K"/j(s;jKXmmQmyc5v*v y<0΅sR݇v szP]L Hr1 Iydc$( 5*RFb*טBM$# \5l$B,($;i Kxlw'0Krv R|WVͶ$jQ#gu01hZ&C(^Xj] .9D3tca 0|PqԔ RZZOA"SS ĭNQlf8}oӧ* Ny%Qڂdվ epoSuw${x\Ml[! %tG{Z\IT=NӬ2W7 v OΈNݢk6`'Ht|~i>~.^8BS֘x2~1[_{ߕ* `vٮڑ,Q~x6p]`͌c0\!̂&JJdAԵ-djט],1H!h [;dJ$\c 'ƄW4Y+=o?>w] p1표&1S܍d^| ;ޥn_u-|kbKv:= >תtGU 2;Ό `Ǚ4vX{]FRFִdƅp7|\Ikn}87}?纺yu^20[:Q xXS4apGT jX7ƝCZ*uSNϩ0P" Glv̫߽sDOX[$N:q69i4^{EH844?+ p%ŤK{ P6#Nαj~S$|7?C{lśA[& ђ* g9ֿEPغ|W@ѵZUJm{#iUʻ# RՎUŷL>* ɐf}cS(нW*D _JJy -j[Wy6?j 3ăyI#,'5ϾhIS RgM䋪9`էbOzN̻c> XP&ajPxepӹ2vFm6oi=;z]y熛4:CV]\NA54onqJH^ߩ>orrFyT?-K@YY⻩g~rj17CuaN q}lol{uҶit[TxgǏ&$! ]Fy҆&Yu%lWr@O MPV2Gr]ǫF{"hqSu<-b$,ҟ`常|L~U?oJXuRc;n ?,#4kE&.FbgFp'g@TM/ ďU|q[Ra2J IyVOga/lipZ\N1TLD3yOx *z]vD@>oT18ch`3P\XiMlyc)]AGXwV qv5 瘧E݃n$~wGq!'V)uTjwd-eA ȠSVI!N8agxyd\&*n.r(e|$S.ܥ @)Eo"`qJzC/Cf,@ eC#o^./zwFn")^K\⻕iҩXQ!VǕsag@c3o3Λ%c}⎨rc q>ĽGئM1dCl)י6ٶvC/3"%CEݑ޾žXTC+Ƌ}oe_!sh`/Za  ՟&@D(೜ÂfAbtL{4~1Ҋ0, xKOY[MUEƥC%%Vh}n2;iSb n%j<$j Sӯs 7mv8,Q1X^g"}ѷ^9dd m3_XqA얍P_̳('ElXL1HB6l^~1*-̆aCYC;^ 59hlgXUttvFl,'jycLpwB1 0C2gҊ V[)'}d|buF3OY>1?LwpM411af4NkeT86EHySwr-<ϱd]d~=*IUۮDV K\hf\N 3qnfUH>4F٠]z5Y1dY9bU@soar{ߩ>XF5A筂+&5Y:B_60+sV%Dm<@Z z|2  wO!s9=tuc/K=°V_,lR=b*"&1{k:XJzEo,Ȳn 줋O-бC+[{'Ibop0L{Ĉ&'$*^ cp'r( ysa&F?^bz;58F#Qo\BWWML7Cjgs\ٹ_N_8dVH~r.#D) vOG;G?nz=l^3G'f_ޯpU{2jol[K&<dɉ!DuEP’jK8prGdM|r* N͞ T:"!O#7tU[5Y&QmXWf4Uz uD3wFϼQ5y;=_n{Ÿ>u[>u4^,$?!~0NyCklikKZ "n,)^+tRf$ԭo=,)rqfDMQŢkbizC4#fbL{n~ͣ:<}qKj=\Em̓%0FE䒡@ǖiz򁑔x(-+8[zq"ay RvfBiZ+XTRHf CMׇ0D7 IdME2 Ym$imRmxktzI]PX  Q3-b[ /OwAEpcjV۳ik<al8zjb2"PޝYU #:BaVyh}Pyqa`vJːwAaGNN_W;%[k42\dZ1T7FN13hBjutujj`PA[/jݭaGpB譕;Vv+p@TNm"fOT].xy1???fSG:7 `G;cU(yт#AW$ETy5[3J1ReÈ"րzYVɼ|i:"-szxO]L|2d¡\S37ǁLtG|_=%fcYsA[ͦsw@"IG! /aj;ɼX;.R]^b Sj gJX nkdyjܗ٢u9z@$´`_q@]"GY 9XANtn>M2]8O(Zwb̠ Єcq&7=`hVȆCeA>xALtV8,t 3>ni=5hW{1yLgݏ33.u-TŸ9? ]p5^g+I`8Is6=h9yU [;S#_p 4r}9H"=>s]m@ϛlW/Q\GoD}̿P;k>YWFlޖMM?M5oj:1euR2>d2Pdz.~kߙ!fWv3-|#L>zzx+7Um\\vD Ú:! c$l!`*d) hR^>B?>?!y>pvjW=lo+ {/}JĜy*mQT)\bb 34O^%kf7RYu{n6\vZZBq!@A91$MeCq&'Q537\@ZА峳rrNK̼q]'5`V̬y3JP}0.^A \y< 7 u=f3m&A jeWÂ7A5S-slD]l[^IcAҝ 5Mbc(oHԧ?J^qkѹVx QҿyGԴ%"D z,9?Kn& =\(=S"FV{y@6:^yK>6-7~`3.htQ(eeЈT跠eO(?CR@[$pd%,ϒ--tFBQPվ`ya0 [^N#o`2a'w8ЖZ*KlȒrE lU鼸A^g dn/Gǯv."O5Y!qSm> 1!x+3I r SN\$p 0&,)+FqgtgEUyn!a5,v+tuh$z!d>D{jIJ!ymѵ6C泔j@$WɞiK1BíBW**͔g 2sAon\0SxԙݱwQ*娲^kUޗ.arqIxj0::rzO!S17!^ P,Ý?1tf%~ %)ubw9_h "= hLj|_<֚C"6O|Ι1s(/aM~rA7,sY >D⼜Wu59e#BWq*r-8ߨZWaPDAiWb*дн39 5u+Õ_*6-7' OEņAʳ39HonMQx\o*9ףBϱ`I8X!ޚ16QNOvǀX~><Q?ޣ#^#Zbx2Ga>;<:@ 6d;:e>_I|"Wɏ +z!oh8o5B^s`_>]Wzl8O{gK9:?XG3@_]v]ǷqV`rCoی8ԊOF|Ni[9(ATxkLFTv@[7{ljYUӓspiMdx˧G<}{jHg㻽á!1GkֻLEpM߿諪N  Lہ9|:`~i܁\l%%)Ch@01-nLo0tRUmiEƎ>D̏(zs EoSA_=;wGz~D/ʛKXvdC^eMF蒁$PVv}ߌ@v?1Cӣl{tz+v }I_ҙu?~gB5N/Yx'}ZfE/n /g5:#bB\kbè@SnR4>e Fho]E%Ý~Cwsn,r7!úE"-jpeo/ )_Tu[ߏu2ׁVʹ[7vƊ!;djhI1S|IK֫p8JadIbІjBGiPśg3U_iFӝSx\v ?AV`)ǒ{gsږ%dB 4N;xFdj+3 ߵDio)zJP'5¶<ho-yK(H;浒1"Onw (rZcd<l+>'|Aˣf8fSg}]ku=~e.,%`g[nEaw p3n= U2[Y1)]xy=ײV͊?2HiACN2ܮ+*>yԍ$U𹋝4L<ɖV ݋`we# l *URG_-IqcOM_0aSBӴ"C|D;i CTӖ8e؄Q'}&7Wl).6%IE8gޱ&գ9%) -zk~~c:9Ez4b2b:EBx}\K*ɭyכc3H]{Nu8kJu Rn+`D#ndOnb74v5$_BXoO] Ņ+g*A֪q0k,GjTj:%P̕sr lSJb*n2k wՋP H*z5pza>odmAEdrZ #?G-Lx"?ɮνߐۥ> Muݩ";hpF3t(m0 a"ΰUҺ8o.q}=bY$xqP2j'xN V rgWRC%3eAS•Ok|UC _z/E91({$.dԱpwrUC;tPi˺!? na϶ZQ9i/-A LPw)[-YI&E X8p ^$y#iLnEhqSSVr\|0Nhzyr&@:β8<OZ3s,&X2  GL5lv{j['E"򔇃"'V9Pm=!u6ae aaIds@4ss3FpSZ^Rf~sEM CqvⱢ 0->ھ=&;IoAko ,NS wK[5;zm#-1"n6K 5y֟lv4t=FlAp-B('w$r XEUW'9uZ 9cF$ճhݴQ=Wk) C<s Łc.ޣf*]RXX_'z*2g>:/#r*OP2py[vKbl nj<.e>qlɬ6!x峌X,Z>8' >dΙsRuQ B)mEn].  vsi3G0P0u~IJL7W] #D0޻{&l OS1^ nxQb!ԼmbOf<B[`g1h5"da8KMX !@WdZwGA Nt`D,u;HȚa&hk.ɗ7=v^MzvXt0PVٮ}t[)^V" x1jxߦജ.Ţ VLY@Cyrgz+@gͮ}bfDcwmJKnYmnn1r/o5B.i7T9"wW|]ǓܬA_yIVm.Ӭ, OQ۬> I8s$D0z)d~&@O7_N;?^`69_N U!т( BfsA=vhRmzq%bXIw]MVZm5a#JP |'a66Z^x(׹]qra&_Ͷ(&G0t)҂# Ӹ!+]0N-㯈ުR~2Bͧ% &,2sq V,ټ $;Qx>*/ןKO9Pgpt<8 G{ZO`f+;0L 6"=K>{5,+aˆ! Bd1/<]ٻ_ap3sb,qjnT.*!'j/'l+YI378a3.I9+? 1@O)ajk H-zL8SisR[snzZD1C3RCc\؛ҙ +ؕU =_7.t0id(pT>[3i9-S6 z#a s.`= /mlQE Ls>BUrJ9}p>  ~:&! b2tIfZ|jn)ضVL?ڸ1sex55F'xqt4V"mؠϴ2բwa1'dkKVaE~\ 4Ŗ~IGwg 6uvU'Bf& n^l QJ <8 )Eў%+'H[]0Hbj$"j>D/f! ٍ!-Go' `!:{CQ#hkےd'K6/E!V$ C<`y/Vs49&b#~>qScA ǛЛRlZ]pO3q W鎴)A\k<++m'>^.9.~`‡DIFq:Rp/5<Zp\E9{]#Bc uu2+T5k(jF'EQk}J; s=ݢ=u[D*Q Jw+,?8HXɤmIeYi9Av Aq"}&,B'é%2WF\&$p6}*c̓u{+h2W2f3UѫA&rI=)јAeLB L)oe Z:=nKN[~^u%؅1v=ˬ> $fqs9c?AOhb PI@y9/޻*"d>Fk2=h#7Y4k6{e*pY\md|YBJ/a_9? g*ze0AhTp xs[kgR\Bx?/($dS{,M(?v"H>r0p-ؑcJiw끇:Ϊ=͐^2uñA?zR{ f>L|7c@7Xz ic"ѵOUF^,f^E85xVz=XP_Oi̗{Q0͚ҵJ/?O)4uz+ozi w`3Cap=&nKOn"d(VM67_ )],f i]:Gґus+  8}%G@ad rļ'r׀F^X{9xAg3#PsHhVwN>0&F=2_]e~-]zʮx@y>yc!*ﰑQ] rXB"+ZfS@K L[[m=?ŸS& TX6ԭ'u{&P[ЏR.sWHmkc O .5AVcbIM3g1ER1۞k>nH`Yfٟ.H==x\ VTVX#'Y_Ǒ!WfaD׬œPzLa 8ʈ{ea =)^X:C)1Mf~4`Ekcћ!enqnsLy9#rۉAa5j.W< l]YBNTQ4ټ8lHMpo8GA|Uhmp%ʼ)<77:}l{RzFj%xjcѡ>@5Bj=mM_,+$uOjriOY"ب^~B^<{u_Lr.C !my6E 苞 –ko;d7: `'3[V$;)p M+c#@e6UpJWA 6"e.N0 \vHx_/)C~M!gS_ ͩDy E%"@T#Αp,fL ܱ/v,KG+ӹY+NObhz}\k襯)%ޥ8|QἲǗ.W Uʹ5PaPt 6F(|h]EP&]LME0U~c"m]շxnIJT"pq i]qa7$~4tZZnü2V#s;]=);G#y7/<  Gԝ:]|zT%r_GdLvk7=ܝؗ(|%Y@$ B:2詻 t)B~`cbS`efZU`5-R rNs~S: 3QxzT&~GPotXy\duUޙ /k4ZV< F)'!0Jܚlo+2]n*EPeAMCf%le!\fI&G.a&5$܆D{>ͭn^ Mz}TI(TK^+uoZ="=x*b`wƕ7um;/)f/45C49~jK զ6"E9 Z0"&^s_9jv =Ff0f{zz)lAo$|/wc]JoCs?? 8]&VûUՕ 6^Y+8Yae%2tpT$1! kwރigv\z=|#7s;l!r3# N1XUkޒ:f#!|Z N UX+IaPq]**m=-dVI~I7sz1 y|^ MQ1n!^*]#i@߶mӷ)\S<.ҭUtnc|)[ɭd[Cluw'ѻߴh2;>ߵP?6&g^|+`+ł2­Qb H.gH`>3]ڛ?Se\HA8jll%We"_DJa*Y'g6/FbCrIi8/}7[S;* M\@!9Y zp,>*O^Zfqfq4hr&zǾoM0JW Yn֩",ucl2#@l>q *U7apvEh6' X^Y<љ^+V\xك쟨5n` x3LsC@7)vCr>ޑvHM_xjI7nV5duno}%dC2~/ʉћ^@ןjK 2%p򻴂퓇ܓVURLɮ¶_.UYWvp߾Wvyy,N|sK5Sn]O0<>eDz+#;/3/Bfg`qqAȻ*b;0$0p h:7Mvpdv_?4Z2Urf@h;E$VgDnHB؝dw$_\e5Aݩ*8/ J~[w)"$af0|֚iI8c+[Y_JMX0NXx%^t>gdQ&ki3M.GKg%[u;QHyvkʍD1mAVLCf.B˹Mk|Ikq"{Y}`|IeyfR, (& /xz?W0Qa3l9-ۆf׫:6EG_Z0qS)@B!D̹cd2DdNVu w[ĶpBEGf_F+Pnc0$G3+ 3N,9m(U6n.9 G/q gҞc\%K|1s#=y34 =' _:%{IiCxGҟxz߽Kkwcm! O]^rr`Q g-crJOQyb<-QZ\ϑCxMFN$tP>bXoGzV> @(B60kx@ ]DYw9w29La\Hdq;LzrZr4w2F+;Q#7&0IwQvVJ ҕiz8ڟ-ڥ6qaB;dz8{ͱ/kGX#~WJPW24$:̄]J]$OVf^wky}R5 ,ɳX#H:K;B@_>U(0lFUt?Q72e?Q:0NG 6? IKGm@8g;UZL'oj9=JKP-%s#;_JՇY*?B^w];c5aˏ[O\bֻ%Щ[yi0aF!w6!k9ȴ^sv2[~R={A*OX ZrsS*--bB`3l75.vlu9H:Ego  &{ť^u^5 Pŷ(ء8* Nz+@ ?^ܱ櫑P~BJg6#Jzu}uϵd ^~X;0)T_@*g""%g<'v|$egfq15[K01]vmzIӐ)=w auߵF7#T,%OaՓK[|h-$a!2|M~HzP]%V/:zN?ݳGe'>mK&t,X3HYLH!`yMKkyE}S[Cҿ{=q$]=LE)綳qc]1\IȔާWE'v`OӭXT9[mOO쌲'i2zqV6#T$k~_+ kJxR+rc38h}DPdlG>7>=_O4{ż6N}c\_IOdkE| |^iKl<U:<-:|@A``#%3Ђ{VδN|P%bIo1,S`p>{쵹Wu|ܑJ^G$ViJDyfD(D(|RsSP$jyVAux6Ewm_M$<^g#6v9#&G?NM5R(@o$eݰl@0A@&\ֽsIb9}\&#h5S:^ӓK1&N Yn{AcǾ)ac2ZfNAp>NaG~PkԿv/qxU%nqp0=?:mգ+p.h_,ޝ|y0)okɢCnB|?Bjg蚸1eq%v]2^2M?ے OZģ׹sot)c,и)Uۑt' ^Sd5pzjh2;2$[SwWs6߫b<As[fs8{S|C8xƪ $q ZjQ{}brDfeMr&PI?mPR.4;KOa oQ @iStr܇FKn $*LkE,D|S!"ډ$`黹YW"Njs:SŘpa|pӸm!)'Q 7e&p[yB\)X_̇qӎHˆJUXbp+ W,5CŤXܲ U܎sxCKġ&Q_ JƢMRZ%Hb_nA E^~J1Jݖm۲],Bj}>ma8AҟcҺv-(VR+ (#~+&9ͫm՜xy;qˠRnS5!hlsXQG%=]]29;H4&%J~EM%&Pt~9%Ӫ9 :|w_N Go'j5P4k0N,ҮFEJ g5Z[I ǖy@i93~sl=OݬA GhaªOrz~t*6{m0=a:gٛbt>pe),+C7NKT|_m"2TQӴfׂ}M?"9Ջmu%ƚK}.O,2J').8*k$Db!]oGfy =X?קrZh/6CݰyXGH @6ټÜ!JRp9`ʵpt\dz{EN'{ յm ,.uW&G~.Ĥػ=rh8Uh,xYT"s/Q4L3q sqcvh?rM"_,@:3';(Ng$r@3o؏Q;7ʦgC55M-T!gS0,it;$7sS$}ՙ)1;ݟ^4i2Prz{]K֙|0?tƸxx_qGE;X~{~17E @ϲyv]a@T|8,K,ׁ<%4q޼ sޔhSB~pC҆b@tEk6kq~[`*G@jOf3Ug](C4U%ض 2mMcg>n_Ma&Hk=t:?EQ{p&CmDGO& soJ8!PHxͿc0Yqdn!ǙU<#, M[:`qcƿl@ {2up@t CbHc9XRUp{ }ӍE5z~aaF]_ j/j Y71bcWLaa#Vil icp XrH$O}Vy>!g^h9_?̏ALY0df1<@bB0wy%|y_yVe3PBz(0 ښUBAJgkdMAs.]/i\ k#j1T@gGjL{L+0"#-*9,nJvwP~usGwl'0H ܚ;^_yϋOr1x9 d|Nۖ7x-C 7:TdaVL}!.ņxZY͔%xB2|]}̹'A/8߿`OTr 5"@G p$P[9 w+tT5.]xu-g1пEq~Uf EQ3h=7~ڀil7P(XgJի"i=@5l ,`^*}A[ !*DR9$ASfaίV9G.)JX vyoMkOYtSB<X@)U;텺DvH% ˌTe׾CL1D0Ν^ PڡDɟ Ab#CÏLVASiv+ZCNS/+;ĶlhKq2΋bt:#Xt 6R6\f( 5 .WF#K;Ϟe1zU+ڹa:2=ؐfl6DМMQSC?AP-Z[k"Z T⹶#|UzIY?u _RPBND.DvSSkvKjO)^w'Oܵ6Rc&ifvB6է(afȣgՍq>=݄tAOge O< ˵NOl =U 7- L4¦ps>ٜ C\ EB{FpBr|11u[HrT,!>?n8b>BVAܔHz~o;" E|>}@865.3]nV1QrA~ cd D]aT VNF<9ـ/s3BȿL@U*'@@i~mBǓC +ʀ=T@ڙ/-p)3*ߴ!ə3څG鄳TA4<|4^;Nq0Κled|2`K;| ?+ݕǓHCiަ'O{RӚ;>?spcn׶#iNAC{_{o݀8gKE&w3|堾]_Ef}xyPm@6,J>L{۩_&g3Z`'wQ{lbߤ̱Q+BEro/HR*%K 8v}tw4!c9/i5D۪ce7ded؇6hEXdpC^ឍ NS!e6V=x0Fsp08k=uJ d>n?v_9|9[AvEqI.{:⻃SƱbt_׌w8<AYSnwGǬ d]h~ `p_2g'ct[pǪ|HRi vGϚ\H-JB4F1ֲtbo:\,ZԤpjQ> xupE2 FL _\^![F<ӬGT5sx#FGնSVTofVb>߆ A!8lW=llM4j^Ď4ͰʥanLr:YP1@z ߖ _0lQ}s7+ƾ|K2(C^NF"lJVX9&8ZiYCw `#[ۺXChF?@pyvDK:hk6M5 ꁩX{߻ 48 ϋ x+i>FΉ`򈇚'U3)ƣ HTI2!z@@n0<Ęi7~ d,TUC1Vcq05)Ajڃ\ Ww9Ya%f/BGzIX/lEQMa2 YIK =tIQ1M2K!9#Cd'l~5,.;љyPsb ¿S#P (R!`I}-o"2(2P pRI:@K:L*SQK`(!!_l]ԩIiA2Q V_VmyhIsP;qo2!26~xg-;F<^CMZCzu(sjÂUYn| ^4|:rU@ŧPj:ssXWz)Ѳ9y]Niԁ7Ӑ^OfgJ\wTl'|~z败S P5'Ng M 7uMAT"Q_r5KPʦؐ{+ˑ@c?/nr)fߵm?%. ЦA iKNC;ވk虭;]i@@aq'7ܧ5ڛn$mf{n8y>ko$#֞Tӟʐk0Y baD;g\1fCV Y&R Nk q4̬ 96 >4lӚT5s9@l!<ȆfT` &;9bOB}6h*͞>Nߖ/_rg;WY|x9Mm[ҳ^>^gȤIE{p T@+f@Qёf rw~3[RH vq+^iwCA{^[+܉21Y [# 2 #ˌ:V-N"r{|./"(/3LSHPK6 lݨ@ׯ$gj -1 H.X1TsϖF~/CX+ϑs2v:btޝdN>>%ϔsZ] BG.6-l(`0FFSNqDogYXOֲ|b xp 1]+דw_p_-iXѓzOZG+ Oq/C?O](3X0fTY \K~DPlGbj iwH:']?3s`R-87%QG_-畋Ot]0z\^%*:蛴J{y>ǜzyUAki=v3㵟onnVdyjO".$sg_=~ABk{"ƣM$"zZ*i~qE,l@ȸ{NNJP[ >gԥ걌3R5SRwDW|oA"POLsWK2q ] y#]HDDd.tw*$7~R>^`Xq+u>fHmXqEy\i'@TFOh(%7n0!hyr(V4gu(xhGjp= G-B90$BoN ]?-Uj@BQ!{bQuз" d>#s,7'E!"9|DEqfK@/=^~!p ;⩂GjG*ޒb3гZ?+nKgXap*aS ~(VOfRZCr 6\g0yA(Tk[љĨ3s-X$g%~ ŃC\""x38[\y=. ky8f͋mƲCʶr6/ȧ\U>Dzml~ H poU0k 3Ϗ ז-)'?c S⬄ cLWζ|Lu՟çU)ާPl"~3He)=nlFcGqV*S3)cGK8;a}e%@&QW19wGt\nr[,RL}A|< ]4{ %s7 'Lg4u4@0w׳H,hzvt(CP0`e*|I1NM}]F#d1w@njvݛϔTTxOqxώ70 I> iI ';q{Vl)ZZ\Ե%]^u'ȴt)}Rd J?'@ PʍL- Zuj< X %g])P)``Mhx~8H`Bjcޚ:802/՛Ћ WTj ZZ`(SR|8)IqPzD%$QD5D&B6 m+Q'VusœU`gޞfhs9xυ) PZr'bMf!؝t4L@@v7r}bva P|h\U5e~jd2uBFvC,HeYU$(>0 5ż x@E}hbI`;e(!1̘ =wm)CBRфb[WqzSGcRم)dO=^&; ,Jjrt):8W55w)AЮP~m5z'&Dh@V-3ǁ)ґ>QuD혲pҽT+u0ЪS%ͳ񨚊8l *BPZrJx<锨Uy6( al$uk/oy _fXv%@S*7|oA׉3Cb~GWgR!Q9[!8e8ÙaڪiQ0 #G3ӧʷXw8*x7'^st61Y[0Mۘ<A,pHxtJmun 6Im>/_We[+M簻=u׽Q:깬6%s:cuw7tNѴmv7jHH+ӽ׈[M6iLh:қT~B,4nXNmpz P'8: 'solAl><]Ll+dý]hzP`46e156Z )`4˶1,B ҍ]w~wL+(Cw |ߧe,>[{^ :I9l*8d`JOpٌ 8栫}EOo'كܝ KRb"SݍKB:#<=4{5*xRZ B\e0΃:g~SȩdW4+[⊂`RiC[AcDd긕yj?!>Ӡo2di:C- 3sqf`=0hbTOIPJ{ojqcѿӥ(C|5S08@$[ݓ5M8vРE< {6@U=m#G^m`+-$sm^ 4 a/)[MCXdJv]6,;w*{҂Ɏ;3ceGM 4ZSR2#1bLTJ<ʱ-:SA pHܷk}C# lr60xo.] gAkxj mǍVּ7ll `꣐e  mH>f[PU!@p_4f EXt +ELXd D[ sqԌ IcJ`pne {qGfm!|k%{'gS(&WyӣPRCH^m%ERE<)=?"Y8PTTL G {< ln|RX?$ٙ7tP;R'}P.Z< /AB*ȊފS^09`{zv8{!N;^}􏱌Y!tX)Bhtq0qkA]Mn|:3C )b ZN@kѯ‚~i>#-A[ޡ|!>-̋N:MDgy2b0#\|eHt{8t^sgx. fLRE6MǺB4~hӲK?(dX`(}zNlLFR,1'bאZ ﱠ;-Vg~ӿ3E<@KqХ̀p,#'KZhNo]W@'nЇQћAesXjxfq`Dj64jjb_Q:tN5% ȃtxM"5UʊQ 01$C0qшcR˞ \A`#sᏐ&j`а.,^2`]G8mk|3FQvwKm#p!km,B{@ w3(U8 DM?~OHdrbkCdHrKaջj!v+0@ Q/sg8'̄,>Ps< Mw&Q4 ‘z'`';4⑱Hl>I+A]E-R ũ׸.ZmOT-jucs8T<`fzPE{ĹPVpQbmW[)B+9oPkZΨqQ/u/j PcUU 6ĝgu +/*hp7Y#zUS<)&> +Ym8SF!ޞTp :w^X =\@ F(Wې,'tB6V.*R.g1Q2"U$uCt[ӼdJe6^'M$)XN%#?+:xAfK B77im8+D>:*T0G0sՆz0Ѣ|ֆb)AӒ_!T͚?1[j _yh^)h2PS`xLE;n)ׇG=j :=h} [<3J.^p?k?q<&B(AQMIG =GF6>fi@@u pjw#(١0nKmAw`B6d6 VT`VA*YJļ@}Z܅:\H$.<lepUH1nۇ*p 4 #ת,ltkLBnjE&UQbITKF:~ܷuF_ G42&ߋ5u}#NoO h1ޏ+*L//Gv]뇐"75}YʠFZ=yzmP/tLMNuBSkquw6]o)Fl$}T%<dțcy>R٩UB /F$2QEk* o7 4rI{ovRpp؏V'*ɽk2v㠈AϕȖ{nsndS*^xh-_?W j#JڇaֿuDw;`NCOm/HݤkmDѤa-2="|)գ*/IE\qfvjҾP@#B"|ɫ%:ӣl;WUU';R5rKI-1/DƸe!}#=8WG0\SG|syCWCJY|SL5ox^HB3leT2>;a]Q08iE`5O/?I'ꡞI䍰x&+lsԬ! z:ྋo\lAq 1ՇeDsp>GNy!(fBu!D.&Րyr-wuPv, 3\DF Q\%uXы _WVbTS,{nx?y–my$w uM6Yor*q.8/I~H0< ԅ̦+4TF6)2 ;T|R_wI8ɒݱA|sV eFL𥬐DDY-U@xHLuq2r˧e9Uq 8 m#vwG>2q*BW.sS|"8xiC:lmAsYFm Y?t-?jT[t6Թ-j#`8+R2I ,/[VN'mŢڊt.k4kmרmH.?7{QZu> M<]sPOG ;o+IU?%cRwJ},%6N Л S)iv-XdGӋlɪzOYNƔE-C^37i;x>%;8cD٪Ӕ*לmêeHy v)nIX4]ARPu(ospXzf9ЩTmMF,7*J$x eÓ1X#[ "г wΧ΁H'8] o>{ 8^ #[ތvG&66qXCVG:L%sНaT P!\X.O¤ṋSZc43286{_:nQ\C ܽ{\ *Wylg~mpR֦ӡ7cÑ!䂄XjT,F AC>}jV(P~{ Rߗ=Ⱥ c0t.#+<OJm, ][s)}1i1nJ /յr~\ m@KEA#'rg>)ށo͹N)!{~hʦQTgBNAQe_Ot-Jf ΫOD<ŒSF=7ýnA@$:VwwhwAb2Eǝ|ur }ѧ4CsA(7{!/9JKF&НRj`YAfK&%V/|u dyb7O1._]c8_ˉi{?0%2c 4/<$: !c}b];fԜDSc1ְ0 gU1lqf؏6A\l(OG;a'8p@;,xlU{TTawWTD">(NBah}/A9W*VPn޺щ [ofpwF{ # *)bש((Dž|mCJqqmۇqT035,e&X 0*Vɢc$4߿o R;_txo py! M{DhyW3(g(X5y|ebm4c.Uc%uB@>0pET9rݢ=^F\=`MoExn:v NowEuWv{R_Hg ~ mqdp$͟@B8]֭=qQTᦫ:QU P8NI ]i^JQm!n-( [R,wU9o2SةSm<kJ`;y>uߡ2vg8$JE165mG5HVrw -RbU|R Z81~dkDEd2cZf|~Yj nw`骜~DoQkbu8K!?OucT.$S` Vd=aUP9TTBOSȨ U[/[Qڡoe-vnxWbqRLw 6n΄֮r߼U;sbx~6)Z?Pѩ/ey}wt]Ix~lx@sC&L ;H>ȗwu꧸'뢀{Z{uC.py߰ˎJ[ " B'BQEzH{uz}$NCgOzC \򨷌P%L?ydjߥMmdIToۍ"*ZBیm>ݰIyy(Y@ҷ>Pn. q< ",!ٙ1^m|6KJ $ջj n/YL9暇/fO /jwKxi#' _t룓ؐ2\U}״yrZpTZh&"D+#"dwwɣ Y#I0۟et8KfJ\7h}<$x @W4s(%btM[om襸3@,>PB ot$X*A霎&9~4l9e|RBз$, @߂{L6Y.έD&4Wn A`:g4cI+T/Myg^\'wTap%Kӂ;V0i!T/Hf_N&D-1joq఻}ՇH0"3@Q'JTB6vJd|Fm=C j~1T|Om'^Q{H^b fE'\X"z/)@bYDv^V.Fޑ&1l,xڥ[O9#@V={7i+.W^u欏It`bc֡J%-3ܼ)댺EeCĻJ*T+e^Xw?7p|7!bW$pK㨤2 )P!vNFUOJ/LTy嗨GPӆKBQ\,Hi <;Um 5ݲm2P{mBc&I 7Mܵ3P]7JYs#DH؄Zra6훡?."6lM2b*UO?PgeP-}k%rP\]ǎiǶW0[).Xl1.0qQ,`\!*mxMmz6cflX#_:P-Nli= U,6uP43x9˴c@mc KJvzPQ[K|Y00,H.t>`8OcwA}#B wRCwaAiU9s<36 `tWz;S>>>x"A`^'YfԸ%‡Y1p.#Dc'Ry&_.KLfTR U,JN>%kɳRU!7EB āפy/enu_%4q?_=U4?5b>2 ޘTZ @ Wn8R%uĘlK~+m&G_VyAhX1sc:n$n9@8{xq+Ul=b5zDLVQeeup^RČ}; ⿸::z[oo0.$-fH 5UZ'mnl} hžUmn}p+ΏP^?Hl];̗Z?Ho_ѫ?Uek"kx5`g0 ͶQ%H/6Y%c$%cb\N׌ݴ2G 8h:ʛ^"] }, "app.getDetails": { "contexts": [ "blessed_extension", "unblessed_extension", "content_script", "blessed_web_page", "web_page" ] }, "app.getIsInstalled": { "contexts": [ "blessed_extension", "unblessed_extension", "content_script", "blessed_web_page", "web_page" ] }, "app.installState": { "contexts": [ "blessed_extension", "unblessed_extension", "content_script", "blessed_web_page", "web_page" ] }, "app.runningState": { "contexts": [ "blessed_extension", "unblessed_extension", "content_script", "blessed_web_page", "web_page" ] }, "appviewTag": { "internal": true, "dependencies": ["permission:appview"], "contexts": ["blessed_extension"] }, "autofillPrivate": [{ "dependencies": ["permission:autofillPrivate"], "contexts": ["blessed_extension"] }, { "channel": "trunk", "contexts": ["webui"], "matches": [ "chrome://md-settings/*", "chrome://settings/*", "chrome://settings-frame/*" ] }], "automationInternal": { "internal": true, "dependencies": ["manifest:automation"], "contexts": ["blessed_extension"] }, "automation": { "dependencies": ["manifest:automation"], "contexts": ["blessed_extension"] }, "autotestPrivate": { "dependencies": ["permission:autotestPrivate"], "contexts": ["blessed_extension"] }, "bookmarkManagerPrivate": [{ "dependencies": ["permission:bookmarkManagerPrivate"], "contexts": ["blessed_extension"] }, { "channel": "stable", "contexts": ["webui"], "matches": [ "chrome://bookmarks/*" ] }], "bookmarks": [{ "dependencies": ["permission:bookmarks"], "contexts": ["blessed_extension"], "default_parent": true }, { "channel": "stable", "contexts": ["webui"], "matches": [ "chrome://bookmarks/*" ] }], "bookmarks.export": { "whitelist": [ "D5736E4B5CF695CB93A2FB57E4FDC6E5AFAB6FE2", // http://crbug.com/312900 "D57DE394F36DC1C3220E7604C575D29C51A6C495", // http://crbug.com/319444 "3F65507A3B39259B38C8173C6FFA3D12DF64CCE9" // http://crbug.com/371562 ] }, "bookmarks.import": { "whitelist": [ "D5736E4B5CF695CB93A2FB57E4FDC6E5AFAB6FE2", // http://crbug.com/312900 "D57DE394F36DC1C3220E7604C575D29C51A6C495", // http://crbug.com/319444 "3F65507A3B39259B38C8173C6FFA3D12DF64CCE9" // http://crbug.com/371562 ] }, "brailleDisplayPrivate": { "dependencies": ["permission:brailleDisplayPrivate"], "contexts": ["blessed_extension"] }, "browser": { "dependencies": ["permission:browser"], "contexts": ["blessed_extension"] }, "browserAction": { "dependencies": ["manifest:browser_action"], "contexts": ["blessed_extension"] }, // This API is whitelisted on stable and should not be enabled for a wider // audience without resolving security issues raised in API proposal and // review (https://codereview.chromium.org/25305002). "browserAction.openPopup": [{ "channel": "dev", "dependencies": ["manifest:browser_action"], "contexts": ["blessed_extension"] }, { "channel": "stable", "dependencies": ["manifest:browser_action"], "whitelist": [ "63ED55E43214C211F82122ED56407FF1A807F2A3", // Dev "FA01E0B81978950F2BC5A50512FD769725F57510", // Beta "B11A93E7E5B541F8010245EBDE2C74647D6C14B9", // Canary "F155646B5D1CA545F7E1E4E20D573DFDD44C2540", // Google Cast Beta "16CA7A47AAE4BE49B1E75A6B960C3875E945B264", // Google Cast Stable // The extensions below here only use openPopup on a user action, // so are safe, and can be removed when the whitelist on that // capability is lifted. See crbug.com/436489 for context. "A4577D8C2AF4CF26F40CBCA83FFA4251D6F6C8F8", // http://crbug.com/497301 "A8208CCC87F8261AFAEB6B85D5E8D47372DDEA6B", // http://crbug.com/497301 "EFCF5358672FEE04789FD2EC3638A67ADEDB6C8C" // http://crbug.com/514696 ], "contexts": ["blessed_extension"] }], "browsingData": { "dependencies": ["permission:browsingData"], "contexts": ["blessed_extension"] }, "cast.channel": { "dependencies": ["permission:cast"], "contexts": ["blessed_extension"] }, "cast.streaming.rtpStream": { "dependencies": ["permission:cast.streaming"], "contexts": ["blessed_extension"] }, "cast.streaming.receiverSession": { "dependencies": ["permission:cast.streaming"], "contexts": ["blessed_extension"] }, "cast.streaming.session": { "dependencies": ["permission:cast.streaming"], "contexts": ["blessed_extension"] }, "cast.streaming.udpTransport": { "dependencies": ["permission:cast.streaming"], "contexts": ["blessed_extension"] }, "certificateProvider": { "dependencies": ["permission:certificateProvider"], "contexts": ["blessed_extension"] }, "certificateProviderInternal": { "internal": true, "dependencies": ["permission:certificateProvider"], "contexts": ["blessed_extension"] }, "chromeosInfoPrivate": [{ "dependencies": ["permission:chromeosInfoPrivate"], "contexts": ["blessed_extension"] }, { "channel": "stable", "contexts": ["webui"], "matches": [ "chrome://version/*" ], "platforms": ["chromeos"] }], "chromeWebViewInternal": [{ "internal": true, "dependencies": ["permission:webview"], "contexts": ["blessed_extension"] }, { "internal": true, "channel": "dev", "contexts": ["webui"], "matches": [ "chrome://chrome-signin/*", "chrome://media-router/*", "chrome://oobe/*" ] }], "cloudPrintPrivate": { "dependencies": ["permission:cloudPrintPrivate"], "contexts": ["blessed_extension"] }, "commandLinePrivate": { "dependencies": ["permission:commandLinePrivate"], "contexts": ["blessed_extension"] }, "commands": { "dependencies": ["manifest:commands"], "contexts": ["blessed_extension"] }, "contentSettings": { "dependencies": ["permission:contentSettings"], "contexts": ["blessed_extension"] }, "contextMenus": { "dependencies": ["permission:contextMenus"], "contexts": ["blessed_extension"] }, "contextMenusInternal": { "internal": true, "channel": "stable", "contexts": ["blessed_extension"] }, "cookies": { "dependencies": ["permission:cookies"], "contexts": ["blessed_extension"] }, "cryptotokenPrivate": { "dependencies": ["permission:cryptotokenPrivate"], "contexts": ["blessed_extension"] }, "dashboardPrivate": [{ "channel": "stable", "contexts": ["blessed_web_page", "web_page"], "matches": ["https://chrome.google.com/*"] }, { "channel": "stable", "contexts": ["blessed_extension"], "whitelist": [ "B44D08FD98F1523ED5837D78D0A606EA9D6206E5" // Web Store ] }], "dataReductionProxy": { "dependencies": ["permission:dataReductionProxy"], "contexts": ["blessed_extension"] }, "debugger": { "dependencies": ["permission:debugger"], "contexts": ["blessed_extension"] }, "declarativeContent": { "dependencies": ["permission:declarativeContent"], "contexts": ["blessed_extension"] }, "desktopCapture": [{ "dependencies": ["permission:desktopCapture"], "contexts": ["blessed_extension"] }, { "dependencies": ["permission:desktopCapturePrivate"], "whitelist": [ "63ED55E43214C211F82122ED56407FF1A807F2A3", // Dev "FA01E0B81978950F2BC5A50512FD769725F57510", // Beta "B11A93E7E5B541F8010245EBDE2C74647D6C14B9", // Canary "F155646B5D1CA545F7E1E4E20D573DFDD44C2540", // Google Cast Beta "16CA7A47AAE4BE49B1E75A6B960C3875E945B264", // Google Cast Stable "C17CD9E6868D7B9C67926E0EC612EA25C768418F", // http://crbug.com/457908 "226CF815E39A363090A1E547D53063472B8279FA" // http://crbug.com/574889 ], "contexts": ["blessed_extension"] }], "developerPrivate": [{ "dependencies": ["permission:developerPrivate", "permission:management"], "contexts": ["blessed_extension"] }, { "channel": "stable", "contexts": ["webui"], "matches": [ "chrome://extensions/*", "chrome://extensions-frame/*", "chrome://chrome/extensions/*" ] }], // All devtools APIs are implemented by hand, so don't compile them. "devtools.inspectedWindow": { "nocompile": true, "dependencies": ["manifest:devtools_page"], "contexts": ["blessed_extension"] }, "devtools.network": { "nocompile": true, "dependencies": ["manifest:devtools_page"], "contexts": ["blessed_extension"] }, "devtools.panels": { "nocompile": true, "dependencies": ["manifest:devtools_page"], "contexts": ["blessed_extension"] }, "dial": { "dependencies": ["permission:dial"], "contexts": ["blessed_extension"] }, "downloads": { "dependencies": ["permission:downloads"], "contexts": ["blessed_extension"] }, "downloadsInternal": { "internal": true, "channel": "stable", "contexts": ["blessed_extension"] }, "easyUnlockPrivate": { "dependencies": ["permission:easyUnlockPrivate"], "contexts": ["blessed_extension"] }, "echoPrivate": { "dependencies": ["permission:echoPrivate"], "contexts": ["blessed_extension"] }, "enterprise.deviceAttributes": { "dependencies": ["permission:enterprise.deviceAttributes"], "contexts": ["blessed_extension"] }, "enterprise.platformKeys": { "dependencies": ["permission:enterprise.platformKeys"], "contexts": ["blessed_extension"] }, "enterprise.platformKeysInternal": { "dependencies": ["permission:enterprise.platformKeys"], "internal": true, "contexts": ["blessed_extension"] }, "enterprise.platformKeysPrivate": { "dependencies": ["permission:enterprise.platformKeysPrivate"], "contexts": ["blessed_extension"] }, "experienceSamplingPrivate": { "dependencies": ["permission:experienceSamplingPrivate"], "contexts": ["blessed_extension"] }, "experimental.devtools.audits": { "dependencies": ["permission:experimental", "manifest:devtools_page"], "contexts": ["blessed_extension"] }, "experimental.devtools.console": { "dependencies": ["permission:experimental", "manifest:devtools_page"], "contexts": ["blessed_extension"] }, "extension": { "channel": "stable", "extension_types": ["extension", "legacy_packaged_app"], "contexts": ["blessed_extension"] }, "extension.getURL": { "contexts": ["blessed_extension", "unblessed_extension", "content_script"] }, "extension.getViews": [ { "channel": "stable", "contexts": ["blessed_extension"], "extension_types": ["extension", "legacy_packaged_app"] }, { // TODO(yoz): Eliminate this usage. "channel": "stable", "contexts": ["blessed_extension"], "extension_types": ["platform_app"], "whitelist": [ "A948368FC53BE437A55FEB414106E207925482F5" // File manager ] } ], "extension.inIncognitoContext": { "contexts": ["blessed_extension", "unblessed_extension", "content_script"] }, "extension.lastError": { "contexts": ["blessed_extension", "unblessed_extension", "content_script"] }, "extension.onRequest": { "contexts": ["blessed_extension", "unblessed_extension", "content_script"] }, "extension.sendRequest": { "contexts": ["blessed_extension", "unblessed_extension", "content_script"] }, "extensionOptionsInternal": [{ "internal": true, "contexts": ["blessed_extension"], "dependencies": ["permission:embeddedExtensionOptions"] }, { "internal": true, "channel": "trunk", "contexts": ["webui"], "matches": ["chrome://extensions-frame/*", "chrome://extensions/*"] }], // This is not a real API, only here for documentation purposes. // See http://crbug.com/275944 for background. "extensionsManifestTypes": { "internal": true, "channel": "stable", "contexts": ["blessed_extension"] }, "feedbackPrivate": { "dependencies": ["permission:feedbackPrivate"], "contexts": ["blessed_extension"] }, "fileBrowserHandler": { "dependencies": ["permission:fileBrowserHandler"], "contexts": ["blessed_extension"] }, "fileBrowserHandlerInternal": { "internal": true, "dependencies": ["permission:fileBrowserHandler"], "contexts": ["blessed_extension"] }, "screenlockPrivate": { "dependencies": ["permission:screenlockPrivate"], "extension_types": ["platform_app"], "contexts": ["blessed_extension", "unblessed_extension"] }, "fileManagerPrivate": { "dependencies": ["permission:fileManagerPrivate"], "contexts": ["blessed_extension"] }, "fileManagerPrivateInternal": { "internal": true, "dependencies": ["permission:fileManagerPrivate"], "contexts": ["blessed_extension"] }, "fileSystem": { "dependencies": ["permission:fileSystem"], "contexts": ["blessed_extension"] }, "fileSystemProvider": { "dependencies": ["permission:fileSystemProvider"], "contexts": ["blessed_extension"] }, "fileSystemProviderInternal": { "internal": true, "dependencies": ["permission:fileSystemProvider"], "contexts": ["blessed_extension"] }, "firstRunPrivate": { "dependencies": ["permission:firstRunPrivate"], "contexts": ["blessed_extension"] }, "fontSettings": { "dependencies": ["permission:fontSettings"], "contexts": ["blessed_extension"] }, "gcdPrivate": { "dependencies": ["permission:gcdPrivate"], "contexts": ["blessed_extension"] }, "gcm": { "dependencies": ["permission:gcm"], "contexts": ["blessed_extension"] }, "history": { "dependencies": ["permission:history"], "contexts": ["blessed_extension"] }, "hotwordPrivate": { "dependencies": ["permission:hotwordPrivate"], "contexts": ["blessed_extension"] }, "i18n": { "channel": "stable", "extension_types": ["extension", "legacy_packaged_app", "platform_app"], "contexts": ["blessed_extension", "unblessed_extension", "content_script"] }, "identity": { "dependencies": ["permission:identity"], "contexts": ["blessed_extension"] }, "identity.getAccounts": { "channel": "dev", "dependencies": ["permission:identity"], "contexts": ["blessed_extension"] }, "identityPrivate": { "dependencies": ["permission:identityPrivate"], "contexts": ["blessed_extension"] }, "idltest": { "dependencies": ["permission:idltest"], "contexts": ["blessed_extension"] }, "inlineInstallPrivate": { "dependencies": ["permission:inlineInstallPrivate"], "contexts": ["blessed_extension"] }, "input.ime": { "dependencies": ["permission:input"], "contexts": ["blessed_extension"] }, "inputMethodPrivate": [{ "dependencies": ["permission:inputMethodPrivate"], "contexts": ["blessed_extension"] }, { "channel": "stable", "contexts": ["webui"], "matches": [ "chrome://md-settings/*", "chrome://settings/*" ] }], "instanceID": { "dependencies": ["permission:gcm"], "contexts": ["blessed_extension"] }, "languageSettingsPrivate": [{ "dependencies": ["permission:languageSettingsPrivate"], "contexts": ["blessed_extension"] }, { "channel": "stable", "contexts": ["webui"], "matches": [ "chrome://md-settings/*", "chrome://settings/*", "chrome://settings-frame/*" ] }], "launcherPage": { "dependencies": ["manifest:launcher_page"], "contexts": ["blessed_extension"] }, "launcherSearchProvider": { "dependencies": ["permission:launcherSearchProvider"], "contexts": ["blessed_extension"] }, "logPrivate": { "dependencies": ["permission:logPrivate"], "contexts": ["blessed_extension"] }, "webcamPrivate": { "dependencies": ["permission:webcamPrivate"], "contexts": ["blessed_extension"] }, // This is not a real API, only here for documentation purposes. // See http://crbug.com/275944 for background. "manifestTypes": { "internal": true, "channel": "stable", "contexts": ["blessed_extension"] }, "mediaGalleries": { "dependencies": ["permission:mediaGalleries"], "contexts": ["blessed_extension"] }, "mediaPlayerPrivate": { "dependencies": ["permission:mediaPlayerPrivate"], "contexts": ["blessed_extension"] }, "mdns": { "dependencies": ["permission:mdns"], "contexts": ["blessed_extension"] }, "mimeHandlerViewGuestInternal": { "internal": true, "contexts": "all", "channel": "stable", "matches": [""] }, "musicManagerPrivate": { "dependencies": ["permission:musicManagerPrivate"], "contexts": ["blessed_extension"] }, "notificationProvider": { "dependencies": ["permission:notificationProvider"], "contexts": ["blessed_extension"] }, "notifications": { "dependencies": ["permission:notifications"], "contexts": ["blessed_extension"] }, "omnibox": { "dependencies": ["manifest:omnibox"], "contexts": ["blessed_extension"] }, "pageAction": { "dependencies": ["manifest:page_action"], "contexts": ["blessed_extension"] }, "pageCapture": { "dependencies": ["permission:pageCapture"], "contexts": ["blessed_extension"] }, "passwordsPrivate": [{ "dependencies": ["permission:passwordsPrivate"], "contexts": ["blessed_extension"] }, { "channel": "trunk", "contexts": ["webui"], "matches": [ "chrome://md-settings/*", "chrome://settings/*", "chrome://settings-frame/*" ] }], "permissions": { "channel": "stable", "extension_types": ["extension", "legacy_packaged_app", "platform_app"], "contexts": ["blessed_extension"] }, "platformKeys": { "dependencies": ["permission:platformKeys"], "contexts": ["blessed_extension"] }, "platformKeysInternal": [{ "dependencies": ["permission:platformKeys"], "internal": true, "contexts": ["blessed_extension"] },{ "dependencies": ["permission:enterprise.platformKeys"], "internal": true, "contexts": ["blessed_extension"] }], "preferencesPrivate": { "dependencies": ["permission:preferencesPrivate"], "contexts": ["blessed_extension"] }, "privacy": { "dependencies": ["permission:privacy"], "contexts": ["blessed_extension"] }, "processes": { "dependencies": ["permission:processes"], "contexts": ["blessed_extension"] }, "proxy": { "dependencies": ["permission:proxy"], "contexts": ["blessed_extension"] }, "imageWriterPrivate": { "dependencies": ["permission:imageWriterPrivate"], "contexts": ["blessed_extension"] }, "quickUnlockPrivate": { "channel": "stable", "contexts": ["webui"], "matches": [ "chrome://md-settings/*", "chrome://settings/*", "chrome://settings-frame/*" ], "platforms": ["chromeos"] }, "resourcesPrivate": [{ "dependencies": ["permission:resourcesPrivate"], "contexts": ["blessed_extension"] }, { "channel": "stable", "contexts": ["webui"], "matches": [ "chrome://print/*" ] }], "rtcPrivate": { "dependencies": ["permission:rtcPrivate"], "contexts": ["blessed_extension"] }, "sessions": { "dependencies": ["permission:sessions"], "contexts": ["blessed_extension"] }, "settingsPrivate": [{ "dependencies": ["permission:settingsPrivate"], "contexts": ["blessed_extension"] }, { "channel": "trunk", "contexts": ["webui"], "matches": [ "chrome://md-settings/*", "chrome://settings/*", "chrome://settings-frame/*" ] }], "signedInDevices": { "dependencies": ["permission:signedInDevices"], "contexts": ["blessed_extension"] }, "streamsPrivate": { "dependencies": ["permission:streamsPrivate"], "contexts": ["blessed_extension"] }, "syncFileSystem": { "dependencies": ["permission:syncFileSystem"], "contexts": ["blessed_extension"] }, "systemIndicator": { "dependencies": ["manifest:system_indicator"], "contexts": ["blessed_extension"] }, "systemPrivate": { "dependencies": ["permission:systemPrivate"], "contexts": ["blessed_extension"] }, "tabCapture": { "dependencies": ["permission:tabCapture"], "contexts": ["blessed_extension"] }, "tabs": { "channel": "stable", "extension_types": ["extension", "legacy_packaged_app"], "contexts": ["blessed_extension", "extension_service_worker"] }, "terminalPrivate": { "dependencies": ["permission:terminalPrivate"], "contexts": ["blessed_extension"] }, "topSites": { "dependencies": ["permission:topSites"], "contexts": ["blessed_extension"] }, "tts": { "dependencies": ["permission:tts"], "contexts": ["blessed_extension"] }, "ttsEngine": { "dependencies": ["permission:ttsEngine"], "contexts": ["blessed_extension"] }, "usersPrivate": [{ "dependencies": ["permission:usersPrivate"], "contexts": ["blessed_extension"] }, { "channel": "trunk", "contexts": ["webui"], "matches": [ "chrome://md-settings/*", "chrome://settings/*", "chrome://settings-frame/*" ] }], "virtualKeyboardPrivate": { "dependencies": ["permission:virtualKeyboardPrivate"], "contexts": ["blessed_extension"] }, "wallpaper": { "dependencies": ["permission:wallpaper"], "contexts": ["blessed_extension"] }, "wallpaperPrivate": { "dependencies": ["permission:wallpaperPrivate"], "contexts": ["blessed_extension"] }, "webNavigation": { "dependencies": ["permission:webNavigation"], "contexts": ["blessed_extension"] }, "webrtcAudioPrivate": { "dependencies": ["permission:webrtcAudioPrivate"], "contexts": ["blessed_extension"] }, "webrtcDesktopCapturePrivate": { "dependencies": ["permission:webrtcDesktopCapturePrivate"], "contexts": ["blessed_extension"] }, "webrtcLoggingPrivate": { "dependencies": ["permission:webrtcLoggingPrivate"], "contexts": ["blessed_extension"] }, "webstore": { // Hosted apps can use the webstore API from within a blessed context. "channel": "stable", // Set extension_types to 'all' to prevent webstore from being filtered. // Technically, webstore is not in apps or extensions, but it is currently // displayed on /extensions/webstore and /apps/webstore. The "contexts" // restriction effectively restricts this to hosted apps and webpages. "extension_types": "all", "contexts": ["blessed_web_page", "web_page"], // Any webpage can use the webstore API. "matches": [""] }, "webstorePrivate": { "dependencies": ["permission:webstorePrivate"], // NOTE: even though this is only used by the webstore hosted app, which // normally would mean blessed_web_page, component hosted apps are actually // given the blessed_extension denomination. Confusing. "contexts": ["blessed_extension"] }, "webstoreWidgetPrivate": { "dependencies": ["permission:webstoreWidgetPrivate"], "contexts": ["blessed_extension"] }, "webviewTag": { "internal": true, "channel": "stable", "dependencies": ["permission:webview"], "contexts": ["blessed_extension"] }, "windows": { "dependencies": ["api:tabs"], "contexts": ["blessed_extension"] } } DVx(oԶ_~UP:80BE5(dcbOtl`!JWacLk({_F;:F~Fb߄,!-(diAKW{LLAx0+88B&d CC3-8@O+0 Yrcf.y.ђq)+18VZv1LO2]jQAP6eG,ul1]@kM09L{Ee'>kdCկhoN@ O﬒ Fe}B3[~"8$0=!)h6P4{#aLh[{oA <Se5/3E(c^ [N6{ըJ͜r#DAdkl'w"#tR%1M)u/5 ˒8N@YP v/w%`{"ilUK-zKr]׾AY7p5ģ9{m'Ni{~$RXq~$N0UHS`l9P)Du#ZiE}nyE_;3th h}hLPaLteJeJ3;G+M@ͽoD_V`IlS (0BPLI+-Uu2lř0Xw-Vʽ[?Ʋ[i%@B@?YO( DS&޴O|$5tj F~'~Yn^(* LjEN2ɻy 9KqŒ-VdدN3D,4QD놈1u7xe{F[ZS F8>B~Ǘ6",D6yɲ.=Am1!X#A@Dj2t/l!!(|Îkv1_[Lˬ7D.ar&ET{dZ~ I HBH 2VU[5V?mD>51Z+mMFfSaj}|N^ ,B6  gL%OK0kh-?M{DT,Q,,ZH YPDK,mx}-:!YylA3.I]22>"N'}aBF:nw)_ec/+)s9tڗFP)]iz㏳Sj;^UneA>{C_iI31Cј{ږw+zmG5usVC۬LB'jnE&tgJwRXk թ>:~'}?.v8t#°g<#K . }㚯uAvBjW?,`Hc3x,w5"" B?7I%atiN1(X]er/ 3D^0*E%)5\sv>$^zv<ۺq̐F,ֈ|yOQHa7'ۛz=5N}XH-fwfBNF'["Gr*ힲvn)-]\IsJьm O֪euJByd*qExdC&^,d[HS :X"مVzpn.RNImB'ˆLz D,{#]&̃AyZ˂u;Zܣ;^aʙ!Yp^_ p\HQ$=$*r5>7m)GqE,ޡM:al/(s AvBB9b]ZE`EL/&)ipJ^X^~+o@ke(?^PSv9 w%k@)^jA*1۟UKP[E|tK#E4"Fغ$fňN7u3 tfccyxAZh[+¢ɇ. " ZyL-\[E.Biyh,%fd&TC\A1@k%QY,REmLN(AW{ Ps^y ړUڨ QmZK(=TUx:;XHY1-,hWH5A8URǯ>W LBԢ "C]QvS Ws(Ze6ΰ_!v@X] ZO{CbrƻNQ>@H(e}'^YYEQ)@_˹mܠ5/tX Ŏ4cIt}F= ޣgV{!8ڣ~&}!@#>VzW1f`D~2 ^w|+'~uď4OR8'x$fɄhdg _ U(š,- x st\BKQxu}C`$"t%G I5@_Btw'28C`L m` 3[9}ði |-zN\. 8|%5n q,.ڝ Z,Da#]~=[*i|֌, %{fI,zؓ0$QCm129Z"fl%@ ,.c9QA0\#s <ꨝ޶. 7VZ18X,4֌rhAbmͦ!2 _f& Ȃʷ"?p+ w`i>cOlײj Ks;.G[,*V EqͤqZTV)=V(5_anOș=/P>ŷW-.fϭ6}Ɩ4تN!<M LK q秬pqh GX=~'b'SV;ϱУ<~hs79;@c'O#}qO6ϞZٮCJ/A05B-UsF?`Uޱ 8f L(d }39@I f1]W60K:4;Az1$Š'{k !C9id 2Ms^)V#M!6#B?VW8Lj QM9n8%Lӡ3  =7%JKV j?ISXo&q8.$ \l=IMK-3tX 4{7  "FD{µ`h"T3Z ̀6Le95 =ɇ6.\Ej͆x}n%v<Ҵ\7O/lQNJPlqZڃtx`XX8VUˡ3R9+܃>0 Xb>⦚2&YPۙE8Lb`ŰOH{ڳԄqN׃)s,P8PF$ؿN;hJxF"cl^s}jM^&{2օSN􆰣N萎:. d?؎$<6Hdo0't$۠;o!(LS/:|EI_rq;y RE[` `m`+Jp -d`LMs r|v*sl/pXN-+6=m1 -R8 av$M>%sx; J{*G{O} R'"e'=8|$Z}&FUݐc,q:5 Р?YAdy4*]3ikfF =Q$"M٤*_HC c9Ma6;S$mba!&ŦD}3v7+bMJ4c?ܵtU8*MQ[yZl$LCX6gzr8,@y#7<gGh6{ˎD,Q0uVгG4BKp#XݟƷ< b.N~40tej6GU? v6S%~AshGGf jw'żʛkqS{ޤf{ݫ?lr.*eu /}s1S?o6 ՚V:ևDkуd(^qՏdi?T`;FitlSϡf8%  , -:x?WKΝ¥N*^=T_o-.A3QpQL,Oc4Cw2/l#0G4Mgx+"r×'y; uv%qiP =|-WP9,tSmT`9m>1jj4 E, V @$2{cHsU_P|JA+&#򔬥DŽ([~KȾv)u߾B`f!`}fw4Ҳ3?XaxTaj0q9Aڰd?~ eK}MGݑ)!?oK!NeQ?b~]D4ޭėHCډ_DO^UadC;:p3aO8c^o5.<&9Ay9ם ۔Ȯ@;]m}7< vi}ƢyC h:cV L J#~!kբSwx5[c?nW @my䱎_w]u.eim5+~ RGkLA2nB6.{FSJ&Ζsid߶ӛkP";E =J3c4o'H4C!É@vMqqcc!>-cl3I̶b=.Oz,D`R!F|bY r,rJ̣U6/'y"Z`{ې`y0~PLQ+jtM.Q^yK,E-e.'܇¾y!%sђ1/BaM ̜Q{AO+H_Z>[o0yl6[aM0]f}nom{, ?oF,NwF~HnZUڙwjR};4ɊGYe߇/N-U׷TЧRv`O91?2e5{)vDWQYm< 0*)G-X/J⾳&>F S@GH)H Q}]XFͺ^ǁZdZL0EjvꌭRu }~5gጅ1a9jLbNirc m;:1uo#WNh"փ {_:1'2i ;<F4C&iPnq 7A6qs<'LWkJ -DATDBW2ZqU1 oXEč_rO%۷N"ϯ,ݷwgJVtF:C`Jmhk(PjPٝR"NMmK 3p;J/-P*]k[}\PR FIVҨ ;\oC3},ܟgoMݺrMWg gJip!%ZZO)ZRu*Mڮ^Trin&rjn%zZ"6<j0]JT+P=ˀV/b l:5Չn2Xdg[X^4jE@pdUq[|UPVM.7f}f;TE~z)wI}Jރ֯D =Bu^Kˌ*Ai+XO1nPS-_w\ADCw}]80(o+$+V]/Z,9[c}Ib0jLTٜP&lJ0Y4%>˗K=ly,2W[RT󮘼zUv{_5ȏdQZX~/ EPTXK2\DVDGhC *P.\?n;k惎=ʅ XvV i}hBB`b[.s(ϝtGIkh6v-n )<|v(]-1w;Q2p[yͫi^3Zīc0*ߞ Q\5ЫtN p#gT$e֩,8Rv9_*i7=)B"*ClA.ÃGyg-* cagZgRKW1Od&@8*FUnAï ^rwd ^R\5gFk#|Qw+!+HUJGp3M2\|]WV& P- H[x٫V~I\wś:|9L1K]6ɘri;Ψt/MF$H]V';xt`JfpttTtY\kx!{pԼ!gRXil8*5yyʱ,£V VGZ]4)jEc p#/I/3.oєⷴгertEza4˘_w>@DG:#Ol?=0lVX/lpYQ)e>UPso/^4oxe[w@ 7>{ª7y+ ZZ1Vŧjc}s;e F1sxq Z ?72,~ZVyT}=c8N:j2AP͸=`huIL m/D)OtMlOU } ̘D˷q%a!>]e.[Va>rR`J< 4( ԫk;jhhhhrNiMMzBs~R ?%Ls!acadI^cVDn+d72KoYvdfT u(Q./iЮJX~jO-yiuZ̈uq+ybnf:?a[.3A`#221<ϔ -%w /e+_$X[O$1ٷYqt*}ut %Pu'#əׇKJҐ+6ٌ-}ŀc3-Q{XIXK !{^?:hP3[?hKcl#hsC8Wk]Y5/Ju7`БK,рuEg\'@F+ځds[Fŏ\r5IRYWlq& ܾ>,:>rKBh[z *|gro©~e6 I]j75/iU0?!^~|~>s:V+3䀣E/_QF!œvR$m>yKmX\a3^źS+^ߡCঀ5l6ܾ(lCa¸+m\S* =uNe[#cel t0ݻjemT5;rǏg?*Eݥ嬺3Rֈ]>cgg}dف1E"v`RN*ct?U^2L*ծ}ϬG{ggYpvf?W(VxT\ALV7ʬXk}k9Ju0^}]̭|v>d HlL+r ^i!ք6 |+u * S ޱ~Zq{ڭ,=n+T4ρw# u*oyeNU;No BASYIh ,hGkÅ:F;blO?+V fddD7džoCb/&c {aTZ)R\o.ԪK=`I V{ǓNS;sdD +,>)Bߓ$rД֐„P=4 8 Nl33}{goh$y`R`V-]/I27P@unω {S\yA]LO,nowypC򬹖$IOwSZG{0>n6~{d w#e~_wejxg7_Wj +wgd| Ꞌ16Q4u[kA(Xh dMi+"W]<DYѻAIs8 ; -'hwׯC^|4 2SSmWDn_ -:QѻYQّhhD< H];?LBJ1Y@÷iCAH݃s NcfUoMg@Ⱥ~ ჱ ?{SdjE7d:%,Zg@;7#1Rƛ,6?\XzN$]<m w^q}w ^׉-zt`wmf1XJ{7Xq&a3?mdV/;ȓ](pʕN HϏr n)f9e8Qy~" 0"Z8/isSr:t%A#(*H@:vl.|~9Ū7Gh aQtM[@z~ (u70ymn LkqFH雁ё/ȳW0VV?EBb޺uJx,k.ݽyhfUN I} fۋ)Fw^%:2x,8tEq@ÕbMgW&.c=#E)x0DX hH&Ha^â am7Tr%h?_5P*E@1KS $ppBځUDK %i`S ϧ`gP Ku~,]00/0=~'en8"VJ|TT<ƼY0%v!3Kn2Clq_eo".z]h߰g.8Aum* n$nD=>DPͼ6<=`*޸~8羡 vCGr3ne {1RzĽpy1d1&f~Y |S`r{^~ԬJOaUIsvn{&14IpH ޾*]=˅IVDcncVZKz=aP#o^!ZL`V1jG)'HJ"jՉY HWtID@,:95/O (?e+&%KaAYaK b ;IwD(˜8k̩a#`?"1vsbX7۞6nu^6܅Q8ɍmn4rr#t)jtmRV ]Y52ra>(3Ū5GC'Mn5zZ?,|ťH\mA {wpiW>>Må Tƞp(dԵx,~YX'PLܹ=F^k&#L"B|Z{!Н[]VkW~u%ܘd[7Qƞɐ^lKQ:-ӋR:uaX@&crK3ٌ1!@ LѦ^qt0۠$p0 XƒZS~ Ft?_^L6eeyb 􃇲 (%,ZWB0Ug?lrkr $cd[+m4d~9`1_yLKBDXjoo cџ=p޿߲;P@yd%G<-bZXq>~ipq\ff*WQ &"LHK6g+WX$3ӠI^mzmYibl#8}3B\ڌļ*σ2d(甂h݌ZڦhøS[Dc =F YrPceǁ;9H2jMc&JfJڨ)Ձfgez9~*Qrgr̩l#*1aѣʛ?c;7kbe/?\J.keY!xX1m.wK,Ϲ^|֖&u7>@Eg(3T9X7UnUxӽȉZyi־K?Bz6+k͙f)Od7M.?* wf8ٷ^N I 땕1(a'<`"b㨻*ZЁ1Tԃ _-E-gu#ToZTYռ#od7 m<(qw^-qHϗdHBQySzĤ ST[=%xKBc1d=I!Y @7;(kTI BJI6tU9xO5I*Ȑ#}iW"2eG7KpC:۔%oӽ:s,E? ;a -hۦ?=Y.(5٢~IJ. p/AּAsCҜW"`xղ~n7q)7<Ԁ*޾j?]1 WU9!lԃȸ]eS!=4̋m[J;} el(>N Rj\&}!L+JҁuSyErD[w_ ^g9ûZ {fF$fRǰ&1/S~8%Ce78^L̲eIJz~Ў/,}߁l\j3OoJ)W~g.^-A:Wx} wNJow܄uիĈ%(-b߳cz~s,k?:Ρ~Y as%C!z5s 3KX.Fݢ lfQĠcv 84>G\^8B"'>g2̅7Q2gM}o1ң< NӉmi0V/a_Ed\eX;(I`.HLثXw/BLpH&E3mʫBX;XETlnAmM7_ɡP ~3R`;8-Fg%l/>`9c|[ J=U:SOZPu#|^#on#i|7Gn9P4ϐQrSI mHk-b/-&im% 6Uj Yے5?wf_uPXR~/Nv>\j|W ;( kl6/glKdܫ=DE9LFi:39' =GEҵI N}75,4(Fy"^Ti̹[Z%wRfy ]'mײ]oJF5ãxQoix)#"yhWi \66ω)$7!@L;id.USoO̠clm)"4Nn;<9q:L4,z8qw{UQYZsXUvJ2s/}]8Dz N^Q#oǥ63#5s\dMJCsXﱾ? {7dY4iM1控i<j̳:.6ԛct)wfʏj2RJ:KO}g&qďع6Gs&a;ka V62\ۯZyyדsͳi mF5r.526𺷹31KoRW Q4w0E%8PF$R$WJ" y."z.H9@7^l8HݘE~Y`s 2$S6W?A gF4g`f&z'AH@k~Tu?'Ym2#, ri`I+l×\_r!R F,Ý~jͨvjt*7#ƈ6k&b.GqUVԲIs5{% Ū#S.vS``R]+;.T^e;dEǢV%XCl TWA~}ū;V[1@1įx*4|4E!Bzu'.2>YËK jq6T܉d__ӽ6q]pN(\}-u n3&+ZO+~Wm**^ݥ,UMQʺ| mF=5镓W&+h{G&_(A޻ ŶLOC$܊4v-BV Uwȫ!wX4(uWEI:wB_-e}.7^o7Ʒj_+W> tEp!6aő]T˯Lg/ِX1oJ—"|Pf"eP|8^ƕ2'e;m-C2"X2BQBfXkLb:`{#edHDF⪈V~fARw|p5,!vX51"^U B- 1z|e?^21~3L$!Uנ|-󾍛>KUm \|B"G$BӰ]sp(A\^dZ->_|&mR1\K,[x7w^%!0v9 >ߐuTeh-o?n0kup=iTr_N7Y!K1Olo9 ;Z(w>e"% ǏLߙ@r@2H4".Q9MmUO骘*#wnbt:u&R_}1WEmaƺӮAW爵@s.E` DVok6TuΛܓ0a8  ʠJ]bF!76ؐY'H؋'ɬӆc2TĪ}6 V"J |zRif=7nZuG?aW A ?߷ ?vgW'.L<[3?lC:+ ]{|맹nεYR,q֔DMe aaЁ`ROnDl|zv@Ȋ`AA򃡾e8:OrS|,@%W1h!r:yU+څx}bm/XfvGV_ȝ7;td&C$;+I (6 mú0u(F>Lx3ĪAh,t ;uv 8Dg$W²G^A̞$#Neg=xOwԧ{u"Mt%Wrelî4+^仠mwr}GN ,+~F?Z/8'b 9'B>^<:Vqw3ue(Ct #ܝY19ckn8`[wZ$RuXm_tW9Rotno]+u;D~u6-fNb^Yo-t%p[n8S'R%Wz8L}vw,igWX*[ud#S '}W bJv5MV<"WeѤ E-;bQy 12%)փf٥Pf(xMP*aG9<'J@"=@$w,`7+<>oG_EW?S%ŒV޳=H,^R_/@ lIR &{RVJP?F,ےm /s!"'ߑ %Gng$T t^${XΨF_}ᇿ .G\<.3_Rc;2סlzAz?GΟQcdbߧӥN/GW~ =dф;4Hugg[󰄍(+c(qTEH@a>].>8Z8 DElo{'z# VQ6[jrdJhw|5xT/،024[[ M"̐Km|gD”7vA2@!Dt @ί%0q!;.aam0U[hCl 3TEd[f{}d <Ix$l_oDQcHmQChԭX5=#հ㵺-GB [ɻ^Z>JpO+25xke]лε3TBvwL[#:EFVB&t) Ǡ*G- z* 'BVΉ/)(Ǹ|󁭁)-$DW#;O&&6@Lߡΐ)NlS%O< uKtc8L oܲ%r2OX/Nk`L%*VH@e/X2Uyփ*LM]*yBV鰽i挀9OE ]qPN@ zM:dڥSiRmwneo QoZyD3*,;,wU(x,O^,1z Bc j-rbb@H-{1Y") £ȋI:҇4y_|+(ꝍ6@A #+klUekvZ{d962l%L~<pm[r qkqˇ'y0^MM7:@0@s#XWPUCqv1SztնFeC\[1ߗUk@ o'SSI@x<$=~}+7X2@21Mú7 J=nn{1:I`gDHKHȑ~1_|ZlE-Nd*K2δRTshK%DA U.$jDLj20솸~ay}4VI+%Wl}L IA*5t^a )(Snڔ-} Rr'to,5g̦A=]lM"h %5~]}VSS3Y|m6bb%hO$ټm1+4t]YA iBm6qMlcr+-Eo%<Zmwu@|FR"dVϮyLQ؏ZIpf(oaժV [4sZYJ/Y z~8t'xy>{1H6"؊Q)ա "mbnQB]M }QVT|9=4 '(%l4!AD\.EA L!ݼ o(Q/8M2sD cZx;Imf5I7,]7FsO]j:>)q VR|Gn  6}OsBw&AtXSLyZ]I”4'7liLsk*,l`;+EwuGDպ\‡Em[[^R-6,{XVDLTSM`޽0V,5,#g3 #9aiDCeU(k[CB8A襙D^gc.Q};zS&Js10Đ0lrUY3ZO-CiNS=֥ 4WN(Y:N}?dA|% LK4HplD1 9QlsS] $QC?t*P"7WDgD>u5:-LN[Q kLUUX(^|E}u!؆pj'T\1D2 ߪ|ͤ & CbVǪitW 1P5 .RR>"ДdAUN!vm*>[!S{>ė7>h9 wf#ye o/^JmZ}"H|.Ǯmtuvȗ#̦S&߁zֳ[ TwT0K0xɸ/ryꀺ8DPs%.TɡdQjRe%/Gt,wvdKuz,f> >WUZ+@jU)I[{yؐl|nH(LlM:[v'Xlܓ,ojny$"OWq6 s5$vGpgmSV"ƺaxvzbC'& ueeGa@gpY kbDFk[:0IC%lOJ7 h1*$/dUr{l).QO8ڥ)%Cz#tT4$n_lI^s'ߞNH[!!_Kƒ!z= @k|#!+v0(\Kl,i7>fi"ᑑ4a 7tb [JJ|l0ЈWG< .yqTXJ iTB # RMoF;;a[ =|+I܊#`rFi1Xhi%ky{\UVtM_?͇27;_w#&l>"-J\i oyjk\(os=3^%| f{SVRo>I[ȑ*3NAc&jR دW 䔩:0Lea9h8xUKZ2޻R:0/Q(tՓc2T< zV!\ v/}ԀxĝqN8|ԇ.CÕHvx儯vnHڑtmxtҐ*ҦM" ޫԤp]qs^b}Y[H7U0ޜk˰PY{N`9YbJF}vp1mcYunQwѽ@-\)Bs47~`w9Y,m*4fht]p*=?C4*<ٷml~s;iwgWkA# wR196:cBĘ͋Մ@XR=T]aS-3تԮQ; "UjipZmSweUt-)e)<ƅf]3UaP>=]L[ȈCdj~0y9.t3' /&qhWD52`z0JF&8g:Aw0 Q{l l$tĽb*^ZX5ͪI$NoX~J"TØD(= Ԏxf>^emG[0aqvҫa8*7@`EhQTz5`vWL{p SȦWoA+7W)Yσ*x Yl`ސfmZBRn i+#GQr%ț{m0YXk܀佐|XfaDZ4W=VXꁞ5jLę QZ1A ei:]k]N,=-_sVηbVFiϕM8N ^<4Z A<qn9TU3H#fC-v]Ka֠ßiTTv [G1{V9g}Ŕ ЖeGt|,+w_{lC"']4Xo͎;45WMokJe\ǐ YJA1p꺐uzЋIV}&z +ŀQ+E, /*7QZ %lv3o(L}P xר汧z$A'EtHDٷaJ]XiӪ`rr%1ZT ik`hC#ͱq@#C;/1XιH"Ҋlpm#v;JgS9W zWHH'c(Jw򗪰R.[WH p+FTZJQܥ T#;ꫯ4zgy66fDO>92W4D(C٫r6~M4<'6&EOXĞ!=/=ܔw%R+\yǵ04 iشT\e¾K |/o~VYba^T\UJ(bHq@ b:`|'ׅvuIbQ"#Sj'SЯ.y]O@8d &qҠb&dbZJBF5:ʬ * o>PDr`+RF $j2&"S!WSӞj- ,kJnh}~Jli Ii _ZY6g~Ǿ9OQg" ֘J-v}T2 29hDGI)(&ݰB,`]7MB漐jJ"{KŗEDeK™*8ihHB}6~_ߌ>F>g fΗݿ59;&=УN=mwK".Z:"K\)pG+su|eye@Yŝ1kwV\pVxL -<(4w;+ X%Ѭ帓W'A0uG2ƝPɂ, zr uE Aˋ ]HskP8wbe/Gn_4Hqьlo>eimevx%QہbLVS0O|tgK<ϧg[\pu- <;LY?lҷn=mjGvn ,ˤ-q$!vX扮<Ą2ٓYf! i-T ieܞć4!b]7i+ "b2�ő)RHNl,A+w`I_N8|\[YYqT>R w@C 3N F:e/?Cܶk8VsI? !%>4塀!}HOq y{<B$jW pBϚ!ֹBlgr~PuI<&T'ns0Cܳ+K87t |PZ=Z<]inI';; eN㻀YܾvS$Z[XCqߚp! 8vT۸c !ԯQx|oU(zk̓!XcZW$zd˸\dܾ(٨KT|"&*rVf4X~beX6"p> *v 4jI0`iK:c0⇟ue$ҔC@шbN7Z՝8Bps8 Ԓf ńL~GCy *+hX}Яg b[̨tY jîC"p*8&j*\|z# sJծ͢KdC%]X9!]4o>j0C,E@XEHL:Xb 뗃 O6v 1( /|$ɑzڦh.5O+1d:U9dzٵnQkwU4L&FXšJȂ49V-5 jőgh AL2c@6~,'{;q:P<\(@8TFY4&Q`RY pH =(K#mN𐈤x(s Riu]y WZ ;8ZKirs ӣ$ >AY 1Xhmf6qi0߷bQYStVy' LHmr {̛Wb)^c5{7f*AoV zcm7gpQ tR;*)4z 7 J$izX\d"uf^7NɫGxǜTא'Qu ltUe=;a!G*>zV0FC&GȜ) 9=R5kPq +jЋ+g9+Җ]+eo,ý{X DAnyg we4p`2-K X 6EHR-B$/3cc(ZD6.r_ IR0#d{+$: \' nmDg;L= B;GiT#N> |-~)}*6ѭ{?s8-,Li{^>+1aZLۍCiH#nHRغ4TwK Q*^T~$xxeQkSp\qVq(㑗 `':eY]Y:{GR]Spd)%=<(v$}"^K8\T# Qv6tz X5f2l .%  _k[fT^k긑kYah 9"R/qS)XT撢!n^6OԉiƽfD6sER C:O,%vz NE"57k\ڢa:kTB9_E$Ó# "\uh9yGr݉saGSTo!髌0`Fg'Yh3Ԁ8kCN 7O׺x*k(WIPMkYdN+)c3Y]7gm^irvP(ݜnx(^[`@_S %*֩ii6ubp*%P1 jOMuWZ hvMΒl߯#޽j嫹hхNȭMˑe= INX/X<#Ҁ$DD*^I 鎥}ibJ^>ژcufgC%υXK,vېKpSKR6=__:(F[efn57IN5횄0B)y_dOn +o;dj"*!~? jJ %E0iwC *sIo՝"LLk;^%Y޳1tZ>\EZxi,!:jvs^"^F,+OMi 6э`uNTBpxҴ0~zT5A|IصgS}d_|hi$W!ds/۲e LMYmQ?%5;̺\J9.p]u<Ŷ?B5hܥ,IX^#%CQwMgQgf\v}",bY`b4| $C!'׸{ʾIe+C.R6%vE+s#-D{ rB n3|X :5j;w%Y"e@a: ߺi\-gۆMOx\ |Lf7 j8 |@cIՅrX[ҘRΚ ӑa9?L,pRdSe5Qw6%lP}w0W+[u> sZoP,}S LSJ`1^mv[>+k>P Վ}VghվO}d='x}bqMLm*/Mntd=[>&~PyK]  3+cLZi_Fd s̜L$Uk#T&p臀m5㶎J}6Υj骬'|OTArX @ Hs]z±k}I5S@2ȯ19R'1;)ST@yADPNq$yd&nrY7:[G7`~ v=w%5vTt1 +QXs6,.)f Ѵ"o: fӒ|)ETMPgUq窹<&\ v:NbfġR}Ԟ_ET8uѰOOur@P죦@G6JQW\-U[l :y?JzXla3Asu5V y%+|+xȒNb#bVQWsm cm^-U.MS-&{O)Yxy6GQUVEP]!g]N">K7l;A z%-3$1̺`i6jsEYC[9mhyÆ?ˆDk&'-UYڅ{* /#$ꊸ>TYq 6r n( 'YLRk&FёɁ%GTySwu^ۘ3je͙S){E~j#&ޅ`Yڈ*2"}GJZi] z@Hݥv`kz5tpzncboӖ wEhy6q|8=:6Ȕ `]g]JU5Y0 _c_' _wLzec[S򳄪=5mvJRiσD]Ê=6RPt7[]ЀVV`ׄ[Ic>fKڍ,3gsۂ!W7,65dՎkcܴwBS)H|+k|^zKt9lO3A '&4vDICnqiq"gS'_tƼ]Ѥ6q08e5oIc"Kph)o+%㮳e'gN0?'.P_ȃQb;~q21 ñ}ƨ($Y{;Dlz/t}=V^47 F,77hTYµ2x  ZCr&m'cvOwa2=[miTŜz*tV>k~.!9Zi *aލu #Uτ4˽xO“K6Yd/T8}903+t8 >$u-vM,VGTb*]e}B?"R? BeR[Ǥ(1ӆ<)M>G5Z~YD?pSYoe_$_v:ܒTm-35p~0(H-WdwH&Ta[CK= DNm?阢+YdŹz%l${'[,a06.V.&(GMBxhE_HchRZ<`q3svl`z˻4g^o oaz*`-;ݣWVnMv8In7ψ[_H2T!,6rkژ/򣔠Y]]5H `+y.G^;@:ɽL"E +Hq&2jys/4&WqTӅCq.>}["«r%B?h1Szvsgb0WMdWжvi47崀17Դq&-G:tPKsv1܇wג<>կY6ikіRQl_suXu8<,1DQlGfhc-⥇ Bjc/Et[qyhuR^-H䕏v'Ƭ8:SVGNØ~+֬J5>T,E'5 - ab3+t>[;C&ŲCpߣD('GA53$@3\$y6Cmp5nXBcљ09l:羥A>V[{blU6) ;$||XEt>o Q|ZQzu嚳R7%ILSWTPNްq0st}iHg!6@Ln>ѵ,)2suԵ+/q& !RO,n Qb!+ŢzbuJ oDrT[35{L*747:G-7a.qD)ނ?bVٗStM|di_0d5;»YN* S<*H ق1 ͓cd-(Qdq7o9w|73P{K4C&DqgDV X煑xjlyA\2$ZBoZ Cf }'9UXk㚈p6ngq%#cٲ1.p'=4NW`/{71mա[쏲@XA:jSDp]&xcG'y4i^6p}uzѦ,6CA[H/&}ysNE݊ʵe񸢨O9 9{3*'L=0=?y2ãSn2L,'Ŏ4q—2˭F{)GW+ob[!77No\A 5(,R2ؠsWK R1}ǿQZ;L< Hщ5K@/{fQ9jjQ ʉFUa(ØfBڽ̓9id/)o#Aꯢu3N?ULv#P$ U"PDErO|O*L \ya(X^@>6HƬGd6aQq:oƖļOk V+;'B`L%SX8[䗗r!/A[ߴs>Mcۢ{u ;,g9fh:=[\Iؿ']!O5b"N_|p<<ɕ. l=0<!%yTR$Ʃ `p.fa@_P|  ,LTV; ;g?e3K8P{ .#$rQ|5~)"IrfДcZJ@ yѣ,z N`(+7Ψ6mJ &} g1kRMZaםF΢Q´+ÉjA}nIƩQrrAs3JsAF_w/ѩY=^|/qjM^WG pFE-St" =Ow4U~j'lzcFy^|i]ڏqXj4H)-Ė^+ a\!DG̬ 40 ֎ܻ(?U)wg%9SO_ (w"JWZ:},KK0HyQ)U+$h;t13֞]^/h)-qGBa+?0Qʰ4őt>b<]?T1]*= Gӗ9qF$zo] 싒'$ kpfS|HJ]#U165Y 1z5wnFҙ9OQIv@[Z1.ҤSF{$?Ygg =QZb=60Mn"-J6j:F=.6(J]HmsbxHθqZAXf !T}ck?_?X0.tʜIІ|7y8>j_VC%~y2O*G4J㬌"$5?V̳hlU[?jg8y}R{Iqy7R{J+@: ?/3o@:Wn(q}`o't\\Cp>ij74S0+,SbR'ϯM΋!Os߯'bY.vdGפʞ_*@o "9fVQ_[7g~ȗ/ 56k#?^𻟿1M8KZ֘9ò=:s0xĨXnC%iX 3G<0f:h\TtUSv,P&h*.L_C5IlBa'5 fZ6R\Cta#R}= K2c6rL+|]qҌmLlE0+fԵ6B[G4{gT}Jk|Jo311G5NH0ʱfet((7d !6DW JGK 2<(cph9MDv;'$!]͔ս:Ɇ+ eevC%?Y߶/οƎb_[cZ3#W,iрe? 륤Kxnu7m-ZCJL_Yg<;:To;W6eSMxcڻ|@f(bx/SD{+m1{}Œ^| 9pS%/{2,T yFXr+5F29S];LNEf P*xqƵE l_qp_0@cAI7LK}IHV)pt vFl1QFzN%!tP(CM,?}OKxO 6tLu$ny ʦZoJ4ԫm*$K1Qcu-)һkWB;㙄({>V1U$Bf?#aP"騡8D}־#,3p3h5{qBtbyNt}+9 qid9Zu^xE?mAG~_APa0t#@4J 1R6:zt;+*("Ë"Xg(o4>qJ}Ʈ:lÁE\Q!X,b' C.ڻE1wp H/kcr%>9 /.-[iZG L䫍C-m  ԟIoFvѪ1ev:?p1m54,QuiYqJc0x$@V}f3M~/gjUIXQ2˂IYfӳ?AwYςȿXL[pn ޼WQS*1 rj!;k̮`"*ӹ3p1ӏֱ8=g%?yʓ3 ȬO`!.ͥ9b;uD^iwjjm9OTqIlw؜Y3)ֆ`g0V}WzQ%h?oPcx$LH# K>57ݙޭL1s&>F(YKGڳFrwnp2ΌC'\d9îqDX\ro^ZP/ ZgNs)m-aJ&{`C[+T&vTqE_ ?P-=oMy(G= I+ҔZRYWMAvk|R6/uRwNX==Ɯd9zz%FO,@a5^ Q[ީt+ItcOIQ-O'l(ӀpEHBQxv}:yȹo`S&l3M:.yz`(SW.]6YHx 9NtCc>C/-/ sչRuum'y%#84j" UHnSU7&nK_"M5C'>ga6XtLpr.)BK~V^3D2`z,L핷+\! ^À2 ;9yKZ}_hW9&<ǁ\:dZ/s\zeP̳WJX׳[6s:Մ.-mɦ1a)6>|yo8R.2,[-IwE &@J8dMPqouƜ<7{b&R-aCgz`5^Ű\EXԨBnbUdB^UP۠nVuXb5s/3I ޤ샕֢3pRJKvARGw%ukS0peڮs/ 16]@0`1i<yMH 39yK/ N[|2[xIHʩbZD {'z2Up)~Z3|-ͯG ?)ԩӧ(кX_:B; Ue(O5A!CP,y@mDY2;k%pe @ eF6,$E{=YW0dOs*7뾩)< j\ᳰgH&1smPYeSjolN e -*(2gz6m{=3y __AaTJdoVߠI  bOḳ92fhq5Lܭ}5c*>|Gi.u7-\S3Fz+\?/oc?nx>s:MTѥD⵳p@Y, ʬ]~U8 itG(h3Lȏ~f*ʳZoP ץH᱔҇'5SGazi /툺]h{NE<ȝ\/pQA=`ip&eK[ID Q=.DIg5,>ofGNf%@ӻT5Mq  \{TI毑M-$%tK BvI`!} JM-7*1BLLM$|(m?=$4[EisswLCN<1MdE pmh|\DCF=W+YfVm-T c|O=9&4-htzE7B.H iL>ȋBlJjQ^dNi>+6 M6Mi4RU5c)T 9~tKz&RU=iZbIBՃ?Ըp>G mүs;&MӸooy)LXsW'Ǧٔmrj "3 pyy6L$?7HC<d뷝l?,DV{A[LTezo.Ѭ} ~rِpJsZ>)ڹuSkC U@'5N)$?PN: n-坋 hЯ0BPu3Ru!/sS:Pr+6WW:Sh%K2g dV$Ap fXA!gU|\G*V# poϫ= HFK|fq 3e -Y; bC/q6'w gjNl S^WӸmoP/Ƕ$*}^2NoQa/z^P_j18 oh(M_-,#tJJN؜]i}yQp݅#2ǧW^eJa/006mx *F<,h1e4~TX:z) <"8%үN3FZ._+?lcC p3 ?Dq9 ^sGq;0_W4Zٗ Nlw_.AϜgF>N%9ҠPք? Pb/lL`&Kb"# KvO݋C%}/'B{UB,GmT[^ٍ0Naiȫ[C@ak^cΏLM3J gggY|W~gI )& ЁVOUCҠAvNJ-@o!yXڴ2OTuȦgqe&E tۗLc"}<կ<,:s ?@ 8;oA>}HeqCJ8qlL59Qm^ tO+UXb( tO #.3XS]=c2uE0TO/4W L!EhIv-3\<g ?e& Nsm%\匥 p)ѽWྫྷ&j}NIq2`Zi7MKJd4C2lL*5mp^ӓ3XSvk=$,K<7 G4ׅjemiQMCC$T*"vIef PL`B lHlW[v%nGmF}`[mn$gԉ5MѸ]PC^`P2O6qD{vwCkPl$a?KNR'pK]dt9Z$_Iufz nn4Wvwi]mi ,i'ήg/%s?sZn6mZlYJ?!B ނRB | KmB2sq<XlKuݚ8 n (J ґU Ҋ5$&6 x[>cm= a>[\cN|VH&q`ё185`|F 0 7F/imZlx ,!n쪕Ud{3Zܦqyip?FIXB 6l@`^Ap(BG7&_f䱑 -5=vK/UEkftzYU ޵tzlCf`VSִ[Wً,(:MZ)`'|#hSdr (m(P3PRu6خjKg)4lq?穜/M~s&iACW֗|sw#NښS`hPn *՘Y S/MS1)'-ʹժvK?U̫7Ŵ ׉29IYUKR Z C=UfW)Zjo\w%/s﷚5":xDlAwz G X紳˄3*%d 2z%U V ).z㤾nʧ>3!7‚HP- Kb3<>>`WQJSr h}CT00ƍOWp!А[8C'ίt:;#f<.gW<,*Gajv$Xm:3EVc9]x8e6Sl+b}3,6A{UH!=l6DxcHWegPۆDhΒfAӓ8A&&}9EnT7tH).2v`isMJԬ"gJ c2LjجaN$`WHY%0)}7K+lk\JmH3G!`~4.cOw7|$QSeB5,Ul0m>O]`_s@׭#:DÙf*;ka_;0Ȥb: V,/jg>uqUyλ4E-=!vGt(42H=B[2l>y' [Ja4h?;>K3n @59 K<CjP$&X`2gZ≀kbV?6mM"}Yֶ?K21|h&QHd@F$U֌4^qᆾwwul;gF+Zþ3v͢`<)Hq͜HX.GN"zk™+"9 Xc]%U,I!;OkhdjZy@@r42)a[3HgUhe,` 4#0}n Y2 b65*Q|(@{0ryYw|,Ћ,B/m-7)GTHӠL=%v5bSKUFE \5*nLr~+Amim|5ZtSWq У[ùy&L.:yj={݇x&pJ]l݉dɋ}1PqL:?XU= (drr7 7gi) y-8>`+z$p#R \=8F2bu`>]>-jEaS|/qͥ  W&)1(D[Y(2 KŒ?RZiÔ?qwqxkmRqW[Z[5K7+c_qR; ԠMC6@M~Ɵ[%'LVoE:/^A,(Q:V\a1&%@l>Хع$jBO((PS`- gYe!V1ʘ\?49Vql) ھ<9v4Mii>~Cq'_^p遽4 J8YK@n,"i|@ _^݊, >] nVZgRAUW+lοxyWn1zWX~} ?oxG嗿A5Κ 0Ƀ2" +DV1s?o?m،xa n]>C)y1~/ ?N^{ANJ"2> ;ā7&V1QREYs("6Vfx;Qec "İc5" u&Xú)\/R"`z4v`q+O%>Q@'ftmD* /&a@@rXD sxJ.dGpuL%\&qC~"Z˛?E 8.H`La>'O񔥫pX" &'>_UTVA{neiyO_Y7VdSKWp_5fT/'Tvw+"lK@}3|Gq;Q_̝jق`fk$X3Te<`:I īnBZC/[-`u!n@|v(6^S =usd=sF\[ޗ4n0H6i}{;W{oJL")"+*5U ѼJ(v?Z:k=c-ۙ\ ߱+Y3sq~CӤb ו:B5ܲۉ0&PTjxH+Y>u:.2*a2XOM{Twƙ>-_%;(8qʃm:%N65}Z0E݁|{t\b6S;6z!m+qS3;#ԂD\`I.:;ۄnz93yT1-&:#&W޾n6#Y~O&e]dw\=m onV鎤gL&ʹu6{n${:LjὍoJ䧠JK+[v(fd;A/B;SNs;}ՍC5EeLbqpq'K扫硼|쩁AA˺E \7Fٍh ,(C~w:5C\{_M}}CF(^|U۽L_q%n2 g:2 f7mnf_y%:س53ܔM\J2 b/Xsp9ֆs[UL*6w)&rH׮[Ced4h5Z;WJ[RvN\>6C#QY5x< xu=9\W C@ia@ caA+*&c+@x- ; eDDJ. H5!K"",aˇ>..c)L;$Ǚ&K7)r"LP6|4i=KD}MT:eBL;SQsiDՈzx'` 7c=E $ z♾ԛNy$ymY{Ubx&-R&c-Q3i 镬&{IlfQ ߈eh_D/ΚYK݆vsGzVͿR@sajCSᔃ!dPDSra-rD_9RF=lnZ_HSR:ԕduɍpR죞or$/o64:Ⴄq^^iJ}"oxX36Y${{Ap{2,3*Zo +YsAa{NY `<{rNܫ|:}AzΓd) v-Җ퐜USMm,1{+Or,=ن)vMh<0!m{XZF.w=oq<_]޽b)׾L<]‚?akG( oXj.ǩaϞ2q҆EhVyS[P A'B 4k@ܭY_ |WV2RbEwU?4)7ЬlZ,P2˕5k$ˣLPH\gYz s[T"t S`. ݖ]T@BcUNErN,;jLynϊ-MlN&g>Jԣ~gTf㸈579x2P*)Ii> D1>"eyï~ۯFr„o5#wM-KA136Aogͭ=J(Gm{NuiZL ÔY' jWfq/,Z5cRmx^?"DZR.(0o$[[4:M 3v(OBbZ($7mtҵa~M'ʿ#ۘsf(-;u?qv|^4*xVbLOӆӵLQ83=$E^WN% d]FdȇҔFtR2sn)<TKlhHJ[_Bq!„KŮkps@Ko0*껩='5?rѶnI<=tC9oW{}(dPJg) Sۑ rE{ȅ5LGcvaW_"bC_on#lIvjc_L;ZdJ+`r>̓;g*kƛ-+/% bn(m2< qkEA/Zvq'x!I1ӣC1 ,J J8CpyI=1 <G"nq#˪vkEyb$ȹ´`5+YI]!k;7@ d{Ca ǿl?/4*M8UeY"Azğ[ Li^0oR.Fڜ[=De \-ೌ'̷v>%ǖ%U||%8YU0SYmzZPuߺx~N6c2OKu b/ER-p5s#Ae@AhĖ~RuŮ*~HJ=M<1^-p| '$15 rE*vk$:ۚhϡ9e'75ȯĿINġ&'zhUiOu3Yh$[!QZ=5 @nC|ezy N9Ï̝F$q2X" &ǰ9NyB ɑ}6RĈdeܴeag~Tn#rCk7 z@Ș_L*~@aXirL "ZSiڨ۪< [`dlt>ҧehp`J[2U$ԧ+E%EYvNJXn1Fj4v_jR2 "S5<,1jI>X3|X !çNTs du)\UO<}WۘqR8JԌ_1@+t-yAZ?908(]4G;5ܐT`?&`?-R ~s-lJ{O7kif`u8n|^8yqBe+SW!{FJGM)]0k$N{`0#k~ 7O"*64?_H /=W\\4 fFYcR}X/!YsO1)H֗p5d}MG5ʯ!מ 57鸚>1~kٵy{xCgXPW~lg݊e7Opz/y xTó= DJ]gdpNa~r`6Hp\;%92h }[7;"&nKLa"?.T5?-^]; _kcI ,K\\侍M1WUe3 ɨ=+=1'G-{vB*J^riZX?+ 'yJF^z[Lg{^372ΆMyQ Xq(jHO Vڶ _1.ޒ&B֝ן]\oBSۄҞpZ b@UX`]!yZݲ$QsN@21ش&0 ds֚$JY45mI"/Ny^ `@ƙV%&>m+YSq;K9DO3 ?VvuY3WGoUc!G6/s/ Q~~·QA@vJ mPv;[WV,Hu,(R ZH<# 3tǝuvjlYKLkԴ7!EryZ(?Է)Q`T9v+PVBʔحChb1`r6ڮpól@=`͹"_32Ѩ;k%arr $rhR!NG‹ǽ5&We>D0 ׌,?c$?(6 6j~%~=B9<>>>7eӠi[7xE jO\THP2&C.QpkO.Ah%~M ϐ 񎄠zcyĻx`NC䒯8Q{66ـ?5X2<K+KY]!vY7Vt)o6>Ѻ3Z]*t6!@+ #GПkQ<<`U~\:ȏl 3 Xn&I c"WUr=zPpw O]I>ʗ]>'ⴟ7"(0ު+~w#*ʶ!.}KApyyȉ/G! v7&DK+竈2#* 1ޫBZ\eVAͲB=\v,-c5C+;XŻџL$ҮSmbƿePBGIzJBKpT)Ga@8=+'h{DnuԺZ^)@++=zLK^.;*1gyPXs v7Egkdsf~=NCIZ!'u`FWszd"{Qoq#1 h.4 %; p!ShZ4 kx3]ri$Y8{uEq&ԟE8< >ג~E0ghaP(`;^&n8np!E?sbx*F\u JF2]]S+CE]Ѡt.XwQ/Gyy) Ǻqt$50uB/=  x鳢FaFю L6"g܁E]HmL*F)%}~uWK_AN" ~CnyZ &dG&%h[i1S{h# w|: EcF+c.U^4yTbM'0["RPtjHت.?&e]F8^I_mVm8׭p+)xo{r0q{}1ע7-" |l#q Օ!+y횋X)ySM]=Q28k-Nc0J?ckZ`O&~)Xj kg&!z8+QRy !^TySUUȤ7ykR4rMװCRnj9s+8N)]]lj` MBrT/ J2Ѩ=aJBZ9"ZA̎LؒCk qJ;- s0j:*΋pj69mTTo}$(We` ]$s驻@0 $4`joXx  )Y_~jJkIK=_y 0 i_kP5ӘVX5S`h`*F |dĻoSf-b=g73T/oIZ=7p F ߤZ7I 2  R_ex֓X)IrVnjwIeINbչs0#S*?L8d7kyU.++WXͤh*)~rK/@ i#\ KiDYh9`\D^)Yu#\S+Eݦ0&(e8+A@G@NU05^C_uĤ)|4*h朱* a4rd( / V8[eP0ⷃ5nSAY̸Nk|$⃚f15<>moR`['p!i(B /w8/UBNp1ZAw/~#;|<_5na643&CHP4}vk30LwGB66Fgŧ7bۊaC A}l=L%{;SzzTSjiW<l[v,C_îqkcpTj9qN}@o( -1wǏ_ fS&lM: c\(&>N-D !~v,j"J]&}|`m!6QzP6PQz[Uڐa X.z=,} $EX74WS*|&^vŤqw!~CNzuD?JKyÑ8tĿI OF^vWO dD`FKV]Oa7x[^]w17777ODuB2SgFۘ8;HjIf$Q=oC,,߃+P: ;?ߜK]gdk7avZYy׬-CO̟5JLe*#t7r^ 0q ʔނa?&}S0ҙaB̃K|p CJQF<~0%ӎqu1Pn HBPlI24R)FN/3<+wN7~tk[p'L5C3c4H/ R, 19 bհj:Wɞ|bB5vT ]UMN <HF"5.S1.kMDGr6ldqfp[Ƅˬ se:񦐔xx^fGoWآLi@{4?{jiokEu^ŏ;9eR24D\XS6w0X m]DOK }եVG;{=76+L8'z:ӢW7 ip8?LΣO:vut~[z+R+"v WxUy)Q饆z< DIEe Cryd+ⲏT^c1(0dT]#+#WQLA=&ҶXfF[dergVEf^fjgXU6ߍ ]u"ۖCYL4i7PmBJ3{$1T #>{+,9zmU.n)o~n^/Xڲ"smM"&䮄\Z2ץٕdQg'9L]KG ,1 TerKO4AiNi/'o! Vg7hhD83 ),LSޤdSHJ$4R1(O526ILR<`x%/y7:Ǚ<Ȏt~d">[޻ X >"L.N!GNUVn,vpX"եZӢ\"y`٬RSp53I`XA ǜMgPS|h<AE,L8\8pd7p3+/ <ն?]hknG0 aƞHаfQ'…ԗ,2n|#72 H@. i=Wz.=p(ڞT⭦ kӳ≝_@5 =Q,=4 $CJ,R(\ 7G⓻482WT`R5kzwm~a*__A_haCm_`/ Jk@uX`~\ƋfmzooE>!_OBHAIo< Ae PzB֖I>A@Սt>$)Yx"`}?O;B_ 1Z9@g0i @":rf4NCg,XCk7HZ=6J1 . ~iqH󇛾1nkNf%D|i;i[č+v{밇uGe6H˜Ђ?Wi7_J4Cic I8ŐTi>/([/?=3#J%QI>j*u#gXg1w\ٙ>ݙŖz)|6*.,6Ss`ZfWMArߦi M0Gbf IKY8Kx^㴗g9;2;ʹlb&?Y7nq0G]!?Ef(W~ueT ZN 9| RJ YrH/?J1¸ oGy,s^EA G`T\89\{=Y`` _٠s1$hhm2G_fcd;$)$wזM 3vEJ3p%T5?Vw%gn/e*ʣi2A@iEʖdtt:רּuJ#eEg;R(&I{y^MG7o>~?M쿥S\J%Ͼ$m' x;UʡXbº粆)Cȼ60nOP eܑ@/jwF\kkq:Z2BӕvqgXp"4\$o:<7*:j9:$m j).xl'fCB7,,\s_o 򴵡Rn{ik2/VZ?-DtyhқSƞX?/ 9(rP!Mbz~ID}ɲ)EOYgE)ٺ-e~ DGqtM~y~lI:4@mj)'EBײlG9VkvId3i>ӷA:A %LTG6^zXKh#y 5heW^~V: %Ԩr2FOL %PaLuiI_`l%x^ij$D; x%OTR)ЦfpkdYiŊ;A蕐Ww}LEXIMfg4H+ _5\Ax哥Ol'^N ~fxS! h=r;-w;5ࠨ[nk__rgΌ[~N:T9wU YBsW vraB"XP9۱s~btַ $*2B# ֪@_f H?.a^"FPn=AQ?8{W#A}祴>/Uk7=L$v9u.ٔ:x6/f(EtJcc9E(Ξ(Mw.%AsA(@'Fo{訕NQlvvY2 (T7"FH%r7)$;5ϱj偞d-/xn=钤L?FImόs&uN@GĝސB=:(-åJ-# WU#@=MzkR\ H(q}%UTP@H,>5}(Q0b¸ėi SDU5,;'P0.Ms "kfFEΟ<3U^H7" 2.bB'>DeםL Vi05#qcM.\7]7]̳nuBH0tDPc} 1 'd ;N^=E9W f=͠f; OrbH^im6i{>MIg:PKꀁu\{Jo0y܇^%NJt0.CgNj]`d0G[y:hQi9b+/9뺷_UdeW_=m {>C{u.Ae11s-2_v |v-jwÆyϋy>K^EZSd*a 6H{Ek[͐ M!=":I3DlѯXz9GeܭG4p (e C__CvlY!e>Q* L/c hnZ>"_wC+a"{o wrij~b],x3 ŅU<׬$Y }%t.]}x=6\bRdS\tZOh ^xڧ> KUf2hlъlNqS$87_Nr2e<MZ߈e8EN>cG\3bKlW}k)+z=Le;8/&bzQ{_z+F4`q7Yu6XUy =)EC]n{g`W ,OIQx֥0vKѥ*pe׎+w%FxƔa<R:pxݾ'gI{iwW*v?,Q.1@D]py6d[H7, 6!Zppǰ]̑sTK, "u q_NEA*؝pm4\…ǡUEr?)dŖb["zۿtEө!JeCؖ$;=(H&N:'mp007Yٰ![/;9 E$|Epw|^/NknP8pS4 }0=Cg@UDE(H ~S3**8ҠMٟZ8{0&@Aqq=Fiﹽ<=_N)f)=_bO^Ԓ-^ǮJ[qo+-,# Ij@ts'59X':Թ` (>9şi5PbV1Қ!&b:2#M8^8ɸ‡vtYaN͇[/f)-#B .VLрs 4njIKu C2!׍YR8ޤng7FtSjB1\v@@"${g:x.y ;3;frL,mQa$~?1\8&(J1Qvz-Bę0t$Јý\EJ1R75 ^=b{ >hm7d Ӆݽ$2yEMu9G3y V[\Y`kF a=K đq ˻4=sk-eYiszKiX,whde?id fԀZMWɒ@A7vs&vػX۳#wP(^;'fF*/|qjɰ]o`>N4A`YUI&@ur2J20OYWczMiąn Ozde-I;4)A+yk[a5RLC 1APCURO\p\\ﰦU<-kk׹H,b|x*6jEk?oe9qvrjG9pdƕg9o˚d7ͱ,gp>7D7Ia$ʋWX5&Ayk2SdٌG,D N- @VĴ -2 ܬ) 8:ɷO2I~a!,֭*n,OHz|"و p;OT#PIoQY7uE+kRjܥOko*=:z'HV@b2}o51>[ c~{K7S'o3)2JvLTbp`gPզ*%#x zv)g2IK%)' Aj U"2{e?ȢvˡLڸ"²RaoA4$wŽ?fIrGfƏ|%DNJ*11$P6;֬?%~BG̬$$g Q]Kg˃u2Xmbp8E24? 1R 0̊ftwld";gdyG=Bt>a#!XBvw?yrV Z.ߏ enF;D0ÍQ:5be#a4c`D_BtMOMir#M:X,йS`& 2r eJEYNTvXK{.őpHWNxᯁO)FȆ¬lk&AoWT({$*)z,zQV$%U_;}+Tq2g tآ*A*LSHp`Be%56> J`e[ XW'1"Flջ`ȬH<8O UCqXw&5WJ:j;v2~ H]*"䃊IwRq{]:ز–,L/d|'Aט4"\"oD>/PM֞BgHLp |p4&^\6lht SYLQfHu:~gGn#3];ļUN\Я'|BC]IH-]<@iAU\-rLI|H )-b1E U%[T^g\<4MwzHB IòOs]!4[{ڀ;D/+԰e[~|F³泓ad;f{H-9 ] וIѦHݫ2Gs㉮ugAzPQ.Ɣ3lSh(W" .{F<*FzJ̦?9Zbsnܵjyw}\ @UY_): ?KN|,2 rV멑 kRD<VrR:I*"gP@ yȇjj6-Գzո>c"t,hJA+| \jy7K[ |^Bcn Pf)Dp5qA(콰cLm8^G1{!4D^*o k<^<`*M} DHRrd]5d8[%'?i}mb saE)j1[6)1a=KʻNhhTeg-.HKj-4+o'ua@R{(u(2yhJ۟GlvzkuN֞lJF@)[ApA{] o={¹ GYhNi͍~ѕz+GDЯSrfvB'7i{Na͙m5 'R+DҧhĘxo02%\ݿ m'usSsީϊ%AҞD-+>> 5W dWTl;/uHJT`#A3 4Ma2. 7Y;kK)x> rܴAU(К؅8 ro88ΪLH\5(]& ?Bɜ`=jP`fq MS @ScER7{Tvܳ?IUT_` iz3W5upd~BݣBlcO3`*d:.on8fr-,T+A;wP Xr 뭪7 s m`ìaܥ}( *TZd<(O>(YS:~Z/.]Δi5` 6 f_(h.̤5͏`AŸ1&[mZ?{\0l0>.:$bXO),`g+5FJ쭽i-)?lEwmL[kL:5R]XxnƒACH =PX-/ni/`aGiJbƵٷ [Vfq\F רG14WWj9̌9Wlg$zdZ0"$=Y*CIW2ښ|wSҿm"wBf; " M{~R~5*ޫ-lۜUjYrUuOu9c"Zs! F Y 2nO0IB ơ(S W)Hn BX8!z0 E`F 8br9FAb9@'@Q0a@7C@zg&Nw;!@sx%͢q@H:?m/wʽ@vTAS]`݅ڂ`:Pԏ%j,Gy1%u#灹c̚UޘϐUgnfdD#w9?A]Q=G"M45У JXlX g541( ih ﰏX1u\ &]mzzh1*} /Ռy<pYМQV!,c<捕:Hh^Q&By?aKF儘?17^U :S/vhӵrL K I򝿕`^r_Z:&:x/y9c$3>[@~Eׅ{rp_A,7? EwxY\Arj[]%WH6 mE,Gmv¿源bͥJS.bJy}FP_TT8WaC)}t% #G R 7RJ)0)䧔B]D bbOMHLXebUE凁~K虾䖇[l\t̞`OHmtW @(3ݭ+QJ1"]6KmogsKU4PѮet'>!6p픲Pɨ71rd62>!m*?4LsPF?]@…pN,@#HS3eڿ2uEldz0{ aTʓq<=݆'dr4?}`N̝G%BѶ^}QאB+yNn d D̈́vx<[ ߞ3o4ad"Ur-,ۯ5]g$é)`F.^?I-܂qpHճ$'qKMmtxmF^}6G;<q^1HmB*r&~5:R_%JF͟]&r [)2QjL[ej ;LaPF_{B=4OQĜ56ZơHV>D;sU\}4WYIg -X^Qa\3bRp"IzE7(F8.L\5Sa≬0miw&Ld4,Sˠ cBOyB[w@,;`f>cZ`yBɳY V1Gz.ok$4bQǂcu `߁ V@z"߲"}Z7}qQ;Ӱ߱v[KP#D& BnwYڍf8mkFOk^P6b^do4_s+IhY|VCpy6ԍdqeN[EBkhGznW*<%4? e%|i;O ӂߎA"|EP^ikGB`xkF%snVt%4.p֜8hyja86,;h:TlKNKgż3΅ɌDܤdTDHA;W:b L ?WUR ?"m[kE_/6}k \r2m8Ρ7-_/~v\#cfWTL!o@˫:8!B?R?Ū94:9(eUZ;NUpBzJ[z4bTwa]ϸd8YŊL=&U7GI;-fZ +YS7 z=v/ m0tUF(ZKW"ao 8@FlP͝qөFCA̸m LwS*j Hi9+`ǡq=JZw:ވ{UrUwzG-5 L`|R;: 9|B 4Ie͝[NȯTy<0 }a6bOW^Y_Uesʵyu?_fhPA^eUiyhC@ HI_i8IWlh(wxrdLh"NF1湸ޤ>hPh7)t Vs^_ L\M%Jf6Jz׌ޅMi3U~b5}0,[1STχK|pBOb6dP"!Gr؞Y%(6t')0u.ˇ0t~#X"ʪ&uO`xsB#//̀ Mx%XۡILDDg1񼗸r8;.R7pdP׋ s!@Ǚ+2+x_?%yҽN9.\զd+ZpMf_/D {3q>:TsCnD|QxU ɛNstq0Ĺ~[o*@'/a/4DR^As|E/6ggfwK۪,?p#cl1xm2X0*LC 1XڗUq+w{q3Թ?Lf hL GU 紉M''`0~]f#/GA Y +ܚU˙PҸ^;k8/ͮHzBw9beL[s_<۝Q:N*h]/Znkg1eU:_.rq &Dwtz̸tB'7 w35϶cMR%;3Xztv>nO!l$^~Av0N%Ͱ -xش$Gu$J3+w };CO `2Wc GS6]JorYS8Kdx^Y,#T9d*/o0k~Pi YV0<\p ̜{0 u5 2~fx~YJW!5!{+X7kTqykeAm4uZwbˎIDcɧw8NSչES晘(_35AӶiS6 e@yHA:*9|O=۽`yzrUvrpL+,8ǁ RHn CQPFE(( HfsLR,4܍ I&MHSsyƖ2, z}>F)1 0' 0l%Q Ln tUtU7Le;xPf'R'2) p%, P%&) 2' `zr2k`L2~[9mKV '@r2 F1+"@r3P !YFQi 4hܔMб4lcf柛ɿ5%j}^|R.+4"*W8وJ  Ӭ+.gLuw\rQ^Q ng\Nf~猾<+q]9N>b$WuI;[bvJdJ:;D\[(+DfBk8&"FDG"FDۈ(80 !ݏZS-|t/7G\NGR?ڟ 3)J{܎R$nW]bd5NT-;7-MXw_9\~y" %Ql>_o7DHC?}Dva#TOmeL00-ZHUzI׺|MPb_qۄH`~84>Y*SpebZz2R!eM]]t<1(# -_yYnvM畴Q^Se@e8ys!5\I'6 (ZN,ah }šIl|`@C)MJ_`Vc]K:;eOl =+d6)r^ZR7yuZWSK|,(0_w h31J7cco^RXbf*~gև2!#`B/ǚ'gE* +rRNe=rF4Rr4)愆 yl9R]Z 3HFNr|)MvZZ\$]{z<,urٽvRDxC4oѭu.FkbXz1$uq]NG:kRڷшvsq8&szF $Nisҗ(\ZLAPp%f~˩# *,}@\EXVDVv}Z2:#:GF@˅,tͪ\#+Κh>,FaLaFiy|2CG(2!1UWޏngE 4mX;~,kޢF%\"2eqz>4M7mWɓ$OB'T"kdڕf}y_]w %6@F@4U5ZeRn.lWfm?*>4Bq˛{ul* S(Nb)TջD!NEGڥlvzh 7J[]@m7;pAo/Dr)FH?#'>`d)Ȉ!r c@ +:P unZKu9H`t]9b]X_MaV֓.֨t9ODYE=։$䫼LsdOm ЦVVYh@f}4肸upLz8w(aCx+RwQYkƖɓ$&{a{gܓl+⹞1|yă;vӨ y+CmBE1'vgO&)yo !B瞡*})!600Rr H,q$$k;K~Ys?ĜF\%'NhFy^Ͳm|\i쁽Ow۟.b@/!GƪѯX'^πK;"A>P0HZB/:Y>5v{HS%f ` 7W4!`)ai nU4vza=BRZwhM Rf^tӞ% qVHۛ ujTƿP|dB(/F|P7V_ZOձ"Ǽ_L3rLy([sKlmiͪ!^A~ i9`(BC6 `6|`Ct6`C6 O"ci&8@iq~y+5v} A$0QD m#Z*c"mݰ2 فQ'Q%4 ǀ9$qʦ`SGKJ9)f* C:eDf`爩tR=% Ok{ܝR9O ۘyIUrhv3z鏗&B'n/ٍ%Ԋv{rRŎEknWHfgjOZɟ>ɥo~q~(B-a}U,xaӈIY1;Qh) #)?mp+QVtǿBdC7zSۛ9|,mxI1]8?~-l׵8Z~A]S0r#+/-#SI{OW | }+l$NK]v+1׵媂Q _&M(snǽC+̝}G"ʼ : ǞP=ڡZv۝XQ T]!mZ(jjʒwE_N® ;-E""n# &a}7wǻ>UO>5b;* 4?.\|^^Zs s^i]Cu wO$?$ |JzTXW?6Mv?qΪKq>Ίc2~hc=uiv `g^3@k@( VՎ2/dP jQj:!s0xڦHSN_C> YΝ'kBA,K>CYFtenTvhOt| ; y]= l1`7C.{2q^JoFl5p`YM 5m%gVQ@:Q"{Z PAg@) fњs Dp0$"7/3KZ-YJ;~fn56 [فB"Ewi.~ࡴpeI w!d%\]rߵ^e-nY$\f8~dIIS46/ 'bY&&,PYE鲡fVڭNM,rY! %Ut&I:`e߯1FDW7<,3 kê#o#Rh+`P; uxx1 ʢ_ Ϣߪ6k5ڻPóxɴv!)}gS׬tqxMWsju+vϥL[ՙ=&~^6Z5k*IT-y9gऴ-bŅ$IyH~]e^pE{LXKՏߏmz)nCF@F?v0zPlN]d[Dk~=jzdiQ7eb"]j;?YInZ7>;CD$hҴ"٣\iN$ȑeȤ՘4[Q[O^.ClL5!wm艙Ӣ2NSGֺו'o*Kzb ^uҩ@y8nxR6WE.DXj!z-QBxw>iK28xB!Mh hXPi ~it%Nk(- F@ԋvvr΄_p; Awveaϳ^X,V/ |3n c.\6.5S˿{# Yp|9V^`:cdKC} WKTNEW1!ُu/^ֵOa6,8`o3/5ߴ8Bw-&(RlÏ^w>̈́-WpD5.Se#@oWGȗ]/2.I4?wWzE W&7XD[sˀ&S)_[}PF7ƴˮZDܛ6fnc:l@dSh+zcB2"m 6nyR;Ї\EY(f#7+@o]8sW{aӳ6IV!Ͻ%@&KnWo6<^ wIk+N8 /|u[.e63ӝ ڐDڕ7/YWGsiu֛܋Ys;wO ^g77Y՘rjQ ?y:rfÎmlCΉfda4Y{$wonڶ",za8Dߵ}BnS ZNwt?ԦR./o,,͵F˭rhR =[cfS񇎡v<,~ތblLCbllbnNx  A]袏ai(,q '] '9oi[.֕|^Ztꅐi-5h~#)VZB+/ZRS9jԵ Pɘe3 !OfZH/,,fQøwEhL4:& %w {=L:@03լp 2VևP#;+Tug)y?> ):SaΔ"XKJ?Y?hȢd`2le.[ʫ;秃OnLP2Z/D,c-  uMn]ݭ1JJ6SE<Ix2[y(~68KPˢ()W`峫~II ܦ_cKXcL|)(tixH0Z ~2` .j%h]4kZ|{WF,x CƨMQ11-njykf^ cҕx'. zM wQWNY'Yes}s,]C MЛs9O\)Le_tѝmcQ\ U7Fl.DL!17?Ar,:m"1eH_!hdXY$Bm6^dRKf&lQ<@J$; ?.RٵVFKt&fWcI=%1OXАbAOCF:W^4]I3]ܡQMRJd}$ d"C^Ffe}=+vK,D~ XYM[^JeP|]QDp]K`b>rP*ȇ[N)"&)OH $LBHcuL64<3K"Wd P㤲 hm/+Tt[Tq-"J+[tىE\T֤L%8PK&2 !L6_(l/_>GpT:Yi(e5Q(Pn"h'@5bU8=5&$݋I͙6]9BC!.gODf>~FHɖ]yhmqBJ$]C2@ ^e*e%vLu>O0z}.dLNx>,z"S:̍i!uUǿ+d@RDP`{c `/sܧKw3hv>WȾJ FeʒeܼZ"&ZUdZ൦;43SNbKe0-[GD&u~UV%_hZD8}:+z8ċZ狶jĒ$ёNFMZxe`M"( ^ E`+#顒r5-E? #j>ӟwMg0IVs8;Ec5W{nSߎGn~brOi${\ZHDpiZ9-$Wo}g-ԲvJV5p=P? %yr g."yC 2BX^Ed\J^9̼W2F9c/:T=PRCt6hl@-V 'xpNr6")iH1&p`cv~8%$8{կŭ,ìC#:R@AéTGyLEetSs#DȩT1YNj#f L8ƄΑиoݢRuwIxR*VHIg~ݚ=$K3_ - $1ezfse>(4$LYc rj S`46əD sx_yp.Μa挎z ɉA&ӄfVdrozbkeB?y$Xi~y x2\I~Z aȠ+1\j!XO%MZ1&adm/;Q'R/Ō\cd'J=JH+f+R=zF3}BЀ:xret씜}\p7Rp(P (7&Gl!DD-,;ڂ?Ʒ + Y5삢!ǺAkF`hXUxR}bBI%29WQK@Cτ'ewrNVZ".DqCń1P**?'hkT7/=MSǕs.K~wҳ7/Ͽk֊I;DFq9(kGgW' ,buk&.XM<{7`D1r=m/$}PEiүTj/p>?hNՈO[lOy[Oky*#UT5mucJeRőBe/l}=ݑL ƮeSAF 9u\ 1zً?Zz:zϝMhF?k<$Ѳ}ŀ#axRi:4}k d%>ybǽ\OŁU0.W4HZnPDRJ)gm6~@HG$C1M/.IUhĤt)QuRU~GECBG\0v~I@| f).º)pxuUnBq ^1y!9ra=fo)W(WMpw@ ?`ŏ`ߑNA>rx_ٛ޲G>'cb{әb } ژS-.W!qtmJ4{.-sU=5Sr=rlK:uY4=876?u ֚q"^w݅pXq/Qzo@erIZxUMVo^FSH὇S̠VƎa ma-f#FLv }r\dȱ eRˋ_ N$`;'L}|E#[FƟmpx}z>FƩ_G0u!yroQ*; yV&6GEˍ(5ENطC]8Utx$ǏғYϟX)wV"&p;s>@ _n·K3hM9}~}jiz~7`B{;CSoQf^Zjv[ڧ}k;^鋱kpHth_Їnp)n@ nnMR?:{F͈˜HNOjp$d1#T,^h*|z0 jaڱȱʚp&/U5V_yp+#HJD.gy>6ӷyll/c?߇,g7:7ygw؍nGg廒=G/#1 x.ƭ ݱR.|#+syb:Vr]Hɮ@/ȖaT=!1at;/So#ȩD.%L)mi-T4_SÛ[y="a&D)1;HJꡨݗU0d$mXM1 g6[gDN H :gmq[ $ \wb*Ɔrp2l&rԅE. d*n,tH~NeNŷ f}%||{=QD`GN,Χ#s֥7)/}p=)3%T& R ' i\9J! g!Lc̫㒤6ApggYQĉZsObmVBYZu_,$X(]ic|Co)iYJI'pBMrj1dNyl*;J/yV2LaL5fY;.:zT, 'J%!Z"XN/m,Ps,Yh<>k&Jʹ稧 :a>L֒1Šyv;d7E"B4ADA%-AFy|"R!?(%cfT6ZFD! ^4.e`\F[=`,\ɽJp ;@ԁrGi"NRbYസbZ 0gKfqt&9  Ccy룅=-jOz q{7T^9$ ^SZ0ڶ`5F29 qp㾠wLCrQ +ifQ\_Nr\|"פ*ml~R&+?]\fS_/OV6S"2Dit$u߾xPĀȰl&GL&E1bQU7dpYRBV;z,3|"}ʙ1>r0“2yk?C%S gGlNeCW|qŐγNZ`N3w/E:mE>Cq7;^ x/h}ąG QyDŽ~C}{1W|5F'?8h9NZzX,Z<xz?j ^EXWI:*xV Aٴ/654Qv+M="dbQա jlGnw=.^R?9{А^OH+7oOãB@4:kB@^).F1x3x-cҙD{dј`TwArҩCԃ& -zmU ;z\jΜՇz*vraZbH2]y3XJycjŨu_Wo+0auAߏDYrލjPl"chɡ|e9.EgvrW'm74'y%9_qd8=" KI$ hܰ I' ML1=o mcrj!5mBs;\KPMlW`ݾoje k'4zVf\D^䯽mT؂F'o3XP;yn9 B Z9E9wxt*BpLGDG ]&bV<Ή9iE~w'%5tڴx:vivvix#yѭj~=FLS>ҳ>,֎c𖣁|qVAsFszvZ}lxgc# #䜲U !V;# rql)1}.kg[EI*P/^y-g+-&-jQ4JjObvK브u7R 7)^$ׇ烺rĿqPkhxy af:u iά57ࠒSanheU!tR䥜89Ғ0˔U8* pu, ѷI~ȽC3[O #opeݞS@ªٟ㻒~&eOǻ;*L&Wi)pcδn853 EZ k8%'tMς;W:,,l<*[zC{Y8rnO6Mo feg]n#n#>~KDقvapn(y^ʝ:B&V'pKy}H5;CIIJE-#6]}#/splЖz~*Eq1a-5=6;&c3y'#,,ȶ YΖ)ܢ' tk!`Jor%UOzcx+?sQ^[<{@a#rz#pİxbNR| TڤvN &FOv[&F>F"y {?.2Wcq6;V9ier!1 &tG\W.#X[ÐasV.g@E ߃C .k'3Cф- c*SZ  ͎\ 晆_2\lýD^4"~1~WҌLFC~+g@:$-We$z%v,IO 1eX;;t6C7x?3 Lݥ)NfQk]Nn7ybRwk9ҚPZZ Ɣ}&  z@ͼ2fƠIDB&u:@B`mٖi9ą(sh3V Ú ^}1:WEu>; \ ᾦ~>gZ E眣.{X8zoz\{nut'#@z-N'TX?GR`+߁b6Ǹ"wfcd=%B\!ChJHK:+ҭ_cy?oW1:+P &10y3I\Pd+tA=J Hh'ohh %j&[0OT+|RnοzDu^)^)߳ZlKRdtس8c&%ʻAÖҜv1Shk#^ښY8BCMtŝN'͔pY |QZ.e<)-]}3B e!:t3zgY_.ZIf?M"|Tov;0mư\JS9R~jnHįhfDݒ%؄BӱMU~ޓ.ы4SWy3sYiE?=Id .YoPX~"?Ybp9jކ^p.%ZՊ4A/AS>~j9$aIX(qp[ `:LMY0 ~KN,*zSE\ϛ H <;# j0W\X'~_q=jia,+hԴVS}˯>* BP tmÛqV|_c4{.{J/I8xt<6|<жI)z 3A!8`TF΂>;]u䤧O}_p` #IZmd) ʖtzH~|9]/9j2O6zCJւŃn_y0|E\1RJTլy;_AQxQ3j'Gm~uOlԻG?f~ΩK!C0n_p10:J\@a<Ա7ƳG~JT9ڟ ]2#&>{j@YRUYoVyM`+zPg)TìhoK/~;aGnﶘI;]> 8k K̡JN|^Z.Uon8}% HyMH۷MV/Yc1"m:'qw9THz.?lzw7,KRۛ"mSbn0L9<'4<6/쨨,w V}Ϳ>72:NOC),A(!IA,dx즵sJC;M,ѥoĊ!Z]tnpf67-jLM%.J۩;]DąL,һbd?V*yCƀ8Ym&^j`XnX8H-[_$n⒏zJ)no08bjXgFX "muu1!p6GjO9 Gf|X~'Q+>: S}ƔߦpkK˅_:Ǵxta'K(a.dɀQN eYz;jjkwWSp nnI#[\n-=y.  h-b1Л6ڞLx~=3у?Z" mt%S~0Qȅ61G J6Hԝxs黔x9_Nn&Aᅢiz$@0@JO1& y?FtQ|znVqd_ޔ1kO4) O{1V 3ǧl:Yv 7[bCMJI \& }+lyh&y f B(eb6Ex7kX1y۳kc=ruH";' >,Iʉ~O|׵?!Q8h b12uQ#!7J-> AӚ0G q IqCCh'9 ]$>1<ϟVW;%:3_{(rakFJj!ίd4g;_A56/Ȅag D!7UE'&p^#;dVbM}a_<0z;Gk ,䏞zRkP7fUdR\lGngJqs@yx}*5S -}46Sՠy_W9^@t3¥$3ױoa5XLۋ A66}8ZS Xh{%jIr-ї,tQr ^;-w"j;:(f9Yg5 !wC Ɨ&1Ut5B`|%p"f88@$ 7^o#XڇhNϏg24 %9FЖ5SٟF>kᣉFV*cH~b*p阗9:Vl yɅ}d:Ϸ@mg#1&ƴ|עmHq$+"Bl4"oBo*ʠ>YȡkJ>,k͂ϻ| >;vy@QJg98N6;>)2Khe+p@2EqKB׹tIDid&x 喙MLdKL%a%j>F#rx~N)Uy6?TσȞe$rѹM b0 O8[KZ0#n/LNZqϷ)f ěe N"~@* ^5+wnjYJyi2#E8GqF.̅(>2up*-ٖ7FQ{?9eKl_4P=9@IFwiCv=`3{bzA*BDW8ktܔ]:y,3iNUbL:Xg%cTDq!f.0|vSr9]Y?F35-[bŇs;ݮǘE HkuZ1Z_ 1u]$t$^1zΉ՝m28L} zǃ_!*F~QDJC Y Q[rkP~Fy.F o .0of6rhym{r%NWM(^|]܉=||$g݂+rd)O> E[ZglQ_/:z3kYĄXS JϻGVY^m4^ࢅ*j\>N՞XrD/ݪ#;1QM>rUGd:^C5y^לb6@%!91=};ho+zad+j'-Zs ӆ[9wɘ& v`O(R&SiVNB!Fs3Lgm-Z%Ur*\$Owei'^/{XfDŽ_=3Uo 1]E<-Ԋ8 HfIS8,(d Ɋ,q Rߺ4>*@|Ϛ>OpOfR."WZc!1ϐj{sUUƝ,vv/ezY&V'JcUj pl-:kШ kU` k}h;[,/iڎd=!%GO\ !,Grb> D]mj|ت%#av1kB kΦH#֩aW` 21$\BHS4|;ҚFb<p%-6տ  ?;-\@8|HJ$="%z%I1·c5̞<5m+ւČhyqvsj,hSpCVDGɑsӧv1#nbI4x1.<ﰾj{P }1Nw>vXqꫡѮ?}!r: {Lsv-ؿrǥ^eqxɇ'6J?>3_lC6,((EuG*cD8L>ԞgpV1S LXo;=v1V"0gx4P\[ ]1p&ĉ:%X҉Oi ҀGr8$ڎȋ K 929zBbB)}R&L6Gwµ[VpTItrJ.IZ5z()41y0Uf]TZsgM }q>!'8q5~ {(D6,-/w;K{R炂 Ya֩t匯jAg1YfBhÊ7.Αbޗ6Xn{MSeҰZ dzulrBоv$H ™Ct\ q/idKl9 r2FuAq[^>6ELlȕ9q~|ς"'ΣΏ>ol2$=U/83_W^ m⹶Jxҹᄡ~!L,s1Wx [Y0䀿Eo , 22 ֶe1sD>T͡ln[B36au#{љeo8.+)[5 #Pi(ҷJ^U6*tlhlċqķ#~x6ka@:0X;-wճx^1Bݎ$!qySi%4jXHǂlg 1#ݕͩ=j-`(˲G`C0$τ#tC@ /r{J)0VTJ^ɥ|0,xOb:Q  `lk05e-V|b?a,;ۊJ\WF,MHAx/ިL{pO;/ ǭ U .6CD, -;T)n.m.=B?]2/ v!'i|WxXߎacBS$ų^d\/l D*@-eALU3_ɱICNO.m9,sdxdklZ,ϗ`O;2;˞hӳ"ΜաLZWi*:g2WѰSF`$yGVҴӁ#Kdm*Fk6((gx#5K)Mm V%녯3jRqy1`Th/3EWnc bGruXՈ"AK{;<M96QI$[uy-l/0ΰ gFɍS?"UHn 'sЗ?Np3\}>2da~ՓPӋ z .pPĆ2ݖ~; 塚LË!>ɬPP ۢZ*{@:+q%(&W]W5=兇md jhVz*:dMS1[zA Eh~\e#ךuLfTsNۆh>ϐ+71,ǣ_TЁ4w;~ew$% DVCXHaSaDM@FW!@t@KYqhz7 Bߘ%'r$ԒEӁb-ıڢ 0xL;9kY| N;Z!f93Oۋ&9j~>vPd$p?x%YN=…Q!#xΜ Qk&⸈Bd=:Ԫs誺;dćjZ(mִ&(6 FMvξGB|~k!,*o~:-CmHvU֢K(vp@p J$уeSN\W|W> GhՒ`J&dH];>% k8le9OHbwh9@)Dyy(  J}$L :TiS 7#7zl" N kfS%FSZ&eo(fqITdN? @c^+j)a8tyl/ 0_fyG7`idgmMZm[l>Ti8n&QHlz ;p:'\])wxl|(oOT 'G4{][/ZC"t۾ܣ}JIó\?tpTu,.hA,I0ӑڃwkgX [<x7a^Daf}n×S@E;%Ӌm aKmĈ/FXz'U9B*,# 9¤nOq\ou>LK( nmdWhf"!R}2+_)7}HXI„`#!dg,{$.G(Q%1ի8۰Ig( + w1L]̗,22d%wͤ]O1 fi7zy^haݕQu( _eӿmCAM ;# 3ꔇ&1Yc%>P#sDZfmǒ}rc޳ժOMl16m.LZ.M{b`vm$喝2_]N|V#&&+<( b+Ԗ ~賕ӨiN6t&k.$S>h{wfe~ʍ_⃼\c~MDU_@5@1'q2#Q0ǴY/\"522ѦwN]$;@ՖlD'w.1(e~GPC`q{vKmS$/J՛ݻc[*.vҶ|t"G:jOž97=uɀh#K cSfZr,T9g~.)rCag.y}wi|zPT'01{;XG= tE@.w|;36 qQ܅Nm`60w | ̠܇O8d4o&ͣ5U4,` z=Nfc#<z?Ycф(K#@nv:Jx%$Bz1V>l&^1] 0dŗs~UvqLmQȵgu}-|-VriP ǐ, #,_. iOrcr:?1=OM~@ 3H5#Y^WBVkYub#t͋oy,vCd[Jf|ֹ ?DͣTQL JpuPa1c?>dɔވVf.ๅHͨP=I" Ozc0Is.?`N GF}AGRD:c;Z ]T8V)7 {ݪA68+gգl:0,em x8)̑w<0\LDŽI^S: dOjf -܎oT0Xؽi72dv$fV95jػܼe^=eu"y/jxWuv 1*6%{ԼZFTnI#&! u@~j,R1xR/cfڴ9閆BMtm̮כь6Brni1Mu$%x68J@a]Sn Yb' snW|j+u6 knU!+^0Vmr!Uirq a(^鲄tc"X4qȰIBmGXwUg9kk&Q|ThϾNvh XC)S\$olV;90=/֭ޗM^5Un \x5V1 Y_]teq+ )@ _%i@#/DxaHLsȓ D(br'aMH_^xtiDi̼zm[U4!ӚrG~c`WCO#XE}s.^t&eWĕ4Te6Ivzj,B Gb$Cqp)''/H.ghUkTAxDxh{R?n(1gC6G Y{~lUjL@kk_?PxE&fW58"ӪKE?9ȬQE%e H ޺wk#q^ ug\|l;$lsޠ]aЁ/Uh(_UHYc1y!*-ۦ҉"yZv517j_G'EZwTIJ80ŭ+T>o+%QBQͼI%%tPǔb( A[ig:k0268eUz3"l 3jgFoWL Ev$Ŕζ;lPx g*f P;Og ̪lHpC4'_ /g5)-2;dBkV|mAI [ºßQһIz7ץ'[,Vm_u$Ƕt|zUG8xLjjuH03<, =9uq@*: 8JBrw!ʍF )\pVm v/hȤ~<9ٶN{ARojͿZ!'^I1)ZVBLKYc>\2;}F ð}߲-:C1y1Im*?vkHyCoVame 뢨|(}A~oX(;H6h@D &>M'S9 o$w| y/@\+zhov$}Jp\.+CM-;3i,evo"ބ;aI? &Q6ܚ")LMG͑IBhW%3@H/A?Υ\ϥ\ͥ\FM8"|c[Ҕl|'zJ3t0&򨴆?{l\,~_-~B{#[4l/ rbeqa7aܡWHg 5?nĽqAΊgg/RwU*K|zpMI(٪^Z o= *PÐ֩ufᤡz7gʔKG%|V>N2O&#^){+6f1o—Y5kE"ˋ$޵ϫ$R":NB#X3pժyr~d`x%bm&I#j'T(7N i5A!E7~@j%lDSL.Qg)ȓwkHh7Αmt. fnv ?%32;Y"a?y]oQ٬ɘ˻(J7#—s$Xђ!2u_hnuG( Z O%a & R|NPTɂVIm.>B845&uANޠ.MT=z *DZqY-lۀjb+J_}"#b, `>Do*l50iLY ZUGd#_mfheh՝bxXgZ_Or^i,&)> |\OE Aӓx_-waVdږŵ=8Rs]$yy1]]+SC_>I(z奏gG,~n9ƒ}.v${ +_C2// Copyright (c) 2011 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. function $(id) { return document.getElementById(id); } function toggle(o) { var licence = o.nextSibling; while (licence.className != 'licence') { if (!licence) return false; licence = licence.nextSibling; } if (licence.style && licence.style.display == 'block') { licence.style.display = 'none'; o.textContent = 'show license'; } else { licence.style.display = 'block'; o.textContent = 'hide license'; } return false; } document.addEventListener('DOMContentLoaded', function() { if (cr.isChromeOS) { var keyboardUtils = document.createElement('script'); keyboardUtils.src = 'chrome://credits/keyboard_utils.js'; document.body.appendChild(keyboardUtils); } var links = document.querySelectorAll('a.show'); for (var i = 0; i < links.length; ++i) { links[i].onclick = function() { return toggle(this); }; } $('print-link').onclick = function() { window.print(); return false; }; });

    // Copyright (c) 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. /* Id for tracking automatic refresh of crash list. */ var refreshCrashListId = undefined; /** * Requests the list of crashes from the backend. */ function requestCrashes() { chrome.send('requestCrashList'); } /** * Callback from backend with the list of crashes. Builds the UI. * @param {boolean} enabled Whether or not crash reporting is enabled. * @param {boolean} dynamicBackend Whether the crash backend is dynamic. * @param {boolean} manualUploads Whether the manual uploads are supported. * @param {array} crashes The list of crashes. * @param {string} version The browser version. * @param {string} os The OS name and version. */ function updateCrashList( enabled, dynamicBackend, manualUploads, crashes, version, os) { $('countBanner').textContent = loadTimeData.getStringF('crashCountFormat', crashes.length.toLocaleString()); var crashSection = $('crashList'); $('disabledMode').hidden = enabled; $('crashUploadStatus').hidden = !enabled || !dynamicBackend; // Make the height fixed while clearing the // element in order to maintain scroll position. crashSection.style.height = getComputedStyle(crashSection).height; // Clear any previous list. crashSection.textContent = ''; var productName = loadTimeData.getString('shortProductName'); for (var i = 0; i < crashes.length; i++) { var crash = crashes[i]; if (crash.local_id == '') crash.local_id = productName; var crashBlock = document.createElement('div'); if (crash.state != 'uploaded') crashBlock.className = 'notUploaded'; var title = document.createElement('h3'); var uploaded = crash.state == 'uploaded'; if (uploaded) { title.textContent = loadTimeData.getStringF('crashHeaderFormat', crash.id, crash.local_id); } else { title.textContent = loadTimeData.getStringF('crashHeaderFormatLocalOnly', crash.local_id); } crashBlock.appendChild(title); if (uploaded) { var date = document.createElement('p'); date.textContent = "" if (crash.capture_time) { date.textContent += loadTimeData.getStringF( 'crashCaptureAndUploadTimeFormat', crash.capture_time, crash.upload_time); } else { date.textContent += loadTimeData.getStringF('crashUploadTimeFormat', crash.upload_time); } crashBlock.appendChild(date); var linkBlock = document.createElement('p'); var link = document.createElement('a'); var commentLines = [ 'IMPORTANT: Your crash has already been automatically reported ' + 'to our crash system. Please file this bug only if you can provide ' + 'more information about it.', '', '', 'Chrome Version: ' + version, 'Operating System: ' + os, '', 'URL (if applicable) where crash occurred:', '', 'Can you reproduce this crash?', '', 'What steps will reproduce this crash? (If it\'s not ' + 'reproducible, what were you doing just before the crash?)', '1.', '2.', '3.', '', '****DO NOT CHANGE BELOW THIS LINE****', 'Crash ID: crash/' + crash.id ]; var params = { template: 'Crash Report', comment: commentLines.join('\n'), // TODO(scottmg): Use add_labels to add 'User-Submitted' rather than // duplicating the template's labels (the first two) once // https://bugs.chromium.org/p/monorail/issues/detail?id=1488 is done. labels: 'Restrict-View-EditIssue,Stability-Crash,User-Submitted', }; var href = 'https://code.google.com/p/chromium/issues/entry'; for (var param in params) { href = appendParam(href, param, params[param]); } link.href = href; link.target = '_blank'; link.textContent = loadTimeData.getString('bugLinkText'); linkBlock.appendChild(link); crashBlock.appendChild(linkBlock); } else { if (crash.state == 'pending_user_requested') var textContentKey = 'crashUserRequested'; else if (crash.state == 'pending') var textContentKey = 'crashPending'; else if (crash.state == 'not_uploaded') var textContentKey = 'crashNotUploaded'; else continue; var crashText = document.createElement('p'); crashText.textContent = loadTimeData.getStringF(textContentKey, crash.capture_time); crashBlock.appendChild(crashText); if (crash.file_size != '') { var crashSizeText = document.createElement('p'); crashSizeText.textContent = loadTimeData.getStringF('crashSizeMessage', crash.file_size); crashBlock.appendChild(crashSizeText); } // Do not show "Send now" link for already requested crashes. if (crash.state != 'pending_user_requested' && manualUploads) { var uploadNowLinkBlock = document.createElement('p'); var link = document.createElement('a'); link.href = ''; link.textContent = loadTimeData.getString('uploadNowLinkText'); link.local_id = crash.local_id; link.onclick = function() { chrome.send('requestSingleCrashUpload', [this.local_id]); }; uploadNowLinkBlock.appendChild(link); crashBlock.appendChild(uploadNowLinkBlock); } } crashSection.appendChild(crashBlock); } // Reset the height, in order to accommodate for the new content. crashSection.style.height = ""; $('noCrashes').hidden = crashes.length != 0; } /** * Request crashes get uploaded in the background. */ function requestCrashUpload() { // Don't need locking with this call because the system crash reporter // has locking built into itself. chrome.send('requestCrashUpload'); // Trigger a refresh in 5 seconds. Clear any previous requests. clearTimeout(refreshCrashListId); refreshCrashListId = setTimeout(requestCrashes, 5000); } document.addEventListener('DOMContentLoaded', function() { $('uploadCrashes').onclick = requestCrashUpload; requestCrashes(); });


    /* Copyright 2013 The Chromium Authors. All rights reserved. * Use of this source code is governed by a BSD-style license that can be * found in the LICENSE file. */ a:visited { color: orange; } .hidden { visibility: hidden; } // Copyright 2013 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. var domDistiller = { /** * Callback from the backend with the list of entries to display. * This call will build the entries section of the DOM distiller page, or hide * that section if there are none to display. * @param {!Array} entries The entries. */ onReceivedEntries: function(entries) { $('entries-list-loading').classList.add('hidden'); if (!entries.length) $('entries-list').classList.add('hidden'); var list = $('entries-list'); domDistiller.removeAllChildren(list); for (var i = 0; i < entries.length; i++) { var listItem = document.createElement('li'); var link = document.createElement('a'); var entry_id = entries[i].entry_id; link.setAttribute('id', 'entry-' + entry_id); link.setAttribute('href', '#'); link.innerText = entries[i].title; link.addEventListener('click', function(event) { domDistiller.onSelectArticle(event.target.id.substr("entry-".length)); }, true); listItem.appendChild(link); list.appendChild(listItem); } }, /** * Callback from the backend when adding an article failed. */ onArticleAddFailed: function() { $('add-entry-error').classList.remove('hidden'); }, /** * Callback from the backend when viewing a URL failed. */ onViewUrlFailed: function() { $('view-url-error').classList.remove('hidden'); }, removeAllChildren: function(root) { while(root.firstChild) { root.removeChild(root.firstChild); } }, /** * Sends a request to the browser process to add the URL specified to the list * of articles. */ onAddArticle: function() { $('add-entry-error').classList.add('hidden'); var url = $('article_url').value; chrome.send('addArticle', [url]); }, /** * Sends a request to the browser process to view a distilled version of the * URL specified. */ onViewUrl: function() { $('view-url-error').classList.add('hidden'); var url = $('article_url').value; chrome.send('viewUrl', [url]); }, /** * Sends a request to the browser process to view a distilled version of the * selected article. */ onSelectArticle: function(articleId) { chrome.send('selectArticle', [articleId]); }, /* All the work we do on load. */ onLoadWork: function() { $('list-section').classList.remove('hidden'); $('entries-list-loading').classList.add('hidden'); $('add-entry-error').classList.add('hidden'); $('view-url-error').classList.add('hidden'); $('refreshbutton').addEventListener('click', function(event) { domDistiller.onRequestEntries(); }, false); $('addbutton').addEventListener('click', function(event) { domDistiller.onAddArticle(); }, false); $('viewbutton').addEventListener('click', function(event) { domDistiller.onViewUrl(); }, false); domDistiller.onRequestEntries(); }, onRequestEntries: function() { $('entries-list-loading').classList.remove('hidden'); chrome.send('requestEntries'); }, } document.addEventListener('DOMContentLoaded', domDistiller.onLoadWork); $1 $2

    $6
    $8
    // Copyright 2014 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. // This variable will be changed by iOS scripts. var distiller_on_ios = false; function addToPage(html) { var div = document.createElement('div'); div.innerHTML = html; document.getElementById('content').appendChild(div); fillYouTubePlaceholders(); } function fillYouTubePlaceholders() { var placeholders = document.getElementsByClassName('embed-placeholder'); for (var i = 0; i < placeholders.length; i++) { if (!placeholders[i].hasAttribute('data-type') || placeholders[i].getAttribute('data-type') != 'youtube' || !placeholders[i].hasAttribute('data-id')) { continue; } var embed = document.createElement('iframe'); var url = 'http://www.youtube.com/embed/' + placeholders[i].getAttribute('data-id'); embed.setAttribute('class', 'youtubeIframe'); embed.setAttribute('src', url); embed.setAttribute('type', 'text/html'); embed.setAttribute('frameborder', '0'); var parent = placeholders[i].parentElement; var container = document.createElement('div'); container.setAttribute('class', 'youtubeContainer'); container.appendChild(embed); parent.replaceChild(container, placeholders[i]); } } function showLoadingIndicator(isLastPage) { document.getElementById('loadingIndicator').className = isLastPage ? 'hidden' : 'visible'; } // Sets the title. function setTitle(title) { var holder = document.getElementById('titleHolder'); holder.textContent = title; document.title = title; } // Set the text direction of the document ('ltr', 'rtl', or 'auto'). function setTextDirection(direction) { document.body.setAttribute('dir', direction); } // Maps JS Font Family to CSS class and then changes body class name. // CSS classes must agree with distilledpage.css. function useFontFamily(fontFamily) { var cssClass; if (fontFamily == "serif") { cssClass = "serif"; } else if (fontFamily == "monospace") { cssClass = "monospace"; } else { cssClass = "sans-serif"; } // Relies on the classname order of the body being Theme class, then Font // Family class. var themeClass = document.body.className.split(" ")[0]; document.body.className = themeClass + " " + cssClass; } // Maps JS theme to CSS class and then changes body class name. // CSS classes must agree with distilledpage.css. function useTheme(theme) { var cssClass; if (theme == "sepia") { cssClass = "sepia"; } else if (theme == "dark") { cssClass = "dark"; } else { cssClass = "light"; } // Relies on the classname order of the body being Theme class, then Font // Family class. var fontFamilyClass = document.body.className.split(" ")[1]; document.body.className = cssClass + " " + fontFamilyClass; updateToolbarColor(); } function updateToolbarColor() { // Relies on the classname order of the body being Theme class, then Font // Family class. var themeClass = document.body.className.split(" ")[0]; var toolbarColor; if (themeClass == "sepia") { toolbarColor = "#BF9A73"; } else if (themeClass == "dark") { toolbarColor = "#1A1A1A"; } else { toolbarColor = "#F5F5F5"; } document.getElementById('theme-color').content = toolbarColor; } function useFontScaling(scaling) { pincher.useFontScaling(scaling); } /** * Show the distiller feedback form. * @param questionText The i18n text for the feedback question. * @param yesText The i18n text for the feedback answer 'YES'. * @param noText The i18n text for the feedback answer 'NO'. */ function showFeedbackForm(questionText, yesText, noText) { // If the distiller is running on iOS, do not show the feedback form. This // variable is set in distiller_viewer.cc before this function is run. if (distiller_on_ios) return; document.getElementById('feedbackYes').innerText = yesText; document.getElementById('feedbackNo').innerText = noText; document.getElementById('feedbackQuestion').innerText = questionText; document.getElementById('feedbackContainer').classList.remove("hidden"); } // Add a listener to the "View Original" link to report opt-outs. document.getElementById('closeReaderView').addEventListener('click', function(e) { if (distiller) { distiller.closePanel(true); } }, true); document.getElementById('feedbackYes').addEventListener('click', function(e) { if (distiller) { distiller.sendFeedback(true); } document.getElementById('feedbackContainer').className += " fadeOut"; }, true); document.getElementById('feedbackNo').addEventListener('click', function(e) { if (distiller) { distiller.sendFeedback(false); } document.getElementById('feedbackContainer').className += " fadeOut"; }, true); document.getElementById('feedbackContainer').addEventListener('animationend', function(e) { var feedbackContainer = document.getElementById('feedbackContainer'); feedbackContainer.classList.remove("fadeOut"); document.getElementById('contentWrap').style.paddingBottom = window.getComputedStyle(feedbackContainer).height; feedbackContainer.className += " hidden"; setTimeout(function() { // Close the gap where the feedback form was. var contentWrap = document.getElementById('contentWrap'); contentWrap.style.transition = '0.5s'; contentWrap.style.paddingBottom = ''; }, 0); }, true); document.getElementById('contentWrap').addEventListener('transitionend', function(e) { var contentWrap = document.getElementById('contentWrap'); contentWrap.style.transition = ''; }, true); updateToolbarColor(); var pincher = (function() { 'use strict'; // When users pinch in Reader Mode, the page would zoom in or out as if it // is a normal web page allowing user-zoom. At the end of pinch gesture, the // page would do text reflow. These pinch-to-zoom and text reflow effects // are not native, but are emulated using CSS and JavaScript. // // In order to achieve near-native zooming and panning frame rate, fake 3D // transform is used so that the layer doesn't repaint for each frame. // // After the text reflow, the web content shown in the viewport should // roughly be the same paragraph before zooming. // // The control point of font size is the html element, so that both "em" and // "rem" are adjusted. // // TODO(wychen): Improve scroll position when elementFromPoint is body. var pinching = false; var fontSizeAnchor = 1.0; var focusElement = null; var focusPos = 0; var initClientMid; var clampedScale = 1; var lastSpan; var lastClientMid; var scale = 1; var shiftX; var shiftY; // The zooming speed relative to pinching speed. var FONT_SCALE_MULTIPLIER = 0.5; var MIN_SPAN_LENGTH = 20; // The font size is guaranteed to be in px. var baseSize = parseFloat(getComputedStyle(document.documentElement).fontSize); var refreshTransform = function() { var slowedScale = Math.exp(Math.log(scale) * FONT_SCALE_MULTIPLIER); clampedScale = Math.max(0.5, Math.min(2.0, fontSizeAnchor * slowedScale)); // Use "fake" 3D transform so that the layer is not repainted. // With 2D transform, the frame rate would be much lower. document.body.style.transform = 'translate3d(' + shiftX + 'px,' + shiftY + 'px, 0px)' + 'scale(' + clampedScale/fontSizeAnchor + ')'; }; function saveCenter(clientMid) { // Try to preserve the pinching center after text reflow. // This is accurate to the HTML element level. focusElement = document.elementFromPoint(clientMid.x, clientMid.y); var rect = focusElement.getBoundingClientRect(); initClientMid = clientMid; focusPos = (initClientMid.y - rect.top) / (rect.bottom - rect.top); } function restoreCenter() { var rect = focusElement.getBoundingClientRect(); var targetTop = focusPos * (rect.bottom - rect.top) + rect.top + document.body.scrollTop - (initClientMid.y + shiftY); document.body.scrollTop = targetTop; } function endPinch() { pinching = false; document.body.style.transformOrigin = ''; document.body.style.transform = ''; document.documentElement.style.fontSize = clampedScale * baseSize + "px"; restoreCenter(); var img = document.getElementById('fontscaling-img'); if (!img) { img = document.createElement('img'); img.id = 'fontscaling-img'; img.style.display = 'none'; document.body.appendChild(img); } img.src = "/savefontscaling/" + clampedScale; } function touchSpan(e) { var count = e.touches.length; var mid = touchClientMid(e); var sum = 0; for (var i = 0; i < count; i++) { var dx = (e.touches[i].clientX - mid.x); var dy = (e.touches[i].clientY - mid.y); sum += Math.hypot(dx, dy); } // Avoid very small span. return Math.max(MIN_SPAN_LENGTH, sum/count); } function touchClientMid(e) { var count = e.touches.length; var sumX = 0; var sumY = 0; for (var i = 0; i < count; i++) { sumX += e.touches[i].clientX; sumY += e.touches[i].clientY; } return {x: sumX/count, y: sumY/count}; } function touchPageMid(e) { var clientMid = touchClientMid(e); return {x: clientMid.x - e.touches[0].clientX + e.touches[0].pageX, y: clientMid.y - e.touches[0].clientY + e.touches[0].pageY}; } return { handleTouchStart: function(e) { if (e.touches.length < 2) return; e.preventDefault(); var span = touchSpan(e); var clientMid = touchClientMid(e); if (e.touches.length > 2) { lastSpan = span; lastClientMid = clientMid; refreshTransform(); return; } scale = 1; shiftX = 0; shiftY = 0; pinching = true; fontSizeAnchor = parseFloat(getComputedStyle(document.documentElement).fontSize) / baseSize; var pinchOrigin = touchPageMid(e); document.body.style.transformOrigin = pinchOrigin.x + 'px ' + pinchOrigin.y + 'px'; saveCenter(clientMid); lastSpan = span; lastClientMid = clientMid; refreshTransform(); }, handleTouchMove: function(e) { if (!pinching) return; if (e.touches.length < 2) return; e.preventDefault(); var span = touchSpan(e); var clientMid = touchClientMid(e); scale *= touchSpan(e) / lastSpan; shiftX += clientMid.x - lastClientMid.x; shiftY += clientMid.y - lastClientMid.y; refreshTransform(); lastSpan = span; lastClientMid = clientMid; }, handleTouchEnd: function(e) { if (!pinching) return; e.preventDefault(); var span = touchSpan(e); var clientMid = touchClientMid(e); if (e.touches.length >= 2) { lastSpan = span; lastClientMid = clientMid; refreshTransform(); return; } endPinch(); }, handleTouchCancel: function(e) { if (!pinching) return; endPinch(); }, reset: function() { scale = 1; shiftX = 0; shiftY = 0; clampedScale = 1; document.documentElement.style.fontSize = clampedScale * baseSize + "px"; }, status: function() { return { scale: scale, clampedScale: clampedScale, shiftX: shiftX, shiftY: shiftY }; }, useFontScaling: function(scaling) { saveCenter({x: window.innerWidth/2, y: window.innerHeight/2}); shiftX = 0; shiftY = 0; document.documentElement.style.fontSize = scaling * baseSize + "px"; clampedScale = scaling; restoreCenter(); } }; }()); window.addEventListener('touchstart', pincher.handleTouchStart, false); window.addEventListener('touchmove', pincher.handleTouchMove, false); window.addEventListener('touchend', pincher.handleTouchEnd, false); window.addEventListener('touchcancel', pincher.handleTouchCancel, false); // Copyright 2014 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. // Applies DomDistillerJs to the content of the page and returns a // DomDistillerResults (as a javascript object/dict). (function(options, stringify_output) { try { function initialize() { // This include will be processed at build time by grit. (function () {var $gwt_version = "2.7.0";var $wnd = window;var $doc = $wnd.document;var $moduleName, $moduleBase;var $stats = $wnd.__gwtStatsEvent ? function(a) {$wnd.__gwtStatsEvent(a)} : null;var $strongName = '490FC1D3D3192E0BEEAC8D33AFF8C78A';var aa=2147483647,ba={3:1,12:1},ca={3:1,15:1,12:1},da={3:1,4:1},ea={3:1,5:1,6:1,4:1},ga={10:1,18:1,3:1,11:1,9:1},h={3:1,5:1,14:1,6:1,4:1,13:1},ha={46:1},ka={25:1},la={3:1,31:1},ma={3:1,11:1,9:1,29:1},na={3:1,5:1,4:1},_,oa,pa={};function qa(){}function ra(a){function b(){}b.prototype=a||{};return new b}function k(){} function n(a,b,c){var d=pa[a],e=d instanceof Array?d[0]:null;d&&!e?_=d:(_=pa[a]=b?ra(pa[b]):{},_.cM=c,_.constructor=_,!b&&(_.tM=qa));for(d=3;d>>0).toString(16);return a+b};_.toString=function(){return this.tS()};Ua={3:1,220:1,11:1,2:1};!Array.isArray&&(Array.isArray=function(a){return"[object Array]"===Object.prototype.toString.call(a)});function Va(a){return a.toString?a.toString():"[JavaScriptObject]"}function xa(a){return!Array.isArray(a)&&a.tM===qa}function r(a,b){return null!=a&&(va(a)&&!!Ua[b]||a.cM&&!!a.cM[b])} function ya(a){return Array.isArray(a)&&a.tM===qa}function va(a){return"string"===typeof a}function s(a){return null==a?null:a}function Wa(a){return~~Math.max(Math.min(a,aa),-2147483648)}var Ua;function Xa(a){if(null==a.n)if(a.B()){var b=a.c;b.C()?a.n="["+b.k:b.B()?a.n="["+b.w():a.n="[L"+b.w()+";";a.b=b.v()+"[]";a.j=b.A()+"[]"}else{var b=a.g,c=a.d,c=c.split("/");a.n=Ya(".",[b,Ya("$",c)]);a.b=Ya(".",[b,Ya(".",c)]);a.j=c[c.length-1]}}function Ta(a){Xa(a);return a.n}function Za(a){Xa(a);return a.j} function gb(){this.i=hb++;this.a=this.k=this.b=this.d=this.g=this.j=this.n=null}function ib(a){var b;b=new gb;b.n="Class$"+(a?"S"+a:""+b.i);b.b=b.n;b.j=b.n;return b}function t(a){var b;b=ib(a);jb(a,b);return b}function u(a,b){var c;c=ib(a);jb(a,c);c.f=b?8:0;c.e=b;return c}function kb(){var a;a=ib(null);a.f=2;return a}function w(a,b){var c=a.a=a.a||[];return c[b]||(c[b]=a.u(b))}function Ya(a,b){for(var c=0;!b[c]||""==b[c];)c++;for(var d=b[c++];ca||a>=b)throw new Mc("Index: "+a+", Size: "+b);}function Nc(a){if(null==a)throw new Oc;} function Pc(a,b){if(0>a||a>b)throw new Mc("Index: "+a+", Size: "+b);}function Qc(a,b){var c,d,e,f;a=""+a;c=new Rc;for(d=f=0;db?"ie10":-1!=a.indexOf("msie")&&9<=b&&11>b?"ie9":-1!=a.indexOf("msie")&&8<=b&&11>b?"ie8":-1!=a.indexOf("gecko")||11<=b?"gecko1_8":"unknown";if("safari"!==a)throw new Pe(a);}n(59,12,ba);t(59); n(22,59,ba);t(22);function Pe(a){this.e=""+("Possible problem with your *.gwt.xml module file.\nThe compile time user.agent value (safari) does not match the runtime user.agent value ("+a+").\nExpect more errors.");ac(this,this.e)}n(100,22,ba,Pe);t(100);n(60,1,{});_.tS=function(){return this.a};t(60);function Qe(){ac(this,this.e)}function Mc(a){Yb.call(this,a)}n(30,19,ca,Qe,Mc);t(30);function Re(){ac(this,this.e)}n(180,30,ca,Re);t(180);function Kc(a){Yb.call(this,a)}n(36,19,ca,Kc);t(36); function Se(){Se=k;Te=new Ue(!1);Ve=new Ue(!0)}function Ue(a){this.a=a}n(47,1,{3:1,47:1,11:1},Ue);_.t=function(a){var b=this.a;return b==a.a?0:b?1:-1};_.eQ=function(a){return r(a,47)&&a.a==this.a};_.hC=function(){return this.a?1231:1237};_.tS=function(){return""+this.a};_.a=!1;var Te,Ve;t(47);function We(a){this.a=a}n(37,1,{3:1,37:1,11:1},We);_.t=function(a){return this.a-a.a};_.eQ=function(a){return r(a,37)&&a.a==this.a};_.hC=function(){return this.a};_.tS=function(){return String.fromCharCode(this.a)}; _.a=0;var Xe=t(37);function Ye(){Ye=k;Ze=E(Xe,ea,37,128,0)}var Ze;n(75,1,{3:1,75:1});t(75);function $e(a){Yb.call(this,a)}n(23,19,{3:1,15:1,23:1,12:1},$e);t(23);function af(){ac(this,this.e)}n(181,19,ca,af);t(181);function bf(a){this.a=a}function cf(a){var b,c;return-129a?(b=a+128,c=(df(),ef)[b],!c&&(c=ef[b]=new bf(a)),c):new bf(a)}n(38,75,{3:1,11:1,38:1,75:1},bf);_.t=function(a){var b=this.a;a=a.a;return ba?1:0};_.eQ=function(a){return r(a,38)&&a.a==this.a};_.hC=function(){return this.a}; _.tS=function(){return""+this.a};_.a=0;var ff=t(38);function df(){df=k;ef=E(ff,ea,38,256,0)}var ef;function gf(a,b){return a>10&1023)&65535)+String.fromCharCode(d)):b=String.fromCharCode(b&65535);return a.lastIndexOf(b,c)}function rf(a,b,c,d,e){if(null==c)throw new Oc;if(0>b||0>d||0>=e||b+e>a.length||d+e>c.length)return!1;a=a.substr(b,e);c=c.substr(d,e);return a===c}function zf(a){var b=(160).toString(16),b="\\u"+"0000".substring(b.length)+b;return a.replace(RegExp(b,"g"),String.fromCharCode(32))} function Af(a,b){var c;c=Xf("");return a.replace(RegExp(b,"g"),c)}function Yf(a,b){var c;c=Xf("");return a.replace(RegExp(b),c)} function lg(a,b){for(var c=RegExp(b,"g"),d=[],e=0,f=a,g=null;;){var l=c.exec(f);if(null==l||""==f){d[e]=f;break}else d[e]=f.substring(0,l.index),f=f.substring(l.index+l[0].length,f.length),c.lastIndex=0,g==f&&(d[e]=f.substring(0,1),f=f.substring(1)),g=f,e++}if(0d&&(a[d]=null);return a};_.tS=function(){return Xg(this)};t(211);function Yg(a,b){var c,d,e;c=b.W();e=b.X();d=a.P(c);return!(s(e)===s(d)||null!=e&&ua(e,d))||null==d&&!a.N(c)?!1:!0} function Zg(a,b){var c,d,e;for(d=a.O().D();d.Q();)if(c=d.R(),e=c.W(),s(b)===s(e)||null!=b&&ua(b,e))return c;return null}function $g(a,b){return b===a?"(this Map)":""+b}function ah(a){return a?a.X():null}n(210,1,ha);_.M=function(a){return Yg(this,a)};_.N=function(a){return!!Zg(this,a)};_.eQ=function(a){var b;if(a===this)return!0;if(!r(a,46)||this.J()!=a.J())return!1;for(b=a.O().D();b.Q();)if(a=b.R(),!this.M(a))return!1;return!0};_.P=function(a){return ah(Zg(this,a))};_.hC=function(){return bh(this.O())}; _.J=function(){return this.O().J()};_.tS=function(){var a,b,c,d;d=new Ug("{");a=!1;for(c=this.O().D();c.Q();)b=c.R(),a?d.a+=", ":a=!0,d.a+=$g(this,b.W()),d.a+="\x3d",d.a+=$g(this,b.X());d.a+="}";return d.a};t(210);function ch(a,b){return va(b)?G(a,b):!!dh(a.a,b)}function eh(a,b){return va(b)?H(a,b):ah(dh(a.a,b))}function H(a,b){return null==b?ah(dh(a.a,null)):a.c.eb(b)}function G(a,b){return null==b?!!dh(a.a,null):void 0!==a.c.eb(b)}function fh(a,b,c){return va(b)?L(a,b,c):gh(a.a,b,c)} function L(a,b,c){return null==b?gh(a.a,null,c):a.c.hb(b,c)}function hh(a){ih();a.a=jh.bb();a.a.b=a;a.c=jh.cb();a.c.b=a;a.b=0;kh(a)}n(113,210,ha);_.N=function(a){return ch(this,a)};_.O=function(){return new lh(this)};_.P=function(a){return eh(this,a)};_.J=function(){return this.b};_.b=0;t(113);n(212,211,ka);_.eQ=function(a){if(a===this)a=!0;else if(r(a,25)&&a.J()==this.J())a:{var b;Nc(a);for(b=a.D();b.Q();)if(a=b.R(),!this.H(a)){a=!1;break a}a=!0}else a=!1;return a};_.hC=function(){return bh(this)}; t(212);function lh(a){this.a=a}n(63,212,ka,lh);_.H=function(a){return r(a,24)?Yg(this.a,a):!1};_.D=function(){return new mh(this.a)};_.J=function(){return this.a.b};t(63);function nh(a){if(a.a.Q())return!0;if(a.a!=a.b)return!1;a.a=a.c.a._();return a.a.Q()}function oh(a){if(a._gwt_modCount!=a.c._gwt_modCount)throw new ph;return y(nh(a)),a.a.R()}function mh(a){this.c=a;this.a=this.b=this.c.c._();this._gwt_modCount=a._gwt_modCount}n(64,1,{},mh);_.Q=function(){return nh(this)};_.R=function(){return oh(this)}; t(64);n(213,211,{31:1});_.S=function(){throw new Vg("Add not supported on this list");};_.F=function(a){this.S(this.J(),a);return!0};_.eQ=function(a){var b,c,d;if(a===this)return!0;if(!r(a,31)||this.J()!=a.J())return!1;d=a.D();for(b=this.D();b.Q();)if(a=b.R(),c=d.R(),!(s(a)===s(c)||null!=a&&ua(a,c)))return!1;return!0};_.hC=function(){var a,b,c;c=1;for(b=this.D();b.Q();)a=b.R(),c=31*c+(null!=a?Ga(a):0),c=~~c;return c};_.D=function(){return new M(this)}; _.U=function(){throw new Vg("Remove not supported on this list");};t(213);function qh(a){if(-1==a.c)throw new af;a.d.U(a.c);a.b=a.c;a.c=-1}function M(a){this.d=a}n(7,1,{},M);_.Q=function(){return this.bb)throw new Mc("fromIndex: "+b+" \x3c 0");if(c>d)throw new Mc("toIndex: "+c+" \x3e size "+d);if(b>c)throw new $e("fromIndex: "+b+" \x3e toIndex: "+c);this.c=a;this.a=b;this.b=c-b}n(50,213,{31:1},th);_.S=function(a,b){Pc(a,this.b);uh(this.c,this.a+a,b);++this.b};_.T=function(a){return sh(this,a)};_.U=function(a){z(a,this.b);a=this.c.U(this.a+a);--this.b;return a};_.J=function(){return this.b};_.a=0;_.b=0;t(50); function vh(a){a=new mh((new lh(a.a)).a);return new wh(a)}function xh(a){this.a=a}n(65,212,ka,xh);_.H=function(a){return ch(this.a,a)};_.D=function(){return vh(this)};_.J=function(){return this.a.b};t(65);function wh(a){this.a=a}n(114,1,{},wh);_.Q=function(){return nh(this.a)};_.R=function(){return oh(this.a).W()};t(114);function yh(a,b){var c;c=a.d;a.d=b;return c}n(48,1,{48:1,24:1});_.eQ=function(a){return r(a,24)?zh(this.c,a.W())&&zh(this.d,a.X()):!1};_.W=function(){return this.c};_.X=function(){return this.d}; _.hC=function(){return Ah(this.c)^Ah(this.d)};_.Y=function(a){return yh(this,a)};_.tS=function(){return this.c+"\x3d"+this.d};t(48);function Bh(a,b){this.c=a;this.d=b}n(49,48,{48:1,49:1,24:1},Bh);t(49);n(216,1,{24:1});_.eQ=function(a){return r(a,24)?zh(this.W(),a.W())&&zh(this.X(),a.X()):!1};_.hC=function(){return Ah(this.W())^Ah(this.X())};_.tS=function(){return this.W()+"\x3d"+this.X()};t(216);function Ch(a,b){var c;c=Dh(a,b.W());return!!c&&zh(c.d,b.X())}n(218,210,ha); _.M=function(a){return Ch(this,a)};_.N=function(a){return!!Dh(this,a)};_.O=function(){return new Eh(this)};_.P=function(a){return ah(Dh(this,a))};t(218);function Eh(a){this.a=a}n(97,212,ka,Eh);_.H=function(a){return r(a,24)&&Ch(this.a,a)};_.D=function(){return new Fh(this.a)};_.J=function(){return this.a.c};t(97);function Gh(a){a=new Fh((new Hh(a.a)).a);return new Ih(a)}function Jh(a){this.a=a}n(192,212,ka,Jh);_.H=function(a){return!!Dh(this.a,a)};_.D=function(){return Gh(this)};_.J=function(){return this.a.c}; t(192);function Ih(a){this.a=a}n(193,1,{},Ih);_.Q=function(){return this.a.a.Q()};_.R=function(){return this.a.a.R().W()};t(193);function Kh(a,b){var c;c=Lh(a,b);try{return y(c.b!=c.d.c),c.c=c.b,c.b=c.b.a,++c.a,c.c.c}catch(d){d=Cc(d);if(r(d,55))throw new Mc("Can't get element "+b);throw Dc(d);}}n(214,213,{31:1});_.S=function(a,b){var c;c=Lh(this,a);Mh(c.d,b,c.b.b,c.b);++c.a;c.c=null};_.T=function(a){return Kh(this,a)};_.D=function(){return Lh(this,0)}; _.U=function(a){var b,c;b=Lh(this,a);try{return c=(y(b.b!=b.d.c),b.c=b.b,b.b=b.b.a,++b.a,b.c.c),Nh(b),c}catch(d){d=Cc(d);if(r(d,55))throw new Mc("Can't remove element "+a);throw Dc(d);}};t(214);function Oh(a){a.b=E(lb,da,1,0,3)}function uh(a,b,c){Pc(b,a.b.length);a.b.splice(b,0,c)}function P(a,b){a.b[a.b.length]=b;return!0}function Ph(a,b){var c;c=b.K();if(0==c.length)return!1;Ne(c,0,a.b,a.b.length,c.length,!1);return!0}function N(a,b){z(b,a.b.length);return a.b[b]} function Qh(a,b){for(var c=0;cd&&(b[d]=null);return b}function Q(){Oh(this)}function Uh(a){Oh(this);a=Ie(a.b,a.b.length);Ne(a,0,this.b,0,a.length,!1)}n(8,213,la,Q,Uh);_.S=function(a,b){uh(this,a,b)};_.F=function(a){return P(this,a)}; _.G=function(a){return Ph(this,a)};_.H=function(a){return-1!=Qh(this,a)};_.T=function(a){return N(this,a)};_.I=function(){return 0==this.b.length};_.U=function(a){return Rh(this,a)};_.J=function(){return this.b.length};_.K=function(){return Ie(this.b,this.b.length)};_.L=function(a){return Th(this,a)};t(8); function Vh(a,b,c,d,e,f){var g,l,m;if(7>d-c)for(a=c,g=a+1;ga&&0>1),Vh(b,a,l,m,-e,f),Vh(b,a,m,g,-e,f),0>=f.Z(a[m-1],a[m]))for(;c=g||e=f.Z(a[e],a[l])?b[c++]=a[e++]:b[c++]=a[l++]}function bh(a){var b,c;c=0;for(b=a.D();b.Q();)a=b.R(),c+=null!=a?Ga(a):0,c=~~c;return c}function Wh(){Wh=k;Xh=new Yh}var Xh; function Zh(a,b){Nc(a);Nc(b);return va(a)?a==b?0:a=a.b>>1)for(d=a.c,c=a.b;c>b;--c)d=d.b;else for(d=a.a.a,c=0;ca||a>=b)throw new Re;}n(124,213,la);_.S=function(a,b){hk(a,this.a.b.length+1);uh(this.a,a,b)};_.F=function(a){return P(this.a,a)};_.G=function(a){return Ph(this.a,a)};_.H=function(a){return-1!=Qh(this.a,a)};_.T=function(a){return hk(a,this.a.b.length),N(this.a,a)};_.I=function(){return 0==this.a.b.length}; _.D=function(){return new M(this.a)};_.U=function(a){return hk(a,this.a.b.length),this.a.U(a)};_.J=function(){return this.a.b.length};_.K=function(){var a=this.a;return Ie(a.b,a.b.length)};_.L=function(a){return Th(this.a,a)};_.tS=function(){return Xg(this.a)};t(124);function ik(a){var b;b=a.a.b.length;if(0c?0:1;d=d.a[c]}return null}function kk(a,b,c,d,e,f,g,l){var m;if(d){(m=d.a[0])&&kk(a,b,c,m,e,f,g,l);m=d.c;var q,v;c.kb()&&(q=Zh(m,e),0>q||!f&&0==q)||c.lb()&&(v=Zh(m,g),0e?0:1;b.a[e]=lk(a,b.a[e],c,d);mk(b.a[e])&&(mk(b.a[1-e])?(b.b=!0,b.a[0].b=!1,b.a[1].b=!1):mk(b.a[e].a[e])?b=nk(b,1-e):mk(b.a[e].a[1-e])&&(b=(f=1-(1-e),b.a[f]=nk(b.a[f],f),nk(b,1-e))))}else return c;return b}function mk(a){return!!a&&a.b}function nk(a,b){var c,d;c=1-b;d=a.a[c];a.a[c]=d.a[b];d.a[b]=a;a.b=!0;d.b=!1;return d}function ok(){var a=null;this.b=null;!a&&(a=(Wh(),Wh(),Xh));this.a=a} n(96,218,{3:1,46:1},ok);_.O=function(){return new Hh(this)};_.J=function(){return this.c};_.c=0;t(96);function Fh(a){var b=(pk(),qk),c;c=new Q;kk(a,c,b,a.b,null,!1,null,!1);this.a=new rh(c,0)}n(74,1,{},Fh);_.Q=function(){return this.a.Q()};_.R=function(){return this.a.R()};t(74);function Hh(a){this.a=a}n(98,97,ka,Hh);t(98);function rk(a,b){Bh.call(this,a,b);this.a=E(sk,da,58,2,0);this.b=!0}n(58,49,{48:1,49:1,24:1,58:1},rk);_.b=!1;var sk=t(58);function tk(){}n(188,1,{},tk); _.tS=function(){return"State: mv\x3d"+this.c+" value\x3d"+this.d+" done\x3d"+this.a+" found\x3d"+this.b};_.a=!1;_.b=!1;_.c=!1;t(188);function pk(){pk=k;qk=new uk("All",0);vk=new wk;xk=new yk;zk=new Ak}function uk(a,b){C.call(this,a,b)}n(29,9,ma,uk);_.kb=function(){return!1};_.lb=function(){return!1};var qk,vk,xk,zk,Bk=u(29,function(){pk();return D(w(Bk,1),ea,29,0,[qk,vk,xk,zk])});function wk(){C.call(this,"Head",1)}n(189,29,ma,wk);_.lb=function(){return!0};u(189,null); function yk(){C.call(this,"Range",2)}n(190,29,ma,yk);_.kb=function(){return!0};_.lb=function(){return!0};u(190,null);function Ak(){C.call(this,"Tail",3)}n(191,29,ma,Ak);_.kb=function(){return!0};u(191,null);function Ck(a){this.a=new ok;Wg(this,a)}n(90,212,{3:1,25:1},Ck);_.F=function(a){var b=this.a,c=(Se(),Te);a=new rk(a,c);c=new tk;b.b=lk(b,b.b,a,c);c.b||++b.c;b.b.b=!1;return null==c.d};_.H=function(a){return!!Dh(this.a,a)};_.D=function(){return Gh(new Jh(this.a))};_.J=function(){return this.a.c}; t(90); function Dk(a){var b;if(!(0Gk.Bb(f)&&(f=c.replace(RegExp("[^\\|\\-]*[\\|\\-](.*)","gi"),"$1"));else if(-1!=f.indexOf(": "))f=c.replace(RegExp(".*:(.*)","gi"),"$1"),3>Gk.Bb(f)&&(f=c.replace(RegExp("[^:]*[:](.*)","gi"),"$1")); else if(e&&(150f.length)){f=e.getElementsByTagName("H1");e="";for(d=0;d=Gk.Bb(f)&&(f=c);c=f}else c="";dk(b,c);p==p&&dk(a.a,$doc.title)}}function Ik(a){var b,c;this.b=a;this.a=new gk;this.e=(b={},b[6]=[],b);this.d=(c={},c);b=U();this.f=new Jk(a,this.e);a=U()-b;if(void 0==a)throw new TypeError;this.e[1]=a;this.g=""}n(102,1,{},Ik);t(102);function Kk(){}n(103,1,{},Kk);t(103); function Lk(a){var b,c,d,e,f,g,l,m,q,v,I,mb,Aa,ki,jn,li,ia,lf,mf,kn,ln;v=U();var mn=$doc.documentElement.textContent,nn,on;Fk();Gk=(nn=RegExp("[\\u3040-\\uA4CF]","g"),on=RegExp("[\\uAC00-\\uD7AF]","g"),nn.test(mn)?new Mk:on.test(mn)?new Nk:new Ok);m=(ki={},ki[10]=[],ki);c=new Ik($doc.documentElement);var rn=(Dk(c),Kh(c.a,0));if(void 0==rn)throw new TypeError;m[1]=rn;var ni;if(void 0!=a[2]){if(void 0===a[2])throw new TypeError;ni=a[2]}else ni=0;Pk=ni;V("DomDistiller debug level: "+Pk);b=(jn={},jn); var oi;if(oi=void 0!=a[1]){if(void 0===a[1])throw new TypeError;oi=a[1]}var sn=oi,lc,Vd,pi,Wd,Uc,qi,Xd,tn,nf,Vc;Uc=U();qi=new Kk;Xd=new Qk;tn=c.b.querySelectorAll('meta[name\x3d"viewport"][content*\x3d"width\x3ddevice-width"]');nf=new Rk(Xd);nf.i=0=$d.c?!be||0.555556>=be.c?16>=$d.d?!ae||15>=ae.d?!be||4>=be.d?$a=!1:$a=!0:$a=!0:$a=!0:40>=$d.d?!ae||17>=ae.d?$a=!1:$a=!0:$a=!0:$a=!1,el($d, $a));O=uf}$k(J,O,"Classification Complete");var hr=(fl(),gl),zi,Hn,Ai,vf,In,wf,pb;zi=!1;pb=new M(J.a);a:for(;pb.bCi.b.length)O=!1;else{yf=!1;ce=new rh(Ci,0);for(qb=ce.R();ce.Q();)if(Ea=qb,qb=ce.R(),S(Ea.b,"de.l3s.boilerpipe/HEADING")&&!(S(Ea.b,"STRICTLY_NOT_CONTENT")||S(qb.b,"STRICTLY_NOT_CONTENT")||S(Ea.b,"de.l3s.boilerpipe/TITLE")||S(qb.b,"de.l3s.boilerpipe/TITLE")))if(qb.a){yf=!0;Mn=Ea.a;Yk(Ea,qb);qb=Ea;ce.V();var Nn=Ea;S(Nn.b,"de.l3s.boilerpipe/HEADING")&&Nn.b.a.c.ib("de.l3s.boilerpipe/HEADING");Mn||R(Ea.b,"BOILERPLATE_HEADING_FUSED")}else Ea.a&&(yf=!0,el(Ea,!1));O=yf}$k(J, O,"HeadingFusion");O=kl((ll(),ml),J);$k(J,O,"BlockProximityFusion: Distance 1");var ir=(nl(),ol),Di,ab,Ei,Tn;Tn=J.a;Di=!1;for(ab=new M(Tn);ab.bXc.b.length)O=!1;else{Gi=-1;rb=null;Fi=0;de=-1;for(tb=new M(Xc);tb.bGi&&(rb=Ka,Gi=Hi,de=Fi)),++Fi;for(sb=new M(Xc);sb.bEf&&S(cb.b,"de.l3s.boilerpipe/MIGHT_BE_CONTENT")&&S(cb.b,"de.l3s.boilerpipe/LI")&&0==cb.c?(el(cb,!0),Mi=!0):Ef=aa;O=Mi;$k(J,O,"List at end filter");var mr=c.d,Ni,Ff,Oi,Bb;Ff=0;for(Bb=new M(J.a);Bb.bPk||(Mf?V("FINAL SCORE: "+he+" : "+A(Mf,"src")):V("Null image attempting to be scored!"));ge=he}else ge=0;26<=ge&&(!fe||Qi=ie.a.b.length)&&(Si=ie.a.b.length-1),io=Ti.p,Ti.p=Ma,je.p=Ma,Ma=io)):Ma||(Ma=Nf.p);var jo=U()-Uc;if(void 0==jo)throw new TypeError;c.e[3]=jo;Uc=U();var Ui,Kb,cd;cd=new Tg;for(Kb=new M(lc.a.a);Kb.b=mo[6].length)throw new RangeError;Vd=mo[6][Wd];if(void 0===Vd[1])throw new TypeError; var sr="Timing: "+Vd[1]+" \x3d ";if(void 0===Vd[2])throw new TypeError;V(sr+Vd[2])}var no=c.e;if(void 0===no[1])throw new TypeError;var tr="Timing: MarkupParsingTime \x3d "+no[1]+"\nTiming: DocumentConstructionTime \x3d ",oo=c.e;if(void 0===oo[2])throw new TypeError;var ur=tr+oo[2]+"\nTiming: ArticleProcessingTime \x3d ",po=c.e;if(void 0===po[3])throw new TypeError;var vr=ur+po[3]+"\nTiming: FormattingTime \x3d ",qo=c.e;if(void 0===qo[4])throw new TypeError;V(vr+qo[4])}if(void 0==pi)throw new TypeError; b[1]=pi;if(void 0==b)throw new TypeError;m[2]=b;var ro=((null==c.g||!c.g.length)&&(c.g="auto"),c.g);if(void 0==ro)throw new TypeError;m[9]=ro;for(Aa=new M(c.c);Aa.bZf.b.b.length)){for(var T=Zf.b,yr=0>Zf.a,bg=$f,zr=oc.a?oc.a.d:"",gd=void 0,zo=void 0,Nb=void 0,gd=0,Nb=new M(T);Nb.b=oe)cg=null;else{ij="";me=new Rl; eg=E(Sl,da,69,T.b.length,0);for(qc=0;qcnd.a.b.length||1==N(nd.a,0).a||xj.length>=N(nd.a,0).b.length)ve=!1;else{for(od=0;od10-pd?0:10-pd,W(K,"score\x3d"+x.d+": linktxt is a num ("+pd+")"));for(var vc=g,wc=ja,ap=K,xc=void 0,vg=void 0,wg=void 0,xc=Bj.length;xc=g)for(f=0;fd?d=!1:(d/=e.height,d=1.3<=d&&3>=d));d&&(d=new Wm,d.e=e.src,d.a=b,d.f=e.width,d.b=e.height,P(this.f,d))}}return Th(this.f,E(Xm,da,27,this.f.b.length, 0))};_.tb=function(){if(null==this.i){var a,b,c;this.i="";a=this.j.getElementsByTagName("*");for(c=0;cc?-1:1,c!=d.a?0!=d.a&&(d=(e=new an,P(a.a,e),e),0!=c&&P(d.b,a.b)):0==c&&(d.b.b=E(lb,da,1,0,3)),P(d.b,b),a.b=b,d.a=c)}function bn(){this.a=new Q}n(125,1,{},bn);_.b=null;t(125);function an(){this.b=new Q}n(82,1,{},an);_.a=0;t(82);function Bm(a){this.b=new cn(a);this.a=new Q;this.d=new Q}n(182,1,{},Bm); _.mb=function(){this.a.U(this.a.b.length-1);this.d.U(this.d.b.length-1)};_.nb=function(a){if(!this.b.a)return!1;P(this.a,a);P(this.d,null);1==this.d.b.length&&(this.c=new dn(a),Sh(this.d,0,this.c));if(en(this.b,a))for(a=0;aa.b.length)return null;c=(z(0,a.b.length),a.b[0]);b=(z(1,a.b.length),a.b[1]);if(d=2==a.b.length)d=c.a,e=b.a,d=4<(d>e?d:e);if(d)return null;d=b.a-c.a;if(0==d)return null;b=~~((b.b-c.b)/d);if(0==b)return null;c=c.b-b*c.a;if(0!=c&&c!=-b)return null;for(d=2;d=f||f>=b.b.length-1)return q;c=(z(f,b.b.length),b.b[f]).a;(z(f-1,b.b.length),b.b[f-1]).a==c-1&&(z(f+1,b.b.length),b.b[f+1]).a==c+1&&(q.b=!0,q.c=(z(f+1,b.b.length),b.b[f+1]).b);return q}if((0==e||1==e)&&1==(z(0,b.b.length),b.b[0]).a&&2==(z(1,b.b.length), b.b[1]).a||2==e&&3==(z(2,b.b.length),b.b[2]).a&&kf((z(1,b.b.length),b.b[1]).b)&&!kf((z(0,b.b.length),b.b[0]).b))return q.b=!0,q;f=b.b.length;if((c==f-1||c==f-2)&&(z(f-2,b.b.length),b.b[f-2]).a+1==(z(f-1,b.b.length),b.b[f-1]).a)return q.b=!0,q;for(e+=1;e=a.b.length)return!1;c=(z(0,a.b.length),a.b[0]);if(1!=c.a&&!c.b.length)return!1;d=!1;for(f=new M(a);f.b=l.length||(l=l[1],m=lo.exec(l),l=-1,m&&1= l?(Nl(a.a,new Ql(l,"")),g=!0):Ll(a.a))}b=g}else Ll(a.a),b=!1;if(d||!b)return!1;break;case 1:if(b=f,F("A",b.tagName)){if(d)return!1;++a.c;(b=Kl(a,b,e))?(Nl(a.a,b.a),b=!0):(Ll(a.a),b=!1);if(!b)return!1;break}default:if(!f.hasChildNodes())break;c=!0;d?f=f.lastChild:f=f.firstChild}return Ml(a,f,c,d,e)} function Kl(a,b,c){var d,e,f,g;if(!nm(b))return null;g=Hk(b.innerText);g=Af(g,"[()\\[\\]{}]");g=ug(g);g=so(g);if(!(0<=g&&100>=g))return null;d=A(b,"href");d.length?(c.setAttribute("href",d),f=c.href):f="";d=!f.length;e=!1;c=null;if(!d){e="javascript:"===f.substr(0,11);c=Hl(f);if(!c||!e&&!F(c.d.host,a.d.d.host))return null;c.d.hash=""}if(!(a=d||e)){b=getComputedStyle(b,null);b=b.cursor.toUpperCase();Tc();a=(Ge(),He);Nc(b);a=a[":"+b];b=D(w(lb,1),da,1,3,[b]);if(!a)throw new $e(Qc("Enum constant undefined: %s", b));a=a==(Tc(),Qd)}return a?new to(g,""):new to(g,Va(c.d).replace(Gl,""))}function Fl(a){this.a=new bn;this.e=a}n(108,1,{},Fl);_.b="";_.c=0;_.d=null;var Gl,fo=null,lo=null,go=null;t(108);function to(a,b){this.a=new Ql(a,b)}n(79,1,{},to);t(79); function lm(){lm=k;sm=RegExp("(next|weiter|continue|\x3e([^\\|]|$)|\u00bb([^\\|]|$))","i");wm=RegExp("(prev|early|old|new|\x3c|\u00ab)","i");xm=/article|body|content|entry|hentry|main|page|pagination|post|text|blog|story/i;vm=RegExp("combx|comment|com-|contact|foot|footer|footnote|masthead|media|meta|outbrain|promo|related|shoutbox|sidebar|sponsor|shopping|tags|tool|widget","i");pm=RegExp("print|archive|comment|discuss|e[\\-]?mail|share|reply|all|login|sign|single|as one|article|post|\u7bc7","i"); tm=/pag(e|ing|inat)/i;ym=/p(a|g|ag)?(e|ing|ination)?(=|\/)[0-9]{1,2}$/i;um=/(first|last)/i;om=/\/?(#.*)?$/;qm=/\d/;mm=new Yi}function W(a,b){var c;3>Pk||(c="",ch(mm,a)&&(c=eh(mm,a)),!c.length||(c+="; "),fh(mm,a,c+b))}function Il(a){lm();var b,c;c=$doc.implementation.createHTMLDocument();b=c.createElement("base");b.href=a;(c.head||c.getElementsByTagName("head")[0]).appendChild(b);a=c.createElement("a");c.body.appendChild(a);return a} function Jl(a,b){lm();var c,d;d=a.getElementsByTagName("BASE");if(0==d.length)return b;c=Il(b);d=A(d[0],"href");c.setAttribute("href",d);return c.href}var pm,um,om,ym,vm,sm,qm,tm,xm,wm,mm;function rm(a,b,c){this.b=a;this.d=0;this.c=b;this.a=c}n(109,1,{},rm);_.b=-1;_.d=0;t(109);function wo(a){var b;null==a.a&&(b=(null==a.c&&(a.c=Wl(a.d)),a.c),b.length?a.a=(Fk(),lg(b,"\\/")):a.a=E(p,h,2,0,4));return a.a}function xo(a){this.d=a} function Hl(a){var b;try{b=new URL(a)}catch(c){b=null}return b?new xo(b):null}n(69,1,{69:1},xo);_.tS=function(){return Va(this.d)};_.a=null;_.b=null;var gm=_.c=null,Sl=t(69);function Wl(a){a=a.pathname.replace(/;.*$/,"");a=a.replace(/^\//,"");return a.replace(/\/$/,"")}function Do(a){var b,c;if(2>a.b)return!1;c=wo(a.g);if(4!=c[a.b].length)return!1;b=so(c[a.b-1]);return 0=b&&(a=so(c[a.b-2]),1970a)?!0:!1} function Eo(a,b){var c,d,e,f;f=b.length;e=f-a.f.length;if(!og(b,a.e))return!1;c=a.c;for(d=gf(a.d,e);cd?(e=(Ye(),Ze)[d],!e&&(e=Ze[d]=new We(d)),d=e):d=new We(d),d=/[-_;,]/.test(d);if(d||c+a.f.length==f)return!0}else if(c==a.d&&0<=so(b.substr(c,e-c)))return!0;return!1} function Zl(a,b,c,d){var e;a=Va(a.d);a:{if(47==a.charCodeAt(c-1)&&bb)throw new $e("Value in path component is an invalid number: "+e);d=a.substr(0,c)+"[*!]"+a.substr(d,a.length-d);this.g=Hl(d);if(!this.g)throw new $e("Invalid URL: "+ d);this.i=d;this.a=b;this.d=c;this.c=pf(this.i,47,this.d);c=wo(this.g);for(this.b=0;this.be||47!=a.charCodeAt(this.c)?!1:0<=so(tg(a,this.c+1,d))):a=!1}else a=Eo(this,a);return a}; _.Ab=function(a){var b,c;b=wo(a).length;c=wo(this.g).length;if(b>c)return!1;if(1==b&&1==c){c=wo(a)[0];a=wo(this.g)[0];var d;if(c.length&&a.length)for(d=gf(c.length,a.length),b=0;bd&&g>d&&c.charCodeAt(f)==a.charCodeAt(g);--f,--g,e++);return 2*(e+b)>=c.length}a:{e=wo(a);d=wo(this.g);b=!1;for(c=a=0;ae)throw new $e("Query value is an invalid number: "+d);b=(b?"?":"\x26")+c+"\x3d";a=a.d.href.replace(b+d,b+"[*!]");this.i=Hl(a);if(!this.i)throw new $e("Invalid URL: "+a);this.j=a;this.a=e;this.c=a.indexOf("[*!]");this.e=pf(this.j, 63,this.c-1);this.b=pf(this.j,38,this.c-1);-1==this.b&&(this.b=this.e);!Mo&&(Mo=/\/$/);this.d=tg(this.j,0,this.b).replace(Mo,"");e=this.j.length;this.g=e-this.c-4;0!=this.g&&(this.f=pg(this.j,e-this.g+1))}n(183,1,{},Tl); _.zb=function(a){var b,c;if(0!=this.g&&!jf(a,this.f))return!1;c=a.length-this.g;if(!og(a,this.d))return!1;if(this.b==c||c==this.b-1&&47==this.j.charCodeAt(c))return!0;b=tg(a,this.b,c).toLowerCase();!No&&(No=/^\/|(.html?)$/i);return No.test(b)?!0:rf(a,this.b,this.j,this.b,this.c-this.b)?0<=so(tg(a,this.c,c)):!1};_.Ab=function(a){a=(null==a.c&&(a.c=Wl(a.d)),a.c);var b=this.i;null==b.c&&(b.c=Wl(b.d));return F(a,b.c)};_.tS=function(){return this.j};_.a=0;_.b=0;_.c=0;_.e=0;_.f="";_.g=0; var Mo=null,No=null;t(183); function Oo(){Oo=k;Uo=new Yi;L(Uo,"http://schema.org/ImageObject",(Vo(),Wo));L(Uo,"http://schema.org/Article",Xo);L(Uo,"http://schema.org/BlogPosting",Xo);L(Uo,"http://schema.org/NewsArticle",Xo);L(Uo,"http://schema.org/ScholarlyArticle",Xo);L(Uo,"http://schema.org/TechArticle",Xo);L(Uo,"http://schema.org/Person",Yo);L(Uo,"http://schema.org/Organization",Zo);L(Uo,"http://schema.org/Corporation",Zo);L(Uo,"http://schema.org/EducationalOrganization",Zo);L(Uo,"http://schema.org/GovernmentOrganization",Zo); L(Uo,"http://schema.org/NGO",Zo);$o=new Yi;L($o,"IMG","SRC");L($o,"AUDIO","SRC");L($o,"EMBED","SRC");L($o,"IFRAME","SRC");L($o,"SOURCE","SRC");L($o,"TRACK","SRC");L($o,"VIDEO","SRC");L($o,"A","HREF");L($o,"LINK","HREF");L($o,"AREA","HREF");L($o,"META","CONTENT");L($o,"TIME","DATETIME");L($o,"OBJECT","DATA");L($o,"DATA","VALUE");L($o,"METER","VALUE")}function pp(a){var b,c,d;b=new Q;for(c=0;cf||0>m||0>l||f+l>Aa||m+l>q)throw new Qe;if(0!=(I.f&1)&&0==(I.f&4)||mb==v)0m;)g[l]=a[--f];else for(l=m+l;md?-1:c=g.length)return Op(Yp,"",(Y(),Sp));c=null;for(d=b=0;db&&(b=e.length,c=e);d=c;if(!d||1>=d.length)return Op(Zp,"",(Y(),Sp));if((c=a.caption)&&Np(c)||a.tHead||a.tFoot||Mp(f,Hp))return Op($p, "",(Y(),Up));c=new Q;for(e=new M(f);e.b0.95*b){m=!1;b=e.getElementsByTagName("META"); for(l=0;l=c.b.length)return Op(iq,"",(Y(),Sp));if(Mp(f,Ip))return Op(jq,"",(Y(),Sp));f=(e.offsetHeight||0)|0;return 00.9*f?Op(kq,"",(Y(),Sp)):Op(lq,"",(Y(),Up))}var Lp,Kp,Jp,Hp,Ip; function Qp(){Qp=k;Rp=new Z("INSIDE_EDITABLE_AREA",0);Tp=new Z("ROLE_TABLE",1);Vp=new Z("ROLE_DESCENDANT",2);Wp=new Z("DATATABLE_0",3);$p=new Z("CAPTION_THEAD_TFOOT_COLGROUP_COL_TH",4);aq=new Z("ABBR_HEADERS_SCOPE",5);bq=new Z("ONLY_HAS_ABBR",6);cq=new Z("MORE_95_PERCENT_DOC_WIDTH",7);dq=new Z("SUMMARY",8);Xp=new Z("NESTED_TABLE",9);Yp=new Z("LESS_EQ_1_ROW",10);Zp=new Z("LESS_EQ_1_COL",11);eq=new Z("MORE_EQ_5_COLS",12);fq=new Z("CELLS_HAVE_BORDER",13);gq=new Z("DIFFERENTLY_COLORED_ROWS",14);hq=new Z("MORE_EQ_20_ROWS", 15);iq=new Z("LESS_EQ_10_CELLS",16);jq=new Z("EMBED_OBJECT_APPLET_IFRAME",17);kq=new Z("MORE_90_PERCENT_DOC_HEIGHT",18);lq=new Z("DEFAULT",19);mq=new Z("UNKNOWN",20)}function Z(a,b){C.call(this,a,b)}n(16,9,{3:1,11:1,9:1,16:1},Z);var aq,$p,fq,Wp,lq,gq,jq,Rp,iq,Zp,Yp,kq,cq,hq,eq,Xp,bq,Vp,Tp,dq,mq,nq=u(16,function(){Qp();return D(w(nq,1),ea,16,0,[Rp,Tp,Vp,Wp,$p,aq,bq,cq,dq,Xp,Yp,Zp,eq,fq,gq,hq,iq,jq,kq,lq,mq])});function Y(){Y=k;Up=new oq("DATA",0);Sp=new oq("LAYOUT",1)} function oq(a,b){C.call(this,a,b)}n(56,9,{3:1,11:1,9:1,56:1},oq);var Up,Sp,pq=u(56,function(){Y();return D(w(pq,1),ea,56,0,[Up,Sp])});function qq(a,b){var c;c=rq(b);a.appendChild(c);return c}function rq(a){var b;b=a.cloneNode(!1);1==a.nodeType&&(a=getComputedStyle(a,null).direction,!a.length&&(a="auto"),b.setAttribute("dir",a));return b}function sq(a,b){var c;c=a.parentNode;c||(c=rq(b),c.appendChild(a));return c}function tq(a){return tl(N(a.j,N(a.i,0).a))}function uq(a,b){return S(a.b,b)} function Yk(a,b){a.g+="\n";a.g+=b.g;a.d+=b.d;a.e+=b.e;a.c=0==a.d?0:a.e/a.d;a.a|=b.a;Ph(a.i,b.i);a.b.G(b.b);a.f=gf(a.f,b.f)}function el(a,b){if(b==a.a)return!1;a.a=b;return!0}function vq(a){var b;b="["+(N(a.j,N(a.i,0).a).j+"-"+N(a.j,N(a.i,a.i.b.length-1).a).j+";");b+="tl\x3d"+a.f+";";b+="nw\x3d"+a.d+";";b+="ld\x3d"+a.c+";";b=b+"]\t"+((a.a?"\u001b[0;32mCONTENT":"\u001b[0;35mboilerplate")+"\u001b[0m,");b+="\u001b[1;30m"+Xg(new Ck(a.b))+"\u001b[0m";return b+="\n"+a.g} function Xk(a,b){var c,d;this.j=a;this.i=new Q;P(this.i,cf(b));c=N(this.j,b);this.b=(d=c.e,c.e=new dj,d);this.d=c.i;this.e=c.g;this.f=c.n;this.g=c.o;this.c=0==this.d?0:this.e/this.d}n(72,1,{},Xk);_.tS=function(){return vq(this)};_.a=!1;_.c=0;_.d=0;_.e=0;_.f=0;t(72);function Zk(a){this.a=a}n(81,1,{},Zk);t(81);function wq(){wq=k;xq=new dj;R(xq,"IMG");R(xq,"PICTURE");R(xq,"FIGURE");yq=D(w(p,1),h,2,4,["data-src","data-original","datasrc","data-url"])} function zq(a){var b;b=$doc.createElement("FIGCAPTION");b.textContent=a.innerText||"";return b}function Aq(a,b){var c,d,e;if(!S(xq,b.tagName))return null;a.b="";c="IMG"==b.tagName?b:Im(b,"IMG");if("FIGURE"===b.tagName){d=Im(b,"PICTURE");!d&&(d=Im(b,"IMG"));if(!d)return null;Bq(a,c);(c=Im(b,"FIGCAPTION"))?(e=c.querySelectorAll("A[HREF]"),c=0b.length)return null;b=A(b[0],"data-tweet-id");return b.length?new Iq(a,"twitter",b,null):null}function Kq(){Fq()}n(133,1,{},Kq);_.Cb=function(a){var b;a&&S(Gq,a.tagName)?(b=null,"BLOCKQUOTE"===a.tagName?b=Hq(a):"IFRAME"===a.tagName&&(b=Jq(a)),b&&2<=Pk&&(V("Twitter embed extracted:"),V(" ID: "+b.b)),a=b):a=null;return a};_.Db=function(){return Gq}; var Gq;t(133);function Lq(){Lq=k;Mq=new dj;R(Mq,"IFRAME")}function Nq(a){var b,c;if(!a||!S(Mq,a.tagName))return null;c=a.src;if(!Nm(c,"player.vimeo.com"))return null;b=$doc.createElement("a");b.href=c;c=Sc(b,"pathname");b=Sm(pg(Sc(b,"search"),1));a:{var d;d=lg(c,"/");for(c=d.length-1;0<=c&&"video"!==d[c];c--)if(0c&&(c=d.indexOf("\x26"));0>c&&(c=d.length);b=d.substr(0,c);d=Sm(d.substr(c+1,d.length-(c+1)));a:{c=lg(b,"/");for(b=c.length-1;0<=b&&"embed"!==c[b];b--)if(0Pk))if(b){V("\u001b[0;34m\x3c\x3c\x3c\x3c\x3c "+c+" \x3e\x3e\x3e\x3e\x3e");if(!(1>Pk)){b="";for(c=new M(a.a);c.bc.b.length)return!1;d=!1;g=(z(0,c.b.length),c.b[0]);for(f=new rh(c,1);f.b=e?(e=!0,a.a?g.f!=c.f&&(e=!1):S(c.b,"BOILERPLATE_HEADING_FUSED")&&(e=!1),S(g.b,"STRICTLY_NOT_CONTENT")!=S(c.b,"STRICTLY_NOT_CONTENT")&&(e=!1),S(g.b,"de.l3s.boilerpipe/TITLE")!=S(c.b,"de.l3s.boilerpipe/TITLE")&&(e=!1),!g.a&&S(g.b,"de.l3s.boilerpipe/LI")&&!S(c.b,"de.l3s.boilerpipe/LI")&& (e=!1),e?(Yk(g,c),qh(f),d=!0):g=c):g=c):g=c;return d}function Tq(a){this.a=a}n(85,1,{},Tq);_.tS=function(){return Xa(Uq),Uq.n+": postFiltering\x3d"+this.a};_.a=!1;var pl,ml,Uq=t(85);function Vq(){Vq=k;dl=RegExp("[\\?\\!\\.\\-\\:]+","g")}function Wq(a,b,c){var d,e;e=lg(b,c);if(1!=e.length)for(b=0;bd||g.length>e.length))d=f,e=g;return 0==e.length?null:ug(e)} function cl(a){Vq();var b;if(a)for(this.a=new dj,a=Lh(a,0);a.b!=a.d.c;){b=(y(a.b!=a.d.c),a.c=a.b,a.b=a.b.a,++a.a,a.c.c);var c=void 0;b=zf(b);b=Af(b,"'");b=ug(b).toLowerCase();0!=b.length&&R(this.a,b)&&(c=Xq(b,"[ ]*[\\|\u00bb|-][ ]*"),null!=c&&R(this.a,c),c=Xq(b,"[ ]*[\\|\u00bb|:][ ]*"),null!=c&&R(this.a,c),c=Xq(b,"[ ]*[\\|\u00bb|:\\(\\)][ ]*"),null!=c&&R(this.a,c),c=Xq(b,"[ ]*[\\|\u00bb|:\\(\\)\\-][ ]*"),null!=c&&R(this.a,c),c=Xq(b,"[ ]*[\\|\u00bb|,|:\\(\\)\\-][ ]*"),null!=c&&R(this.a,c),c=Xq(b,"[ ]*[\\|\u00bb|,|:\\(\\)\\-\u00a0][ ]*"), null!=c&&R(this.a,c),Wq(this.a,b,"[ ]+[\\|][ ]+"),Wq(this.a,b,"[ ]+[\\-][ ]+"),R(this.a,Yf(b," - [^\\-]+$")),R(this.a,Yf(b,"^[^\\-]+ - ")))}else this.a=null}n(136,1,{},cl);var dl;t(136);function ql(){ql=k;rl=new Yq(!0)}function Yq(a){this.a=a}n(87,1,{},Yq);_.a=!1;var rl;t(87);function Zq(a,b,c){b=N(a.d,b);c=N(a.d,c);return a.c||(b.nodeType!=c.nodeType?0:1!=b.nodeType||b.nodeName===c.nodeName)?b.parentNode==c.parentNode:!1} function il(a,b){var c,d,e,f,g,l,m,q,v,I;a.g=b.a;if(2>a.g.b.length)return!1;d=a.g;e=$doc.documentElement;l=new Q;for(f=0;fa.e?I==e&&++e:Zq(a,v,c)&&(g=!0,el(N(a.g,c),!0),d[I]=d[e++]);else if(N(a.g,v).c<=a.f&&!N(a.g,v).a&&!uq(N(a.g,v),"STRICTLY_NOT_CONTENT")&&!uq(N(a.g,v),"de.l3s.boilerpipe/TITLE")){for(I=m;Ia.e)I==m&&++m;else if(Zq(a,v,c)){g=!0;el(N(a.g,v),!0);l[I]=l[m++]; break}I==q?d[f++]=v:l[q++]=v}return g}function ar(a,b,c,d,e){this.b=a;this.a=b;this.c=c;this.f=d;this.e=e}n(138,1,{},ar);_.a=!1;_.b=!1;_.c=!1;_.e=0;_.f=0;t(138);function hl(){var a=new br;a.a=!0;return a}function jl(a){return new ar(a.b,a.a,a.c,a.e,a.d)}function br(){this.c=this.a=this.b=!1;this.d=this.e=0}n(84,1,{},br);_.a=!1;_.b=!1;_.c=!1;_.d=0;_.e=0;t(84);function nl(){nl=k;ol=new cr("de.l3s.boilerpipe/TITLE")}function cr(a){this.a=a}n(86,1,{},cr);var ol;t(86); function fl(){fl=k;gl=new dr(D(w(p,1),h,2,4,["STRICTLY_NOT_CONTENT"]))}function dr(a){this.a=a}n(137,1,{},dr);var gl;t(137); function er(a,b){var c,d,e,f,g,l,m;m=nm(b);g=l=!1;m||(a.i&&a.d&&(a.f||(g=b.classList.contains("hidden")),(a.f||g)&&(l=!0)),a.i&&-1!=A(b,"class").indexOf("continue")&&(l=!0));var q=m||l,v;2>Pk||(v=getComputedStyle(b,null),V((q?"KEEP ":"SKIP ")+b.tagName+": id\x3d"+b.id+", dsp\x3d"+v.display+", vis\x3d"+v.visibility+", opaq\x3d"+v.opacity));if(!m&&!l)return R(a.e,b),!1;try{if(S(a.b,b.tagName))for(f=new M(a.c);f.bPk||(c=B(b),V("TABLE: "+d+", id\x3d"+b.id+", class\x3d"+A(b,"class")+", parent\x3d["+c.tagName+", id\x3d"+c.id+", class\x3d"+A(c,"class")+"]"));if(d==(Y(),Up))return g=a.a,Wk(g,g.d),P(g.b.a,new Ir(b)), !1;break;case "VIDEO":return g=a.a,d=new Jr(b),Wk(g,g.d),P(g.b.a,d),!1;case "OPTION":case "OBJECT":case "EMBED":case "APPLET":return a.a.c=!0,!1;case "HEAD":case "STYLE":case "SCRIPT":case "LINK":case "NOSCRIPT":case "IFRAME":case "svg":return!1}d=a.a;Or();f=getComputedStyle(b,null);c=new Pr;e=b.tagName;switch(f.display){case "inline":break;case "inline-block":case "inline-flex":c.a=!0;break;case "block":if("none"!==f["float"]&&"SPAN"===e)break;default:c.b=!0,c.a=!0}if("HTML"!==e&&"BODY"!==e&&"ARTICLE"!== e)switch(l=A(b,"class"),f=b.classList.length,m=A(b,"id"),(Qr.test(l)||Qr.test(m))&&2>=f&&(f=c.d,f[f.length]="STRICTLY_NOT_CONTENT"),e){case "ASIDE":case "NAV":e=c.d;e[e.length]="STRICTLY_NOT_CONTENT";break;case "LI":e=c.d;e[e.length]="de.l3s.boilerpipe/LI";break;case "H1":e=c.d;e[e.length]="de.l3s.boilerpipe/H1";e=c.d;e[e.length]="de.l3s.boilerpipe/HEADING";break;case "H2":e=c.d;e[e.length]="de.l3s.boilerpipe/H2";e=c.d;e[e.length]="de.l3s.boilerpipe/HEADING";break;case "H3":e=c.d;e[e.length]="de.l3s.boilerpipe/H3"; e=c.d;e[e.length]="de.l3s.boilerpipe/HEADING";break;case "H4":case "H5":case "H6":e=c.d;e[e.length]="de.l3s.boilerpipe/HEADING";break;case "A":c.a=!0,b.hasAttribute("href")&&(c.c=!0)}P(d.a.a,c);c.a&&++d.f;c.c&&(e=d.g,e.e=!0,e.j+=" ");d.c|=c.b;d=(Se(),a.f?Ve:Te);P(a.g.a,d);a.f|=g;return!0} function Rk(a){var b;this.g=new jk;this.e=new dj;this.a=a;this.c=new Q;P(this.c,new Eq);P(this.c,new Kq);P(this.c,new Oq);P(this.c,new Sq);this.b=new dj;for(b=new M(this.c);b.b=b)return 0;c=(a.offsetWidth||0)|0;a=0;b=c/b;1.4500000476837158b?a=1:1.2999999523162842b&&(a=0.4000000059604645);return Wa(this.a*a)};_.Hb=function(){return this.a};_.a=0;t(157);function wl(a){this.b=25;this.a=a}n(158,217,{},wl); _.Fb=function(a){var b;if(!this.a)return 0;a=Lm(this.a).b.length-1-(Lm(Jm(this.a,a)).b.length-1);b=0;4>a?b=1:6>a?b=0.6000000238418579:8>a&&(b=0.20000000298023224);return Wa(this.b*b)};_.Hb=function(){return this.b};_.b=0;t(158);function xl(){this.a=15}n(159,217,{},xl);_.Fb=function(a){var b;a=Lm(a);for(b=new M(a);b.b img, #loadingIndicator > svg { display: block; height: 2.5em; margin: auto; width: 2.5em; } /* Margins for Show Original link. */ .light #closeReaderView { border-top: 1px solid #E0E0E0; color: #4285F4; } .dark #closeReaderView { border-top: 1px solid #555; color: #3adaff; } .sepia #closeReaderView { border-top: 1px solid rgb(147, 125, 102); color: #55F; } video::-webkit-media-controls-fullscreen-button { display: none; } #closeReaderView { /* TODO(mdjones): Remove the "display: none;" style when the Reader Mode bar behaves like the toolbar when scrolling. */ display: none; flex: 0 0 auto; font-family: 'Roboto', sans-serif; font-weight: 700; line-height: 14px; padding: 24px 16px; font-size: 14px; text-align: right; text-decoration: none; text-transform: uppercase; width: 100%; } #content { margin: 24px 16px 24px 16px; } #mainContent { flex: 1 1 auto; margin: 0px auto; width: 100%; } @media screen { #mainContent { max-width: 35em; } } #articleHeader { margin-top: 24px; width: 100%; } #titleHolder { font-size: 1.714rem; line-height: 1.417; margin: 0 16px; } blockquote { border-left: 4px solid #888; padding-left: 1em; } cite { opacity: .8; font-style: italic; } hr { opacity: .5; border-style: solid; height: 1px 0 0 0; width: 75%; } q { opacity: .8; display:block; font-style: italic; font-weight: 600; } embed, img, object, video { max-width: 100%; } /* TODO(sunangel): make images zoomable. */ img { display: block; height: auto; margin: 0.6rem auto 0.4rem auto; } /* TODO(nyquist): set these classes directly in the dom distiller. */ embed+[class*='caption'], figcaption, img+[class*='caption'], object+[class*='caption'], video+[class*='caption'] { opacity: .8; display: table; margin-bottom: 1rem; font-size: 0.857rem; line-height: 1.667; } ol, ul { margin-left: 1.296rem; } code, pre { border: 1px solid; border-radius: 2px; } pre code { border: none; } pre { line-height: 1.642; padding: .5em; white-space: pre-wrap; } body .hidden { display: none; } /* Footer feedback form. */ #contentWrap { display: flex; flex-direction: column; flex-grow: 1; overflow: auto; position: relative; z-index: 1; } .footerFeedback { display: flex; flex-direction: column; font-size: 14px; z-index: 2; background-color: #4285F4; color: #fff; width: 100%; } .feedbackContent { font-size: 14px; font-family: sans-serif; background-color: #4285F4; clear: both; padding: 14px; } #feedbackQuestion { font-size: 1.4em; font-weight: 700; text-align: center; width: 100%; } .feedbackButtonWrap { margin-top: 14px; text-align: center; width: 100%; } .feedbackButton { -webkit-user-select: none; background-color: #FFFFFF; border-radius: 3px; color: #4285F4; display: inline-block; font-weight: 900; height: 35px; margin: 0px 4% 0px 4%; padding-top: 8px; text-align: center; text-transform: uppercase; user-select: none; width: 40%; } .clear { clear: both; } /* Feedback fade out */ .fadeOut { animation-duration: 0.5s; animation-name: fadeOutAndSwipeAnimation; } @keyframes fadeOutAndSwipeAnimation { from { margin-left: 0%; opacity: 1; } to { margin-left: -100%; opacity: 0; } } /* Iframe sizing. */ .youtubeContainer { height: 0px; /* This is the perecnt height of a standard HD video. */ padding-bottom: 56.25%; position: relative; width: 100%; } .youtubeIframe { height: 100%; left: 0px; position: absolute; top: 0px; width: 100%; } /* Copyright 2015 The Chromium Authors. All rights reserved. * Use of this source code is governed by a BSD-style license that can be * found in the LICENSE file. */ /* The following are iOS specific rules for rendering on WebKit instead of * Blink. */ #mainContent { -webkit-flex: 1 1 auto; } #closeReaderView { -webkit-flex: 0 0 auto; } #contentWrap { -webkit-flex-flow: column; display: -webkit-flex; } Material design circular activity spinner with CSS3 animation // Copyright 2014 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. (function() { var elems = document.querySelectorAll( 'meta[property="og:type"],meta[name="og:type"]'); for (var i in elems) { if (elems[i].content && elems[i].content.toUpperCase() == 'ARTICLE') { return true; } } var elems = document.querySelectorAll( '*[itemtype="http://schema.org/Article"]'); for (var i in elems) { if (elems[i].itemscope) { return true; } } return false; })() // Copyright 2015 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. (function() { function hasOGArticle() { var elems = document.head.querySelectorAll( 'meta[property="og:type"],meta[name="og:type"]'); for (var i in elems) { if (elems[i].content && elems[i].content.toUpperCase() == 'ARTICLE') { return true; } } return false; } var body = document.body; if (!body) { return false; } return JSON.stringify({ 'opengraph': hasOGArticle(), 'url': document.location.href, 'numElements': body.querySelectorAll('*').length, 'numAnchors': body.querySelectorAll('a').length, 'numForms': body.querySelectorAll('form').length, 'innerText': body.innerText, 'textContent': body.textContent, 'innerHTML': body.innerHTML, }); })() d@Er`M@ #@S͡<@?8t@?h?_D?.v?˞b?/@k?@jɩ˿?.+?`?$!? ?NB? ?^Kҹ? H?ÓI?8p@K)Ŀ Pg?eA45ʖ@Q?M2??$5C?@Y+@X? H?`2c?`?-,} ?_U:??s8'j@lHp@ uO?/@B~? ?CC2z ^@mc߷@h)r?O?@κp?][?j@ _@!NM?`?>(?3IϺ?| }?=,?Ʒ9,@06:?j@积f@.$?@t*טp@7d?7@UF@8%; ??h`?S0~@B0Ǹ@7`+??l?d?RB???v$Z`Z@Rah@k#u?|@??' !?@=hH@ S??_ H?A׵??Ɵ9@?12c7??wк䵿j@v5`@#1N?@ $[@P?@d9;@i&4?V@ȥұ@=G??1b?#@'??jg??i9^?d?D?ƀt?`?x]%0?-Ѧ&(%?BB?T?P2)?"wd?? :?@ٙ/?`z@t/)}@7 _Jp?p@?qƭN@J213ą@B?0?GpE?@w `@2腆ӥ??0YA?@`ti@ ?-@k(m@qX޸? ?@T²??MIVTd A@kHUfJ? |@q] W? ?Y[]?@͇;п? ? @@@F_ȿ/@z+pN?C@Z? @@@b ƿgE@FG=Z<@}qU? ?@M¿#@p $?B@C4,㵿rW@":?gE@y7mr@>"2z Q@I$Ĺ?  @XctG@Q{Ho?`@?G4?`*C?AЪs5?9Wr?`ƃ?Ul9C@vk{@y?8.2? ;@& Q@4%l? @!@OC=? @Kut泿a?济Ȗ豿;@?~3?z@s(ǰ  ]@F]n?0b@.@??ngpN@jEhԲ?!@sC@$_ @ebJ?WQ??Rx@V33r(?l@ ]@hD6?`W@o{! ?8}O=? @^]@e.-?v@BĀꩿhs@71FJ?pګ?uS';@+!믿0>`@R7V8?@/LeAU@x9;@h&x?z@h- /@0x)Ы?@D@\ ^˅ N@x ? B@݄諿 ?S6[? ?:X,@>8X?`@?Q v.??Pa-hƭPT?b?0 ?H~K?F%X?06{?el@D@ \?U@/n?eG:ox@:]?l@=C`@!?p8H@Q59@ʮ?@K@eGG@G wa?@G@zHF59@Myi.5?@K@dj;@)c?a?}橿@rH Q@|1@|Rk?PT?<2?0 ?53dZ`@?(#??NS=wPT?./ .?0 ?,jrHH禿{?,l?@,X Mya?uRdbd0@@2;)??@X6??0"? @?q?}NY@&/X? ?#\^z3?%e@@>!ϰگ@A{O}?@Vcி@Z,@N? %@Rii?@@@`qIP@K~5?p@}|#o?`@@LJ㬿H@{ ?p6d@ցzxߺ?ď㓥@(x8?0n@z3{`-g?:\a>?`_V]@\o=q? 1@F\??oWVT @I@^ @?@]~A?`@@St*E@3|?`@@ܹ}:z͠$@ 1?` @j.̢PC?;짿hv@P? !@X??ʈhDf?D@dֳw@4D(?@T\6t@_iI? @(.➿Ь$@k?W@7$`8r@|?ߺ?ۀj #?(??B6"z1g@@@ /&k?`=d@ CsR@zQX? @]20c?;@F\%e@@qQQCE@bN? @V? <@#  2@ 0??# O?ڶ@:) z`-g?~}c?p45?.& |@a5~?O@As? @J@@? `@.?|@}&?@:Q@jetĚ @YE>?y&6@xM@`<Ǘ?1g@.R@D@?O@X:t@e^?n@ pc@W!gԖ?(@!OnXv@|ZNV? R@Ҋ:W@?D?IYT@ҷc$@.˼?%e@@j(^@07:?`=d@/#b ?7zA?4z ?p45?CY|?y꧖? c]?(P`-g?.s? // Copyright (c) 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. /** * This variable structure is here to document the structure that the template * expects to correctly populate the page. */ /** * Takes the |experimentalFeaturesData| input argument which represents data * about all the current feature entries and populates the html jstemplate with * that data. It expects an object structure like the above. * @param {Object} experimentalFeaturesData Information about all experiments. * See returnFlagsExperiments() for the structure of this object. */ function renderTemplate(experimentalFeaturesData) { // This is the javascript code that processes the template: jstProcess(new JsEvalContext(experimentalFeaturesData), $('flagsTemplate')); // Add handlers to dynamically created HTML elements. var elements = document.getElementsByClassName('experiment-select'); for (var i = 0; i < elements.length; ++i) { elements[i].onchange = function() { handleSelectExperimentalFeatureChoice(this, this.selectedIndex); return false; }; } elements = document.getElementsByClassName('experiment-disable-link'); for (var i = 0; i < elements.length; ++i) { elements[i].onclick = function() { handleEnableExperimentalFeature(this, false); return false; }; } elements = document.getElementsByClassName('experiment-enable-link'); for (var i = 0; i < elements.length; ++i) { elements[i].onclick = function() { handleEnableExperimentalFeature(this, true); return false; }; } elements = document.getElementsByClassName('experiment-restart-button'); for (var i = 0; i < elements.length; ++i) { elements[i].onclick = restartBrowser; } $('experiment-reset-all').onclick = resetAllFlags; highlightReferencedFlag(); } /** * Highlight an element associated with the page's location's hash. We need to * fake fragment navigation with '.scrollIntoView()', since the fragment IDs * don't actually exist until after the template code runs; normal navigation * therefore doesn't work. */ function highlightReferencedFlag() { if (window.location.hash) { var el = document.querySelector(window.location.hash); if (el && !el.classList.contains('referenced')) { // Unhighlight whatever's highlighted. if (document.querySelector('.referenced')) document.querySelector('.referenced').classList.remove('referenced'); // Highlight the referenced element. el.classList.add('referenced'); el.scrollIntoView(); } } } /** * Asks the C++ FlagsDOMHandler to get details about the available experimental * features and return detailed data about the configuration. The * FlagsDOMHandler should reply to returnFlagsExperiments() (below). */ function requestExperimentalFeaturesData() { chrome.send('requestExperimentalFeatures'); } /** * Asks the C++ FlagsDOMHandler to restart the browser (restoring tabs). */ function restartBrowser() { chrome.send('restartBrowser'); } /** * Reset all flags to their default values and refresh the UI. */ function resetAllFlags() { // Asks the C++ FlagsDOMHandler to reset all flags to default values. chrome.send('resetAllFlags'); requestExperimentalFeaturesData(); } /** * Called by the WebUI to re-populate the page with data representing the * current state of all experimental features. * @param {Object} experimentalFeaturesData Information about all experimental * features in the following format: * { * supportedFeatures: [ * { * internal_name: 'Feature ID string', * name: 'Feature name', * description: 'Description', * // enabled and default are only set if the feature is single valued. * // enabled is true if the feature is currently enabled. * // is_default is true if the feature is in its default state. * enabled: true, * is_default: false, * // choices is only set if the entry has multiple values. * choices: [ * { * internal_name: 'Experimental feature ID string', * description: 'description', * selected: true * } * ], * supported_platforms: [ * 'Mac', * 'Linux' * ], * } * ], * unsupportedFeatures: [ * // Mirrors the format of |supportedFeatures| above. * ], * needsRestart: false, * showBetaChannelPromotion: false, * showDevChannelPromotion: false, * showOwnerWarning: false * } */ function returnExperimentalFeatures(experimentalFeaturesData) { var bodyContainer = $('body-container'); renderTemplate(experimentalFeaturesData); if (experimentalFeaturesData.showBetaChannelPromotion) $('channel-promo-beta').hidden = false; else if (experimentalFeaturesData.showDevChannelPromotion) $('channel-promo-dev').hidden = false; bodyContainer.style.visibility = 'visible'; var ownerWarningDiv = $('owner-warning'); if (ownerWarningDiv) ownerWarningDiv.hidden = !experimentalFeaturesData.showOwnerWarning; } /** * Handles a 'enable' or 'disable' button getting clicked. * @param {HTMLElement} node The node for the experiment being changed. * @param {boolean} enable Whether to enable or disable the experiment. */ function handleEnableExperimentalFeature(node, enable) { // Tell the C++ FlagsDOMHandler to enable/disable the experiment. chrome.send('enableExperimentalFeature', [String(node.internal_name), String(enable)]); requestExperimentalFeaturesData(); } /** * Invoked when the selection of a multi-value choice is changed to the * specified index. * @param {HTMLElement} node The node for the experiment being changed. * @param {number} index The index of the option that was selected. */ function handleSelectExperimentalFeatureChoice(node, index) { // Tell the C++ FlagsDOMHandler to enable the selected choice. chrome.send('enableExperimentalFeature', [String(node.internal_name) + '@' + index, 'true']); requestExperimentalFeaturesData(); } // Get data and have it displayed upon loading. document.addEventListener('DOMContentLoaded', requestExperimentalFeaturesData); // Update the highlighted flag when the hash changes. window.addEventListener('hashchange', highlightReferencedFlag); XR#7):;lyWæ6gJ-h"i8c0X@"[|mP5t<ע\I*eܘ*/f:ܺEU7^@Jn6I,rQ\?՗2MC^msȗwQbr[փC'9(&+-bƤS(G߮LM/Dwڑ.|ROO$<" x(>B%T,fLӐ;d(_M6?]o6ݿJ7^eNH[,ɞ<m1)zEԗ-;)V&}9?i^lZ~~/pJ&_rKsc#2`$ FZi2𷕐/ʂKKDZ-oAwWc붙LR#KXhKKx/f6|w h0Mȅr4\ƫvXdvxRNzt [gT솓^A]neD%\ %߾-,TEb~l! Ҹ-8#4 V:w> GmC#x^2+:q"+ U"@QmIԑp3FɗݳWﲭ M0K2l-9È̢Ϭ|l/%*!2@A6-)숔; yyU67apI<tC@H#\\GK-jMg h0z,8pGWm {٩[S͐Q}Cd X%` &>ʘ)d[t&i,(4ShduB=;;c^jɦS^V%e2a4W80n *#xF&E1K޶Ƣ(0jVRN?t3tuL#\akj4$[NѪ3ٵ%2gX6)Lb V߱"UcP)('W 3ag 7UOq.\7DJ|3u m9xxI(P*Tx ڶ5X"$"CPҤEsjcVwT6v<M]-iH|I%5z7vrr flx?ϯ 2.wiH$KV']utIc~0b 9!j-w]"c-XB4;,G>ƌW9>רSjcg3CDU|+r2Pwݩ>~Uknf|$:C=_a}^ZǴ4՗O!zorkYԈGF,'

    Network Log Export

    INSTRUCTIONS: Start logging, reproduce the problem, and then stop logging. Make sure to send the email before starting to log again. Otherwise, the log will be deleted.

    Logs can be loaded in net-internals of desktop Chrome.

    WARNING: Logs contain a list of sites visited from when logging started to when logging stopped. They may also contain general network configuration information, such as DNS and proxy configuration. If private information is not stripped, the logs also contain cookies and credentials.

    ADVANCED: This section should normally be left alone.

    
    
    
    // Copyright (c) 2013 The Chromium Authors. All rights reserved.
    // Use of this source code is governed by a BSD-style license that can be
    // found in the LICENSE file.
    
    /**
     * Main entry point called once the page has loaded.
     */
    function onLoad() {
      NetExportView.getInstance();
    }
    
    document.addEventListener('DOMContentLoaded', onLoad);
    
    /**
     * This class handles the presentation of our profiler view. Used as a
     * singleton.
     */
    var NetExportView = (function() {
      'use strict';
    
      /**
       * Delay in milliseconds between updates of certain browser information.
       */
      /** @const */ var POLL_INTERVAL_MS = 5000;
    
      // --------------------------------------------------------------------------
    
      /**
       * @constructor
       */
      function NetExportView() {
        $('export-view-start-data').onclick = this.onStartData_.bind(this);
        $('export-view-stop-data').onclick = this.onStopData_.bind(this);
        $('export-view-send-data').onclick = this.onSendData_.bind(this);
    
        window.setInterval(function() { chrome.send('getExportNetLogInfo'); },
                           POLL_INTERVAL_MS);
    
        chrome.send('getExportNetLogInfo');
      }
    
      cr.addSingletonGetter(NetExportView);
    
      NetExportView.prototype = {
        /**
         * Starts saving NetLog data to a file.
         */
        onStartData_: function() {
          var logMode =
              document.querySelector('input[name="log-mode"]:checked').value;
          chrome.send('startNetLog', [logMode]);
        },
    
        /**
         * Stops saving NetLog data to a file.
         */
        onStopData_: function() {
          chrome.send('stopNetLog');
        },
    
        /**
         * Sends NetLog data via email from browser.
         */
        onSendData_: function() {
          chrome.send('sendNetLog');
        },
    
        /**
         * Updates the UI to reflect the current state. Displays the path name of
         * the file where NetLog data is collected.
         */
        onExportNetLogInfoChanged: function(exportNetLogInfo) {
          if (!exportNetLogInfo.useMobileUI) {
            document.getElementById('export-view-send-data').style.display =
              "none";
            document.getElementById('export-view-deletes-log-text').style.display =
              "none";
          }
    
          if (exportNetLogInfo.file) {
            var message = '';
            if (exportNetLogInfo.state == 'LOGGING')
              message = 'NetLog data is collected in: ';
            else if (exportNetLogInfo.logType != 'NONE')
              message = 'NetLog data to send is in: ';
            $('export-view-file-path-text').textContent =
                message + exportNetLogInfo.file;
          } else {
            $('export-view-file-path-text').textContent = '';
          }
    
          // Disable all controls.  Useable controls are enabled below.
          var controls = document.querySelectorAll('button, input');
          for (var i = 0; i < controls.length; ++i) {
            controls[i].disabled = true;
          }
    
          $('export-view-deletes-log-text').hidden = true;
          $('export-view-private-data-text').hidden = true;
          $('export-view-send-old-log-text').hidden = true;
          if (exportNetLogInfo.state == 'NOT_LOGGING') {
            // Allow making a new log.
            $('export-view-strip-private-data-button').disabled = false;
            $('export-view-include-private-data-button').disabled = false;
            $('export-view-log-bytes-button').disabled = false;
            $('export-view-start-data').disabled = false;
    
            // If there's an existing log, allow sending it.
            if (exportNetLogInfo.logType != 'NONE') {
              $('export-view-deletes-log-text').hidden = false;
              $('export-view-send-data').disabled = false;
              if (exportNetLogInfo.logType == 'UNKNOWN') {
                $('export-view-send-old-log-text').hidden = false;
              } else if (exportNetLogInfo.logType == 'NORMAL') {
                $('export-view-private-data-text').hidden = false;
              }
            }
          } else if (exportNetLogInfo.state == 'LOGGING') {
            // Only possible to stop logging. Radio buttons reflects current state.
            document.querySelector('input[name="log-mode"][value="' +
                                   exportNetLogInfo.logType + '"]').checked = true;
            $('export-view-stop-data').disabled = false;
          } else if (exportNetLogInfo.state == 'UNINITIALIZED') {
            $('export-view-file-path-text').textContent =
                'Unable to initialize NetLog data file.';
          }
        }
      };
    
      return NetExportView;
    })();
    
    
    
      
      
      
      
      
      
      
      
    
    
      

    NTP Tiles Internals

    Sources

    TOP_SITES
    enabled yes no
    SUGGESTIONS_SERVICE
    enabled yes no
    POPULAR
    URL
    Country
    Version
    enabled no
    WHITELIST
    enabled yes no

    Sites

    Source ??? TOP_SITES SUGGESTIONS_SERVICE POPULAR WHITELIST ???
    URL
    // Copyright 2016 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. cr.define('chrome.ntp_tiles_internals', function() { 'use strict'; var initialize = function() { receiveSites({}); $('submit-update').addEventListener('click', function(event) { event.preventDefault(); chrome.send('update', [{ "popular": { "overrideURL": $('override-url').value, "overrideCountry": $('override-country').value, "overrideVersion": $('override-version').value, }, }]) }); chrome.send('registerForEvents'); } var receiveSourceInfo = function(state) { jstProcess(new JsEvalContext(state), $('sources')); } var receiveSites = function(sites) { jstProcess(new JsEvalContext(sites), $('sites')); } // Return an object with all of the exports. return { initialize: initialize, receiveSourceInfo: receiveSourceInfo, receiveSites: receiveSites, }; }); document.addEventListener('DOMContentLoaded', chrome.ntp_tiles_internals.initialize); /* Copyright 2016 The Chromium Authors. All rights reserved. * Use of this source code is governed by a BSD-style license that can be * found in the LICENSE file. */ html { font-size: 20px; } div.section { width: 100%; display: inline-block; margin-left: auto; margin-right: auto; } div.section.hidden { display: none; } h2 { color: rgb(74, 142, 230); font-size: 100%; margin-bottom: 0; } .err { color: red; } .section-details { width: 100%; } .section-details th { background: rgb(239, 243, 255); } .section-details td { vertical-align: top; } .section-details td.detail { text-align: right; width: 1px; white-space: nowrap; } .section-details td.value input { width: 100%; } #json-value { font-size: 75%; } // Copyright (c) 2016 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. /** * Takes the |nearbyUrlsData| input argument which holds metadata for web pages * broadcast by nearby devices. * @param {Object} nearbyUrlsData Information about web pages broadcast by * nearby devices */ function renderTemplate(nearbyUrlsData) { // This is the javascript code that processes the template: jstProcess(new JsEvalContext(nearbyUrlsData), $('physicalWebTemplate')); } function requestNearbyURLs() { chrome.send('requestNearbyURLs'); } function physicalWebItemClicked(index) { chrome.send('physicalWebItemClicked', [index]); } function returnNearbyURLs(nearbyUrlsData) { var bodyContainer = $('body-container'); renderTemplate(nearbyUrlsData); bodyContainer.style.visibility = 'visible'; } document.addEventListener('DOMContentLoaded', requestNearbyURLs); /* Copyright (c) 2016 The Chromium Authors. All rights reserved. * Use of this source code is governed by a BSD-style license that can be * found in the LICENSE file. */ body { font-family: Sans-Serif; font-size: .9em; } .physicalWebTemplate { display: flex; width: 100%; margin-bottom: .8em; text-decoration: none; } .physicalWebIcon { flex: 0 0 2.5em; padding-left: .2em; } .physicalWebIcon img { width: 75%; } .physicalWebText { flex: 1; padding-top: .2em; min-width: 0; } .physicalWebText .title { display: block; color: #222; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; margin-bottom: .2em; } .physicalWebText .resolvedUrl { display: block; width: 100%; font-size: 80%; color: #888; margin-bottom: .1em; word-wrap: break-word; word-break: break-all; } .physicalWebText .description { display: block; height: 2.5em; line-height: 1.25; font-size: 90%; color: #444; overflow: hidden; text-overflow: ellipsis; }
    Proximity Auth Debug /* Copyright 2015 The Chromium Authors. All rights reserved. * Use of this source code is governed by a BSD-style license that can be * found in the LICENSE file. */ html, body { width: 100%; height: 100%; margin: 0; padding: 0; overflow: hidden; font-family: "Roboto", sans-serif; } #logs { width: 40%; border-left: 1px solid rgba(0,0,0,0.12); } // Copyright 2015 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. Polymer({ is: 'log-panel', properties: { /** * List of displayed logs. * @type {Array<{{ * text: string, * date: string, * source: string * }}>} */ logs: Array, }, observers: [ 'logsChanged_(logs.*)', ], /** * @type {boolean} * @private */ isScrollAtBottom_: true, /** * Called after the Polymer element is initialized. */ ready: function() { this.$.list.onscroll = this.onScroll_.bind(this); this.async(this.scrollToBottom_); }, /** * Called when the list of logs change. */ logsChanged_: function() { if (this.isScrollAtBottom_) this.async(this.scrollToBottom_); }, /** * Clears the logs. * @private */ clearLogs_: function() { this.$.logBuffer.clearLogs(); }, /** * Event handler when the list is scrolled. * @private */ onScroll_: function() { var list = this.$.list; this.isScrollAtBottom_ = list.scrollTop + list.offsetHeight == list.scrollHeight; }, /** * Scrolls the logs container to the bottom. * @private */ scrollToBottom_: function() { this.$.list.scrollTop = this.$.list.scrollHeight; }, /** * @param {LogMessage} log * @return {string} The filename stripped of its preceeding path concatenated * with the line number of the log. * @private */ computeFileAndLine_: function(log) { var directories = log.file.split('/'); return directories[directories.length - 1] + ':' + log.line; }, }); // Copyright 2015 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. Polymer({ is: 'content-panel', properties: { /** * The index of the selected page that is currently shown. * @private */ selected_: { type: Number, value: 0, } }, /** * Called when a page transition event occurs. * @param {Event} event * @private */ onSelectedPageChanged_: function(event) { var newPage = event.detail.value instanceof Element && event.detail.value.children[0]; if (newPage && newPage.activate != null) { newPage.activate(); } } }); // Copyright 2015 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. Polymer({ is: 'local-state', properties: { /** * The current CryptAuth enrollment status. * @type {{ * lastSuccessTime: ?number, * nextRefreshTime: ?number, * recoveringFromFailure: boolean, * operationInProgress: boolean, * }} SyncState */ enrollmentState_: { type: Object, value: { lastSuccessTime: null, nextRefreshTime: null, recoveringFromFailure: true, operationInProgress: false, }, }, /** * The current CryptAuth device sync status. * @type {SyncState} */ deviceSyncState_: { type: Object, value: { lastSuccessTime: null, nextRefreshTime: null, recoveringFromFailure: true, operationInProgress: false, }, }, /** * List of unlock keys that can unlock the local device. * @type {Array} */ unlockKeys_: { type: Array, value: [ { publicKey: 'CAESRQogOlH8DgPMQu7eAt-b6yoTXcazG8mAl6SPC5Ds-LTULIcSIQDZ' + 'DMqsoYRO4tNMej1FBEl1sTiTiVDqrcGq-CkYCzDThw==', friendlyDeviceName: 'LGE Nexus 4', bluetoothAddress: 'C4:43:8F:12:07:07', unlockKey: true, unlockable: false, connectionStatus: 'connected', remoteState: { userPresent: true, secureScreenLock: true, trustAgent: true }, }, ], }, }, /** * Called when the page is about to be shown. */ activate: function() { LocalStateInterface = this; chrome.send('getLocalState'); }, /** * Immediately forces an enrollment attempt. */ forceEnrollment_: function() { chrome.send('forceEnrollment'); }, /** * Immediately forces an device sync attempt. */ forceDeviceSync_: function() { chrome.send('forceDeviceSync'); }, /** * Called when the enrollment state changes. * @param {SyncState} enrollmentState */ onEnrollmentStateChanged: function(enrollmentState) { this.enrollmentState_ = enrollmentState; }, /** * Called when the device sync state changes. * @param {SyncState} deviceSyncState */ onDeviceSyncStateChanged: function(deviceSyncState) { this.deviceSyncState_ = deviceSyncState; }, /** * Called when the locally stored unlock keys change. * @param {Array} unlockKeys */ onUnlockKeysChanged: function(unlockKeys) { this.unlockKeys_ = unlockKeys; }, /** * Called for the chrome.send('getSyncStates') response. * @param {SyncState} enrollmentState * @param {SyncState} deviceSyncState * @param {Array} unlockKeys */ onGotLocalState: function(enrollmentState, deviceSyncState, unlockKeys) { this.enrollmentState_ = enrollmentState; this.deviceSyncState_ = deviceSyncState; this.unlockKeys_ = unlockKeys; }, /** * @param {SyncState} syncState The enrollment or device sync state. * @param {string} neverSyncedString String returned if there has never been a * last successful sync. * @return {string} The formatted string of the last successful sync time. */ getLastSyncTimeString_: function(syncState, neverSyncedString) { if (syncState.lastSuccessTime == 0) return neverSyncedString; var date = new Date(syncState.lastSuccessTime); return date.toLocaleDateString() + ' ' + date.toLocaleTimeString(); }, /** * @param {SyncState} syncState The enrollment or device sync state. * @return {string} The formatted string to be displayed. */ getNextEnrollmentString_: function(syncState) { var deltaMillis = syncState.nextRefreshTime; if (deltaMillis == null) return 'unknown'; if (deltaMillis == 0) return 'sync in progress...'; var seconds = deltaMillis / 1000; if (seconds < 60) return Math.round(seconds) + ' seconds to refresh'; var minutes = seconds / 60; if (minutes < 60) return Math.round(minutes) + ' minutes to refresh'; var hours = minutes / 60; if (hours < 24) return Math.round(hours) + ' hours to refresh'; var days = hours / 24; return Math.round(days) + ' days to refresh'; }, /** * @param {SyncState} syncState The enrollment or device sync state. * @return {string} The icon to show for the current state. */ getNextSyncIcon_: function(syncState) { return syncState.operationInProgress ? 'icons:refresh' : 'icons:schedule'; }, /** * @param {SyncState} syncState The enrollment or device sync state. * @return {string} The icon id representing whether the last sync is * successful. */ getIconForSuccess_: function(syncState) { return syncState.recoveringFromFailure ? 'icons:error' : 'icons:cloud-done'; }, }); // Interface with the native WebUI component for getting the local state and // being notified when the local state changes. // The local state refers to state stored on the device rather than online in // CryptAuth. This state includes the enrollment and device sync states, as well // as the list of unlock keys. LocalStateInterface = { /** * Called when the enrollment state changes. For example, when a new * enrollment is initiated. * @type {function(SyncState)} */ onEnrollmentStateChanged: function(enrollmentState) {}, /** * Called when the device state changes. For example, when a new device sync * is initiated. * @type {function(DeviceSyncState)} */ onDeviceSyncStateChanged: function(deviceSyncState) {}, /** * Called when the locally stored unlock keys changes. * @type {function(Array)} */ onUnlockKeysChanged: function(unlockKeys) {}, /** * Called in response to chrome.send('getLocalState') with the local state. * @type {function(SyncState, SyncState, Array)} */ onGotLocalState: function(enrollmentState, deviceSyncState, unlockKeys) {}, }; // Copyright 2015 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. Polymer({ is: 'device-list', properties: { /** * The label of the list to be displayed. * @type {string} */ label: { type: String, value: 'Device List', }, /** * Info of the devices contained in the list. * @type {Array} */ devices: Array, /** * Set with the selected device when the unlock key dialog is opened. */ deviceForDialog_: { type: Object, value: null }, /** * True if currently toggling a device as an unlock key. */ toggleUnlockKeyInProgress_: { type: Boolean, value: false, }, }, /** * Shows the toggle unlock key dialog when the toggle button is pressed for an * item. * @param {Event} event */ showUnlockKeyDialog_: function(event) { this.deviceForDialog_ = event.model.item; var dialog = this.querySelector('#unlock-key-dialog'); dialog.open(); }, /** * Called when the unlock key dialog button is clicked to make the selected * device an unlock key or remove it as an unlock key. * @param {Event} event */ toggleUnlockKey_: function(event) { if (!this.deviceForDialog_) return; this.toggleUnlockKeyInProgress_ = true; CryptAuthInterface.addObserver(this); var publicKey = this.deviceForDialog_.publicKey; var makeUnlockKey = !this.deviceForDialog_.unlockKey; CryptAuthInterface.toggleUnlockKey(publicKey, makeUnlockKey); }, /** * Called when the toggling the unlock key completes, so we can close the * dialog. */ onUnlockKeyToggled: function() { this.toggleUnlockKeyInProgress_ = false; CryptAuthInterface.removeObserver(this); var dialog = this.querySelector('#unlock-key-dialog'); dialog.close(); }, /** * Handles when the toggle connection button is clicked for a list item. * @param {Event} event */ toggleConnection_: function(event) { var deviceInfo = event.model.item; chrome.send('toggleConnection', [deviceInfo.publicKey]); }, /** * @param {string} reason The device ineligibility reason. * @return {string} The prettified ineligibility reason. * @private */ prettifyReason_: function(reason) { if (reason == null || reason == '') return ''; var reasonWithSpaces = reason.replace(/([A-Z])/g, ' $1'); return reasonWithSpaces[0].toUpperCase() + reasonWithSpaces.slice(1); }, /** * @param {string} connectionStatus The Bluetooth connection status. * @return {string} The icon id to be shown for the connection state. * @private */ getIconForConnection_: function(connectionStatus) { switch (connectionStatus) { case 'connected': return 'device:bluetooth-connected'; case 'disconnected': return 'device:bluetooth'; case 'connecting': return 'device:bluetooth-searching'; default: return 'device:bluetooth-disabled'; } }, /** * @param {DeviceInfo} device * @return {string} The icon id to be shown for the unlock key state of the * device. */ getIconForUnlockKey_: function(device) { return 'hardware:phonelink' + (!device.unlockKey ? '-off' : ''); }, /** * @param {Object} remoteState The remote state of the device. * @return {string} The icon representing the state. */ getIconForRemoteState_: function(remoteState) { if (remoteState != null && remoteState.userPresent && remoteState.secureScreenLock && remoteState.trustAgent) { return 'icons:lock-open'; } else { return 'icons:lock-outline'; } }, /** * @param {string} reason The device ineligibility reason. * @return {string} The icon id to be shown for the ineligibility reason. * @private */ getIconForIneligibilityReason_: function(reason) { switch (reason) { case 'badOsVersion': return 'notification:system-update'; case 'bluetoothNotSupported': return 'device:bluetooth-disabled'; case 'deviceOffline': return 'device:signal-cellular-off'; case 'invalidCredentials': return 'notification:sync-problem'; default: return 'error'; }; }, /** * @param {number} userPresence * @return {string} */ getUserPresenceText_: function(userPresence) { var userPresenceMap = { 0: 'User Present', 1: 'User Absent', 2: 'User Presence Unknown', }; return userPresenceMap[userPresence]; }, /** * @param {number} screenLock * @return {string} */ getScreenLockText_: function(screenLock) { var screenLockMap = { 0: 'Secure Screen Lock Enabled', 1: 'Secure Screen Lock Disabled', 2: 'Secure Screen Lock State Unknown', }; return screenLockMap[screenLock]; }, /** * @param {number} trustAgent * @return {string} */ getTrustAgentText_: function(trustAgent) { var trustAgentMap = { 0: 'Trust Agent Enabled', 1: 'Trust Agent Disabled', 2: 'Trust Agent Unsupported', }; return trustAgentMap[trustAgent]; }, }); // Copyright 2015 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. Polymer({ is: 'log-buffer', properties: { /** * List of displayed logs. * @type {?Array<{{ * text: string, * time: string, * file: string, * line: number, * severity: number, * }}>} LogMessage */ logs: { type: Array, value: [], notify: true, } }, /** * Called when an instance is initialized. */ ready: function() { // We assume that only one instance of log-buffer is ever created. LogBufferInterface = this; chrome.send('getLogMessages'); }, // Clears the native LogBuffer. clearLogs: function() { chrome.send('clearLogBuffer'); }, // Handles when a new log message is added. onLogMessageAdded: function(log) { this.push('logs', log); }, // Handles when the logs are cleared. onLogBufferCleared: function() { this.logs = []; }, // Handles when the logs are returned in response to the 'getLogMessages' // request. onGotLogMessages: function(logs) { this.logs = logs; } }); // Interface with the native WebUI component for LogBuffer events. The functions // contained in this object will be invoked by the browser for each operation // performed on the native LogBuffer. LogBufferInterface = { /** * Called when a new log message is added. * @type {function(LogMessage)} */ onLogMessageAdded: function(log) {}, /** * Called when the log buffer is cleared. * @type {function()} */ onLogBufferCleared: function() {}, /** * Called in response to chrome.send('getLogMessages') with the log messages * currently in the buffer. * @type {function(Array)} */ onGotLogMessages: function(messages) {}, }; // Copyright 2015 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. Polymer({ is: 'eligible-devices', properties: { /** * List of devices that are eligible to be used as an unlock key. * @type {Array} * @private */ eligibleDevices_: { type: Array, value: null, }, /** * List of devices that are ineligible to be used as an unlock key. * @type {Array} * @private */ ineligibleDevices_: { type: Array, value: null, }, /** * Whether the findEligibleUnlockDevices request is in progress. * @type {boolean} * @private */ requestInProgress_: Boolean, }, /** * Called when this element is added to the DOM. */ attached: function() { CryptAuthInterface.addObserver(this); }, /** * Called when this element is removed from the DOM. */ detatched: function() { CryptAuthInterface.removeObserver(this); }, /** * Called when the page is about to be shown. */ activate: function() { this.requestInProgress_ = true; this.eligibleDevices_ = null; this.ineligibleDevices_ = null; CryptAuthInterface.findEligibleUnlockDevices(); }, /** * Called when eligible devices are found. * @param {Array} eligibleDevices * @param {Array} ineligibleDevices_ */ onGotEligibleDevices: function(eligibleDevices, ineligibleDevices) { this.requestInProgress_ = false; this.eligibleDevices_ = eligibleDevices; this.ineligibleDevices_ = ineligibleDevices; }, /** * Called when the CryptAuth request fails. * @param {string} errorMessage */ onCryptAuthError: function(errorMessage) { console.error('CryptAuth request failed: ' + errorMessage); this.requestInProgress_ = false; this.eligibleDevices_ = null; this.ineligibleDevices_ = null; }, }); // Copyright 2015 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. Polymer({ is: 'reachable-devices', properties: { /** * List of devices that recently responded to a CryptAuth ping. * @type {Array} * @private */ reachableDevices_: { type: Array, value: null, }, /** * Whether the findEligibleUnlockDevices request is in progress. * @type {boolean} * @private */ requestInProgress_: Boolean, }, /** * Called when this element is added to the DOM. */ attached: function() { CryptAuthInterface.addObserver(this); }, /** * Called when this element is removed from the DOM. */ detatched: function() { CryptAuthInterface.removeObserver(this); }, /** * Called when the page is about to be shown. */ activate: function() { this.requestInProgress_ = true; this.reachableDevices_ = null; CryptAuthInterface.findReachableDevices(); }, /** * Called when reachable devices are found. * @param {Array} reachableDevices */ onGotReachableDevices: function(reachableDevices) { this.requestInProgress_ = false; this.reachableDevices_ = reachableDevices; }, /** * Called when the CryptAuth request fails. * @param {string} errorMessage */ onCryptAuthError: function(errorMessage) { console.error('CryptAuth request failed: ' + errorMessage); this.requestInProgress_ = false; this.reachableDevices_ = null; }, }); // Copyright 2015 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. /** * Responsible for interfacing with the native component of the WebUI to make * CryptAuth API requests and handling the responses. */ CryptAuthInterface = { /** * A list of observers of CryptAuth events. */ observers_: [], /** * Adds an observer. */ addObserver: function(observer) { CryptAuthInterface.observers_.push(observer); }, /** * Removes an observer. */ removeObserver: function(observer) { var index = CryptAuthInterface.observers_.indexOf(observer); if (observer) CryptAuthInterface.observers_.splice(index, 1); }, /** * Starts the findEligibleUnlockDevices API call. * The onGotEligibleDevices() function will be called upon success. */ findEligibleUnlockDevices: function() { chrome.send('findEligibleUnlockDevices'); }, /** * Starts the flow to find reachable devices. Reachable devices are those that * respond to a CryptAuth ping. * The onGotReachableDevices() function will be called upon success. */ findReachableDevices: function() { chrome.send('findReachableDevices'); }, /** * Makes the device with |publicKey| an unlock key if |makeUnlockKey| is true. * Otherwise, the device will be removed as an unlock key. */ toggleUnlockKey: function(publicKey, makeUnlockKey) { chrome.send('toggleUnlockKey', [publicKey, makeUnlockKey]); }, /** * Called by the browser when the API request fails. */ onError: function(errorMessage) { CryptAuthInterface.observers_.forEach(function(observer) { if (observer.onCryptAuthError != null) observer.onCryptAuthError(errorMessage); }); }, /** * Called by the browser when a findEligibleUnlockDevices completes * successfully. * @param {Array} eligibleDevices * @param {Array} ineligibleDevices */ onGotEligibleDevices: function(eligibleDevices, ineligibleDevices) { CryptAuthInterface.observers_.forEach(function(observer) { if (observer.onGotEligibleDevices != null) observer.onGotEligibleDevices(eligibleDevices, ineligibleDevices); }); }, /* * Called by the browser when the reachable devices flow completes * successfully. * @param {Array} reachableDevices */ onGotReachableDevices: function(reachableDevices) { CryptAuthInterface.observers_.forEach(function(observer) { if (observer.onGotReachableDevices != null) observer.onGotReachableDevices(reachableDevices); }); }, /** * Called by the browser when an unlock key is toggled. */ onUnlockKeyToggled: function() { CryptAuthInterface.observers_.forEach(function(observer) { if (observer.onUnlockKeyToggled != null) observer.onUnlockKeyToggled(); }); }, }; // This message tells the native WebUI handler that the WebContents backing the // WebUI has been iniitalized. This signal allows the native handler to execute // JavaScript inside the page. chrome.send('onWebContentsInitialized'); Interstitials

    Choose an interstitial

    SSL

    SafeBrowsing

    Captive Portal

    Xms7ί^g`iN'*霒N{WG 3q> W]2;Z@^{& 4 XS+T~&Y>Iܥ`2ޯtghm/**VX"ܢΙ48; j1`tڋL&qѨBh;qg~7jVIЊ9@7Xcc'u E>h2DAq\iSjLxrBZӠ]ߏ/dch7[m.(fp^LiӀs) Gި3 1L C8=N=m 9)RIfBrC"@l[2śo`,$6Y\emLOT:=<=iڝ͍UnFZ74czB)k,YCxZm/ ny9rjF,N3/rs4O Ө7:w%0R{.,͌ibFidS#8r.\EB!ޱ>*w۬avFcUIjA?JMPm 1\S i9V'i&$?RWyx Xnef34% 'VuYvW<)1ۀk=E/2aˡKY-vas $1g9YZ" K lèbH?BW_*VӵzSdڝ M.;cP ySP3)UI1WJg v#V0NL-zMs;S6bΰL 7 䎾suR"|}5Ȩ#?r'?1i6DtEk]$s;e0{\sv4-Kr |b^Gf~DOzEcy\JLi * :k7"z4Nn?zzaǷSGDR&4m:$Pn R5GCZ bs3<_477x>mz(Bgsu۝yk%Woo&> F<&qNL2Ը셗_N؀Pfϼ73RsN WUS 8n0Jl}` `L˸c Wl-F*eR`o,^E<J1mɒwl[k1aV ALQ }yؐ$ <ɐݜ2q\o8oL=45o6ˌH#2:T%o4A vFJA"H wSti! OӷUZP,Xbw8es'j;MČzÈfQK?Pٜ9|D̹ž-)ds x<6ۅP $h!r_:o{~R T:{ZC^ُIs(C3U99% /HI ەEN̢RI_Rxb"BuɈM^Oq.exTw5jBFgsk+Kv]4MB-ņ}yIyz^V,%gn癆{[jk+-5 ސD 2=i'0=ҟ09O!22||fS@<=a#QXBBs1z~34jf6PPH M$G!nWMZRP01ZK-xąN wѶu]Aps-Ʃ1[x\jC2'W/WaG`w[-lgͨnjίo:ߎ?wnK[M؉1rڵU Ìwv{ $CNWl԰{YMd .tf}q8t:XoVN4n/nև m˯\߀SAw@ S~hF*DO(B)hҴ(h:â 9:nhD.:Lnm1΀ʽQnwW#4I?_^8؅Xh+\K7ɪ".?1;lٴ07_X`cB{(YPW9M%6A~@EmW/Kҝ{4b18UП!}i)O'tMD|7K ϮkxUCⅤKN?a̴G$LNF3,ReQB#J,ceHq{vΪÜR#0M>ÊYOۼ.vԄ-*םgн3zvƒNuR~3M_߼3 ϛ/3bOJxz Sync Internals About Types Data Events Sync Node Browser Search
    Include Identifiers

    Sync Protocol Log

    
            
    
          

    Type Info

    Actionable Error

    Type Counters

    Type Total Entries Updates Received Reflected Updates Received Tombstone Updates Received Updates Applied Hierarchy Conflict Application Failures Encryption Conflict Application Failures Server Overwrite Conflicts Local Overwrite Conflicts Commit Attempts Commit Successes Commit Conflicts Commit Errors
    0 0 0 0 0 0 0 0 0 0 0 0 0

    Some personal info may be in the events dump. Be careful about posting data dumps on bug reports.

    
    
    
    include node content WARNING: This is likely to include personal information.
    Details Submodule Event Time
    Last refresh time: Never
    Title
    ID
    Modification Time
    Parent
    Is Folder
    Type
    External ID
    Position Index
    
        

    Quick Search: Unapplied Updates Unsynced Conflicted Deleted

    
      
    // Copyright (c) 2011 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. // Allow platform specific CSS rules. // // TODO(akalin): BMM and options page does something similar, too. // Move this to util.js. if (cr.isWindows) document.documentElement.setAttribute('os', 'win'); cr.ui.decorate('tabbox', cr.ui.TabBox); // Copyright (c) 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. // require cr.js // require cr/event_target.js // require cr/util.js cr.define('chrome.sync', function() { 'use strict'; /** * A simple timer to measure elapsed time. * @constructor */ function Timer() { /** * The time that this Timer was created. * @type {number} * @private * @const */ this.start_ = Date.now(); } /** * @return {number} The elapsed seconds since this Timer was created. */ Timer.prototype.getElapsedSeconds = function() { return (Date.now() - this.start_) / 1000; }; /** @return {!Timer} An object which measures elapsed time. */ var makeTimer = function() { return new Timer; }; /** * @param {string} name The name of the event type. * @param {!Object} details A collection of event-specific details. */ var dispatchEvent = function(name, details) { var e = new Event(name); e.details = details; chrome.sync.events.dispatchEvent(e); }; /** * Registers to receive a stream of events through * chrome.sync.dispatchEvent(). */ var registerForEvents = function() { chrome.send('registerForEvents'); }; /** * Registers to receive a stream of status counter update events * chrome.sync.dispatchEvent(). */ var registerForPerTypeCounters = function() { chrome.send('registerForPerTypeCounters'); } /** * Asks the browser to refresh our snapshot of sync state. Should result * in an onAboutInfoUpdated event being emitted. */ var requestUpdatedAboutInfo = function() { chrome.send('requestUpdatedAboutInfo'); }; /** * Asks the browser to send us the list of registered types. Should result * in an onReceivedListOfTypes event being emitted. */ var requestListOfTypes = function() { chrome.send('requestListOfTypes'); }; /** * Counter to uniquely identify requests while they're in progress. * Used in the implementation of GetAllNodes. */ var requestId = 0; /** * A map from counter values to asynchronous request callbacks. * Used in the implementation of GetAllNodes. * @type {{number: !Function}} */ var requestCallbacks = {}; /** * Asks the browser to send us a copy of all existing sync nodes. * Will eventually invoke the given callback with the results. * * @param {function(!Object)} callback The function to call with the response. */ var getAllNodes = function(callback) { requestId++; requestCallbacks[requestId] = callback; chrome.send('getAllNodes', [requestId]); }; /** * Called from C++ with the response to a getAllNodes request. * @param {number} id The requestId passed in with the request. * @param {Object} response The response to the request. */ var getAllNodesCallback = function(id, response) { requestCallbacks[id](response); requestCallbacks[id] = undefined; }; return { makeTimer: makeTimer, dispatchEvent: dispatchEvent, events: new cr.EventTarget(), getAllNodes: getAllNodes, getAllNodesCallback: getAllNodesCallback, registerForEvents: registerForEvents, registerForPerTypeCounters: registerForPerTypeCounters, requestUpdatedAboutInfo: requestUpdatedAboutInfo, requestListOfTypes: requestListOfTypes, }; }); // Copyright 2014 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. cr.define('chrome.sync.types', function() { var typeCountersMap = {}; /** * Redraws the counters table taking advantage of the most recent * available information. * * Makes use of typeCountersMap, which is defined in the containing scope. */ var refreshTypeCountersDisplay = function() { var typeCountersArray = []; // Transform our map into an array to make jstemplate happy. Object.keys(typeCountersMap).sort().forEach(function(t) { typeCountersArray.push({ type: t, counters: typeCountersMap[t], }); }); jstProcess( new JsEvalContext({ rows: typeCountersArray }), $('type-counters-table')); }; /** * Helps to initialize the table by picking up where initTypeCounters() left * off. That function registers this listener and requests that this event * be emitted. * * @param {!Object} e An event containing the list of known sync types. */ var onReceivedListOfTypes = function(e) { var types = e.details.types; types.map(function(type) { if (!typeCountersMap.hasOwnProperty(type)) { typeCountersMap[type] = {}; } }); chrome.sync.events.removeEventListener( 'onReceivedListOfTypes', onReceivedListOfTypes); refreshTypeCountersDisplay(); }; /** * Callback for receipt of updated per-type counters. * * @param {!Object} e An event containing an updated counter. */ var onCountersUpdated = function(e) { var details = e.details; var modelType = details.modelType; var counters = details.counters; if (typeCountersMap.hasOwnProperty(modelType)) for (k in counters) { typeCountersMap[modelType][k] = counters[k]; } refreshTypeCountersDisplay(); }; /** * Initializes state and callbacks for the per-type counters and status UI. */ var initTypeCounters = function() { chrome.sync.events.addEventListener( 'onCountersUpdated', onCountersUpdated); chrome.sync.events.addEventListener( 'onReceivedListOfTypes', onReceivedListOfTypes); chrome.sync.requestListOfTypes(); chrome.sync.registerForPerTypeCounters(); }; var onLoad = function() { initTypeCounters(); }; return { onLoad: onLoad }; }); document.addEventListener('DOMContentLoaded', chrome.sync.types.onLoad, false); // Copyright (c) 2011 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. // require: cr.js // require: cr/event_target.js /** * @fileoverview This creates a log object which listens to and * records all sync events. */ cr.define('chrome.sync', function() { 'use strict'; var eventsByCategory = { notifier: [ 'onIncomingNotification', 'onNotificationStateChange', ], manager: [ 'onActionableError', 'onChangesApplied', 'onChangesComplete', 'onClearServerDataFailed', 'onClearServerDataSucceeded', 'onConnectionStatusChange', 'onEncryptedTypesChanged', 'onEncryptionComplete', 'onInitializationComplete', 'onPassphraseAccepted', 'onPassphraseRequired', 'onStopSyncingPermanently', 'onSyncCycleCompleted', ], transaction: [ 'onTransactionWrite', ], protocol: [ 'onProtocolEvent', ] }; /** * Creates a new log object which then immediately starts recording * sync events. Recorded entries are available in the 'entries' * property and there is an 'append' event which can be listened to. * @constructor * @extends {cr.EventTarget} */ var Log = function() { var self = this; /** * Creates a callback function to be invoked when an event arrives. */ var makeCallback = function(categoryName, eventName) { return function(e) { self.log_(categoryName, eventName, e.details); }; }; for (var categoryName in eventsByCategory) { for (var i = 0; i < eventsByCategory[categoryName].length; ++i) { var eventName = eventsByCategory[categoryName][i]; chrome.sync.events.addEventListener( eventName, makeCallback(categoryName, eventName)); } } } Log.prototype = { __proto__: cr.EventTarget.prototype, /** * The recorded log entries. * @type {array} */ entries: [], /** * Records a single event with the given parameters and fires the * 'append' event with the newly-created event as the 'detail' * field of a custom event. * @param {string} submodule The sync submodule for the event. * @param {string} event The name of the event. * @param {dictionary} details A dictionary of event-specific details. */ log_: function(submodule, event, details) { var entry = { submodule: submodule, event: event, date: new Date(), details: details, textDetails: '' }; entry.textDetails = JSON.stringify(entry.details, null, 2); this.entries.push(entry); // Fire append event. var e = cr.doc.createEvent('CustomEvent'); e.initCustomEvent('append', false, false, entry); this.dispatchEvent(e); } }; return { log: new Log() }; }); // Copyright (c) 2011 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. // require: cr.js // require: cr/ui.js // require: cr/ui/tree.js (function() { /** * A helper function to determine if a node is the root of its type. * * @param {!Object} node The node to check. */ var isTypeRootNode = function(node) { return node.PARENT_ID == 'r' && node.UNIQUE_SERVER_TAG != ''; }; /** * A helper function to determine if a node is a child of the given parent. * * @param {!Object} parent node. * @param {!Object} node The node to check. */ var isChildOf = function(parentNode, node) { if (node.PARENT_ID != '') { return node.PARENT_ID == parentNode.ID; } else { return node.modelType == parentNode.modelType; } }; /** * A helper function to sort sync nodes. * * Sorts by position index if possible, falls back to sorting by name, and * finally sorting by METAHANDLE. * * If this proves to be slow and expensive, we should experiment with moving * this functionality to C++ instead. */ var nodeComparator = function(nodeA, nodeB) { if (nodeA.hasOwnProperty('positionIndex') && nodeB.hasOwnProperty('positionIndex')) { return nodeA.positionIndex - nodeB.positionIndex; } else if (nodeA.NON_UNIQUE_NAME != nodeB.NON_UNIQUE_NAME) { return nodeA.NON_UNIQUE_NAME.localeCompare(nodeB.NON_UNIQUE_NAME); } else { return nodeA.METAHANDLE - nodeB.METAHANDLE; } }; /** * Updates the node detail view with the details for the given node. * @param {!Object} node The struct representing the node we want to display. */ function updateNodeDetailView(node) { var nodeDetailsView = $('node-details'); nodeDetailsView.hidden = false; jstProcess(new JsEvalContext(node.entry_), nodeDetailsView); } /** * Updates the 'Last refresh time' display. * @param {string} The text to display. */ function setLastRefreshTime(str) { $('node-browser-refresh-time').textContent = str; } /** * Creates a new sync node tree item. * * @constructor * @param {!Object} node The nodeDetails object for the node as returned by * chrome.sync.getAllNodes(). * @extends {cr.ui.TreeItem} */ var SyncNodeTreeItem = function(node) { var treeItem = new cr.ui.TreeItem(); treeItem.__proto__ = SyncNodeTreeItem.prototype; treeItem.entry_ = node; treeItem.label = node.NON_UNIQUE_NAME; if (node.IS_DIR) { treeItem.mayHaveChildren_ = true; // Load children on expand. treeItem.expanded_ = false; treeItem.addEventListener('expand', treeItem.handleExpand_.bind(treeItem)); } else { treeItem.classList.add('leaf'); } return treeItem; }; SyncNodeTreeItem.prototype = { __proto__: cr.ui.TreeItem.prototype, /** * Finds the children of this node and appends them to the tree. */ handleExpand_: function(event) { var treeItem = this; if (treeItem.expanded_) { return; } treeItem.expanded_ = true; var children = treeItem.tree.allNodes.filter( isChildOf.bind(undefined, treeItem.entry_)); children.sort(nodeComparator); children.forEach(function(node) { treeItem.add(new SyncNodeTreeItem(node)); }); }, }; /** * Creates a new sync node tree. Technically, it's a forest since it each * type has its own root node for its own tree, but it still looks and acts * mostly like a tree. * * @param {Object=} opt_propertyBag Optional properties. * @constructor * @extends {cr.ui.Tree} */ var SyncNodeTree = cr.ui.define('tree'); SyncNodeTree.prototype = { __proto__: cr.ui.Tree.prototype, decorate: function() { cr.ui.Tree.prototype.decorate.call(this); this.addEventListener('change', this.handleChange_.bind(this)); this.allNodes = []; }, populate: function(nodes) { var tree = this; // We store the full set of nodes in the SyncNodeTree object. tree.allNodes = nodes; var roots = tree.allNodes.filter(isTypeRootNode); roots.sort(nodeComparator); roots.forEach(function(typeRoot) { tree.add(new SyncNodeTreeItem(typeRoot)); }); }, handleChange_: function(event) { if (this.selectedItem) { updateNodeDetailView(this.selectedItem); } } }; /** * Clears any existing UI state. Useful prior to a refresh. */ function clear() { var treeContainer = $('sync-node-tree-container'); while (treeContainer.firstChild) { treeContainer.removeChild(treeContainer.firstChild); } var nodeDetailsView = $('node-details'); nodeDetailsView.hidden = true; } /** * Fetch the latest set of nodes and refresh the UI. */ function refresh() { $('node-browser-refresh-button').disabled = true; clear(); setLastRefreshTime('In progress since ' + (new Date()).toLocaleString()); chrome.sync.getAllNodes(function(nodeMap) { // Put all nodes into one big list that ignores the type. var nodes = nodeMap. map(function(x) { return x.nodes; }). reduce(function(a, b) { return a.concat(b); }); var treeContainer = $('sync-node-tree-container'); var tree = document.createElement('tree'); tree.setAttribute('id', 'sync-node-tree'); tree.setAttribute('icon-visibility', 'parent'); treeContainer.appendChild(tree); cr.ui.decorate(tree, SyncNodeTree); tree.populate(nodes); setLastRefreshTime((new Date()).toLocaleString()); $('node-browser-refresh-button').disabled = false; }); } document.addEventListener('DOMContentLoaded', function(e) { $('node-browser-refresh-button').addEventListener('click', refresh); var Splitter = cr.ui.Splitter; var customSplitter = cr.ui.define('div'); customSplitter.prototype = { __proto__: Splitter.prototype, handleSplitterDragEnd: function(e) { Splitter.prototype.handleSplitterDragEnd.apply(this, arguments); var treeElement = $("sync-node-tree-container"); var newWidth = parseFloat(treeElement.style.width); treeElement.style.minWidth = Math.max(newWidth, 50) + "px"; } }; customSplitter.decorate($("sync-node-splitter")); // Automatically trigger a refresh the first time this tab is selected. $('sync-browser-tab').addEventListener('selectedChange', function f(e) { if (this.selected) { $('sync-browser-tab').removeEventListener('selectedChange', f); refresh(); } }); }); })(); // Copyright (c) 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. // require: cr.js cr.define('chrome.sync', function() { var currSearchId = 0; var setQueryString = function(queryControl, query) { queryControl.value = query; }; var createDoQueryFunction = function(queryControl, submitControl, query) { return function() { setQueryString(queryControl, query); submitControl.click(); }; }; /** * Decorates the quick search controls * * @param {Array of DOM elements} quickLinkArray The object which * will be given a link to a quick filter option. * @param {!HTMLInputElement} queryControl The object of * type=search where user's query is typed. */ var decorateQuickQueryControls = function(quickLinkArray, submitControl, queryControl) { for (var index = 0; index < allLinks.length; ++index) { var quickQuery = allLinks[index].getAttribute('data-query'); var quickQueryFunction = createDoQueryFunction(queryControl, submitControl, quickQuery); allLinks[index].addEventListener('click', quickQueryFunction); } }; /** * Runs a search with the given query. * * @param {string} query The regex to do the search with. * @param {function} callback The callback called with the search results; * not called if doSearch() is called again while the search is running. */ var doSearch = function(query, callback) { var searchId = ++currSearchId; try { var regex = new RegExp(query); chrome.sync.getAllNodes(function(node_map) { // Put all nodes into one big list that ignores the type. var nodes = node_map. map(function(x) { return x.nodes; }). reduce(function(a, b) { return a.concat(b); }); if (currSearchId != searchId) { return; } callback(nodes.filter(function(elem) { return regex.test(JSON.stringify(elem, null, 2)); }), null); }); } catch (err) { // Sometimes the provided regex is invalid. This and other errors will // be caught and handled here. callback([], err); } }; /** * Decorates the various search controls. * * @param {!HTMLInputElement} queryControl The object of * type=search where the user's query is typed. * @param {!HTMLButtonElement} submitControl The