TOP-UNFINISHED-diagnostic-buoy-component.patch 95 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625
  1. # HG changeset patch
  2. # User Matt A. Tobin <email@mattatobin.com>
  3. # Date 1659988365 0
  4. # Parent 721e1ab786754b9bd15a02f0a3416506594af9eb
  5. No Bug - Create an isolated dianostic component for the suite.
  6. diff --git a/suite/buoy/ATTN-CC-SUITE-PATCHERS.txt b/suite/buoy/ATTN-CC-SUITE-PATCHERS.txt
  7. new file mode 100644
  8. --- /dev/null
  9. +++ b/suite/buoy/ATTN-CC-SUITE-PATCHERS.txt
  10. @@ -0,0 +1,3 @@
  11. +Please exclude this component from any scripted or manual upgrades to the rest
  12. +of the suite (unless you are specifically updating this component).
  13. +It's handling is a special case and should be done seperately.
  14. \ No newline at end of file
  15. diff --git a/suite/buoy/ATTN-L10N-TRANSLATORS.txt b/suite/buoy/ATTN-L10N-TRANSLATORS.txt
  16. new file mode 100644
  17. --- /dev/null
  18. +++ b/suite/buoy/ATTN-L10N-TRANSLATORS.txt
  19. @@ -0,0 +1,3 @@
  20. +This component can be ignored and does not need to be translated. Its only
  21. +purpose is to faciliate SeaMonkey Reconstruction as well as specific
  22. +testing cases.
  23. \ No newline at end of file
  24. diff --git a/suite/buoy/ZZ-buoy-prefs.js b/suite/buoy/ZZ-buoy-prefs.js
  25. new file mode 100644
  26. --- /dev/null
  27. +++ b/suite/buoy/ZZ-buoy-prefs.js
  28. @@ -0,0 +1,2 @@
  29. +pref("toolkit.defaultChromeURI", "chrome://buoy/content/buoy.xhtml");
  30. +pref("prompts.contentPromptSubDialog", false);
  31. diff --git a/suite/buoy/content/buoy.css b/suite/buoy/content/buoy.css
  32. new file mode 100644
  33. --- /dev/null
  34. +++ b/suite/buoy/content/buoy.css
  35. @@ -0,0 +1,9 @@
  36. +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
  37. +/* This Source Code Form is subject to the terms of the Mozilla Public
  38. + * License, v. 2.0. If a copy of the MPL was not distributed with this
  39. + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
  40. +
  41. +html, body {
  42. + height: 100%;
  43. +}
  44. +
  45. diff --git a/suite/buoy/content/buoy.js b/suite/buoy/content/buoy.js
  46. new file mode 100644
  47. --- /dev/null
  48. +++ b/suite/buoy/content/buoy.js
  49. @@ -0,0 +1,85 @@
  50. +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
  51. +/* This Source Code Form is subject to the terms of the Mozilla Public
  52. + * License, v. 2.0. If a copy of the MPL was not distributed with this
  53. + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
  54. +
  55. +// e10s
  56. +var { EzE10SUtils } = ChromeUtils.importESModule(
  57. + "resource:///modules/EzE10SUtils.sys.mjs"
  58. +);
  59. +
  60. +// Devtools
  61. +ChromeUtils.defineESModuleGetters(this, {
  62. + BrowserToolboxLauncher: "resource://devtools/client/framework/browser-toolbox/Launcher.sys.mjs",
  63. +});
  64. +
  65. +Object.defineProperty(this, "BrowserConsoleManager", {
  66. + get() {
  67. + let { loader } = ChromeUtils.importESModule("resource://devtools/shared/loader/Loader.sys.mjs");
  68. + return loader.require("devtools/client/webconsole/browser-console-manager").BrowserConsoleManager;
  69. + },
  70. + configurable: true,
  71. + enumerable: true,
  72. +});
  73. +
  74. +// Main functions
  75. +var gBuoy = {
  76. + homepage: "about:version",
  77. + toContent: function(aURL) {
  78. + var browser = document.getElementById("main-browser");
  79. + EzE10SUtils.loadURI(browser, aURL);
  80. + },
  81. + toChrome: function(inType, uri, features, args) {
  82. + var topWindow = Services.wm.getMostRecentWindow(inType);
  83. +
  84. + if (topWindow) {
  85. + topWindow.focus();
  86. + } else if (features) {
  87. + Services.ww.openWindow(null, uri, "_blank", features, args);
  88. + } else {
  89. + Services.ww.openWindow(
  90. + null,
  91. + uri,
  92. + "_blank",
  93. + "chrome,all,dialog=no,extrachrome,menubar,resizable,scrollbars," +
  94. + "status,location,toolbar,personalbar",
  95. + args
  96. + );
  97. + }
  98. + },
  99. + navHome: function() {
  100. + var browser = document.getElementById("main-browser");
  101. + EzE10SUtils.loadURI(browser, this.homepage);
  102. + },
  103. + navigation: function(aNaviCmd) {
  104. + var browser = document.getElementById("main-browser");
  105. + switch (aNaviCmd) {
  106. + case 'back':
  107. + browser.goBack();
  108. + break;
  109. + case 'forward':
  110. + browser.goForward();
  111. + break;
  112. + case 'reload':
  113. + browser.reload();
  114. + break;
  115. + case 'stop':
  116. + browser.stop();
  117. + break;
  118. + default:
  119. + gBuoy.navHome();
  120. + }
  121. + },
  122. + devtools: function() { BrowserToolboxLauncher.init(); },
  123. + quitApp: function() { Services.startup.quit(Services.startup.eAttemptQuit); },
  124. + startup: function() {
  125. + var browser = document.getElementById("main-browser");
  126. + EzE10SUtils.loadAboutBlank(browser);
  127. + gBuoy.navHome();
  128. + },
  129. +}
  130. +
  131. +// Devtools Compat
  132. +function openWebLinkIn(url, where, params) {
  133. + gBuoy.toContent(url);
  134. +}
  135. diff --git a/suite/buoy/content/buoy.xhtml b/suite/buoy/content/buoy.xhtml
  136. new file mode 100644
  137. --- /dev/null
  138. +++ b/suite/buoy/content/buoy.xhtml
  139. @@ -0,0 +1,155 @@
  140. +<?xml version="1.0"?>
  141. +<!-- This Source Code Form is subject to the terms of the Mozilla Public
  142. + - License, v. 2.0. If a copy of the MPL was not distributed with this
  143. + - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
  144. +
  145. +<!-- Mozilla DocType Reference:
  146. + xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
  147. + xmlns:xbl="http://www.mozilla.org/xbl"
  148. + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
  149. + xmlns:html="http://www.w3.org/1999/xhtml"
  150. + xmlns:svg="http://www.w3.org/2000/svg"
  151. + xmlns:em="http://www.mozilla.org/2004/em-rdf#"
  152. +-->
  153. +
  154. +<!DOCTYPE html>
  155. +
  156. +<html id="main-window"
  157. + xmlns="http://www.w3.org/1999/xhtml"
  158. + xmlns:html="http://www.w3.org/1999/xhtml"
  159. + xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
  160. + windowtype="buoy:main"
  161. + screenX="10"
  162. + screenY="10"
  163. + width="640px"
  164. + height="480px"
  165. + scrolling="false"
  166. + persist="screenX screenY width height sizemode">
  167. + <head>
  168. + <title>SeaMonkey Diagnostic &amp; Testing Buoy</title>
  169. + <link rel="stylesheet" href="chrome://global/skin/global.css" />
  170. + <link rel="stylesheet" href="chrome://buoy/content/buoy.css" />
  171. + <script defer="defer" src="chrome://global/content/customElements.js" />
  172. + <script defer="defer" src="chrome://buoy/content/buoy.js" />
  173. + <script>
  174. + window.addEventListener("load", gBuoy.startup);
  175. + </script>
  176. + </head>
  177. + <body xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
  178. +
  179. + <commandset id="mainCommandSet">
  180. + <command id="cmd_Exit"
  181. + oncommand="gBuoy.quitApp();"/>
  182. + <command id="cmd_DevTools"
  183. + oncommand="gBuoy.devtools();"/>
  184. +
  185. + <command id="cmd_Navigator"
  186. + oncommand="gBuoy.toChrome('navigator:browser',
  187. + 'chrome://navigator/content/navigator.xhtml');"/>
  188. + <command id="cmd_MailNews"
  189. + oncommand="gBuoy.toChrome('mail:3pane',
  190. + 'chrome://messenger/content/messenger.xhtml');"/>
  191. + <command id="cmd_Composer"
  192. + oncommand="gBuoy.toChrome('composer:html',
  193. + 'chrome://editor/content/editor.xhtml');"/>
  194. + <command id="cmd_Preferences"
  195. + oncommand="gBuoy.toChrome('mozilla:preferences',
  196. + 'chrome://communicator/content/pref/preferences.xhtml');"/>
  197. +
  198. + <command id="cmd_AboutAbout"
  199. + oncommand="gBuoy.toContent('about:about');"/>
  200. + <command id="cmd_AboutConfig"
  201. + oncommand="gBuoy.toContent('about:config');"/>
  202. + <command id="cmd_AboutSupport"
  203. + oncommand="gBuoy.toContent('about:support');"/>
  204. + <command id="cmd_AboutVersion"
  205. + oncommand="gBuoy.toContent('about:version');"/>
  206. +
  207. + <command id="cmd_NavHome"
  208. + oncommand="gBuoy.navHome();"/>
  209. +
  210. + <command id="cmd_NavBack"
  211. + oncommand="gBuoy.navigation('back');"/>
  212. + <command id="cmd_NavForward"
  213. + oncommand="gBuoy.navigation('forward');"/>
  214. + <command id="cmd_NavReload"
  215. + oncommand="gBuoy.navigation('reload');"/>
  216. + <command id="cmd_NavStop"
  217. + oncommand="gBuoy.navigation('stop');"/>
  218. +
  219. + <command id="cmd_SeaMonkeyHomePage"
  220. + oncommand="gBuoy.toContent('https://www.seamonkey-project.org/');"/>
  221. + <command id="cmd_GetInvolved"
  222. + oncommand="gBuoy.toContent('https://www.seamonkey-project.org/dev/get-involved');"/>
  223. +
  224. + </commandset>
  225. +
  226. + <vbox flex="1">
  227. + <toolbox id="main-toolbox" style="border-bottom: 1px solid ThreeDShadow;">
  228. + <menubar id="main-menubar">
  229. + <menu id="file-menu" label="File">
  230. + <menupopup id="file-popup">
  231. + <menuitem label="Exit" command="cmd_Exit"/>
  232. + </menupopup>
  233. + </menu>
  234. + <menu id="edit-menu" label="Edit">
  235. + <menupopup id="edit-popup">
  236. + <menuitem label="Configuration Editor" command="cmd_AboutConfig"/>
  237. + <menuitem label="Preferences" command="cmd_Preferences"/>
  238. + </menupopup>
  239. + </menu>
  240. + <menu id="go-menu" label="Go">
  241. + <menupopup id="components-popup">
  242. + <menuitem label="Back" command="cmd_NavBack"/>
  243. + <menuitem label="Forward" command="cmd_NavForward"/>
  244. + <menuitem label="Reload" command="cmd_NavReload"/>
  245. + <menuitem label="Stop" command="cmd_NavStop"/>
  246. + <menuitem label="Home" command="cmd_NavHome"/>
  247. + </menupopup>
  248. + </menu>
  249. + <menu id="components-menu" label="Components">
  250. + <menupopup id="components-popup">
  251. + <menuitem label="Browser" command="cmd_Navigator"/>
  252. + <menuitem label="Messenger" command="cmd_MailNews"/>
  253. + <menuitem label="Composer" command="cmd_Composer"/>
  254. + </menupopup>
  255. + </menu>
  256. + <menu id="tools-menu" label="Tools">
  257. + <menupopup id="tools-popup">
  258. + <menuitem label="About: Pages" command="cmd_AboutAbout"/>
  259. + <menuitem label="Developer Tools" command="cmd_DevTools"/>
  260. + </menupopup>
  261. + </menu>
  262. + <menu id="help-menu" label="Help">
  263. + <menupopup id="help-popup">
  264. + <menuitem label="Get Involved" command="cmd_GetInvolved"/>
  265. + <menuitem label="Troubleshooting Information" command="cmd_AboutSupport"/>
  266. + <menuitem label="About SeaMonkey" command="cmd_AboutVersion"/>
  267. + </menupopup>
  268. + </menu>
  269. + </menubar>
  270. + <toolbar id="navigation-toolbar">
  271. + <toolbarbutton id="back-button" label="&lt; Back" command="cmd_NavBack"/>
  272. + <toolbarbutton id="forward-button" label="&gt; Forward" command="cmd_NavForward"/>
  273. + <toolbarbutton id="reload-button" label="O Reload" command="cmd_NavReload"/>
  274. + <toolbarbutton id="stop-button" label="X Stop" command="cmd_NavStop"/>
  275. + <html:input id="urlbar" style="flex: 1; margin: 2px; padding: 4px;" placeholder="Enter a URL..." />
  276. + <toolbarbutton id="go-button" label="Go -&gt;" oncommand="var urlbar = document.getElementById('urlbar');
  277. + gBuoy.toContent(urlbar.value);
  278. + urlbar.value = '';"/>
  279. + </toolbar>
  280. + <toolbar id="quick-links-toolbar">
  281. + <button label="SeaMonkey Homepage" command="cmd_SeaMonkeyHomePage"/>
  282. + <button label="Get Involved" command="cmd_GetInvolved"/>
  283. + </toolbar>
  284. + </toolbox>
  285. + <browser id="main-browser"
  286. + flex="1"
  287. + type="content"
  288. + primary="true"
  289. + maychangeremoteness="true"
  290. + nodefaultsrc="true" />
  291. + </vbox>
  292. + </body>
  293. +</html>
  294. +
  295. diff --git a/suite/buoy/jar.mn b/suite/buoy/jar.mn
  296. new file mode 100644
  297. --- /dev/null
  298. +++ b/suite/buoy/jar.mn
  299. @@ -0,0 +1,16 @@
  300. +# This Source Code Form is subject to the terms of the Mozilla Public
  301. +# License, v. 2.0. If a copy of the MPL was not distributed with this
  302. +# file, You can obtain one at http://mozilla.org/MPL/2.0/.
  303. +
  304. +#filter substitution
  305. +
  306. +buoy.jar:
  307. +% content buoy %content/buoy/ contentaccessible=yes
  308. + content/buoy/buoy.css (content/buoy.css)
  309. + content/buoy/buoy.js (content/buoy.js)
  310. + content/buoy/buoy.xhtml (content/buoy.xhtml)
  311. +
  312. +[localization] @AB_CD@.jar:
  313. + browser (moz-l10n/browser/**/*.ftl)
  314. + buoy (locale/**/*.ftl)
  315. +
  316. diff --git a/suite/buoy/modules/BrowserWindowTracker.sys.mjs b/suite/buoy/modules/BrowserWindowTracker.sys.mjs
  317. new file mode 100644
  318. --- /dev/null
  319. +++ b/suite/buoy/modules/BrowserWindowTracker.sys.mjs
  320. @@ -0,0 +1,6 @@
  321. +/* This Source Code Form is subject to the terms of the Mozilla Public
  322. + * License, v. 2.0. If a copy of the MPL was not distributed with this
  323. + * file, you can obtain one at http://mozilla.org/MPL/2.0/. */
  324. +
  325. +// This module is deliberately not implemented. It only exists to keep
  326. +// the automated tests happy. See bug 1782621.
  327. diff --git a/suite/buoy/modules/CustomizableUI.sys.mjs b/suite/buoy/modules/CustomizableUI.sys.mjs
  328. new file mode 100644
  329. --- /dev/null
  330. +++ b/suite/buoy/modules/CustomizableUI.sys.mjs
  331. @@ -0,0 +1,360 @@
  332. +/* This Source Code Form is subject to the terms of the Mozilla Public
  333. + * License, v. 2.0. If a copy of the MPL was not distributed with this
  334. + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
  335. +
  336. +// This file is a copy of a file with the same name in Firefox. Only the
  337. +// pieces we're using, and a few pieces the devtools rely on such as the
  338. +// constants, remain.
  339. +
  340. +const lazy = {};
  341. +
  342. +ChromeUtils.defineESModuleGetters(lazy, {
  343. + PanelMultiView: "resource:///modules/PanelMultiView.sys.mjs",
  344. +});
  345. +
  346. +/**
  347. + * gPanelsForWindow is a list of known panels in a window which we may need to close
  348. + * should command events fire which target them.
  349. + */
  350. +var gPanelsForWindow = new WeakMap();
  351. +
  352. +var CustomizableUIInternal = {
  353. + addPanelCloseListeners(aPanel) {
  354. + Services.els.addSystemEventListener(aPanel, "click", this, false);
  355. + Services.els.addSystemEventListener(aPanel, "keypress", this, false);
  356. + const win = aPanel.ownerGlobal;
  357. + if (!gPanelsForWindow.has(win)) {
  358. + gPanelsForWindow.set(win, new Set());
  359. + }
  360. + gPanelsForWindow.get(win).add(this._getPanelForNode(aPanel));
  361. + },
  362. +
  363. + removePanelCloseListeners(aPanel) {
  364. + Services.els.removeSystemEventListener(aPanel, "click", this, false);
  365. + Services.els.removeSystemEventListener(aPanel, "keypress", this, false);
  366. + const win = aPanel.ownerGlobal;
  367. + const panels = gPanelsForWindow.get(win);
  368. + if (panels) {
  369. + panels.delete(this._getPanelForNode(aPanel));
  370. + }
  371. + },
  372. +
  373. + handleEvent(aEvent) {
  374. + switch (aEvent.type) {
  375. + case "click":
  376. + case "keypress":
  377. + this.maybeAutoHidePanel(aEvent);
  378. + break;
  379. + }
  380. + },
  381. +
  382. + _getPanelForNode(aNode) {
  383. + return aNode.closest("panel");
  384. + },
  385. +
  386. + /*
  387. + * If people put things in the panel which need more than single-click interaction,
  388. + * we don't want to close it. Right now we check for text inputs and menu buttons.
  389. + * We also check for being outside of any toolbaritem/toolbarbutton, ie on a blank
  390. + * part of the menu.
  391. + */
  392. + _isOnInteractiveElement(aEvent) {
  393. + function getMenuPopupForDescendant(aNode) {
  394. + let lastPopup = null;
  395. + while (
  396. + aNode &&
  397. + aNode.parentNode &&
  398. + aNode.parentNode.localName.startsWith("menu")
  399. + ) {
  400. + lastPopup = aNode.localName == "menupopup" ? aNode : lastPopup;
  401. + aNode = aNode.parentNode;
  402. + }
  403. + return lastPopup;
  404. + }
  405. +
  406. + let target = aEvent.target;
  407. + const panel = this._getPanelForNode(aEvent.currentTarget);
  408. + // This can happen in e.g. customize mode. If there's no panel,
  409. + // there's clearly nothing for us to close; pretend we're interactive.
  410. + if (!panel) {
  411. + return true;
  412. + }
  413. + // We keep track of:
  414. + // whether we're in an input container (text field)
  415. + let inInput = false;
  416. + // whether we're in a popup/context menu
  417. + let inMenu = false;
  418. + // whether we're in a toolbarbutton/toolbaritem
  419. + let inItem = false;
  420. + // whether the current menuitem has a valid closemenu attribute
  421. + let menuitemCloseMenu = "auto";
  422. +
  423. + // While keeping track of that, we go from the original target back up,
  424. + // to the panel if we have to. We bail as soon as we find an input,
  425. + // a toolbarbutton/item, or the panel:
  426. + while (target) {
  427. + // Skip out of iframes etc:
  428. + if (target.nodeType == target.DOCUMENT_NODE) {
  429. + if (!target.defaultView) {
  430. + // Err, we're done.
  431. + break;
  432. + }
  433. + // Find containing browser or iframe element in the parent doc.
  434. + target = target.defaultView.docShell.chromeEventHandler;
  435. + if (!target) {
  436. + break;
  437. + }
  438. + }
  439. + const tagName = target.localName;
  440. + inInput = tagName == "input";
  441. + inItem = tagName == "toolbaritem" || tagName == "toolbarbutton";
  442. + const isMenuItem = tagName == "menuitem";
  443. + inMenu = inMenu || isMenuItem;
  444. +
  445. + if (isMenuItem && target.hasAttribute("closemenu")) {
  446. + const closemenuVal = target.getAttribute("closemenu");
  447. + menuitemCloseMenu =
  448. + closemenuVal == "single" || closemenuVal == "none"
  449. + ? closemenuVal
  450. + : "auto";
  451. + }
  452. +
  453. + // Keep the menu open and break out of the loop if the click happened on
  454. + // the ShadowRoot or a disabled menu item.
  455. + if (
  456. + target.nodeType == target.DOCUMENT_FRAGMENT_NODE ||
  457. + target.getAttribute("disabled") == "true"
  458. + ) {
  459. + return true;
  460. + }
  461. +
  462. + // This isn't in the loop condition because we want to break before
  463. + // changing |target| if any of these conditions are true
  464. + if (inInput || inItem || target == panel) {
  465. + break;
  466. + }
  467. + // We need specific code for popups: the item on which they were invoked
  468. + // isn't necessarily in their parentNode chain:
  469. + if (isMenuItem) {
  470. + const topmostMenuPopup = getMenuPopupForDescendant(target);
  471. + target =
  472. + (topmostMenuPopup && topmostMenuPopup.triggerNode) ||
  473. + target.parentNode;
  474. + } else {
  475. + target = target.parentNode;
  476. + }
  477. + }
  478. +
  479. + // If the user clicked a menu item...
  480. + if (inMenu) {
  481. + // We care if we're in an input also,
  482. + // or if the user specified closemenu!="auto":
  483. + if (inInput || menuitemCloseMenu != "auto") {
  484. + return true;
  485. + }
  486. + // Otherwise, we're probably fine to close the panel
  487. + return false;
  488. + }
  489. + // If we're not in a menu, and we *are* in a type="menu" toolbarbutton,
  490. + // we'll now interact with the menu
  491. + if (inItem && target.getAttribute("type") == "menu") {
  492. + return true;
  493. + }
  494. + return inInput || !inItem;
  495. + },
  496. +
  497. + hidePanelForNode(aNode) {
  498. + const panel = this._getPanelForNode(aNode);
  499. + if (panel) {
  500. + lazy.PanelMultiView.hidePopup(panel);
  501. + }
  502. + },
  503. +
  504. + maybeAutoHidePanel(aEvent) {
  505. + const eventType = aEvent.type;
  506. + if (eventType == "keypress" && aEvent.keyCode != aEvent.DOM_VK_RETURN) {
  507. + return;
  508. + }
  509. +
  510. + if (eventType == "click" && aEvent.button != 0) {
  511. + return;
  512. + }
  513. +
  514. + // We don't check preventDefault - it makes sense that this was prevented,
  515. + // but we probably still want to close the panel. If consumers don't want
  516. + // this to happen, they should specify the closemenu attribute.
  517. + if (eventType != "command" && this._isOnInteractiveElement(aEvent)) {
  518. + return;
  519. + }
  520. +
  521. + // We can't use event.target because we might have passed an anonymous
  522. + // content boundary as well, and so target points to the outer element in
  523. + // that case. Unfortunately, this means we get anonymous child nodes instead
  524. + // of the real ones, so looking for the 'stoooop, don't close me' attributes
  525. + // is more involved.
  526. + let target = aEvent.originalTarget;
  527. + while (target.parentNode && target.localName != "panel") {
  528. + if (
  529. + target.getAttribute("closemenu") == "none" ||
  530. + target.getAttribute("widget-type") == "view" ||
  531. + target.getAttribute("widget-type") == "button-and-view"
  532. + ) {
  533. + return;
  534. + }
  535. + target = target.parentNode;
  536. + }
  537. +
  538. + // If we get here, we can actually hide the popup:
  539. + this.hidePanelForNode(aEvent.target);
  540. + },
  541. +};
  542. +Object.freeze(CustomizableUIInternal);
  543. +
  544. +export var CustomizableUI = {
  545. + /**
  546. + * Constant reference to the ID of the navigation toolbar.
  547. + */
  548. + AREA_NAVBAR: "nav-bar",
  549. + /**
  550. + * Constant reference to the ID of the menubar's toolbar.
  551. + */
  552. + AREA_MENUBAR: "toolbar-menubar",
  553. + /**
  554. + * Constant reference to the ID of the tabstrip toolbar.
  555. + */
  556. + AREA_TABSTRIP: "TabsToolbar",
  557. + /**
  558. + * Constant reference to the ID of the bookmarks toolbar.
  559. + */
  560. + AREA_BOOKMARKS: "PersonalToolbar",
  561. + /**
  562. + * Constant reference to the ID of the non-dymanic (fixed) list in the overflow panel.
  563. + */
  564. + AREA_FIXED_OVERFLOW_PANEL: "widget-overflow-fixed-list",
  565. +
  566. + /**
  567. + * Constant indicating the area is a menu panel.
  568. + */
  569. + TYPE_MENU_PANEL: "menu-panel",
  570. + /**
  571. + * Constant indicating the area is a toolbar.
  572. + */
  573. + TYPE_TOOLBAR: "toolbar",
  574. +
  575. + /**
  576. + * Constant indicating a XUL-type provider.
  577. + */
  578. + PROVIDER_XUL: "xul",
  579. + /**
  580. + * Constant indicating an API-type provider.
  581. + */
  582. + PROVIDER_API: "api",
  583. + /**
  584. + * Constant indicating dynamic (special) widgets: spring, spacer, and separator.
  585. + */
  586. + PROVIDER_SPECIAL: "special",
  587. +
  588. + /**
  589. + * Constant indicating the widget is built-in
  590. + */
  591. + SOURCE_BUILTIN: "builtin",
  592. + /**
  593. + * Constant indicating the widget is externally provided
  594. + * (e.g. by add-ons or other items not part of the builtin widget set).
  595. + */
  596. + SOURCE_EXTERNAL: "external",
  597. +
  598. + /**
  599. + * Constant indicating the reason the event was fired was a window closing
  600. + */
  601. + REASON_WINDOW_CLOSED: "window-closed",
  602. + /**
  603. + * Constant indicating the reason the event was fired was an area being
  604. + * unregistered separately from window closing mechanics.
  605. + */
  606. + REASON_AREA_UNREGISTERED: "area-unregistered",
  607. +
  608. + /**
  609. + * Add a widget to an area.
  610. + * If the area to which you try to add is not known to CustomizableUI,
  611. + * this will throw.
  612. + * If the area to which you try to add is the same as the area in which
  613. + * the widget is currently placed, this will do the same as
  614. + * moveWidgetWithinArea.
  615. + * If the widget cannot be removed from its original location, this will
  616. + * no-op.
  617. + *
  618. + * This will fire an onWidgetAdded notification,
  619. + * and an onWidgetBeforeDOMChange and onWidgetAfterDOMChange notification
  620. + * for each window CustomizableUI knows about.
  621. + *
  622. + * @param aWidgetId the ID of the widget to add
  623. + * @param aArea the ID of the area to add the widget to
  624. + * @param aPosition the position at which to add the widget. If you do not
  625. + * pass a position, the widget will be added to the end
  626. + * of the area.
  627. + */
  628. + addWidgetToArea(aWidgetId, aArea, aPosition) {},
  629. + /**
  630. + * Remove a widget from its area. If the widget cannot be removed from its
  631. + * area, or is not in any area, this will no-op. Otherwise, this will fire an
  632. + * onWidgetRemoved notification, and an onWidgetBeforeDOMChange and
  633. + * onWidgetAfterDOMChange notification for each window CustomizableUI knows
  634. + * about.
  635. + *
  636. + * @param aWidgetId the ID of the widget to remove
  637. + */
  638. + removeWidgetFromArea(aWidgetId) {},
  639. + /**
  640. + * Get the placement of a widget. This is by far the best way to obtain
  641. + * information about what the state of your widget is. The internals of
  642. + * this call are cheap (no DOM necessary) and you will know where the user
  643. + * has put your widget.
  644. + *
  645. + * @param aWidgetId the ID of the widget whose placement you want to know
  646. + * @returns
  647. + * {
  648. + * area: "somearea", // The ID of the area where the widget is placed
  649. + * position: 42 // the index in the placements array corresponding to
  650. + * // your widget.
  651. + * }
  652. + *
  653. + * OR
  654. + *
  655. + * null // if the widget is not placed anywhere (ie in the palette)
  656. + */
  657. + getPlacementOfWidget(aWidgetId, aOnlyRegistered = true, aDeadAreas = false) {
  658. + return null;
  659. + },
  660. + /**
  661. + * Add listeners to a panel that will close it. For use from the menu panel
  662. + * and overflowable toolbar implementations, unlikely to be useful for
  663. + * consumers.
  664. + *
  665. + * @param aPanel the panel to which listeners should be attached.
  666. + */
  667. + addPanelCloseListeners(aPanel) {
  668. + CustomizableUIInternal.addPanelCloseListeners(aPanel);
  669. + },
  670. + /**
  671. + * Remove close listeners that have been added to a panel with
  672. + * addPanelCloseListeners. For use from the menu panel and overflowable
  673. + * toolbar implementations, unlikely to be useful for consumers.
  674. + *
  675. + * @param aPanel the panel from which listeners should be removed.
  676. + */
  677. + removePanelCloseListeners(aPanel) {
  678. + CustomizableUIInternal.removePanelCloseListeners(aPanel);
  679. + },
  680. + /**
  681. + * Notify toolbox(es) of a particular event. If you don't pass aWindow,
  682. + * all toolboxes will be notified. For use from Customize Mode only,
  683. + * do not use otherwise.
  684. + *
  685. + * @param aEvent the name of the event to send.
  686. + * @param aDetails optional, the details of the event.
  687. + * @param aWindow optional, the window in which to send the event.
  688. + */
  689. + dispatchToolboxEvent(aEvent, aDetails = {}, aWindow = null) {},
  690. +};
  691. +Object.freeze(CustomizableUI);
  692. diff --git a/suite/buoy/modules/EzE10SUtils.sys.mjs b/suite/buoy/modules/EzE10SUtils.sys.mjs
  693. new file mode 100644
  694. --- /dev/null
  695. +++ b/suite/buoy/modules/EzE10SUtils.sys.mjs
  696. @@ -0,0 +1,93 @@
  697. +/* This Source Code Form is subject to the terms of the Mozilla Public
  698. + * License, v. 2.0. If a copy of the MPL was not distributed with this
  699. + * file, you can obtain one at http://mozilla.org/MPL/2.0/. */
  700. +
  701. +import { E10SUtils } from "resource://gre/modules/E10SUtils.sys.mjs";
  702. +
  703. +import { ExtensionParent } from "resource://gre/modules/ExtensionParent.sys.mjs";
  704. +
  705. +export var EzE10SUtils = {
  706. + /**
  707. + * Loads about:blank in `browser` without switching remoteness. about:blank
  708. + * can load in a local browser or a remote browser, and `loadURI` will make
  709. + * it load in a remote browser even if you don't want it to.
  710. + *
  711. + * @param {nsIBrowser} browser
  712. + */
  713. + loadAboutBlank(browser) {
  714. + if (!browser.currentURI || browser.currentURI.spec == "about:blank") {
  715. + return;
  716. + }
  717. + browser.loadURI(Services.io.newURI("about:blank"), {
  718. + triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
  719. + remoteTypeOverride: browser.remoteType,
  720. + });
  721. + },
  722. +
  723. + /**
  724. + * Loads `uri` in `browser`, changing to a remote/local browser if necessary.
  725. + *
  726. + * @see `nsIWebNavigation.loadURI`
  727. + *
  728. + * @param {nsIBrowser} browser
  729. + * @param {string} uri
  730. + * @param {object} params
  731. + */
  732. + loadURI(browser, uri, params = {}) {
  733. + const multiProcess = browser.ownerGlobal.docShell.QueryInterface(
  734. + Ci.nsILoadContext
  735. + ).useRemoteTabs;
  736. + const remoteSubframes = browser.ownerGlobal.docShell.QueryInterface(
  737. + Ci.nsILoadContext
  738. + ).useRemoteSubframes;
  739. +
  740. + const isRemote = browser.getAttribute("remote") == "true";
  741. + const remoteType = E10SUtils.getRemoteTypeForURI(
  742. + uri,
  743. + multiProcess,
  744. + remoteSubframes
  745. + );
  746. + const shouldBeRemote = remoteType !== E10SUtils.NOT_REMOTE;
  747. +
  748. + if (shouldBeRemote != isRemote) {
  749. + this.changeRemoteness(browser, remoteType);
  750. + }
  751. +
  752. + params.triggeringPrincipal =
  753. + params.triggeringPrincipal ||
  754. + Services.scriptSecurityManager.getSystemPrincipal();
  755. + browser.fixupAndLoadURIString(uri, params);
  756. + },
  757. +
  758. + /**
  759. + * Force `browser` to be a remote/local browser.
  760. + *
  761. + * @see E10SUtils.sys.mjs for remote types.
  762. + *
  763. + * @param {nsIBrowser} browser - the browser to enforce the remoteness of.
  764. + * @param {string} remoteType - the remoteness to enforce.
  765. + * @returns {boolean} true if any change happened on the browser (which would
  766. + * not be the case if its remoteness is already in the correct state).
  767. + */
  768. + changeRemoteness(browser, remoteType) {
  769. + if (browser.remoteType == remoteType) {
  770. + return false;
  771. + }
  772. +
  773. + browser.destroy();
  774. +
  775. + if (remoteType) {
  776. + browser.setAttribute("remote", "true");
  777. + browser.setAttribute("remoteType", remoteType);
  778. + } else {
  779. + browser.setAttribute("remote", "false");
  780. + browser.removeAttribute("remoteType");
  781. + }
  782. +
  783. + browser.changeRemoteness({ remoteType });
  784. + browser.construct();
  785. + ExtensionParent.apiManager.emit("extension-browser-inserted", browser);
  786. +
  787. + return true;
  788. + },
  789. +};
  790. diff --git a/suite/buoy/modules/PanelMultiView.sys.mjs b/suite/buoy/modules/PanelMultiView.sys.mjs
  791. new file mode 100644
  792. --- /dev/null
  793. +++ b/suite/buoy/modules/PanelMultiView.sys.mjs
  794. @@ -0,0 +1,1700 @@
  795. +/* This Source Code Form is subject to the terms of the Mozilla Public
  796. + * License, v. 2.0. If a copy of the MPL was not distributed with this
  797. + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
  798. +
  799. +/**
  800. + * Allows a popup panel to host multiple subviews. The main view shown when the
  801. + * panel is opened may slide out to display a subview, which in turn may lead to
  802. + * other subviews in a cascade menu pattern.
  803. + *
  804. + * The <panel> element should contain a <panelmultiview> element. Views are
  805. + * declared using <panelview> elements that are usually children of the main
  806. + * <panelmultiview> element, although they don't need to be, as views can also
  807. + * be imported into the panel from other panels or popup sets.
  808. + *
  809. + * The panel should be opened asynchronously using the openPopup static method
  810. + * on the PanelMultiView object. This will display the view specified using the
  811. + * mainViewId attribute on the contained <panelmultiview> element.
  812. + *
  813. + * Specific subviews can slide in using the showSubView method, and backwards
  814. + * navigation can be done using the goBack method or through a button in the
  815. + * subview headers.
  816. + *
  817. + * The process of displaying the main view or a new subview requires multiple
  818. + * steps to be completed, hence at any given time the <panelview> element may
  819. + * be in different states:
  820. + *
  821. + * -- Open or closed
  822. + *
  823. + * All the <panelview> elements start "closed", meaning that they are not
  824. + * associated to a <panelmultiview> element and can be located anywhere in
  825. + * the document. When the openPopup or showSubView methods are called, the
  826. + * relevant view becomes "open" and the <panelview> element may be moved to
  827. + * ensure it is a descendant of the <panelmultiview> element.
  828. + *
  829. + * The "ViewShowing" event is fired at this point, when the view is not
  830. + * visible yet. The event is allowed to cancel the operation, in which case
  831. + * the view is closed immediately.
  832. + *
  833. + * Closing the view does not move the node back to its original position.
  834. + *
  835. + * -- Visible or invisible
  836. + *
  837. + * This indicates whether the view is visible in the document from a layout
  838. + * perspective, regardless of whether it is currently scrolled into view. In
  839. + * fact, all subviews are already visible before they start sliding in.
  840. + *
  841. + * Before scrolling into view, a view may become visible but be placed in a
  842. + * special off-screen area of the document where layout and measurements can
  843. + * take place asynchronously.
  844. + *
  845. + * When navigating forward, an open view may become invisible but stay open
  846. + * after sliding out of view. The last known size of these views is still
  847. + * taken into account for determining the overall panel size.
  848. + *
  849. + * When navigating backwards, an open subview will first become invisible and
  850. + * then will be closed.
  851. + *
  852. + * -- Active or inactive
  853. + *
  854. + * This indicates whether the view is fully scrolled into the visible area
  855. + * and ready to receive mouse and keyboard events. An active view is always
  856. + * visible, but a visible view may be inactive. For example, during a scroll
  857. + * transition, both views will be inactive.
  858. + *
  859. + * When a view becomes active, the ViewShown event is fired synchronously,
  860. + * and the showSubView and goBack methods can be called for navigation.
  861. + *
  862. + * For the main view of the panel, the ViewShown event is dispatched during
  863. + * the "popupshown" event, which means that other "popupshown" handlers may
  864. + * be called before the view is active. Thus, code that needs to perform
  865. + * further navigation automatically should either use the ViewShown event or
  866. + * wait for an event loop tick, like BrowserTestUtils.waitForEvent does.
  867. + *
  868. + * -- Navigating with the keyboard
  869. + *
  870. + * An open view may keep state related to keyboard navigation, even if it is
  871. + * invisible. When a view is closed, keyboard navigation state is cleared.
  872. + *
  873. + * This diagram shows how <panelview> nodes move during navigation:
  874. + *
  875. + * In this <panelmultiview> In other panels Action
  876. + * ┌───┬───┬───┐ ┌───┬───┐
  877. + * │(A)│ B │ C │ │ D │ E │ Open panel
  878. + * └───┴───┴───┘ └───┴───┘
  879. + * ┌───┬───┬───┐ ┌───┬───┐
  880. + * │{A}│(C)│ B │ │ D │ E │ Show subview C
  881. + * └───┴───┴───┘ └───┴───┘
  882. + * ┌───┬───┬───┬───┐ ┌───┐
  883. + * │{A}│{C}│(D)│ B │ │ E │ Show subview D
  884. + * └───┴───┴───┴───┘ └───┘
  885. + * │ ┌───┬───┬───┬───┐ ┌───┐
  886. + * │ │{A}│(C)│ D │ B │ │ E │ Go back
  887. + * │ └───┴───┴───┴───┘ └───┘
  888. + * │ │ │
  889. + * │ │ └── Currently visible view
  890. + * │ │ │
  891. + * └───┴───┴── Open views
  892. + */
  893. +
  894. +const lazy = {};
  895. +
  896. +ChromeUtils.defineLazyGetter(lazy, "gBundle", function () {
  897. + return Services.strings.createBundle(
  898. + "chrome://messenger/locale/messenger.properties"
  899. + );
  900. +});
  901. +
  902. +/**
  903. + * Safety timeout after which asynchronous events will be canceled if any of the
  904. + * registered blockers does not return.
  905. + */
  906. +const BLOCKERS_TIMEOUT_MS = 10000;
  907. +
  908. +const TRANSITION_PHASES = Object.freeze({
  909. + START: 1,
  910. + PREPARE: 2,
  911. + TRANSITION: 3,
  912. +});
  913. +
  914. +const gNodeToObjectMap = new WeakMap();
  915. +const gWindowsWithUnloadHandler = new WeakSet();
  916. +
  917. +/**
  918. + * Allows associating an object to a node lazily using a weak map.
  919. + *
  920. + * Classes deriving from this one may be easily converted to Custom Elements,
  921. + * although they would lose the ability of being associated lazily.
  922. + */
  923. +var AssociatedToNode = class {
  924. + constructor(node) {
  925. + /**
  926. + * Node associated to this object.
  927. + */
  928. + this.node = node;
  929. +
  930. + /**
  931. + * This promise is resolved when the current set of blockers set by event
  932. + * handlers have all been processed.
  933. + */
  934. + this._blockersPromise = Promise.resolve();
  935. + }
  936. +
  937. + /**
  938. + * Retrieves the instance associated with the given node, constructing a new
  939. + * one if necessary. When the last reference to the node is released, the
  940. + * object instance will be garbage collected as well.
  941. + */
  942. + static forNode(node) {
  943. + let associatedToNode = gNodeToObjectMap.get(node);
  944. + if (!associatedToNode) {
  945. + associatedToNode = new this(node);
  946. + gNodeToObjectMap.set(node, associatedToNode);
  947. + }
  948. + return associatedToNode;
  949. + }
  950. +
  951. + get document() {
  952. + return this.node.ownerDocument;
  953. + }
  954. +
  955. + get window() {
  956. + return this.node.ownerGlobal;
  957. + }
  958. +
  959. + _getBoundsWithoutFlushing(element) {
  960. + return this.window.windowUtils.getBoundsWithoutFlushing(element);
  961. + }
  962. +
  963. + /**
  964. + * Dispatches a custom event on this element.
  965. + *
  966. + * @param {string} eventName Name of the event to dispatch.
  967. + * @param {object} [detail] Event detail object. Optional.
  968. + * @param {boolean} cancelable If the event can be canceled.
  969. + * @returns {boolean} `true` if the event was canceled by an event handler, `false`
  970. + * otherwise.
  971. + */
  972. + dispatchCustomEvent(eventName, detail, cancelable = false) {
  973. + const event = new this.window.CustomEvent(eventName, {
  974. + detail,
  975. + bubbles: true,
  976. + cancelable,
  977. + });
  978. + this.node.dispatchEvent(event);
  979. + return event.defaultPrevented;
  980. + }
  981. +
  982. + /**
  983. + * Dispatches a custom event on this element and waits for any blocking
  984. + * promises registered using the "addBlocker" function on the details object.
  985. + * If this function is called again, the event is only dispatched after all
  986. + * the previously registered blockers have returned.
  987. + *
  988. + * The event can be canceled either by resolving any blocking promise to the
  989. + * boolean value "false" or by calling preventDefault on the event. Rejections
  990. + * and exceptions will be reported and will cancel the event.
  991. + *
  992. + * Blocking should be used sporadically because it slows down the interface.
  993. + * Also, non-reentrancy is not strictly guaranteed because a safety timeout of
  994. + * BLOCKERS_TIMEOUT_MS is implemented, after which the event will be canceled.
  995. + * This helps to prevent deadlocks if any of the event handlers does not
  996. + * resolve a blocker promise.
  997. + *
  998. + * @note Since there is no use case for dispatching different asynchronous
  999. + * events in parallel for the same element, this function will also wait
  1000. + * for previous blockers when the event name is different.
  1001. + *
  1002. + * @param eventName
  1003. + * Name of the custom event to dispatch.
  1004. + *
  1005. + * @resolves True if the event was canceled by a handler, false otherwise.
  1006. + */
  1007. + async dispatchAsyncEvent(eventName) {
  1008. + // Wait for all the previous blockers before dispatching the event.
  1009. + const blockersPromise = this._blockersPromise.catch(() => {});
  1010. + return (this._blockersPromise = blockersPromise.then(async () => {
  1011. + const blockers = new Set();
  1012. + let cancel = this.dispatchCustomEvent(
  1013. + eventName,
  1014. + {
  1015. + addBlocker(promise) {
  1016. + // Any exception in the blocker will cancel the operation.
  1017. + blockers.add(
  1018. + promise.catch(ex => {
  1019. + console.error(ex);
  1020. + return true;
  1021. + })
  1022. + );
  1023. + },
  1024. + },
  1025. + true
  1026. + );
  1027. + if (blockers.size) {
  1028. + const timeoutPromise = new Promise((resolve, reject) => {
  1029. + this.window.setTimeout(reject, BLOCKERS_TIMEOUT_MS);
  1030. + });
  1031. + try {
  1032. + const results = await Promise.race([
  1033. + Promise.all(blockers),
  1034. + timeoutPromise,
  1035. + ]);
  1036. + cancel = cancel || results.some(result => result === false);
  1037. + } catch (ex) {
  1038. + console.error(
  1039. + new Error(`One of the blockers for ${eventName} timed out.`)
  1040. + );
  1041. + return true;
  1042. + }
  1043. + }
  1044. + return cancel;
  1045. + }));
  1046. + }
  1047. +};
  1048. +
  1049. +/**
  1050. + * This is associated to <panelmultiview> elements.
  1051. + */
  1052. +export class PanelMultiView extends AssociatedToNode {
  1053. + /**
  1054. + * Tries to open the specified <panel> and displays the main view specified
  1055. + * with the "mainViewId" attribute on the <panelmultiview> node it contains.
  1056. + *
  1057. + * If the panel does not contain a <panelmultiview>, it is opened directly.
  1058. + * This allows consumers like page actions to accept different panel types.
  1059. + *
  1060. + * @see The non-static openPopup method for details.
  1061. + */
  1062. + static async openPopup(panelNode, ...args) {
  1063. + const panelMultiViewNode = panelNode.querySelector("panelmultiview");
  1064. + if (panelMultiViewNode) {
  1065. + return this.forNode(panelMultiViewNode).openPopup(...args);
  1066. + }
  1067. + panelNode.openPopup(...args);
  1068. + return true;
  1069. + }
  1070. +
  1071. + /**
  1072. + * Closes the specified <panel> which contains a <panelmultiview> node.
  1073. + *
  1074. + * If the panel does not contain a <panelmultiview>, it is closed directly.
  1075. + * This allows consumers like page actions to accept different panel types.
  1076. + *
  1077. + * @see The non-static hidePopup method for details.
  1078. + */
  1079. + static hidePopup(panelNode) {
  1080. + const panelMultiViewNode = panelNode.querySelector("panelmultiview");
  1081. + if (panelMultiViewNode) {
  1082. + this.forNode(panelMultiViewNode).hidePopup();
  1083. + } else {
  1084. + panelNode.hidePopup();
  1085. + }
  1086. + }
  1087. +
  1088. + /**
  1089. + * Removes the specified <panel> from the document, ensuring that any
  1090. + * <panelmultiview> node it contains is destroyed properly.
  1091. + *
  1092. + * If the viewCacheId attribute is present on the <panelmultiview> element,
  1093. + * imported subviews will be moved out again to the element it specifies, so
  1094. + * that the panel element can be removed safely.
  1095. + *
  1096. + * If the panel does not contain a <panelmultiview>, it is removed directly.
  1097. + * This allows consumers like page actions to accept different panel types.
  1098. + */
  1099. + static removePopup(panelNode) {
  1100. + try {
  1101. + const panelMultiViewNode = panelNode.querySelector("panelmultiview");
  1102. + if (panelMultiViewNode) {
  1103. + const panelMultiView = this.forNode(panelMultiViewNode);
  1104. + panelMultiView._moveOutKids();
  1105. + panelMultiView.disconnect();
  1106. + }
  1107. + } finally {
  1108. + // Make sure to remove the panel element even if disconnecting fails.
  1109. + panelNode.remove();
  1110. + }
  1111. + }
  1112. +
  1113. + /**
  1114. + * Ensures that when the specified window is closed all the <panelmultiview>
  1115. + * node it contains are destroyed properly.
  1116. + */
  1117. + static ensureUnloadHandlerRegistered(window) {
  1118. + if (gWindowsWithUnloadHandler.has(window)) {
  1119. + return;
  1120. + }
  1121. +
  1122. + window.addEventListener(
  1123. + "unload",
  1124. + () => {
  1125. + for (const panelMultiViewNode of window.document.querySelectorAll(
  1126. + "panelmultiview"
  1127. + )) {
  1128. + this.forNode(panelMultiViewNode).disconnect();
  1129. + }
  1130. + },
  1131. + { once: true }
  1132. + );
  1133. +
  1134. + gWindowsWithUnloadHandler.add(window);
  1135. + }
  1136. +
  1137. + get _panel() {
  1138. + return this.node.parentNode;
  1139. + }
  1140. +
  1141. + set _transitioning(val) {
  1142. + if (val) {
  1143. + this.node.setAttribute("transitioning", "true");
  1144. + } else {
  1145. + this.node.removeAttribute("transitioning");
  1146. + }
  1147. + }
  1148. +
  1149. + get _screenManager() {
  1150. + if (this.__screenManager) {
  1151. + return this.__screenManager;
  1152. + }
  1153. + return (this.__screenManager = Cc[
  1154. + "@mozilla.org/gfx/screenmanager;1"
  1155. + ].getService(Ci.nsIScreenManager));
  1156. + }
  1157. +
  1158. + constructor(node) {
  1159. + super(node);
  1160. + this._openPopupPromise = Promise.resolve(false);
  1161. + this._openPopupCancelCallback = () => {};
  1162. + }
  1163. +
  1164. + connect() {
  1165. + this.connected = true;
  1166. +
  1167. + PanelMultiView.ensureUnloadHandlerRegistered(this.window);
  1168. +
  1169. + const viewContainer = (this._viewContainer =
  1170. + this.document.createXULElement("box"));
  1171. + viewContainer.classList.add("panel-viewcontainer");
  1172. +
  1173. + const viewStack = (this._viewStack = this.document.createXULElement("box"));
  1174. + viewStack.classList.add("panel-viewstack");
  1175. + viewContainer.append(viewStack);
  1176. +
  1177. + const offscreenViewContainer = this.document.createXULElement("box");
  1178. + offscreenViewContainer.classList.add("panel-viewcontainer", "offscreen");
  1179. +
  1180. + const offscreenViewStack = (this._offscreenViewStack =
  1181. + this.document.createXULElement("box"));
  1182. + offscreenViewStack.classList.add("panel-viewstack");
  1183. + offscreenViewContainer.append(offscreenViewStack);
  1184. +
  1185. + this.node.prepend(offscreenViewContainer);
  1186. + this.node.prepend(viewContainer);
  1187. +
  1188. + this.openViews = [];
  1189. +
  1190. + this._panel.addEventListener("popupshowing", this);
  1191. + this._panel.addEventListener("popuppositioned", this);
  1192. + this._panel.addEventListener("popuphidden", this);
  1193. + this._panel.addEventListener("popupshown", this);
  1194. +
  1195. + // Proxy these public properties and methods, as used elsewhere by various
  1196. + // parts of the browser, to this instance.
  1197. + ["goBack", "showSubView"].forEach(method => {
  1198. + Object.defineProperty(this.node, method, {
  1199. + enumerable: true,
  1200. + value: (...args) => this[method](...args),
  1201. + });
  1202. + });
  1203. + }
  1204. +
  1205. + disconnect() {
  1206. + // Guard against re-entrancy.
  1207. + if (!this.node || !this.connected) {
  1208. + return;
  1209. + }
  1210. +
  1211. + this._panel.removeEventListener("mousemove", this);
  1212. + this._panel.removeEventListener("popupshowing", this);
  1213. + this._panel.removeEventListener("popuppositioned", this);
  1214. + this._panel.removeEventListener("popupshown", this);
  1215. + this._panel.removeEventListener("popuphidden", this);
  1216. + this.window.removeEventListener("keydown", this, true);
  1217. + this.node =
  1218. + this._openPopupPromise =
  1219. + this._openPopupCancelCallback =
  1220. + this._viewContainer =
  1221. + this._viewStack =
  1222. + this._transitionDetails =
  1223. + null;
  1224. + }
  1225. +
  1226. + /**
  1227. + * Tries to open the panel associated with this PanelMultiView, and displays
  1228. + * the main view specified with the "mainViewId" attribute.
  1229. + *
  1230. + * The hidePopup method can be called while the operation is in progress to
  1231. + * prevent the panel from being displayed. View events may also cancel the
  1232. + * operation, so there is no guarantee that the panel will become visible.
  1233. + *
  1234. + * The "popuphidden" event will be fired either when the operation is canceled
  1235. + * or when the popup is closed later. This event can be used for example to
  1236. + * reset the "open" state of the anchor or tear down temporary panels.
  1237. + *
  1238. + * If this method is called again before the panel is shown, the result
  1239. + * depends on the operation currently in progress. If the operation was not
  1240. + * canceled, the panel is opened using the arguments from the previous call,
  1241. + * and this call is ignored. If the operation was canceled, it will be
  1242. + * retried again using the arguments from this call.
  1243. + *
  1244. + * It's not necessary for the <panelmultiview> binding to be connected when
  1245. + * this method is called, but the containing panel must have its display
  1246. + * turned on, for example it shouldn't have the "hidden" attribute.
  1247. + *
  1248. + * @param anchor
  1249. + * The node to anchor the popup to.
  1250. + * @param options
  1251. + * Either options to use or a string position. This is forwarded to
  1252. + * the openPopup method of the panel.
  1253. + * @param args
  1254. + * Additional arguments to be forwarded to the openPopup method of the
  1255. + * panel.
  1256. + *
  1257. + * @resolves With true as soon as the request to display the panel has been
  1258. + * sent, or with false if the operation was canceled. The state of
  1259. + * the panel at this point is not guaranteed. It may be still
  1260. + * showing, completely shown, or completely hidden.
  1261. + * @rejects If an exception is thrown at any point in the process before the
  1262. + * request to display the panel is sent.
  1263. + */
  1264. + async openPopup(anchor, options, ...args) {
  1265. + // Set up the function that allows hidePopup or a second call to showPopup
  1266. + // to cancel the specific panel opening operation that we're starting below.
  1267. + // This function must be synchronous, meaning we can't use Promise.race,
  1268. + // because hidePopup wants to dispatch the "popuphidden" event synchronously
  1269. + // even if the panel has not been opened yet.
  1270. + let canCancel = true;
  1271. + const cancelCallback = (this._openPopupCancelCallback = () => {
  1272. + // If the cancel callback is called and the panel hasn't been prepared
  1273. + // yet, cancel showing it. Setting canCancel to false will prevent the
  1274. + // popup from opening. If the panel has opened by the time the cancel
  1275. + // callback is called, canCancel will be false already, and we will not
  1276. + // fire the "popuphidden" event.
  1277. + if (canCancel && this.node) {
  1278. + canCancel = false;
  1279. + this.dispatchCustomEvent("popuphidden");
  1280. + }
  1281. + });
  1282. +
  1283. + // Create a promise that is resolved with the result of the last call to
  1284. + // this method, where errors indicate that the panel was not opened.
  1285. + const openPopupPromise = this._openPopupPromise.catch(() => {
  1286. + return false;
  1287. + });
  1288. +
  1289. + // Make the preparation done before showing the panel non-reentrant. The
  1290. + // promise created here will be resolved only after the panel preparation is
  1291. + // completed, even if a cancellation request is received in the meantime.
  1292. + return (this._openPopupPromise = openPopupPromise.then(async wasShown => {
  1293. + // The panel may have been destroyed in the meantime.
  1294. + if (!this.node) {
  1295. + return false;
  1296. + }
  1297. + // If the panel has been already opened there is nothing more to do. We
  1298. + // check the actual state of the panel rather than setting some state in
  1299. + // our handler of the "popuphidden" event because this has a lower chance
  1300. + // of locking indefinitely if events aren't raised in the expected order.
  1301. + if (wasShown && ["open", "showing"].includes(this._panel.state)) {
  1302. + return true;
  1303. + }
  1304. + try {
  1305. + if (!this.connected) {
  1306. + this.connect();
  1307. + }
  1308. + // Allow any of the ViewShowing handlers to prevent showing the main view.
  1309. + if (!(await this._showMainView())) {
  1310. + cancelCallback();
  1311. + }
  1312. + } catch (ex) {
  1313. + cancelCallback();
  1314. + throw ex;
  1315. + }
  1316. + // If a cancellation request was received there is nothing more to do.
  1317. + if (!canCancel || !this.node) {
  1318. + return false;
  1319. + }
  1320. + // We have to set canCancel to false before opening the popup because the
  1321. + // hidePopup method of PanelMultiView can be re-entered by event handlers.
  1322. + // If the openPopup call fails, however, we still have to dispatch the
  1323. + // "popuphidden" event even if canCancel was set to false.
  1324. + try {
  1325. + canCancel = false;
  1326. + this._panel.openPopup(anchor, options, ...args);
  1327. +
  1328. + // On Windows, if another popup is hiding while we call openPopup, the
  1329. + // call won't fail but the popup won't open. In this case, we have to
  1330. + // dispatch an artificial "popuphidden" event to reset our state.
  1331. + if (this._panel.state == "closed" && this.openViews.length) {
  1332. + this.dispatchCustomEvent("popuphidden");
  1333. + return false;
  1334. + }
  1335. +
  1336. + if (
  1337. + options &&
  1338. + typeof options == "object" &&
  1339. + options.triggerEvent &&
  1340. + options.triggerEvent.type == "keypress" &&
  1341. + this.openViews.length
  1342. + ) {
  1343. + // This was opened via the keyboard, so focus the first item.
  1344. + this.openViews[0].focusWhenActive = true;
  1345. + }
  1346. +
  1347. + return true;
  1348. + } catch (ex) {
  1349. + this.dispatchCustomEvent("popuphidden");
  1350. + throw ex;
  1351. + }
  1352. + }));
  1353. + }
  1354. +
  1355. + /**
  1356. + * Closes the panel associated with this PanelMultiView.
  1357. + *
  1358. + * If the openPopup method was called but the panel has not been displayed
  1359. + * yet, the operation is canceled and the panel will not be displayed, but the
  1360. + * "popuphidden" event is fired synchronously anyways.
  1361. + *
  1362. + * This means that by the time this method returns all the operations handled
  1363. + * by the "popuphidden" event are completed, for example resetting the "open"
  1364. + * state of the anchor, and the panel is already invisible.
  1365. + */
  1366. + hidePopup() {
  1367. + if (!this.node || !this.connected) {
  1368. + return;
  1369. + }
  1370. +
  1371. + // If we have already reached the _panel.openPopup call in the openPopup
  1372. + // method, we can call hidePopup. Otherwise, we have to cancel the latest
  1373. + // request to open the panel, which will have no effect if the request has
  1374. + // been canceled already.
  1375. + if (["open", "showing"].includes(this._panel.state)) {
  1376. + this._panel.hidePopup();
  1377. + } else {
  1378. + this._openPopupCancelCallback();
  1379. + }
  1380. +
  1381. + // We close all the views synchronously, so that they are ready to be opened
  1382. + // in other PanelMultiView instances. The "popuphidden" handler may also
  1383. + // call this function, but the second time openViews will be empty.
  1384. + this.closeAllViews();
  1385. + }
  1386. +
  1387. + /**
  1388. + * Move any child subviews into the element defined by "viewCacheId" to make
  1389. + * sure they will not be removed together with the <panelmultiview> element.
  1390. + */
  1391. + _moveOutKids() {
  1392. + const viewCacheId = this.node.getAttribute("viewCacheId");
  1393. + if (!viewCacheId) {
  1394. + return;
  1395. + }
  1396. +
  1397. + // Node.children and Node.children is live to DOM changes like the
  1398. + // ones we're about to do, so iterate over a static copy:
  1399. + const subviews = Array.from(this._viewStack.children);
  1400. + const viewCache = this.document.getElementById(viewCacheId);
  1401. + for (const subview of subviews) {
  1402. + viewCache.appendChild(subview);
  1403. + }
  1404. + }
  1405. +
  1406. + /**
  1407. + * Slides in the specified view as a subview.
  1408. + *
  1409. + * @param viewIdOrNode
  1410. + * DOM element or string ID of the <panelview> to display.
  1411. + * @param anchor
  1412. + * DOM element that triggered the subview, which will be highlighted
  1413. + * and whose "label" attribute will be used for the title of the
  1414. + * subview when a "title" attribute is not specified.
  1415. + */
  1416. + showSubView(viewIdOrNode, anchor) {
  1417. + this._showSubView(viewIdOrNode, anchor).catch(console.error);
  1418. + }
  1419. + async _showSubView(viewIdOrNode, anchor) {
  1420. + const viewNode =
  1421. + typeof viewIdOrNode == "string"
  1422. + ? this.document.getElementById(viewIdOrNode)
  1423. + : viewIdOrNode;
  1424. + if (!viewNode) {
  1425. + console.error(new Error(`Subview ${viewIdOrNode} doesn't exist.`));
  1426. + return;
  1427. + }
  1428. +
  1429. + if (!this.openViews.length) {
  1430. + console.error(new Error(`Cannot show a subview in a closed panel.`));
  1431. + return;
  1432. + }
  1433. +
  1434. + const prevPanelView = this.openViews[this.openViews.length - 1];
  1435. + const nextPanelView = PanelView.forNode(viewNode);
  1436. + if (this.openViews.includes(nextPanelView)) {
  1437. + console.error(new Error(`Subview ${viewNode.id} is already open.`));
  1438. + return;
  1439. + }
  1440. +
  1441. + // Do not re-enter the process if navigation is already in progress. Since
  1442. + // there is only one active view at any given time, we can do this check
  1443. + // safely, even considering that during the navigation process the actual
  1444. + // view to which prevPanelView refers will change.
  1445. + if (!prevPanelView.active) {
  1446. + return;
  1447. + }
  1448. + // If prevPanelView._doingKeyboardActivation is true, it will be reset to
  1449. + // false synchronously. Therefore, we must capture it before we use any
  1450. + // "await" statements.
  1451. + const doingKeyboardActivation = prevPanelView._doingKeyboardActivation;
  1452. + // Marking the view that is about to scrolled out of the visible area as
  1453. + // inactive will prevent re-entrancy and also disable keyboard navigation.
  1454. + // From this point onwards, "await" statements can be used safely.
  1455. + prevPanelView.active = false;
  1456. +
  1457. + // Provide visual feedback while navigation is in progress, starting before
  1458. + // the transition starts and ending when the previous view is invisible.
  1459. + if (anchor) {
  1460. + anchor.setAttribute("open", "true");
  1461. + }
  1462. + try {
  1463. + // If the ViewShowing event cancels the operation we have to re-enable
  1464. + // keyboard navigation, but this must be avoided if the panel was closed.
  1465. + if (!(await this._openView(nextPanelView))) {
  1466. + if (prevPanelView.isOpenIn(this)) {
  1467. + // We don't raise a ViewShown event because nothing actually changed.
  1468. + // Technically we should use a different state flag just because there
  1469. + // is code that could check the "active" property to determine whether
  1470. + // to wait for a ViewShown event later, but this only happens in
  1471. + // regression tests and is less likely to be a technique used in
  1472. + // production code, where use of ViewShown is less common.
  1473. + prevPanelView.active = true;
  1474. + }
  1475. + return;
  1476. + }
  1477. +
  1478. + prevPanelView.captureKnownSize();
  1479. +
  1480. + // The main view of a panel can be a subview in another one. Make sure to
  1481. + // reset all the properties that may be set on a subview.
  1482. + nextPanelView.mainview = false;
  1483. + // The header may change based on how the subview was opened.
  1484. + nextPanelView.headerText =
  1485. + viewNode.getAttribute("title") ||
  1486. + (anchor && anchor.getAttribute("label"));
  1487. + // The constrained width of subviews may also vary between panels.
  1488. + nextPanelView.minMaxWidth = prevPanelView.knownWidth;
  1489. +
  1490. + if (anchor) {
  1491. + viewNode.classList.add("PanelUI-subView");
  1492. + }
  1493. +
  1494. + await this._transitionViews(prevPanelView.node, viewNode, false, anchor);
  1495. + } finally {
  1496. + if (anchor) {
  1497. + anchor.removeAttribute("open");
  1498. + }
  1499. + }
  1500. +
  1501. + nextPanelView.focusWhenActive = doingKeyboardActivation;
  1502. + this._activateView(nextPanelView);
  1503. + }
  1504. +
  1505. + /**
  1506. + * Navigates backwards by sliding out the most recent subview.
  1507. + */
  1508. + goBack() {
  1509. + this._goBack().catch(console.error);
  1510. + }
  1511. + async _goBack() {
  1512. + if (this.openViews.length < 2) {
  1513. + // This may be called by keyboard navigation or external code when only
  1514. + // the main view is open.
  1515. + return;
  1516. + }
  1517. +
  1518. + const prevPanelView = this.openViews[this.openViews.length - 1];
  1519. + const nextPanelView = this.openViews[this.openViews.length - 2];
  1520. +
  1521. + // Like in the showSubView method, do not re-enter navigation while it is
  1522. + // in progress, and make the view inactive immediately. From this point
  1523. + // onwards, "await" statements can be used safely.
  1524. + if (!prevPanelView.active) {
  1525. + return;
  1526. + }
  1527. +
  1528. + prevPanelView.active = false;
  1529. +
  1530. + prevPanelView.captureKnownSize();
  1531. +
  1532. + await this._transitionViews(prevPanelView.node, nextPanelView.node, true);
  1533. +
  1534. + this._closeLatestView();
  1535. +
  1536. + this._activateView(nextPanelView);
  1537. + }
  1538. +
  1539. + /**
  1540. + * Prepares the main view before showing the panel.
  1541. + */
  1542. + async _showMainView() {
  1543. + const nextPanelView = PanelView.forNode(
  1544. + this.document.getElementById(this.node.getAttribute("mainViewId"))
  1545. + );
  1546. +
  1547. + // If the view is already open in another panel, close the panel first.
  1548. + const oldPanelMultiViewNode = nextPanelView.node.panelMultiView;
  1549. + if (oldPanelMultiViewNode) {
  1550. + PanelMultiView.forNode(oldPanelMultiViewNode).hidePopup();
  1551. + // Wait for a layout flush after hiding the popup, otherwise the view may
  1552. + // not be displayed correctly for some time after the new panel is opened.
  1553. + // This is filed as bug 1441015.
  1554. + await this.window.promiseDocumentFlushed(() => {});
  1555. + }
  1556. +
  1557. + if (!(await this._openView(nextPanelView))) {
  1558. + return false;
  1559. + }
  1560. +
  1561. + // The main view of a panel can be a subview in another one. Make sure to
  1562. + // reset all the properties that may be set on a subview.
  1563. + nextPanelView.mainview = true;
  1564. + nextPanelView.headerText = "";
  1565. + nextPanelView.minMaxWidth = 0;
  1566. +
  1567. + // Ensure the view will be visible once the panel is opened.
  1568. + nextPanelView.visible = true;
  1569. +
  1570. + return true;
  1571. + }
  1572. +
  1573. + /**
  1574. + * Opens the specified PanelView and dispatches the ViewShowing event, which
  1575. + * can be used to populate the subview or cancel the operation.
  1576. + *
  1577. + * This also clears all the attributes and styles that may be left by a
  1578. + * transition that was interrupted.
  1579. + *
  1580. + * @resolves With true if the view was opened, false otherwise.
  1581. + */
  1582. + async _openView(panelView) {
  1583. + if (panelView.node.parentNode != this._viewStack) {
  1584. + this._viewStack.appendChild(panelView.node);
  1585. + }
  1586. +
  1587. + panelView.node.panelMultiView = this.node;
  1588. + this.openViews.push(panelView);
  1589. +
  1590. + const canceled = await panelView.dispatchAsyncEvent("ViewShowing");
  1591. +
  1592. + // The panel can be hidden while we are processing the ViewShowing event.
  1593. + // This results in all the views being closed synchronously, and at this
  1594. + // point the ViewHiding event has already been dispatched for all of them.
  1595. + if (!this.openViews.length) {
  1596. + return false;
  1597. + }
  1598. +
  1599. + // Check if the event requested cancellation but the panel is still open.
  1600. + if (canceled) {
  1601. + // Handlers for ViewShowing can't know if a different handler requested
  1602. + // cancellation, so this will dispatch a ViewHiding event to give a chance
  1603. + // to clean up.
  1604. + this._closeLatestView();
  1605. + return false;
  1606. + }
  1607. +
  1608. + // Clean up all the attributes and styles related to transitions. We do this
  1609. + // here rather than when the view is closed because we are likely to make
  1610. + // other DOM modifications soon, which isn't the case when closing.
  1611. + const { style } = panelView.node;
  1612. + style.removeProperty("outline");
  1613. + style.removeProperty("width");
  1614. +
  1615. + return true;
  1616. + }
  1617. +
  1618. + /**
  1619. + * Activates the specified view and raises the ViewShown event, unless the
  1620. + * view was closed in the meantime.
  1621. + */
  1622. + _activateView(panelView) {
  1623. + if (panelView.isOpenIn(this)) {
  1624. + panelView.active = true;
  1625. + if (panelView.focusWhenActive) {
  1626. + panelView.focusFirstNavigableElement(false, true);
  1627. + panelView.focusWhenActive = false;
  1628. + }
  1629. + panelView.dispatchCustomEvent("ViewShown");
  1630. + }
  1631. + }
  1632. +
  1633. + /**
  1634. + * Closes the most recent PanelView and raises the ViewHiding event.
  1635. + *
  1636. + * @note The ViewHiding event is not cancelable and should probably be renamed
  1637. + * to ViewHidden or ViewClosed instead, see bug 1438507.
  1638. + */
  1639. + _closeLatestView() {
  1640. + const panelView = this.openViews.pop();
  1641. + panelView.clearNavigation();
  1642. + panelView.dispatchCustomEvent("ViewHiding");
  1643. + panelView.node.panelMultiView = null;
  1644. + // Views become invisible synchronously when they are closed, and they won't
  1645. + // become visible again until they are opened. When this is called at the
  1646. + // end of backwards navigation, the view is already invisible.
  1647. + panelView.visible = false;
  1648. + }
  1649. +
  1650. + /**
  1651. + * Closes all the views that are currently open.
  1652. + */
  1653. + closeAllViews() {
  1654. + // Raise ViewHiding events for open views in reverse order.
  1655. + while (this.openViews.length) {
  1656. + this._closeLatestView();
  1657. + }
  1658. + }
  1659. +
  1660. + /**
  1661. + * Apply a transition to 'slide' from the currently active view to the next
  1662. + * one.
  1663. + * Sliding the next subview in means that the previous panelview stays where it
  1664. + * is and the active panelview slides in from the left in LTR mode, right in
  1665. + * RTL mode.
  1666. + *
  1667. + * @param {panelview} previousViewNode Node that is currently displayed, but
  1668. + * is about to be transitioned away. This
  1669. + * must be already inactive at this point.
  1670. + * @param {panelview} viewNode - Node that will becode the active view,
  1671. + * after the transition has finished.
  1672. + * @param {boolean} reverse Whether we're navigation back to a
  1673. + * previous view or forward to a next view.
  1674. + */
  1675. + async _transitionViews(previousViewNode, viewNode, reverse) {
  1676. + const { window } = this;
  1677. +
  1678. + const nextPanelView = PanelView.forNode(viewNode);
  1679. + const prevPanelView = PanelView.forNode(previousViewNode);
  1680. +
  1681. + const details = (this._transitionDetails = {
  1682. + phase: TRANSITION_PHASES.START,
  1683. + });
  1684. +
  1685. + // Set the viewContainer dimensions to make sure only the current view is
  1686. + // visible.
  1687. + const olderView = reverse ? nextPanelView : prevPanelView;
  1688. + this._viewContainer.style.minHeight = olderView.knownHeight + "px";
  1689. + this._viewContainer.style.height = prevPanelView.knownHeight + "px";
  1690. + this._viewContainer.style.width = prevPanelView.knownWidth + "px";
  1691. + // Lock the dimensions of the window that hosts the popup panel.
  1692. + const rect = this._getBoundsWithoutFlushing(this._panel);
  1693. + this._panel.style.width = rect.width + "px";
  1694. + this._panel.style.height = rect.height + "px";
  1695. +
  1696. + let viewRect;
  1697. + if (reverse) {
  1698. + // Use the cached size when going back to a previous view, but not when
  1699. + // reopening a subview, because its contents may have changed.
  1700. + viewRect = {
  1701. + width: nextPanelView.knownWidth,
  1702. + height: nextPanelView.knownHeight,
  1703. + };
  1704. + nextPanelView.visible = true;
  1705. + } else if (viewNode.customRectGetter) {
  1706. + // We use a customRectGetter for WebExtensions panels, because they need
  1707. + // to query the size from an embedded browser. The presence of this
  1708. + // getter also provides an indication that the view node shouldn't be
  1709. + // moved around, otherwise the state of the browser would get disrupted.
  1710. + const width = prevPanelView.knownWidth;
  1711. + const height = prevPanelView.knownHeight;
  1712. + viewRect = Object.assign({ height, width }, viewNode.customRectGetter());
  1713. + nextPanelView.visible = true;
  1714. + // Until the header is visible, it has 0 height.
  1715. + // Wait for layout before measuring it
  1716. + const header = viewNode.firstElementChild;
  1717. + if (header && header.classList.contains("panel-header")) {
  1718. + viewRect.height += await window.promiseDocumentFlushed(() => {
  1719. + return this._getBoundsWithoutFlushing(header).height;
  1720. + });
  1721. + }
  1722. + } else {
  1723. + this._offscreenViewStack.style.minHeight = olderView.knownHeight + "px";
  1724. + this._offscreenViewStack.appendChild(viewNode);
  1725. + nextPanelView.visible = true;
  1726. +
  1727. + viewRect = await window.promiseDocumentFlushed(() => {
  1728. + return this._getBoundsWithoutFlushing(viewNode);
  1729. + });
  1730. + // Bail out if the panel was closed in the meantime.
  1731. + if (!nextPanelView.isOpenIn(this)) {
  1732. + return;
  1733. + }
  1734. +
  1735. + // Place back the view after all the other views that are already open in
  1736. + // order for the transition to work as expected.
  1737. + this._viewStack.appendChild(viewNode);
  1738. +
  1739. + this._offscreenViewStack.style.removeProperty("min-height");
  1740. + }
  1741. +
  1742. + this._transitioning = true;
  1743. + details.phase = TRANSITION_PHASES.PREPARE;
  1744. +
  1745. + // The 'magic' part: build up the amount of pixels to move right or left.
  1746. + const moveToLeft =
  1747. + (this.window.RTL_UI && !reverse) || (!this.window.RTL_UI && reverse);
  1748. + const deltaX = prevPanelView.knownWidth;
  1749. + const deepestNode = reverse ? previousViewNode : viewNode;
  1750. +
  1751. + // With a transition when navigating backwards - user hits the 'back'
  1752. + // button - we need to make sure that the views are positioned in a way
  1753. + // that a translateX() unveils the previous view from the right direction.
  1754. + if (reverse) {
  1755. + this._viewStack.style.marginInlineStart = "-" + deltaX + "px";
  1756. + }
  1757. +
  1758. + // Set the transition style and listen for its end to clean up and make sure
  1759. + // the box sizing becomes dynamic again.
  1760. + // Somehow, putting these properties in PanelUI.css doesn't work for newly
  1761. + // shown nodes in a XUL parent node.
  1762. + this._viewStack.style.transition =
  1763. + "transform var(--animation-easing-function)" +
  1764. + " var(--panelui-subview-transition-duration)";
  1765. + this._viewStack.style.willChange = "transform";
  1766. + // Use an outline instead of a border so that the size is not affected.
  1767. + deepestNode.style.outline = "1px solid var(--panel-separator-color)";
  1768. +
  1769. + // Now that all the elements are in place for the start of the transition,
  1770. + // give the layout code a chance to set the initial values.
  1771. + await window.promiseDocumentFlushed(() => {});
  1772. + // Bail out if the panel was closed in the meantime.
  1773. + if (!nextPanelView.isOpenIn(this)) {
  1774. + return;
  1775. + }
  1776. +
  1777. + // Now set the viewContainer dimensions to that of the new view, which
  1778. + // kicks of the height animation.
  1779. + this._viewContainer.style.height = viewRect.height + "px";
  1780. + this._viewContainer.style.width = viewRect.width + "px";
  1781. + this._panel.style.removeProperty("width");
  1782. + this._panel.style.removeProperty("height");
  1783. +
  1784. + // We're setting the width property to prevent flickering during the
  1785. + // sliding animation with smaller views.
  1786. + viewNode.style.width = viewRect.width + "px";
  1787. +
  1788. + // Kick off the transition!
  1789. + details.phase = TRANSITION_PHASES.TRANSITION;
  1790. +
  1791. + // If we're going to show the main view, we can remove the
  1792. + // min-height property on the view container.
  1793. + if (viewNode.getAttribute("mainview")) {
  1794. + this._viewContainer.style.removeProperty("min-height");
  1795. + }
  1796. +
  1797. + this._viewStack.style.transform =
  1798. + "translateX(" + (moveToLeft ? "" : "-") + deltaX + "px)";
  1799. +
  1800. + await new Promise(resolve => {
  1801. + details.resolve = resolve;
  1802. + this._viewContainer.addEventListener(
  1803. + "transitionend",
  1804. + (details.listener = ev => {
  1805. + // It's quite common that `height` on the view container doesn't need
  1806. + // to transition, so we make sure to do all the work on the transform
  1807. + // transition-end, because that is guaranteed to happen.
  1808. + if (ev.target != this._viewStack || ev.propertyName != "transform") {
  1809. + return;
  1810. + }
  1811. + this._viewContainer.removeEventListener(
  1812. + "transitionend",
  1813. + details.listener
  1814. + );
  1815. + delete details.listener;
  1816. + resolve();
  1817. + })
  1818. + );
  1819. + this._viewContainer.addEventListener(
  1820. + "transitioncancel",
  1821. + (details.cancelListener = ev => {
  1822. + if (ev.target != this._viewStack) {
  1823. + return;
  1824. + }
  1825. + this._viewContainer.removeEventListener(
  1826. + "transitioncancel",
  1827. + details.cancelListener
  1828. + );
  1829. + delete details.cancelListener;
  1830. + resolve();
  1831. + })
  1832. + );
  1833. + });
  1834. +
  1835. + // Bail out if the panel was closed during the transition.
  1836. + if (!nextPanelView.isOpenIn(this)) {
  1837. + return;
  1838. + }
  1839. + prevPanelView.visible = false;
  1840. +
  1841. + // This will complete the operation by removing any transition properties.
  1842. + nextPanelView.node.style.removeProperty("width");
  1843. + deepestNode.style.removeProperty("outline");
  1844. + this._cleanupTransitionPhase();
  1845. +
  1846. + nextPanelView.focusSelectedElement();
  1847. + }
  1848. +
  1849. + /**
  1850. + * Attempt to clean up the attributes and properties set by `_transitionViews`
  1851. + * above. Which attributes and properties depends on the phase the transition
  1852. + * was left from.
  1853. + */
  1854. + _cleanupTransitionPhase() {
  1855. + if (!this._transitionDetails) {
  1856. + return;
  1857. + }
  1858. +
  1859. + const { phase, resolve, listener, cancelListener } =
  1860. + this._transitionDetails;
  1861. + this._transitionDetails = null;
  1862. +
  1863. + if (phase >= TRANSITION_PHASES.START) {
  1864. + this._panel.style.removeProperty("width");
  1865. + this._panel.style.removeProperty("height");
  1866. + this._viewContainer.style.removeProperty("height");
  1867. + this._viewContainer.style.removeProperty("width");
  1868. + }
  1869. + if (phase >= TRANSITION_PHASES.PREPARE) {
  1870. + this._transitioning = false;
  1871. + this._viewStack.style.removeProperty("margin-inline-start");
  1872. + this._viewStack.style.removeProperty("transition");
  1873. + }
  1874. + if (phase >= TRANSITION_PHASES.TRANSITION) {
  1875. + this._viewStack.style.removeProperty("transform");
  1876. + if (listener) {
  1877. + this._viewContainer.removeEventListener("transitionend", listener);
  1878. + }
  1879. + if (cancelListener) {
  1880. + this._viewContainer.removeEventListener(
  1881. + "transitioncancel",
  1882. + cancelListener
  1883. + );
  1884. + }
  1885. + if (resolve) {
  1886. + resolve();
  1887. + }
  1888. + }
  1889. + }
  1890. +
  1891. + _calculateMaxHeight(aEvent) {
  1892. + // While opening the panel, we have to limit the maximum height of any
  1893. + // view based on the space that will be available. We cannot just use
  1894. + // window.screen.availTop and availHeight because these may return an
  1895. + // incorrect value when the window spans multiple screens.
  1896. + const anchor = this._panel.anchorNode;
  1897. + const anchorRect = anchor.getBoundingClientRect();
  1898. +
  1899. + const screen = this._screenManager.screenForRect(
  1900. + anchor.screenX,
  1901. + anchor.screenY,
  1902. + anchorRect.width,
  1903. + anchorRect.height
  1904. + );
  1905. + const availTop = {},
  1906. + availHeight = {};
  1907. + screen.GetAvailRect({}, availTop, {}, availHeight);
  1908. + const cssAvailTop = availTop.value / screen.defaultCSSScaleFactor;
  1909. +
  1910. + // The distance from the anchor to the available margin of the screen is
  1911. + // based on whether the panel will open towards the top or the bottom.
  1912. + let maxHeight;
  1913. + if (aEvent.alignmentPosition.startsWith("before_")) {
  1914. + maxHeight = anchor.screenY - cssAvailTop;
  1915. + } else {
  1916. + const anchorScreenBottom = anchor.screenY + anchorRect.height;
  1917. + const cssAvailHeight = availHeight.value / screen.defaultCSSScaleFactor;
  1918. + maxHeight = cssAvailTop + cssAvailHeight - anchorScreenBottom;
  1919. + }
  1920. +
  1921. + // To go from the maximum height of the panel to the maximum height of
  1922. + // the view stack, we need to subtract the height of the arrow and the
  1923. + // height of the opposite margin, but we cannot get their actual values
  1924. + // because the panel is not visible yet. However, we know that this is
  1925. + // currently 11px on Mac, 13px on Windows, and 13px on Linux. We also
  1926. + // want an extra margin, both for visual reasons and to prevent glitches
  1927. + // due to small rounding errors. So, we just use a value that makes
  1928. + // sense for all platforms. If the arrow visuals change significantly,
  1929. + // this value will be easy to adjust.
  1930. + const EXTRA_MARGIN_PX = 20;
  1931. + maxHeight -= EXTRA_MARGIN_PX;
  1932. + return maxHeight;
  1933. + }
  1934. +
  1935. + handleEvent(aEvent) {
  1936. + // Only process actual popup events from the panel or events we generate
  1937. + // ourselves, but not from menus being shown from within the panel.
  1938. + if (
  1939. + aEvent.type.startsWith("popup") &&
  1940. + aEvent.target != this._panel &&
  1941. + aEvent.target != this.node
  1942. + ) {
  1943. + return;
  1944. + }
  1945. + switch (aEvent.type) {
  1946. + case "keydown":
  1947. + // Since we start listening for the "keydown" event when the popup is
  1948. + // already showing and stop listening when the panel is hidden, we
  1949. + // always have at least one view open.
  1950. + const currentView = this.openViews[this.openViews.length - 1];
  1951. + currentView.keyNavigation(aEvent);
  1952. + break;
  1953. + case "mousemove":
  1954. + this.openViews.forEach(panelView => panelView.clearNavigation());
  1955. + break;
  1956. + case "popupshowing": {
  1957. + this._viewContainer.setAttribute("panelopen", "true");
  1958. + if (!this.node.hasAttribute("disablekeynav")) {
  1959. + // We add the keydown handler on the window so that it handles key
  1960. + // presses when a panel appears but doesn't get focus, as happens
  1961. + // when a button to open a panel is clicked with the mouse.
  1962. + // However, this means the listener is on an ancestor of the panel,
  1963. + // which means that handlers such as ToolbarKeyboardNavigator are
  1964. + // deeper in the tree. Therefore, this must be a capturing listener
  1965. + // so we get the event first.
  1966. + this.window.addEventListener("keydown", this, true);
  1967. + this._panel.addEventListener("mousemove", this);
  1968. + }
  1969. + break;
  1970. + }
  1971. + case "popuppositioned": {
  1972. + if (this._panel.state == "showing") {
  1973. + const maxHeight = this._calculateMaxHeight(aEvent);
  1974. + this._viewStack.style.maxHeight = maxHeight + "px";
  1975. + this._offscreenViewStack.style.maxHeight = maxHeight + "px";
  1976. + }
  1977. + break;
  1978. + }
  1979. + case "popupshown":
  1980. + // The main view is always open and visible when the panel is first
  1981. + // shown, so we can check the height of the description elements it
  1982. + // contains and notify consumers using the ViewShown event. In order to
  1983. + // minimize flicker we need to allow synchronous reflows, and we still
  1984. + // make sure the ViewShown event is dispatched synchronously.
  1985. + const mainPanelView = this.openViews[0];
  1986. + this._activateView(mainPanelView);
  1987. + break;
  1988. + case "popuphidden": {
  1989. + // WebExtensions consumers can hide the popup from viewshowing, or
  1990. + // mid-transition, which disrupts our state:
  1991. + this._transitioning = false;
  1992. + this._viewContainer.removeAttribute("panelopen");
  1993. + this._cleanupTransitionPhase();
  1994. + this.window.removeEventListener("keydown", this, true);
  1995. + this._panel.removeEventListener("mousemove", this);
  1996. + this.closeAllViews();
  1997. +
  1998. + // Clear the main view size caches. The dimensions could be different
  1999. + // when the popup is opened again, e.g. through touch mode sizing.
  2000. + this._viewContainer.style.removeProperty("min-height");
  2001. + this._viewStack.style.removeProperty("max-height");
  2002. + this._viewContainer.style.removeProperty("width");
  2003. + this._viewContainer.style.removeProperty("height");
  2004. +
  2005. + this.dispatchCustomEvent("PanelMultiViewHidden");
  2006. + break;
  2007. + }
  2008. + }
  2009. + }
  2010. +}
  2011. +
  2012. +/**
  2013. + * This is associated to <panelview> elements.
  2014. + */
  2015. +export class PanelView extends AssociatedToNode {
  2016. + constructor(node) {
  2017. + super(node);
  2018. +
  2019. + /**
  2020. + * Indicates whether the view is active. When this is false, consumers can
  2021. + * wait for the ViewShown event to know when the view becomes active.
  2022. + */
  2023. + this.active = false;
  2024. +
  2025. + /**
  2026. + * Specifies whether the view should be focused when active. When this
  2027. + * is true, the first navigable element in the view will be focused
  2028. + * when the view becomes active. This should be set to true when the view
  2029. + * is activated from the keyboard. It will be set to false once the view
  2030. + * is active.
  2031. + */
  2032. + this.focusWhenActive = false;
  2033. + }
  2034. +
  2035. + /**
  2036. + * Indicates whether the view is open in the specified PanelMultiView object.
  2037. + */
  2038. + isOpenIn(panelMultiView) {
  2039. + return this.node.panelMultiView == panelMultiView.node;
  2040. + }
  2041. +
  2042. + /**
  2043. + * The "mainview" attribute is set before the panel is opened when this view
  2044. + * is displayed as the main view, and is removed before the <panelview> is
  2045. + * displayed as a subview. The same view element can be displayed as a main
  2046. + * view and as a subview at different times.
  2047. + */
  2048. + set mainview(value) {
  2049. + if (value) {
  2050. + this.node.setAttribute("mainview", true);
  2051. + } else {
  2052. + this.node.removeAttribute("mainview");
  2053. + }
  2054. + }
  2055. +
  2056. + /**
  2057. + * Determines whether the view is visible. Setting this to false also resets
  2058. + * the "active" property.
  2059. + */
  2060. + set visible(value) {
  2061. + if (value) {
  2062. + this.node.setAttribute("visible", true);
  2063. + } else {
  2064. + this.node.removeAttribute("visible");
  2065. + this.active = false;
  2066. + this.focusWhenActive = false;
  2067. + }
  2068. + }
  2069. +
  2070. + /**
  2071. + * Constrains the width of this view using the "min-width" and "max-width"
  2072. + * styles. Setting this to zero removes the constraints.
  2073. + */
  2074. + set minMaxWidth(value) {
  2075. + const style = this.node.style;
  2076. + if (value) {
  2077. + style.minWidth = style.maxWidth = value + "px";
  2078. + } else {
  2079. + style.removeProperty("min-width");
  2080. + style.removeProperty("max-width");
  2081. + }
  2082. + }
  2083. +
  2084. + /**
  2085. + * Adds a header with the given title, or removes it if the title is empty.
  2086. + */
  2087. + set headerText(value) {
  2088. + // If the header already exists, update or remove it as requested.
  2089. + let header = this.node.firstElementChild;
  2090. + if (header && header.classList.contains("panel-header")) {
  2091. + if (value) {
  2092. + header.querySelector(".panel-header > h1 > span").textContent = value;
  2093. + } else {
  2094. + header.remove();
  2095. + }
  2096. + return;
  2097. + }
  2098. +
  2099. + // The header doesn't exist, only create it if needed.
  2100. + if (!value) {
  2101. + return;
  2102. + }
  2103. +
  2104. + header = this.document.createXULElement("box");
  2105. + header.classList.add("panel-header");
  2106. +
  2107. + const backButton = this.document.createXULElement("toolbarbutton");
  2108. + backButton.className =
  2109. + "subviewbutton subviewbutton-iconic subviewbutton-back";
  2110. + backButton.setAttribute("closemenu", "none");
  2111. + backButton.setAttribute("tabindex", "0");
  2112. +
  2113. + backButton.setAttribute(
  2114. + "aria-label",
  2115. + lazy.gBundle.GetStringFromName("panel.back")
  2116. + );
  2117. +
  2118. + backButton.addEventListener("command", () => {
  2119. + // The panelmultiview element may change if the view is reused.
  2120. + this.node.panelMultiView.goBack();
  2121. + backButton.blur();
  2122. + });
  2123. +
  2124. + const h1 = this.document.createElement("h1");
  2125. + const span = this.document.createElement("span");
  2126. + span.textContent = value;
  2127. + h1.appendChild(span);
  2128. +
  2129. + header.append(backButton, h1);
  2130. + this.node.prepend(header);
  2131. + }
  2132. +
  2133. + /**
  2134. + * Populates the "knownWidth" and "knownHeight" properties with the current
  2135. + * dimensions of the view. These may be zero if the view is invisible.
  2136. + *
  2137. + * These values are relevant during transitions and are retained for backwards
  2138. + * navigation if the view is still open but is invisible.
  2139. + */
  2140. + captureKnownSize() {
  2141. + const rect = this._getBoundsWithoutFlushing(this.node);
  2142. + this.knownWidth = rect.width;
  2143. + this.knownHeight = rect.height;
  2144. + }
  2145. +
  2146. + /**
  2147. + * Determine whether an element can only be navigated to with tab/shift+tab,
  2148. + * not the arrow keys.
  2149. + */
  2150. + _isNavigableWithTabOnly(element) {
  2151. + const tag = element.localName;
  2152. + return (
  2153. + tag == "menulist" ||
  2154. + tag == "input" ||
  2155. + tag == "textarea" ||
  2156. + // Allow tab to reach embedded documents in extension panels.
  2157. + tag == "browser"
  2158. + );
  2159. + }
  2160. +
  2161. + /**
  2162. + * Make a TreeWalker for keyboard navigation.
  2163. + *
  2164. + * @param {boolean} arrowKey If `true`, elements only navigable with tab are
  2165. + * excluded.
  2166. + */
  2167. + _makeNavigableTreeWalker(arrowKey) {
  2168. + const filter = node => {
  2169. + if (node.disabled) {
  2170. + return NodeFilter.FILTER_REJECT;
  2171. + }
  2172. + const bounds = this._getBoundsWithoutFlushing(node);
  2173. + if (bounds.width == 0 || bounds.height == 0) {
  2174. + return NodeFilter.FILTER_REJECT;
  2175. + }
  2176. + if (
  2177. + node.tagName == "button" ||
  2178. + node.tagName == "toolbarbutton" ||
  2179. + node.classList.contains("text-link") ||
  2180. + (!arrowKey && this._isNavigableWithTabOnly(node))
  2181. + ) {
  2182. + // Set the tabindex attribute to make sure the node is focusable.
  2183. + if (!node.hasAttribute("tabindex")) {
  2184. + node.setAttribute("tabindex", "-1");
  2185. + }
  2186. + return NodeFilter.FILTER_ACCEPT;
  2187. + }
  2188. + return NodeFilter.FILTER_SKIP;
  2189. + };
  2190. + return this.document.createTreeWalker(
  2191. + this.node,
  2192. + NodeFilter.SHOW_ELEMENT,
  2193. + filter
  2194. + );
  2195. + }
  2196. +
  2197. + /**
  2198. + * Get a TreeWalker which finds elements navigable with tab/shift+tab.
  2199. + */
  2200. + get _tabNavigableWalker() {
  2201. + if (!this.__tabNavigableWalker) {
  2202. + this.__tabNavigableWalker = this._makeNavigableTreeWalker(false);
  2203. + }
  2204. + return this.__tabNavigableWalker;
  2205. + }
  2206. +
  2207. + /**
  2208. + * Get a TreeWalker which finds elements navigable with up/down arrow keys.
  2209. + */
  2210. + get _arrowNavigableWalker() {
  2211. + if (!this.__arrowNavigableWalker) {
  2212. + this.__arrowNavigableWalker = this._makeNavigableTreeWalker(true);
  2213. + }
  2214. + return this.__arrowNavigableWalker;
  2215. + }
  2216. +
  2217. + /**
  2218. + * Element that is currently selected with the keyboard, or null if no element
  2219. + * is selected. Since the reference is held weakly, it can become null or
  2220. + * undefined at any time.
  2221. + */
  2222. + get selectedElement() {
  2223. + return this._selectedElement && this._selectedElement.get();
  2224. + }
  2225. + set selectedElement(value) {
  2226. + if (!value) {
  2227. + delete this._selectedElement;
  2228. + } else {
  2229. + this._selectedElement = Cu.getWeakReference(value);
  2230. + }
  2231. + }
  2232. +
  2233. + /**
  2234. + * Focuses and moves keyboard selection to the first navigable element.
  2235. + * This is a no-op if there are no navigable elements.
  2236. + *
  2237. + * @param {boolean} homeKey - `true` if this is for the home key.
  2238. + * @param {boolean} skipBack - `true` if the Back button should be skipped.
  2239. + */
  2240. + focusFirstNavigableElement(homeKey = false, skipBack = false) {
  2241. + // The home key is conceptually similar to the up/down arrow keys.
  2242. + const walker = homeKey
  2243. + ? this._arrowNavigableWalker
  2244. + : this._tabNavigableWalker;
  2245. + walker.currentNode = walker.root;
  2246. + this.selectedElement = walker.firstChild();
  2247. + if (
  2248. + skipBack &&
  2249. + walker.currentNode &&
  2250. + walker.currentNode.classList.contains("subviewbutton-back") &&
  2251. + walker.nextNode()
  2252. + ) {
  2253. + this.selectedElement = walker.currentNode;
  2254. + }
  2255. + this.focusSelectedElement(/* byKey */ true);
  2256. + }
  2257. +
  2258. + /**
  2259. + * Focuses and moves keyboard selection to the last navigable element.
  2260. + * This is a no-op if there are no navigable elements.
  2261. + *
  2262. + * @param {boolean} endKey - `true` if this is for the end key.
  2263. + */
  2264. + focusLastNavigableElement(endKey = false) {
  2265. + // The end key is conceptually similar to the up/down arrow keys.
  2266. + const walker = endKey
  2267. + ? this._arrowNavigableWalker
  2268. + : this._tabNavigableWalker;
  2269. + walker.currentNode = walker.root;
  2270. + this.selectedElement = walker.lastChild();
  2271. + this.focusSelectedElement(/* byKey */ true);
  2272. + }
  2273. +
  2274. + /**
  2275. + * Based on going up or down, select the previous or next focusable element.
  2276. + *
  2277. + * @param {boolean} isDown - whether we're going down (true) or up (false).
  2278. + * @param {boolean} arrowKey - `true` if this is for the up/down arrow keys.
  2279. + *
  2280. + * @returns {DOMNode} the element we selected.
  2281. + */
  2282. + moveSelection(isDown, arrowKey = false) {
  2283. + const walker = arrowKey
  2284. + ? this._arrowNavigableWalker
  2285. + : this._tabNavigableWalker;
  2286. + const oldSel = this.selectedElement;
  2287. + let newSel;
  2288. + if (oldSel) {
  2289. + walker.currentNode = oldSel;
  2290. + newSel = isDown ? walker.nextNode() : walker.previousNode();
  2291. + }
  2292. + // If we couldn't find something, select the first or last item:
  2293. + if (!newSel) {
  2294. + walker.currentNode = walker.root;
  2295. + newSel = isDown ? walker.firstChild() : walker.lastChild();
  2296. + }
  2297. + this.selectedElement = newSel;
  2298. + return newSel;
  2299. + }
  2300. +
  2301. + /**
  2302. + * Allow for navigating subview buttons using the arrow keys and the Enter key.
  2303. + * The Up and Down keys can be used to navigate the list up and down and the
  2304. + * Enter, Right or Left - depending on the text direction - key can be used to
  2305. + * simulate a click on the currently selected button.
  2306. + * The Right or Left key - depending on the text direction - can be used to
  2307. + * navigate to the previous view, functioning as a shortcut for the view's
  2308. + * back button.
  2309. + * Thus, in LTR mode:
  2310. + * - The Right key functions the same as the Enter key, simulating a click
  2311. + * - The Left key triggers a navigation back to the previous view.
  2312. + *
  2313. + * Key navigation is only enabled while the view is active, meaning that this
  2314. + * method will return early if it is invoked during a sliding transition.
  2315. + *
  2316. + * @param {KeyEvent} event
  2317. + */
  2318. + /* eslint-disable-next-line complexity */
  2319. + keyNavigation(event) {
  2320. + if (!this.active) {
  2321. + return;
  2322. + }
  2323. +
  2324. + let focus = this.document.activeElement;
  2325. + // Make sure the focus is actually inside the panel. (It might not be if
  2326. + // the panel was opened with the mouse.) If it isn't, we don't care
  2327. + // about it for our purposes.
  2328. + // We use Node.compareDocumentPosition because Node.contains doesn't
  2329. + // behave as expected for anonymous content; e.g. the input inside a
  2330. + // textbox.
  2331. + if (
  2332. + focus &&
  2333. + !(
  2334. + this.node.compareDocumentPosition(focus) &
  2335. + Node.DOCUMENT_POSITION_CONTAINED_BY
  2336. + )
  2337. + ) {
  2338. + focus = null;
  2339. + }
  2340. +
  2341. + // Extension panels contain embedded documents. We can't manage
  2342. + // keyboard navigation within those.
  2343. + if (focus && focus.tagName == "browser") {
  2344. + return;
  2345. + }
  2346. +
  2347. + const stop = () => {
  2348. + event.stopPropagation();
  2349. + event.preventDefault();
  2350. + };
  2351. +
  2352. + // If the focused element is only navigable with tab, it wants the arrow
  2353. + // keys, etc. We shouldn't handle any keys except tab and shift+tab.
  2354. + // We make a function for this for performance reasons: we only want to
  2355. + // check this for keys we potentially care about, not *all* keys.
  2356. + const tabOnly = () => {
  2357. + // We use the real focus rather than this.selectedElement because focus
  2358. + // might have been moved without keyboard navigation (e.g. mouse click)
  2359. + // and this.selectedElement is only updated for keyboard navigation.
  2360. + return focus && this._isNavigableWithTabOnly(focus);
  2361. + };
  2362. +
  2363. + // If a context menu is open, we must let it handle all keys.
  2364. + // Normally, this just happens, but because we have a capturing window
  2365. + // keydown listener, our listener takes precedence.
  2366. + // Again, we only want to do this check on demand for performance.
  2367. + const isContextMenuOpen = () => {
  2368. + if (!focus) {
  2369. + return false;
  2370. + }
  2371. + const contextNode = focus.closest("[context]");
  2372. + if (!contextNode) {
  2373. + return false;
  2374. + }
  2375. + const context = contextNode.getAttribute("context");
  2376. + const popup = this.document.getElementById(context);
  2377. + return popup && popup.state == "open";
  2378. + };
  2379. +
  2380. + const keyCode = event.code;
  2381. + switch (keyCode) {
  2382. + case "ArrowDown":
  2383. + case "ArrowUp":
  2384. + if (tabOnly()) {
  2385. + break;
  2386. + }
  2387. + // Fall-through...
  2388. + case "Tab": {
  2389. + if (isContextMenuOpen()) {
  2390. + break;
  2391. + }
  2392. + stop();
  2393. + const isDown =
  2394. + keyCode == "ArrowDown" || (keyCode == "Tab" && !event.shiftKey);
  2395. + const button = this.moveSelection(isDown, keyCode != "Tab");
  2396. + Services.focus.setFocus(button, Services.focus.FLAG_BYKEY);
  2397. + break;
  2398. + }
  2399. + case "Home":
  2400. + if (tabOnly() || isContextMenuOpen()) {
  2401. + break;
  2402. + }
  2403. + stop();
  2404. + this.focusFirstNavigableElement(true);
  2405. + break;
  2406. + case "End":
  2407. + if (tabOnly() || isContextMenuOpen()) {
  2408. + break;
  2409. + }
  2410. + stop();
  2411. + this.focusLastNavigableElement(true);
  2412. + break;
  2413. + case "ArrowLeft":
  2414. + case "ArrowRight": {
  2415. + if (tabOnly() || isContextMenuOpen()) {
  2416. + break;
  2417. + }
  2418. + stop();
  2419. + if (
  2420. + (!this.window.RTL_UI && keyCode == "ArrowLeft") ||
  2421. + (this.window.RTL_UI && keyCode == "ArrowRight")
  2422. + ) {
  2423. + this.node.panelMultiView.goBack();
  2424. + break;
  2425. + }
  2426. + // If the current button is _not_ one that points to a subview, pressing
  2427. + // the arrow key shouldn't do anything.
  2428. + const button = this.selectedElement;
  2429. + if (!button || !button.classList.contains("subviewbutton-nav")) {
  2430. + break;
  2431. + }
  2432. + }
  2433. + // Fall-through...
  2434. + case "Space":
  2435. + case "NumpadEnter":
  2436. + case "Enter": {
  2437. + if (tabOnly() || isContextMenuOpen()) {
  2438. + break;
  2439. + }
  2440. + const button = this.selectedElement;
  2441. + if (!button) {
  2442. + break;
  2443. + }
  2444. + stop();
  2445. +
  2446. + this._doingKeyboardActivation = true;
  2447. + // Unfortunately, 'tabindex' doesn't execute the default action, so
  2448. + // we explicitly do this here.
  2449. + // We are sending a command event, a mousedown event and then a click
  2450. + // event. This is done in order to mimic a "real" mouse click event.
  2451. + // Normally, the command event executes the action, then the click event
  2452. + // closes the menu. However, in some cases (e.g. the Library button),
  2453. + // there is no command event handler and the mousedown event executes the
  2454. + // action instead.
  2455. + button.doCommand();
  2456. + let dispEvent = new event.target.ownerGlobal.MouseEvent("mousedown", {
  2457. + bubbles: true,
  2458. + });
  2459. + button.dispatchEvent(dispEvent);
  2460. + dispEvent = new event.target.ownerGlobal.MouseEvent("click", {
  2461. + bubbles: true,
  2462. + });
  2463. + button.dispatchEvent(dispEvent);
  2464. + this._doingKeyboardActivation = false;
  2465. + break;
  2466. + }
  2467. + }
  2468. + }
  2469. +
  2470. + /**
  2471. + * Focus the last selected element in the view, if any.
  2472. + *
  2473. + * @param byKey {Boolean} whether focus was moved by the user pressing a key.
  2474. + * Needed to ensure we show focus styles in the right cases.
  2475. + */
  2476. + focusSelectedElement(byKey = false) {
  2477. + const selected = this.selectedElement;
  2478. + if (selected) {
  2479. + const flag = byKey ? "FLAG_BYKEY" : "FLAG_BYELEMENTFOCUS";
  2480. + Services.focus.setFocus(selected, Services.focus[flag]);
  2481. + }
  2482. + }
  2483. +
  2484. + /**
  2485. + * Clear all traces of keyboard navigation happening right now.
  2486. + */
  2487. + clearNavigation() {
  2488. + const selected = this.selectedElement;
  2489. + if (selected) {
  2490. + selected.blur();
  2491. + this.selectedElement = null;
  2492. + }
  2493. + }
  2494. +}
  2495. diff --git a/suite/buoy/modules/SessionStore.sys.mjs b/suite/buoy/modules/SessionStore.sys.mjs
  2496. new file mode 100644
  2497. --- /dev/null
  2498. +++ b/suite/buoy/modules/SessionStore.sys.mjs
  2499. @@ -0,0 +1,12 @@
  2500. +/* This Source Code Form is subject to the terms of the Mozilla Public
  2501. + * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  2502. + * You can obtain one at http://mozilla.org/MPL/2.0/. */
  2503. +
  2504. +/**
  2505. + * This is a shim for SessionStore in moz-central to prevent bug 1713801. Only
  2506. + * the methods that appear to be hit by comm-central are implemented.
  2507. + */
  2508. +export var SessionStore = {
  2509. + updateSessionStoreFromTablistener(aBrowser, aBrowsingContext, aData) {},
  2510. + maybeExitCrashedState() {},
  2511. +};
  2512. diff --git a/suite/buoy/moz-l10n/browser/appExtensionFields.ftl b/suite/buoy/moz-l10n/browser/appExtensionFields.ftl
  2513. new file mode 100644
  2514. --- /dev/null
  2515. +++ b/suite/buoy/moz-l10n/browser/appExtensionFields.ftl
  2516. @@ -0,0 +1,10 @@
  2517. +# This Source Code Form is subject to the terms of the Mozilla Public
  2518. +# License, v. 2.0. If a copy of the MPL was not distributed with this
  2519. +# file, You can obtain one at http://mozilla.org/MPL/2.0/.
  2520. +
  2521. +## Theme names and descriptions used in the Themes panel in about:addons
  2522. +
  2523. +# "Auto" is short for automatic. It can be localized without limitations.
  2524. +extension-default-theme-name-auto=System theme — auto
  2525. +extension-default-theme-description=Follow the operating system setting for buttons, menus, and windows.
  2526. +
  2527. diff --git a/suite/buoy/moz-l10n/browser/branding/brandings.ftl b/suite/buoy/moz-l10n/browser/branding/brandings.ftl
  2528. new file mode 100644
  2529. --- /dev/null
  2530. +++ b/suite/buoy/moz-l10n/browser/branding/brandings.ftl
  2531. @@ -0,0 +1,17 @@
  2532. +# This Source Code Form is subject to the terms of the Mozilla Public
  2533. +# License, v. 2.0. If a copy of the MPL was not distributed with this
  2534. +# file, You can obtain one at http://mozilla.org/MPL/2.0/.
  2535. +
  2536. +## The following feature names must be treated as a brand.
  2537. +##
  2538. +## They cannot be:
  2539. +## - Transliterated.
  2540. +## - Translated.
  2541. +##
  2542. +## Declension should be avoided where possible, leaving the original
  2543. +## brand unaltered in prominent UI positions.
  2544. +##
  2545. +## For further details, consult:
  2546. +## https://mozilla-l10n.github.io/styleguides/mozilla_general/#brands-copyright-and-trademark
  2547. +
  2548. +-profiler-brand-name = Firefox Profiler
  2549. diff --git a/suite/buoy/moz.build b/suite/buoy/moz.build
  2550. new file mode 100644
  2551. --- /dev/null
  2552. +++ b/suite/buoy/moz.build
  2553. @@ -0,0 +1,15 @@
  2554. +EXTRA_JS_MODULES += [
  2555. + "modules/BrowserWindowTracker.sys.mjs",
  2556. + "modules/CustomizableUI.sys.mjs",
  2557. + "modules/EzE10SUtils.sys.mjs",
  2558. + "modules/PanelMultiView.sys.mjs",
  2559. +]
  2560. +
  2561. +EXTRA_JS_MODULES.sessionstore += [
  2562. + "modules/SessionStore.sys.mjs",
  2563. +]
  2564. +
  2565. +
  2566. +JS_PREFERENCE_FILES += ["ZZ-buoy-prefs.js"]
  2567. +
  2568. +JAR_MANIFESTS += ["jar.mn"]
  2569. diff --git a/suite/moz.build b/suite/moz.build
  2570. --- a/suite/moz.build
  2571. +++ b/suite/moz.build
  2572. @@ -24,16 +24,19 @@ DIRS += [
  2573. if CONFIG["MOZ_THUNDERBIRD_RUST"]:
  2574. DEFINES["MOZ_THUNDERBIRD_RUST"] = 1
  2575. if CONFIG["MOZ_OVERRIDE_GKRUST"]:
  2576. DIRS += [
  2577. "../rust",
  2578. ]
  2579. +if CONFIG['MOZ_SUITE_BUOY']:
  2580. + DIRS += ['buoy']
  2581. +
  2582. if CONFIG['MOZ_IRC']:
  2583. DIRS += ['chatzilla']
  2584. if CONFIG["MAKENSISU"]:
  2585. DIRS += ["installer/windows"]
  2586. if CONFIG["MOZ_BUNDLED_FONTS"]:
  2587. DIRS += ["/browser/fonts"]
  2588. diff --git a/suite/moz.configure b/suite/moz.configure
  2589. --- a/suite/moz.configure
  2590. +++ b/suite/moz.configure
  2591. @@ -99,16 +99,29 @@ def moz_override_cargo_config(enable_rus
  2592. set_config(
  2593. "MOZ_OVERRIDE_CARGO_CONFIG",
  2594. moz_override_cargo_config,
  2595. when="--enable-thunderbird-rust",
  2596. )
  2597. +# =========================================================
  2598. +# = Diagnostic "Buoy" Component
  2599. +# =========================================================
  2600. +option(
  2601. + "--enable-buoy", default=False, help="Enable building of the SeaMonkey Diagnostic Component"
  2602. +)
  2603. +
  2604. +@depends_if("--enable-buoy")
  2605. +def buoy(arg):
  2606. + return True
  2607. +
  2608. +set_config("MOZ_SUITE_BUOY", buoy)
  2609. +
  2610. # Building extensions is disabled by default.
  2611. # =========================================================
  2612. # = ChatZilla extension
  2613. # =========================================================
  2614. option(
  2615. "--enable-irc", default=False, help="Enable building of the ChatZilla IRC extension"
  2616. )