Source

ImmersHUD/ImmersHUD.js

  1. import htmlTemplate from './ImmersHUD.html'
  2. import styles from './ImmersHUD.css'
  3. import { ImmersClient } from '../client'
  4. import { roles } from '../authUtils'
  5. /**
  6. * Web Component heads-up display for Immers profile login.
  7. * Unobtrusively connects your Immersive Web experience to Immers Space,
  8. * allowing immersers to connect with their profiles from your site
  9. * and share your site with their friends. Grants access to profile information
  10. * so you can bring users' preferred identity into your experience.
  11. *
  12. * The HTML attributes are listed in the Properties table below.
  13. * Properties you can access from the element object directly are listed under Members.
  14. *
  15. * The following CSS properties can be set on the immers-hud:
  16. *
  17. * color: text color for floating text
  18. *
  19. * --main-margin: distance from edge of window in overlay mode
  20. *
  21. * --inner-margin: gap between elements
  22. *
  23. * --handle-input-width: size of the handle input
  24. *
  25. * @class ImmersHUD
  26. *
  27. * @fires immers-hud-connected - On successful login, detail.profile will include users {@link Profile}
  28. *
  29. * @prop {'top-left'|'top-right'|'bottom-left'|'bottom-right'} [position] - Enable overlay positioning.
  30. * @prop {string} [token-catcher] - OAuth redirect URL, a page on your domain that runs {@link catchToken} on load (default: current url)
  31. * @prop {string} [access-role] - Requested authorization scope from {@link roles}. Users are given the option to alter this and grant a different level. (default: modAdditive)
  32. * @prop {string} [destination-name] Title for your experience (default: meta[og:description], document.title)
  33. * @prop {string} [destination-url] Sharable URL for your experience (default: current url)
  34. * @prop {string} [destination-description] Social share preview test (default meta[og:description], meta[twitter:description])
  35. * @prop {string} [destination-image] Image url for social share previews (default: meta[og:image], meta[twitter:image])
  36. * @prop {string} [local-immer] Origin of your local Immers Server, if you have one
  37. * @prop {boolean} [allow-storage] Enable local storage of user identity to reconnect when returning to page
  38. * @prop {'true'|'false'} open - Toggles between icon and full HUD view (default: true is user's handle is saved but login is needed, false otherwise)
  39. *
  40. * @example <caption>Load & register the custom element via import (option 1)</caption>
  41. * import { ImmersHUD } from 'immers-client';
  42. * ImmersHUD.Register();
  43. * @example <caption>Load & register the custom element via CDN (option 2)</caption>
  44. * <script type="module" src="https://cdn.jsdelivr.net/npm/immers-client/dist/ImmersHUD.bundle.js"></script>
  45. * @example <caption>Using the custom element in HTML</caption>
  46. * <immers-hud position="bottom-left" access-role="friends"
  47. * destination-name="My Immer" destination-url="https://myimmer.com/"
  48. * token-catcher="https://myimmer.com/"></immers-hud>
  49. *
  50. */
  51. export class ImmersHUD extends window.HTMLElement {
  52. #queryCache = {}
  53. #container
  54. /**
  55. * Live-updated friends list with current status
  56. * @type {FriendStatus[]}
  57. */
  58. friends = []
  59. /**
  60. * Immers client instance
  61. * @type {ImmersClient}
  62. */
  63. immersClient
  64. constructor () {
  65. super()
  66. this.attachShadow({ mode: 'open' })
  67. const styleTag = document.createElement('style')
  68. styleTag.innerHTML = styles
  69. const template = document.createElement('template')
  70. template.innerHTML = htmlTemplate.trim()
  71. this.shadowRoot.append(styleTag, template.content.cloneNode(true))
  72. this.#container = this.shadowRoot.lastElementChild
  73. }
  74. connectedCallback () {
  75. if (this.immersClient) {
  76. // already initialized
  77. return
  78. }
  79. // Immers client setup
  80. const destination = {
  81. name: this.getAttribute('destination-name') || this.#meta('og:title') || document.title,
  82. url: this.getAttribute('destination-url') || window.location.href
  83. }
  84. const description = this.getAttribute('destination-description') || this.#meta('og:description') || this.#meta('twitter:description')
  85. if (description) {
  86. destination.description = description
  87. }
  88. const image = this.getAttribute('destination-image') || this.#meta('og:image') || this.#meta('twitter:image')
  89. if (image) {
  90. destination.previewImage = image
  91. }
  92. this.immersClient = new ImmersClient(destination, {
  93. localImmer: this.getAttribute('local-immer'),
  94. allowStorage: this.hasAttribute('allow-storage')
  95. })
  96. this.immersClient.addEventListener(
  97. 'immers-client-friends-update',
  98. ({ detail: { friends } }) => this.onFriendsUpdate(friends)
  99. )
  100. this.immersClient.addEventListener(
  101. 'immers-client-connected',
  102. ({ detail: { profile } }) => this.onClientConnected(profile)
  103. )
  104. this.immersClient.addEventListener(
  105. 'immers-client-disconnected',
  106. () => this.onClientDisconnected()
  107. )
  108. this.#container.addEventListener('click', evt => {
  109. switch (evt.target.id) {
  110. case 'login':
  111. evt.preventDefault()
  112. this.login()
  113. break
  114. case 'logo':
  115. this.setAttribute('open', this.getAttribute('open') !== 'true')
  116. break
  117. case 'exit-button':
  118. // TODO: add confirmation modal
  119. this.remove()
  120. break
  121. case 'logout':
  122. this.immersClient.logout()
  123. break
  124. }
  125. })
  126. if (this.immersClient.handle) {
  127. this.#el('handle-input').value = this.immersClient.handle
  128. this.immersClient.reconnect().then(connected => {
  129. if (!connected) {
  130. // user has logged in before, but action required to reconnect
  131. // prompt with open, pre-filled login
  132. this.setAttribute('open', true)
  133. }
  134. })
  135. }
  136. }
  137. attributeChangedCallback (name, oldValue, newValue) {
  138. switch (name) {
  139. case 'position':
  140. if (newValue && !ImmersHUD.POSITION_OPTIONS.includes(newValue)) {
  141. console.warn(`immers-hud: unknown position ${newValue}. Valid options are ${ImmersHUD.POSITION_OPTIONS.join(', ')}`)
  142. }
  143. break
  144. case 'open':
  145. this.#el('notification').classList.add('hidden')
  146. break
  147. }
  148. }
  149. login () {
  150. this.immersClient.login(
  151. this.getAttribute('token-catcher') || window.location.href,
  152. this.getAttribute('access-role') || roles[2],
  153. this.#el('handle-input').value
  154. ).then(() => this.immersClient.enter())
  155. }
  156. onClientConnected (profile) {
  157. this.#el('login-container').classList.add('removed')
  158. this.#el('status-container').classList.remove('removed')
  159. // show profile info
  160. if (profile.avatarImage) {
  161. this.#el('logo').style.backgroundImage = `url(${profile.avatarImage})`
  162. }
  163. this.#el('username').textContent = profile.displayName
  164. this.#el('profile-link').setAttribute('href', profile.url)
  165. this.#emit('immers-hud-connected', { profile })
  166. }
  167. onClientDisconnected () {
  168. this.#el('login-container').classList.remove('removed')
  169. this.#el('status-container').classList.add('removed')
  170. this.#el('handle-input').value = ''
  171. this.#el('logo').style.backgroundImage = ''
  172. this.#el('username').textContent = ''
  173. this.#el('profile-link').setAttribute('href', '#')
  174. }
  175. onFriendsUpdate (friends) {
  176. this.friends = friends
  177. if (this.getAttribute('open') !== 'true') {
  178. this.#el('notification').classList.remove('hidden')
  179. }
  180. this.#el('status-message').textContent = `${friends.filter(f => f.isOnline).length}/${friends.length} friends online`
  181. }
  182. #el (id) {
  183. return this.#queryCache[id] ?? (this.#queryCache[id] = this.#container.querySelector(`#${id}`))
  184. }
  185. #emit (type, data) {
  186. this.dispatchEvent(new window.CustomEvent(type, {
  187. detail: data
  188. }))
  189. }
  190. #meta (name) {
  191. const attr = name.startsWith('og:') ? 'property' : 'name'
  192. return document.querySelector(`meta[${attr}="${name}"]`)?.getAttribute('content')
  193. }
  194. static get observedAttributes () {
  195. return ['position', 'open']
  196. }
  197. static get POSITION_OPTIONS () {
  198. return ['top-left', 'bottom-left', 'top-right', 'bottom-right']
  199. }
  200. static Register () {
  201. window.customElements.define('immers-hud', ImmersHUD)
  202. }
  203. }