@internetarchive/ia-topnav 0.0.0-webdev7666-1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (340) hide show
  1. package/.prettierignore +1 -0
  2. package/LICENSE +661 -0
  3. package/README.md +147 -0
  4. package/demo/app-root.ts +183 -0
  5. package/demo/index.html +27 -0
  6. package/dist/demo/app-root.d.ts +13 -0
  7. package/dist/demo/app-root.js +197 -0
  8. package/dist/demo/app-root.js.map +1 -0
  9. package/dist/index.d.ts +2 -0
  10. package/dist/index.js +3 -0
  11. package/dist/index.js.map +1 -0
  12. package/dist/src/assets/img/hamburger.d.ts +7 -0
  13. package/dist/src/assets/img/hamburger.js +51 -0
  14. package/dist/src/assets/img/hamburger.js.map +1 -0
  15. package/dist/src/assets/img/ia-icon.d.ts +7 -0
  16. package/dist/src/assets/img/ia-icon.js +33 -0
  17. package/dist/src/assets/img/ia-icon.js.map +1 -0
  18. package/dist/src/assets/img/icon-audio.d.ts +2 -0
  19. package/dist/src/assets/img/icon-audio.js +28 -0
  20. package/dist/src/assets/img/icon-audio.js.map +1 -0
  21. package/dist/src/assets/img/icon-close.d.ts +2 -0
  22. package/dist/src/assets/img/icon-close.js +20 -0
  23. package/dist/src/assets/img/icon-close.js.map +1 -0
  24. package/dist/src/assets/img/icon-donate-unpadded.d.ts +2 -0
  25. package/dist/src/assets/img/icon-donate-unpadded.js +19 -0
  26. package/dist/src/assets/img/icon-donate-unpadded.js.map +1 -0
  27. package/dist/src/assets/img/icon-donate.d.ts +2 -0
  28. package/dist/src/assets/img/icon-donate.js +18 -0
  29. package/dist/src/assets/img/icon-donate.js.map +1 -0
  30. package/dist/src/assets/img/icon-ellipses.d.ts +2 -0
  31. package/dist/src/assets/img/icon-ellipses.js +19 -0
  32. package/dist/src/assets/img/icon-ellipses.js.map +1 -0
  33. package/dist/src/assets/img/icon-ia-logo.d.ts +2 -0
  34. package/dist/src/assets/img/icon-ia-logo.js +30 -0
  35. package/dist/src/assets/img/icon-ia-logo.js.map +1 -0
  36. package/dist/src/assets/img/icon-images.d.ts +2 -0
  37. package/dist/src/assets/img/icon-images.js +19 -0
  38. package/dist/src/assets/img/icon-images.js.map +1 -0
  39. package/dist/src/assets/img/icon-search.d.ts +2 -0
  40. package/dist/src/assets/img/icon-search.js +19 -0
  41. package/dist/src/assets/img/icon-search.js.map +1 -0
  42. package/dist/src/assets/img/icon-software.d.ts +2 -0
  43. package/dist/src/assets/img/icon-software.js +18 -0
  44. package/dist/src/assets/img/icon-software.js.map +1 -0
  45. package/dist/src/assets/img/icon-texts.d.ts +2 -0
  46. package/dist/src/assets/img/icon-texts.js +18 -0
  47. package/dist/src/assets/img/icon-texts.js.map +1 -0
  48. package/dist/src/assets/img/icon-upload-unpadded.d.ts +2 -0
  49. package/dist/src/assets/img/icon-upload-unpadded.js +18 -0
  50. package/dist/src/assets/img/icon-upload-unpadded.js.map +1 -0
  51. package/dist/src/assets/img/icon-upload.d.ts +2 -0
  52. package/dist/src/assets/img/icon-upload.js +21 -0
  53. package/dist/src/assets/img/icon-upload.js.map +1 -0
  54. package/dist/src/assets/img/icon-user.d.ts +2 -0
  55. package/dist/src/assets/img/icon-user.js +19 -0
  56. package/dist/src/assets/img/icon-user.js.map +1 -0
  57. package/dist/src/assets/img/icon-video.d.ts +2 -0
  58. package/dist/src/assets/img/icon-video.js +19 -0
  59. package/dist/src/assets/img/icon-video.js.map +1 -0
  60. package/dist/src/assets/img/icon-web.d.ts +2 -0
  61. package/dist/src/assets/img/icon-web.js +19 -0
  62. package/dist/src/assets/img/icon-web.js.map +1 -0
  63. package/dist/src/assets/img/icon.d.ts +5 -0
  64. package/dist/src/assets/img/icon.js +17 -0
  65. package/dist/src/assets/img/icon.js.map +1 -0
  66. package/dist/src/assets/img/icons.d.ts +18 -0
  67. package/dist/src/assets/img/icons.js +33 -0
  68. package/dist/src/assets/img/icons.js.map +1 -0
  69. package/dist/src/assets/img/wordmark-stacked.d.ts +2 -0
  70. package/dist/src/assets/img/wordmark-stacked.js +13 -0
  71. package/dist/src/assets/img/wordmark-stacked.js.map +1 -0
  72. package/dist/src/data/menus.d.ts +20 -0
  73. package/dist/src/data/menus.js +676 -0
  74. package/dist/src/data/menus.js.map +1 -0
  75. package/dist/src/desktop-subnav.d.ts +11 -0
  76. package/dist/src/desktop-subnav.js +55 -0
  77. package/dist/src/desktop-subnav.js.map +1 -0
  78. package/dist/src/dropdown-menu.d.ts +19 -0
  79. package/dist/src/dropdown-menu.js +116 -0
  80. package/dist/src/dropdown-menu.js.map +1 -0
  81. package/dist/src/ia-topnav.d.ts +67 -0
  82. package/dist/src/ia-topnav.js +312 -0
  83. package/dist/src/ia-topnav.js.map +1 -0
  84. package/dist/src/lib/format-url.d.ts +2 -0
  85. package/dist/src/lib/format-url.js +2 -0
  86. package/dist/src/lib/format-url.js.map +1 -0
  87. package/dist/src/lib/keyboard-navigation.d.ts +50 -0
  88. package/dist/src/lib/keyboard-navigation.js +136 -0
  89. package/dist/src/lib/keyboard-navigation.js.map +1 -0
  90. package/dist/src/lib/location-handler.d.ts +1 -0
  91. package/dist/src/lib/location-handler.js +5 -0
  92. package/dist/src/lib/location-handler.js.map +1 -0
  93. package/dist/src/lib/make-boolean-string.d.ts +2 -0
  94. package/dist/src/lib/make-boolean-string.js +12 -0
  95. package/dist/src/lib/make-boolean-string.js.map +1 -0
  96. package/dist/src/lib/query-handler.d.ts +4 -0
  97. package/dist/src/lib/query-handler.js +7 -0
  98. package/dist/src/lib/query-handler.js.map +1 -0
  99. package/dist/src/lib/t-sentence-case.d.ts +2 -0
  100. package/dist/src/lib/t-sentence-case.js +10 -0
  101. package/dist/src/lib/t-sentence-case.js.map +1 -0
  102. package/dist/src/login-button.d.ts +16 -0
  103. package/dist/src/login-button.js +83 -0
  104. package/dist/src/login-button.js.map +1 -0
  105. package/dist/src/media-button.d.ts +24 -0
  106. package/dist/src/media-button.js +119 -0
  107. package/dist/src/media-button.js.map +1 -0
  108. package/dist/src/media-menu.d.ts +20 -0
  109. package/dist/src/media-menu.js +148 -0
  110. package/dist/src/media-menu.js.map +1 -0
  111. package/dist/src/media-slider.d.ts +15 -0
  112. package/dist/src/media-slider.js +139 -0
  113. package/dist/src/media-slider.js.map +1 -0
  114. package/dist/src/media-subnav.d.ts +18 -0
  115. package/dist/src/media-subnav.js +126 -0
  116. package/dist/src/media-subnav.js.map +1 -0
  117. package/dist/src/models.d.ts +51 -0
  118. package/dist/src/models.js +2 -0
  119. package/dist/src/models.js.map +1 -0
  120. package/dist/src/more-slider.d.ts +10 -0
  121. package/dist/src/more-slider.js +50 -0
  122. package/dist/src/more-slider.js.map +1 -0
  123. package/dist/src/nav-search.d.ts +19 -0
  124. package/dist/src/nav-search.js +124 -0
  125. package/dist/src/nav-search.js.map +1 -0
  126. package/dist/src/primary-nav.d.ts +45 -0
  127. package/dist/src/primary-nav.js +280 -0
  128. package/dist/src/primary-nav.js.map +1 -0
  129. package/dist/src/save-page-form.d.ts +10 -0
  130. package/dist/src/save-page-form.js +63 -0
  131. package/dist/src/save-page-form.js.map +1 -0
  132. package/dist/src/search-menu.d.ts +20 -0
  133. package/dist/src/search-menu.js +162 -0
  134. package/dist/src/search-menu.js.map +1 -0
  135. package/dist/src/signed-out-dropdown.d.ts +4 -0
  136. package/dist/src/signed-out-dropdown.js +15 -0
  137. package/dist/src/signed-out-dropdown.js.map +1 -0
  138. package/dist/src/styles/base.d.ts +1 -0
  139. package/dist/src/styles/base.js +48 -0
  140. package/dist/src/styles/base.js.map +1 -0
  141. package/dist/src/styles/desktop-subnav.d.ts +2 -0
  142. package/dist/src/styles/desktop-subnav.js +37 -0
  143. package/dist/src/styles/desktop-subnav.js.map +1 -0
  144. package/dist/src/styles/dropdown-menu.d.ts +2 -0
  145. package/dist/src/styles/dropdown-menu.js +170 -0
  146. package/dist/src/styles/dropdown-menu.js.map +1 -0
  147. package/dist/src/styles/ia-topnav.d.ts +2 -0
  148. package/dist/src/styles/ia-topnav.js +87 -0
  149. package/dist/src/styles/ia-topnav.js.map +1 -0
  150. package/dist/src/styles/login-button.d.ts +2 -0
  151. package/dist/src/styles/login-button.js +82 -0
  152. package/dist/src/styles/login-button.js.map +1 -0
  153. package/dist/src/styles/media-button.d.ts +2 -0
  154. package/dist/src/styles/media-button.js +156 -0
  155. package/dist/src/styles/media-button.js.map +1 -0
  156. package/dist/src/styles/media-menu.d.ts +2 -0
  157. package/dist/src/styles/media-menu.js +66 -0
  158. package/dist/src/styles/media-menu.js.map +1 -0
  159. package/dist/src/styles/media-slider.d.ts +2 -0
  160. package/dist/src/styles/media-slider.js +81 -0
  161. package/dist/src/styles/media-slider.js.map +1 -0
  162. package/dist/src/styles/media-subnav.d.ts +2 -0
  163. package/dist/src/styles/media-subnav.js +159 -0
  164. package/dist/src/styles/media-subnav.js.map +1 -0
  165. package/dist/src/styles/more-slider.d.ts +2 -0
  166. package/dist/src/styles/more-slider.js +15 -0
  167. package/dist/src/styles/more-slider.js.map +1 -0
  168. package/dist/src/styles/nav-search.d.ts +2 -0
  169. package/dist/src/styles/nav-search.js +136 -0
  170. package/dist/src/styles/nav-search.js.map +1 -0
  171. package/dist/src/styles/primary-nav.d.ts +2 -0
  172. package/dist/src/styles/primary-nav.js +310 -0
  173. package/dist/src/styles/primary-nav.js.map +1 -0
  174. package/dist/src/styles/save-page-form.d.ts +2 -0
  175. package/dist/src/styles/save-page-form.js +54 -0
  176. package/dist/src/styles/save-page-form.js.map +1 -0
  177. package/dist/src/styles/search-menu.d.ts +2 -0
  178. package/dist/src/styles/search-menu.js +105 -0
  179. package/dist/src/styles/search-menu.js.map +1 -0
  180. package/dist/src/styles/signed-out-dropdown.d.ts +2 -0
  181. package/dist/src/styles/signed-out-dropdown.js +31 -0
  182. package/dist/src/styles/signed-out-dropdown.js.map +1 -0
  183. package/dist/src/styles/user-menu.d.ts +2 -0
  184. package/dist/src/styles/user-menu.js +31 -0
  185. package/dist/src/styles/user-menu.js.map +1 -0
  186. package/dist/src/styles/wayback-search.d.ts +2 -0
  187. package/dist/src/styles/wayback-search.js +48 -0
  188. package/dist/src/styles/wayback-search.js.map +1 -0
  189. package/dist/src/styles/wayback-slider.d.ts +2 -0
  190. package/dist/src/styles/wayback-slider.js +33 -0
  191. package/dist/src/styles/wayback-slider.js.map +1 -0
  192. package/dist/src/tracked-element.d.ts +5 -0
  193. package/dist/src/tracked-element.js +30 -0
  194. package/dist/src/tracked-element.js.map +1 -0
  195. package/dist/src/user-menu.d.ts +10 -0
  196. package/dist/src/user-menu.js +60 -0
  197. package/dist/src/user-menu.js.map +1 -0
  198. package/dist/src/wayback-search.d.ts +4 -0
  199. package/dist/src/wayback-search.js +14 -0
  200. package/dist/src/wayback-search.js.map +1 -0
  201. package/dist/src/wayback-slider.d.ts +18 -0
  202. package/dist/src/wayback-slider.js +99 -0
  203. package/dist/src/wayback-slider.js.map +1 -0
  204. package/dist/test/assets/img/hamburger.test.d.ts +1 -0
  205. package/dist/test/assets/img/hamburger.test.js +13 -0
  206. package/dist/test/assets/img/hamburger.test.js.map +1 -0
  207. package/dist/test/data/menus.test.d.ts +1 -0
  208. package/dist/test/data/menus.test.js +11 -0
  209. package/dist/test/data/menus.test.js.map +1 -0
  210. package/dist/test/dropdown-menu.test.d.ts +1 -0
  211. package/dist/test/dropdown-menu.test.js +20 -0
  212. package/dist/test/dropdown-menu.test.js.map +1 -0
  213. package/dist/test/ia-icon.test.d.ts +1 -0
  214. package/dist/test/ia-icon.test.js +11 -0
  215. package/dist/test/ia-icon.test.js.map +1 -0
  216. package/dist/test/ia-topnav.test.d.ts +1 -0
  217. package/dist/test/ia-topnav.test.js +232 -0
  218. package/dist/test/ia-topnav.test.js.map +1 -0
  219. package/dist/test/login-button.test.d.ts +1 -0
  220. package/dist/test/login-button.test.js +14 -0
  221. package/dist/test/login-button.test.js.map +1 -0
  222. package/dist/test/media-button.test.d.ts +1 -0
  223. package/dist/test/media-button.test.js +13 -0
  224. package/dist/test/media-button.test.js.map +1 -0
  225. package/dist/test/media-menu.test.d.ts +1 -0
  226. package/dist/test/media-menu.test.js +27 -0
  227. package/dist/test/media-menu.test.js.map +1 -0
  228. package/dist/test/media-slider.test.d.ts +1 -0
  229. package/dist/test/media-slider.test.js +47 -0
  230. package/dist/test/media-slider.test.js.map +1 -0
  231. package/dist/test/more-slider.test.d.ts +1 -0
  232. package/dist/test/more-slider.test.js +17 -0
  233. package/dist/test/more-slider.test.js.map +1 -0
  234. package/dist/test/nav-search.test.d.ts +1 -0
  235. package/dist/test/nav-search.test.js +47 -0
  236. package/dist/test/nav-search.test.js.map +1 -0
  237. package/dist/test/primary-nav.test.d.ts +1 -0
  238. package/dist/test/primary-nav.test.js +55 -0
  239. package/dist/test/primary-nav.test.js.map +1 -0
  240. package/dist/test/save-page-form.test.d.ts +1 -0
  241. package/dist/test/save-page-form.test.js +47 -0
  242. package/dist/test/save-page-form.test.js.map +1 -0
  243. package/dist/test/search-menu.test.d.ts +1 -0
  244. package/dist/test/search-menu.test.js +42 -0
  245. package/dist/test/search-menu.test.js.map +1 -0
  246. package/dist/test/user-menu.test.d.ts +1 -0
  247. package/dist/test/user-menu.test.js +28 -0
  248. package/dist/test/user-menu.test.js.map +1 -0
  249. package/dist/test/wayback-slider.test.d.ts +1 -0
  250. package/dist/test/wayback-slider.test.js +80 -0
  251. package/dist/test/wayback-slider.test.js.map +1 -0
  252. package/eslint.config.mjs +53 -0
  253. package/index.ts +3 -0
  254. package/package.json +71 -0
  255. package/prettier.config.js +9 -0
  256. package/src/assets/img/hamburger.ts +49 -0
  257. package/src/assets/img/ia-icon.ts +24 -0
  258. package/src/assets/img/icon-audio.ts +28 -0
  259. package/src/assets/img/icon-close.ts +20 -0
  260. package/src/assets/img/icon-donate-unpadded.ts +19 -0
  261. package/src/assets/img/icon-donate.ts +18 -0
  262. package/src/assets/img/icon-ellipses.ts +19 -0
  263. package/src/assets/img/icon-ia-logo.ts +30 -0
  264. package/src/assets/img/icon-images.ts +19 -0
  265. package/src/assets/img/icon-search.ts +19 -0
  266. package/src/assets/img/icon-software.ts +18 -0
  267. package/src/assets/img/icon-texts.ts +18 -0
  268. package/src/assets/img/icon-upload-unpadded.ts +18 -0
  269. package/src/assets/img/icon-upload.ts +21 -0
  270. package/src/assets/img/icon-user.ts +19 -0
  271. package/src/assets/img/icon-video.ts +19 -0
  272. package/src/assets/img/icon-web.ts +19 -0
  273. package/src/assets/img/icon.ts +8 -0
  274. package/src/assets/img/icons.ts +33 -0
  275. package/src/assets/img/wordmark-stacked.ts +13 -0
  276. package/src/data/menus.ts +691 -0
  277. package/src/desktop-subnav.ts +49 -0
  278. package/src/dropdown-menu.ts +106 -0
  279. package/src/ia-topnav.ts +323 -0
  280. package/src/lib/format-url.ts +2 -0
  281. package/src/lib/keyboard-navigation.ts +158 -0
  282. package/src/lib/location-handler.ts +5 -0
  283. package/src/lib/make-boolean-string.ts +12 -0
  284. package/src/lib/query-handler.ts +7 -0
  285. package/src/lib/t-sentence-case.ts +10 -0
  286. package/src/login-button.ts +75 -0
  287. package/src/media-button.ts +103 -0
  288. package/src/media-menu.ts +143 -0
  289. package/src/media-slider.ts +130 -0
  290. package/src/media-subnav.ts +132 -0
  291. package/src/models.ts +63 -0
  292. package/src/more-slider.ts +42 -0
  293. package/src/nav-search.ts +114 -0
  294. package/src/primary-nav.ts +266 -0
  295. package/src/save-page-form.ts +59 -0
  296. package/src/search-menu.ts +156 -0
  297. package/src/signed-out-dropdown.ts +11 -0
  298. package/src/styles/base.ts +48 -0
  299. package/src/styles/desktop-subnav.ts +37 -0
  300. package/src/styles/dropdown-menu.ts +170 -0
  301. package/src/styles/ia-topnav.ts +87 -0
  302. package/src/styles/login-button.ts +82 -0
  303. package/src/styles/media-button.ts +156 -0
  304. package/src/styles/media-menu.ts +66 -0
  305. package/src/styles/media-slider.ts +81 -0
  306. package/src/styles/media-subnav.ts +159 -0
  307. package/src/styles/more-slider.ts +15 -0
  308. package/src/styles/nav-search.ts +136 -0
  309. package/src/styles/primary-nav.ts +310 -0
  310. package/src/styles/save-page-form.ts +54 -0
  311. package/src/styles/search-menu.ts +105 -0
  312. package/src/styles/signed-out-dropdown.ts +31 -0
  313. package/src/styles/user-menu.ts +31 -0
  314. package/src/styles/wayback-search.ts +48 -0
  315. package/src/styles/wayback-slider.ts +33 -0
  316. package/src/tracked-element.ts +32 -0
  317. package/src/user-menu.ts +57 -0
  318. package/src/wayback-search.ts +10 -0
  319. package/src/wayback-slider.ts +88 -0
  320. package/ssl/server.crt +22 -0
  321. package/ssl/server.key +28 -0
  322. package/test/assets/img/hamburger.test.ts +18 -0
  323. package/test/data/menus.test.ts +15 -0
  324. package/test/dropdown-menu.test.ts +23 -0
  325. package/test/ia-icon.test.ts +15 -0
  326. package/test/ia-topnav.test.ts +343 -0
  327. package/test/login-button.test.ts +19 -0
  328. package/test/media-button.test.ts +19 -0
  329. package/test/media-menu.test.ts +42 -0
  330. package/test/media-slider.test.ts +63 -0
  331. package/test/more-slider.test.ts +21 -0
  332. package/test/nav-search.test.ts +70 -0
  333. package/test/primary-nav.test.ts +94 -0
  334. package/test/save-page-form.test.ts +62 -0
  335. package/test/search-menu.test.ts +58 -0
  336. package/test/user-menu.test.ts +34 -0
  337. package/test/wayback-slider.test.ts +97 -0
  338. package/tsconfig.json +31 -0
  339. package/web-dev-server.config.mjs +32 -0
  340. package/web-test-runner.config.mjs +41 -0
@@ -0,0 +1,49 @@
1
+ import { html, nothing, TemplateResult } from 'lit';
2
+ import { customElement, property } from 'lit/decorators.js';
3
+
4
+ import TrackedElement from './tracked-element';
5
+ import desktopSubnavCSS from './styles/desktop-subnav';
6
+ import icons from './assets/img/icons';
7
+ import formatUrl from './lib/format-url';
8
+ import { IATopNavLink } from './models';
9
+
10
+ @customElement('desktop-subnav')
11
+ export class DesktopSubnav extends TrackedElement {
12
+ @property({ type: String }) baseHost = '';
13
+ @property({ type: Array }) menuItems: IATopNavLink[] = [];
14
+
15
+ static get styles() {
16
+ return desktopSubnavCSS;
17
+ }
18
+
19
+ get listItems() {
20
+ return this.menuItems
21
+ ? this.menuItems.map(
22
+ (link) => html`
23
+ <li>
24
+ <a
25
+ class="${link.title.toLowerCase()}"
26
+ .href="${formatUrl(link.url, this.baseHost)}"
27
+ >${link.title}${DesktopSubnav.iconFor(link.title)}</a
28
+ >
29
+ </li>
30
+ `,
31
+ )
32
+ : nothing;
33
+ }
34
+
35
+ static iconFor(title: string): TemplateResult {
36
+ const subnavIcons: Record<string, TemplateResult> = {
37
+ Donate: icons.donate,
38
+ };
39
+ return subnavIcons[title] ? subnavIcons[title] : html``;
40
+ }
41
+
42
+ render() {
43
+ return html`
44
+ <ul>
45
+ ${this.listItems}
46
+ </ul>
47
+ `;
48
+ }
49
+ }
@@ -0,0 +1,106 @@
1
+ import { CSSResult, html, nothing, TemplateResult } from 'lit';
2
+ import { property } from 'lit/decorators.js';
3
+
4
+ import icons from './assets/img/icons';
5
+ import { defaultTopNavConfig } from './data/menus';
6
+ import formatUrl from './lib/format-url';
7
+ import { makeBooleanString } from './lib/make-boolean-string';
8
+ import { IATopNavConfig, IATopNavLink } from './models';
9
+ import dropdownMenuCSS from './styles/dropdown-menu';
10
+ import TrackedElement from './tracked-element';
11
+ import { ifDefined } from 'lit/directives/if-defined.js';
12
+
13
+ export default class DropdownMenu extends TrackedElement {
14
+ @property({ type: String }) baseHost = '';
15
+ @property({ type: Object }) config: IATopNavConfig = defaultTopNavConfig;
16
+ @property({ type: Boolean }) hideSearch = false;
17
+ @property({ type: Array }) menuItems: IATopNavLink[] | IATopNavLink[][] = [];
18
+ @property({ type: Boolean }) animated = false;
19
+ @property({ type: Boolean }) open = false;
20
+
21
+ static get styles(): CSSResult[] {
22
+ return [dropdownMenuCSS];
23
+ }
24
+
25
+ get dropdownItems() {
26
+ if (!this.menuItems) return nothing;
27
+
28
+ if (!Array.isArray(this.menuItems[0])) {
29
+ const submenu = this.menuItems as IATopNavLink[];
30
+ return this.dropdownSection(submenu);
31
+ }
32
+ return this.menuItems.map((submenu, i) => {
33
+ const joiner = i ? DropdownMenu.dropdownDivider : html``;
34
+ if (!Array.isArray(submenu)) {
35
+ return;
36
+ }
37
+ return [joiner, ...this.dropdownSection(submenu)];
38
+ });
39
+ }
40
+
41
+ static get dropdownDivider() {
42
+ return html`<li role="presentation" class="divider"></li>`;
43
+ }
44
+
45
+ private dropdownSection(submenu: IATopNavLink[]): TemplateResult[] {
46
+ return submenu.map(
47
+ (item) => html`
48
+ <li>
49
+ ${item.url
50
+ ? this.dropdownLink(item)
51
+ : DropdownMenu.dropdownText(item)}
52
+ </li>
53
+ `,
54
+ );
55
+ }
56
+
57
+ dropdownLink(link: IATopNavLink): TemplateResult {
58
+ const calloutText = this.config?.callouts?.[link.title];
59
+ return html`<a
60
+ href="${formatUrl(link.url, this.baseHost)}"
61
+ class=${ifDefined(link.class)}
62
+ tabindex="${this.open ? '' : '-1'}"
63
+ @click=${this.trackClick}
64
+ data-event-click-tracking="${this.config
65
+ ?.eventCategory}|Nav${link.analyticsEvent}"
66
+ aria-label=${calloutText ? `New feature: ${link.title}` : nothing}
67
+ >
68
+ ${link.class === 'mobile-upload' ? icons.uploadUnpadded : nothing}
69
+ ${link.title}
70
+ ${calloutText
71
+ ? html`<span class="callout" aria-hidden="true">${calloutText}</span>`
72
+ : nothing}
73
+ </a>`;
74
+ }
75
+
76
+ static dropdownText(item: IATopNavLink) {
77
+ return html`<span class="info-item">${item.title}</span>`;
78
+ }
79
+
80
+ get menuClass() {
81
+ const hiddenClass = this.hideSearch ? ' search-hidden' : '';
82
+ if (this.open) {
83
+ return `open${hiddenClass}`;
84
+ }
85
+ if (this.animated) {
86
+ return `closed${hiddenClass}`;
87
+ }
88
+ return `initial${hiddenClass}`;
89
+ }
90
+
91
+ render() {
92
+ return html`
93
+ <div class="nav-container">
94
+ <nav
95
+ class="${this.menuClass}"
96
+ aria-hidden="${makeBooleanString(!this.open)}"
97
+ aria-expanded="${makeBooleanString(this.open)}"
98
+ >
99
+ <ul>
100
+ ${this.dropdownItems}
101
+ </ul>
102
+ </nav>
103
+ </div>
104
+ `;
105
+ }
106
+ }
@@ -0,0 +1,323 @@
1
+ import { LitElement, PropertyValues, html, nothing } from 'lit';
2
+ import { customElement, property, state } from 'lit/decorators.js';
3
+
4
+ import { buildTopNavMenus, defaultTopNavConfig } from './data/menus';
5
+ import './desktop-subnav';
6
+ import './dropdown-menu';
7
+ import './media-slider';
8
+ import {
9
+ IATopNavConfig,
10
+ IATopNavMenuConfig,
11
+ IATopNavSecondIdentitySlotMode,
12
+ } from './models';
13
+ import './primary-nav';
14
+ import './search-menu';
15
+ import './signed-out-dropdown';
16
+ import iaTopNavCSS from './styles/ia-topnav';
17
+ import './user-menu';
18
+
19
+ @customElement('ia-topnav')
20
+ export class IATopNav extends LitElement {
21
+ @property({ type: Boolean }) localLinks = false;
22
+
23
+ @property({ type: String }) waybackPagesArchived = '';
24
+
25
+ @property({ type: String }) baseHost = 'https://archive.org';
26
+
27
+ @property({ type: String }) mediaBaseHost = 'https://archive.org';
28
+
29
+ @property({ type: Boolean }) admin = false;
30
+
31
+ @property({ type: Boolean }) canManageFlags = false;
32
+
33
+ @property({ type: Object }) config: IATopNavConfig = defaultTopNavConfig;
34
+
35
+ @property({ type: Boolean }) hideSearch = false;
36
+
37
+ @property({ type: String }) itemIdentifier = '';
38
+
39
+ @property({ type: Boolean }) mediaSliderOpen = false;
40
+
41
+ @property({ type: String }) openMenu = '';
42
+
43
+ @property({ type: String }) screenName: string = '';
44
+
45
+ @property({ type: String }) searchIn = '';
46
+
47
+ @property({ type: String }) searchQuery = '';
48
+
49
+ @property({ type: String }) selectedMenuOption = '';
50
+
51
+ @property({ type: String }) username: string = '';
52
+
53
+ @property({ type: String }) userProfileImagePath =
54
+ '/services/img/user/profile';
55
+
56
+ @property({ type: String })
57
+ secondIdentitySlotMode: IATopNavSecondIdentitySlotMode = '';
58
+
59
+ @property({ type: Object }) currentTab?: {
60
+ mediatype: string;
61
+ moveTo: string;
62
+ };
63
+
64
+ @state() private menus: IATopNavMenuConfig = buildTopNavMenus();
65
+
66
+ private get normalizedBaseHost() {
67
+ return !this.localLinks ? this.baseHost : '';
68
+ }
69
+
70
+ static get styles() {
71
+ return iaTopNavCSS;
72
+ }
73
+
74
+ updated(props: PropertyValues) {
75
+ if (
76
+ props.has('username') ||
77
+ props.has('waybackPagesArchived') ||
78
+ props.has('itemIdentifier') ||
79
+ props.has('localLinks') ||
80
+ props.has('baseHost')
81
+ ) {
82
+ this.menuSetup();
83
+ }
84
+ }
85
+
86
+ firstUpdated() {
87
+ // close open menu on `esc` click
88
+ document.addEventListener(
89
+ 'keydown',
90
+ (e) => {
91
+ if (e.key === 'Escape') {
92
+ this.openMenu = '';
93
+ this.mediaSliderOpen = false;
94
+ }
95
+ },
96
+ false,
97
+ );
98
+ }
99
+
100
+ menuSetup() {
101
+ // re/build the nav
102
+ this.menus = buildTopNavMenus(
103
+ this.username,
104
+ this.normalizedBaseHost,
105
+ this.waybackPagesArchived,
106
+ this.itemIdentifier,
107
+ );
108
+ }
109
+
110
+ menuToggled(e: CustomEvent) {
111
+ const currentMenu = this.openMenu;
112
+ this.openMenu = currentMenu === e.detail.menuName ? '' : e.detail.menuName;
113
+ // Keeps media slider open if media menu is open
114
+ if (this.openMenu === 'media') {
115
+ return;
116
+ }
117
+ this.closeMediaSlider();
118
+ }
119
+
120
+ openMediaSlider() {
121
+ this.mediaSliderOpen = true;
122
+ }
123
+
124
+ closeMediaSlider() {
125
+ this.mediaSliderOpen = false;
126
+ this.selectedMenuOption = '';
127
+ }
128
+
129
+ closeMenus() {
130
+ this.openMenu = '';
131
+ this.closeMediaSlider();
132
+ }
133
+
134
+ searchInChanged(e: CustomEvent) {
135
+ this.searchIn = e.detail.searchIn;
136
+ }
137
+
138
+ trackClick(e: CustomEvent) {
139
+ this.dispatchEvent(
140
+ new CustomEvent('analyticsClick', {
141
+ bubbles: true,
142
+ composed: true,
143
+ detail: e.detail,
144
+ }),
145
+ );
146
+ }
147
+
148
+ trackSubmit(e: CustomEvent) {
149
+ this.dispatchEvent(
150
+ new CustomEvent('analyticsSubmit', {
151
+ bubbles: true,
152
+ composed: true,
153
+ detail: e.detail,
154
+ }),
155
+ );
156
+ }
157
+
158
+ mediaTypeSelected(e: CustomEvent) {
159
+ if (this.selectedMenuOption === e.detail.mediatype) {
160
+ this.closeMediaSlider();
161
+ return;
162
+ }
163
+ this.selectedMenuOption = e.detail.mediatype;
164
+ this.openMediaSlider();
165
+ }
166
+
167
+ get searchMenuOpened() {
168
+ return this.openMenu === 'search';
169
+ }
170
+
171
+ get signedOutOpened() {
172
+ return this.openMenu === 'login';
173
+ }
174
+
175
+ get userMenuOpened() {
176
+ return this.openMenu === 'user';
177
+ }
178
+
179
+ get searchMenuTabIndex() {
180
+ return this.searchMenuOpened ? '' : '-1';
181
+ }
182
+
183
+ get userMenuTabIndex() {
184
+ return this.userMenuOpened ? '' : '-1';
185
+ }
186
+
187
+ get signedOutTabIndex() {
188
+ return this.signedOutOpened ? '' : '-1';
189
+ }
190
+
191
+ get closeLayerClass() {
192
+ return !!this.openMenu || this.mediaSliderOpen ? 'visible' : '';
193
+ }
194
+
195
+ get userMenu() {
196
+ return html`
197
+ <user-menu
198
+ .baseHost=${this.normalizedBaseHost}
199
+ .config=${this.config}
200
+ .menuItems=${this.userMenuItems}
201
+ ?open=${this.openMenu === 'user'}
202
+ .username=${this.username}
203
+ ?hideSearch=${this.hideSearch}
204
+ tabindex="${this.userMenuTabIndex}"
205
+ @menuToggled=${this.menuToggled}
206
+ @trackClick=${this.trackClick}
207
+ @focusToOtherMenuItem=${(e: CustomEvent) =>
208
+ (this.currentTab = e.detail)}
209
+ ></user-menu>
210
+ `;
211
+ }
212
+
213
+ get signedOutDropdown() {
214
+ return html`
215
+ <signed-out-dropdown
216
+ .baseHost=${this.normalizedBaseHost}
217
+ .config=${this.config}
218
+ .open=${this.signedOutOpened}
219
+ ?hideSearch=${this.hideSearch}
220
+ tabindex="${this.signedOutTabIndex}"
221
+ .menuItems=${this.signedOutMenuItems}
222
+ ></signed-out-dropdown>
223
+ `;
224
+ }
225
+
226
+ get signedOutMenuItems() {
227
+ return this.menus.signedOut;
228
+ }
229
+
230
+ /**
231
+ * Most users just get the basic menu items.
232
+ * For users with `/items` priv, additional admin menu items are included too.
233
+ * Having the `/flags` priv adds a further admin item for managing flags.
234
+ */
235
+ get userMenuItems() {
236
+ const basicItems = this.menus.user;
237
+
238
+ let adminItems = this.menus.userAdmin;
239
+ if (this.canManageFlags) {
240
+ adminItems = adminItems.concat(this.menus.userAdminFlags);
241
+ }
242
+
243
+ return this.itemIdentifier && this.admin
244
+ ? [basicItems, adminItems]
245
+ : [basicItems];
246
+ }
247
+
248
+ get allowSecondaryIcon() {
249
+ return this.secondIdentitySlotMode === 'allow';
250
+ }
251
+
252
+ get secondLogoSlot() {
253
+ return this.allowSecondaryIcon
254
+ ? html`
255
+ <slot name="opt-sec-logo" slot="opt-sec-logo"></slot>
256
+ <slot name="opt-sec-logo-mobile" slot="opt-sec-logo-mobile"></slot>
257
+ `
258
+ : nothing;
259
+ }
260
+
261
+ get separatorTemplate() {
262
+ return html`<li class="divider" role="presentation"></li>`;
263
+ }
264
+
265
+ render() {
266
+ return html`
267
+ <div class="topnav">
268
+ <primary-nav
269
+ .baseHost=${this.normalizedBaseHost}
270
+ .mediaBaseHost=${this.mediaBaseHost}
271
+ .config=${this.config}
272
+ .openMenu=${this.openMenu}
273
+ .screenName=${this.screenName}
274
+ .searchIn=${this.searchIn}
275
+ .searchQuery=${this.searchQuery}
276
+ .secondIdentitySlotMode=${this.secondIdentitySlotMode}
277
+ .selectedMenuOption=${this.selectedMenuOption}
278
+ .username=${this.username}
279
+ .userProfileImagePath=${this.userProfileImagePath}
280
+ .currentTab=${this.currentTab}
281
+ ?hideSearch=${this.hideSearch}
282
+ @mediaTypeSelected=${this.mediaTypeSelected}
283
+ @trackClick=${this.trackClick}
284
+ @trackSubmit=${this.trackSubmit}
285
+ @menuToggled=${this.menuToggled}
286
+ >
287
+ ${this.secondLogoSlot}
288
+ </primary-nav>
289
+ <desktop-subnav
290
+ .baseHost=${this.normalizedBaseHost}
291
+ .menuItems=${this.menus.more.links}
292
+ @focus=${this.closeMenus}
293
+ ></desktop-subnav>
294
+ <media-slider
295
+ .baseHost=${this.normalizedBaseHost}
296
+ .config=${this.config}
297
+ .selectedMenuOption=${this.selectedMenuOption}
298
+ .mediaSliderOpen=${this.mediaSliderOpen}
299
+ .menus=${this.menus}
300
+ tabindex="${this.mediaSliderOpen ? '1' : '-1'}"
301
+ @focusToOtherMenuItem=${(e: CustomEvent) =>
302
+ (this.currentTab = e.detail)}
303
+ ></media-slider>
304
+ </div>
305
+ ${this.username ? this.userMenu : this.signedOutDropdown}
306
+ <search-menu
307
+ .baseHost=${this.normalizedBaseHost}
308
+ .config=${this.config}
309
+ .openMenu=${this.openMenu}
310
+ tabindex="${this.searchMenuTabIndex}"
311
+ ?hideSearch=${this.hideSearch}
312
+ @searchInChanged=${this.searchInChanged}
313
+ @trackClick=${this.trackClick}
314
+ @trackSubmit=${this.trackSubmit}
315
+ ></search-menu>
316
+ <div
317
+ id="close-layer"
318
+ class="${this.closeLayerClass}"
319
+ @click=${this.closeMenus}
320
+ ></div>
321
+ `;
322
+ }
323
+ }
@@ -0,0 +1,2 @@
1
+ export default (url: string = '', baseHost: string): string =>
2
+ /^https?:/.test(url) ? url : (`${baseHost}${url}` as string & Location);
@@ -0,0 +1,158 @@
1
+ export default class KeyboardNavigation {
2
+ elementsContainer: HTMLElement;
3
+ menuOption: string;
4
+ focusableElements: HTMLElement[];
5
+ focusedIndex: number;
6
+
7
+ /**
8
+ * Constructor for the KeyboardNavigation class.
9
+ * @param {HTMLElement} elementsContainer - The container element that holds the focusable elements.
10
+ * @param {string} menuOption - The type of menu option ('web' or 'usermenu').
11
+ */
12
+ constructor(elementsContainer: HTMLElement, menuOption: string) {
13
+ this.elementsContainer = elementsContainer;
14
+ this.menuOption = menuOption;
15
+ this.focusableElements = this.getFocusableElements();
16
+ this.focusedIndex = this.getInitialFocusedIndex();
17
+
18
+ this.focusableElements[this.focusedIndex]?.focus();
19
+ this.handleKeyDown = this.handleKeyDown.bind(this);
20
+ }
21
+
22
+ /**
23
+ * Returns the initial focused index based on the menu option.
24
+ * @returns {number} The initial focused index (0 for 'web', 1 for 'usermenu').
25
+ */
26
+ getInitialFocusedIndex(): number {
27
+ return this.menuOption === 'usermenu' ? 1 : 0;
28
+ }
29
+
30
+ /**
31
+ * Gets an array of focusable elements within the container.
32
+ * @returns {HTMLElement[]} An array of focusable elements.
33
+ */
34
+ getFocusableElements(): HTMLElement[] {
35
+ const focusableTagSelectors =
36
+ 'a[href], button, input, [tabindex]:not([tabindex="-1"])';
37
+ const isDisabledOrHidden = (el: Element) =>
38
+ !el.hasAttribute('disabled') && !el.getAttribute('aria-hidden');
39
+
40
+ let elements;
41
+ if (this.menuOption === 'web') {
42
+ // wayback focusable elements
43
+ const waybackSlider =
44
+ this.elementsContainer.querySelector('wayback-slider')?.shadowRoot;
45
+ const waybackSearch = waybackSlider?.querySelector('wayback-search');
46
+ const waybackSearchElements = Array.from(
47
+ waybackSearch?.shadowRoot?.querySelectorAll(focusableTagSelectors) ??
48
+ [],
49
+ );
50
+
51
+ const normalElements = Array.from(
52
+ waybackSlider?.querySelectorAll(focusableTagSelectors) ?? [],
53
+ );
54
+
55
+ // wayback save-form focusable elements
56
+ const savePageForm = waybackSlider?.querySelector('save-page-form');
57
+ const savePageFormElements = Array.from(
58
+ savePageForm?.shadowRoot?.querySelectorAll(focusableTagSelectors) ?? [],
59
+ );
60
+
61
+ elements = [
62
+ ...waybackSearchElements,
63
+ ...normalElements,
64
+ ...savePageFormElements,
65
+ ];
66
+ } else {
67
+ elements = this.elementsContainer.querySelectorAll(focusableTagSelectors);
68
+ }
69
+
70
+ return Array.from(elements).filter(isDisabledOrHidden) as HTMLElement[];
71
+ }
72
+
73
+ /**
74
+ * Handles keyboard events and focuses the appropriate element.
75
+ * @param {KeyboardEvent} event - The keyboard event object.
76
+ */
77
+ handleKeyDown(event: KeyboardEvent) {
78
+ const { key } = event;
79
+ const isArrowKey = [
80
+ 'ArrowDown',
81
+ 'ArrowRight',
82
+ 'ArrowUp',
83
+ 'ArrowLeft',
84
+ ].includes(key);
85
+ const isTabKey = key === 'Tab';
86
+
87
+ if (isArrowKey) {
88
+ this.handleArrowKey(key);
89
+ event.preventDefault();
90
+ } else if (isTabKey) {
91
+ this.handleTabKey(event);
92
+ }
93
+ }
94
+
95
+ /**
96
+ * Handles arrow key events and focuses the next or previous element for topnav sub-nav and usermenu
97
+ * @param {string} key - The key that was pressed ('ArrowDown', 'ArrowRight', 'ArrowUp', or 'ArrowLeft').
98
+ */
99
+ handleArrowKey(key: string) {
100
+ const isDownOrRight = ['ArrowDown', 'ArrowRight'].includes(key);
101
+ if (isDownOrRight) {
102
+ this.focusNext();
103
+ } else {
104
+ this.focusPrevious();
105
+ }
106
+ }
107
+
108
+ /**
109
+ * Focuses the previous focusable element in the container.
110
+ */
111
+ focusPrevious() {
112
+ if (this.focusableElements.length === 0) return;
113
+ this.focusedIndex =
114
+ (this.focusedIndex - 1 + this.focusableElements.length) %
115
+ this.focusableElements.length;
116
+ this.focusableElements[this.focusedIndex]?.focus();
117
+ }
118
+
119
+ /**
120
+ * Focuses the next focusable element in the container.
121
+ */
122
+ focusNext() {
123
+ if (this.focusableElements.length === 0) return;
124
+ this.focusedIndex = (this.focusedIndex + 1) % this.focusableElements.length;
125
+ this.focusableElements[this.focusedIndex]?.focus();
126
+ }
127
+
128
+ /**
129
+ * Handles the Tab key event and focuses the next or previous menu item.
130
+ * @param {KeyboardEvent} event - The keyboard event object.
131
+ */
132
+ handleTabKey(event: KeyboardEvent) {
133
+ if (this.menuOption) {
134
+ const isShiftPressed = event.shiftKey;
135
+ this.focusToOtherMenuItems(isShiftPressed);
136
+ }
137
+
138
+ this.focusableElements[this.focusedIndex]?.blur();
139
+ event.preventDefault();
140
+ }
141
+
142
+ /**
143
+ * Focuses the other parent menu items based on the provided flag.
144
+ * @param {boolean} isPrevious - A flag indicating whether to focus the previous menu item.
145
+ */
146
+ focusToOtherMenuItems(isPrevious: boolean = false) {
147
+ this.elementsContainer.dispatchEvent(
148
+ new CustomEvent('focusToOtherMenuItem', {
149
+ bubbles: true,
150
+ composed: true,
151
+ detail: {
152
+ mediatype: this.menuOption,
153
+ moveTo: isPrevious ? 'prev' : 'next',
154
+ },
155
+ }),
156
+ );
157
+ }
158
+ }
@@ -0,0 +1,5 @@
1
+ /* istanbul ignore file */
2
+
3
+ export default function (url: string) {
4
+ window.location.href = url;
5
+ }
@@ -0,0 +1,12 @@
1
+ export function makeBooleanFromString(value: string): 'true' | 'false' {
2
+ const lowerValue = value.toLowerCase();
3
+ let booleanValue: boolean = false;
4
+ if (lowerValue === 'true' || lowerValue === '1') {
5
+ booleanValue = true;
6
+ }
7
+ return makeBooleanString(booleanValue);
8
+ }
9
+
10
+ export function makeBooleanString(value: boolean): 'true' | 'false' {
11
+ return value ? 'true' : 'false';
12
+ }
@@ -0,0 +1,7 @@
1
+ /* istanbul ignore file */
2
+
3
+ export default {
4
+ performQuery(query: string) {
5
+ window.location.href = `https://web.archive.org/web/*/${query}`;
6
+ },
7
+ };
@@ -0,0 +1,10 @@
1
+ const toSentenceCase = (phrase: string): string => {
2
+ const words = phrase.split(' ');
3
+ const lastWord = words.pop();
4
+ const capitalizedWord = `${lastWord?.substr(0, 1).toUpperCase()}${lastWord?.substr(1)}`;
5
+ return words.length
6
+ ? toSentenceCase(`${words.join(' ')}${capitalizedWord}`)
7
+ : capitalizedWord;
8
+ };
9
+
10
+ export default toSentenceCase;