|
| 1 | +/* |
| 2 | + This file was copied from mashlib/src/global/header.ts file. It is modified to |
| 3 | + work in solid-ui by adjusting where imported functions are found. |
| 4 | + */ |
| 5 | +import { IndexedFormula, NamedNode, sym } from 'rdflib' |
| 6 | +import { loginStatusBox, solidAuthClient } from '../authn/authn' |
| 7 | +import { widgets } from '../widgets' |
| 8 | +import { icon } from './icon' |
| 9 | +import { emptyProfile } from './empty-profile' |
| 10 | +import { throttle, getPod } from './headerHelpers' |
| 11 | +import { getOutliner } from 'solid-panes' |
| 12 | + |
| 13 | +// SolidAuthorization, SolidClam, and SolidSession was copied from mashlib/typings/solid-auth-client |
| 14 | +// access_token, client_id, id_token, at_hash had to be converted to camelcase for typescript compatibility |
| 15 | +interface SolidAuthorization { |
| 16 | + accessToken: string; |
| 17 | + clientId: string; |
| 18 | + idToken: string; |
| 19 | +} |
| 20 | + |
| 21 | +interface SolidClaim { |
| 22 | + atHash: string; |
| 23 | + aud: string; |
| 24 | + azp: string; |
| 25 | + cnf: { |
| 26 | + jwk: string; |
| 27 | + }; |
| 28 | + exp: number; |
| 29 | + iat: number; |
| 30 | + iss: string; |
| 31 | + jti: string; |
| 32 | + nonce: string; |
| 33 | + sub: string; |
| 34 | +} |
| 35 | +export interface SolidSession { |
| 36 | + authorization: SolidAuthorization; |
| 37 | + credentialType: string; |
| 38 | + idClaims: SolidClaim; |
| 39 | + idp: string; |
| 40 | + issuer: string; |
| 41 | + sessionKey: string; |
| 42 | + webId: string; |
| 43 | +} |
| 44 | + |
| 45 | +export async function initHeader (store: IndexedFormula) { |
| 46 | + const header = document.getElementById('PageHeader') |
| 47 | + if (!header) { |
| 48 | + return |
| 49 | + } |
| 50 | + |
| 51 | + const pod = getPod() |
| 52 | + solidAuthClient.trackSession(rebuildHeader(header, store, pod)) |
| 53 | +} |
| 54 | + |
| 55 | +function rebuildHeader (header: HTMLElement, store: IndexedFormula, pod: NamedNode) { |
| 56 | + return async (session: SolidSession | null) => { |
| 57 | + const user = session ? sym(session.webId) : null |
| 58 | + header.innerHTML = '' |
| 59 | + header.appendChild(await createBanner(store, pod, user)) |
| 60 | + } |
| 61 | +} |
| 62 | + |
| 63 | +async function createBanner (store: IndexedFormula, pod: NamedNode, user: NamedNode | null): Promise<HTMLElement> { |
| 64 | + const podLink = document.createElement('a') |
| 65 | + podLink.href = pod.uri |
| 66 | + podLink.classList.add('header-banner__link') |
| 67 | + podLink.innerHTML = icon |
| 68 | + |
| 69 | + const menu = user |
| 70 | + ? await createUserMenu(store, user) |
| 71 | + : createLoginSignUpButtons() |
| 72 | + |
| 73 | + const banner = document.createElement('div') |
| 74 | + banner.classList.add('header-banner') |
| 75 | + banner.appendChild(podLink) |
| 76 | + banner.appendChild(menu) |
| 77 | + |
| 78 | + return banner |
| 79 | +} |
| 80 | + |
| 81 | +function createLoginSignUpButtons () { |
| 82 | + const profileLoginButtonPre = document.createElement('div') |
| 83 | + profileLoginButtonPre.classList.add('header-banner__login') |
| 84 | + profileLoginButtonPre.appendChild(loginStatusBox(document, null, {})) |
| 85 | + return profileLoginButtonPre |
| 86 | +} |
| 87 | + |
| 88 | +async function openDashboardPane (outliner: any, pane: string): Promise<void> { |
| 89 | + outliner.showDashboard({ |
| 90 | + pane |
| 91 | + }) |
| 92 | +} |
| 93 | + |
| 94 | +function createUserMenuButton (label: string, onClick: EventListenerOrEventListenerObject): HTMLElement { |
| 95 | + const button = document.createElement('button') |
| 96 | + button.classList.add('header-user-menu__button') |
| 97 | + button.addEventListener('click', onClick) |
| 98 | + button.innerText = label |
| 99 | + return button |
| 100 | +} |
| 101 | + |
| 102 | +function createUserMenuLink (label: string, href: string): HTMLElement { |
| 103 | + const link = document.createElement('a') |
| 104 | + link.classList.add('header-user-menu__link') |
| 105 | + link.href = href |
| 106 | + link.innerText = label |
| 107 | + return link |
| 108 | +} |
| 109 | + |
| 110 | +async function createUserMenu (store: IndexedFormula, user: NamedNode): Promise<HTMLElement> { |
| 111 | + const fetcher = (<any>store).fetcher |
| 112 | + if (fetcher) { |
| 113 | + // Making sure that Profile is loaded before building menu |
| 114 | + await fetcher.load(user) |
| 115 | + } |
| 116 | + const outliner = getOutliner(document) |
| 117 | + |
| 118 | + const loggedInMenuList = document.createElement('ul') |
| 119 | + loggedInMenuList.classList.add('header-user-menu__list') |
| 120 | + loggedInMenuList.appendChild(createUserMenuItem(createUserMenuLink('Show your profile', user.uri))) |
| 121 | + const menuItems = await getMenuItems(outliner) |
| 122 | + menuItems.forEach(item => { |
| 123 | + loggedInMenuList.appendChild(createUserMenuItem(createUserMenuButton(item.label, () => openDashboardPane(outliner, item.tabName || item.paneName)))) |
| 124 | + }) |
| 125 | + loggedInMenuList.appendChild(createUserMenuItem(createUserMenuButton('Log out', () => solidAuthClient.logout()))) |
| 126 | + |
| 127 | + const loggedInMenu = document.createElement('nav') |
| 128 | + loggedInMenu.classList.add('header-user-menu__navigation-menu') |
| 129 | + loggedInMenu.setAttribute('aria-hidden', 'true') |
| 130 | + loggedInMenu.appendChild(loggedInMenuList) |
| 131 | + |
| 132 | + const loggedInMenuTrigger = document.createElement('button') |
| 133 | + loggedInMenuTrigger.classList.add('header-user-menu__trigger') |
| 134 | + loggedInMenuTrigger.type = 'button' |
| 135 | + const profileImg = getProfileImg(store, user) |
| 136 | + if (typeof profileImg === 'string') { |
| 137 | + loggedInMenuTrigger.innerHTML = profileImg |
| 138 | + } else { |
| 139 | + loggedInMenuTrigger.appendChild(profileImg) |
| 140 | + } |
| 141 | + |
| 142 | + const loggedInMenuContainer = document.createElement('div') |
| 143 | + loggedInMenuContainer.classList.add('header-banner__user-menu', 'header-user-menu') |
| 144 | + loggedInMenuContainer.appendChild(loggedInMenuTrigger) |
| 145 | + loggedInMenuContainer.appendChild(loggedInMenu) |
| 146 | + |
| 147 | + const throttledMenuToggle = throttle((event: Event) => toggleMenu(event, loggedInMenuTrigger, loggedInMenu), 50) |
| 148 | + loggedInMenuTrigger.addEventListener('click', throttledMenuToggle) |
| 149 | + let timer = setTimeout(() => null, 0) |
| 150 | + loggedInMenuContainer.addEventListener('mouseover', event => { |
| 151 | + clearTimeout(timer) |
| 152 | + throttledMenuToggle(event) |
| 153 | + }) |
| 154 | + loggedInMenuContainer.addEventListener('mouseout', event => { |
| 155 | + timer = setTimeout(() => throttledMenuToggle(event), 200) |
| 156 | + }) |
| 157 | + |
| 158 | + return loggedInMenuContainer |
| 159 | +} |
| 160 | + |
| 161 | +function createUserMenuItem (child: HTMLElement): HTMLElement { |
| 162 | + const menuProfileItem = document.createElement('li') |
| 163 | + menuProfileItem.classList.add('header-user-menu__list-item') |
| 164 | + menuProfileItem.appendChild(child) |
| 165 | + return menuProfileItem |
| 166 | +} |
| 167 | + |
| 168 | +async function getMenuItems (outliner: any): Promise<Array<{ |
| 169 | + paneName: string; |
| 170 | + tabName?: string; |
| 171 | + label: string; |
| 172 | + icon: string; |
| 173 | +}>> { |
| 174 | + return outliner.getDashboardItems() |
| 175 | +} |
| 176 | + |
| 177 | +function getProfileImg (store: IndexedFormula, user: NamedNode): string | HTMLElement { |
| 178 | + const profileUrl = widgets.findImage(user) |
| 179 | + if (!profileUrl) { |
| 180 | + return emptyProfile |
| 181 | + } |
| 182 | + const profileImage = document.createElement('div') |
| 183 | + profileImage.classList.add('header-user-menu__photo') |
| 184 | + profileImage.style.backgroundImage = `url("${profileUrl}")` |
| 185 | + return profileImage |
| 186 | +} |
| 187 | + |
| 188 | +function toggleMenu (event: Event, trigger: HTMLButtonElement, menu: HTMLElement): void { |
| 189 | + const isExpanded = trigger.getAttribute('aria-expanded') === 'true' |
| 190 | + const expand = event.type === 'mouseover' |
| 191 | + const close = event.type === 'mouseout' |
| 192 | + if ((isExpanded && expand) || (!isExpanded && close)) { |
| 193 | + return |
| 194 | + } |
| 195 | + trigger.setAttribute('aria-expanded', (!isExpanded).toString()) |
| 196 | + menu.setAttribute('aria-hidden', isExpanded.toString()) |
| 197 | +} |
0 commit comments