-
Notifications
You must be signed in to change notification settings - Fork 93
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Update uiAccount and uiSourceSwitch to class components
- Loading branch information
Showing
6 changed files
with
313 additions
and
200 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,198 @@ | ||
import { selection } from 'd3-selection'; | ||
|
||
import { uiIcon } from './icon.js'; | ||
|
||
|
||
/** | ||
* UiAccount | ||
* This component adds the user account info to the footer. | ||
*/ | ||
export class UiAccount { | ||
|
||
/** | ||
* @constructor | ||
* @param `context` Global shared application context | ||
*/ | ||
constructor(context) { | ||
this.context = context; | ||
|
||
this.user = undefined; // will be replaced later with `null` or actual user details.. | ||
|
||
// D3 selections | ||
this.$parent = null; | ||
|
||
// Ensure methods used as callbacks always have `this` bound correctly. | ||
// (This is also necessary when using `d3-selection.call`) | ||
this.render = this.render.bind(this); | ||
this.tryLogout = this.tryLogout.bind(this); | ||
this.getUserDetails = this.getUserDetails.bind(this); | ||
|
||
// Note that it's possible to run in an environment without OSM. | ||
const osm = context.services.osm; | ||
if (!osm) return; | ||
|
||
// Event listeners | ||
osm.on('authchange', this.getUserDetails); | ||
} | ||
|
||
|
||
/** | ||
* render | ||
* Accepts a parent selection, and renders the content under it. | ||
* (The parent selection is required the first time, but can be inferred on subsequent renders) | ||
* @param {d3-selection} $parent - A d3-selection to a HTMLElement that this component should render itself into | ||
*/ | ||
render($parent = this.$parent) { | ||
if ($parent instanceof selection) { | ||
this.$parent = $parent; | ||
} else { | ||
return; // no parent - called too early? | ||
} | ||
|
||
if (this.user === undefined) { // First time.. | ||
this.getUserDetails(); // Get the user first, this will call render again.. | ||
return; | ||
} | ||
|
||
const context = this.context; | ||
const l10n = context.systems.l10n; | ||
const osm = context.services.osm; | ||
|
||
// enter .userInfo | ||
$parent.selectAll('.userInfo') | ||
.data([0]) | ||
.enter() | ||
.append('div') | ||
.attr('class', 'userInfo'); | ||
|
||
// enter .loginLogout | ||
$parent.selectAll('.loginLogout') | ||
.data([0]) | ||
.enter() | ||
.append('div') | ||
.attr('class', 'loginLogout') | ||
.append('a') | ||
.attr('href', '#'); | ||
|
||
// update | ||
const $userInfo = $parent.select('.userInfo'); | ||
const $loginLogout = $parent.select('.loginLogout'); | ||
|
||
|
||
// Update user... | ||
if (!this.user) { // show nothing | ||
$userInfo | ||
.html('') // Empty out the DOM content and rebuild from scratch.. | ||
.classed('hide', true); | ||
|
||
} else { | ||
$userInfo | ||
.html('') // Empty out the DOM content and rebuild from scratch.. | ||
.classed('hide', false); | ||
|
||
const $$userLink = $userInfo | ||
.append('a') | ||
.attr('href', osm.userURL(this.user.display_name)) | ||
.attr('target', '_blank'); | ||
|
||
// Add user's image or placeholder | ||
if (this.user.image_url) { | ||
$$userLink | ||
.append('img') | ||
.attr('class', 'icon pre-text user-icon') | ||
.attr('src', this.user.image_url); | ||
} else { | ||
$$userLink | ||
.call(uiIcon('#rapid-icon-avatar', 'pre-text light')); | ||
} | ||
|
||
// Add user name | ||
$$userLink | ||
.append('span') | ||
.attr('class', 'label') | ||
.text(this.user.display_name); | ||
} | ||
|
||
|
||
// Update login/logout... | ||
if (!osm) { // show nothing | ||
$loginLogout | ||
.classed('hide', true); | ||
|
||
} else if (osm.authenticated()) { // show "Log Out" | ||
$loginLogout | ||
.classed('hide', false) | ||
.select('a') | ||
.text(l10n.t('logout')) | ||
.on('click', e => { | ||
e.preventDefault(); | ||
osm.logout(); | ||
this.tryLogout(); | ||
}); | ||
|
||
} else { // show "Log In" | ||
$loginLogout | ||
.classed('hide', false) | ||
.select('a') | ||
.text(l10n.t('login')) | ||
.on('click', e => { | ||
e.preventDefault(); | ||
osm.authenticate(); | ||
}); | ||
} | ||
|
||
} | ||
|
||
|
||
/** | ||
* getUserDetails | ||
* Gets the user details, then calls render again. | ||
*/ | ||
getUserDetails() { | ||
const context = this.context; | ||
const osm = context.services.osm; | ||
|
||
if (!osm || !osm.authenticated()) { | ||
this.user = null; | ||
this.render(); | ||
|
||
} else { | ||
osm.userDetails((err, user) => { | ||
this.user = user || null; | ||
this.render(); | ||
}); | ||
} | ||
} | ||
|
||
|
||
/** | ||
* tryLogout | ||
* OAuth2's idea of "logout" is just to get rid of the bearer token. | ||
* If we try to "login" again, it will just grab the token again. | ||
* What a user probably _really_ expects is to logout of OSM so that they can switch users. | ||
*/ | ||
tryLogout() { | ||
const context = this.context; | ||
const l10n = context.systems.l10n; | ||
const osm = context.services.osm; | ||
if (!osm) return; | ||
|
||
const locale = l10n.localeCode(); | ||
const url = osm.wwwroot + `/logout?locale=${locale}&referer=` + encodeURIComponent(`/login?locale=${locale}`); | ||
|
||
// Create a 600x550 popup window in the center of the screen | ||
const w = 600; | ||
const h = 550; | ||
const settings = [ | ||
['width', w], | ||
['height', h], | ||
['left', window.screen.width / 2 - w / 2], | ||
['top', window.screen.height / 2 - h / 2], | ||
] | ||
.map(x => x.join('=')) | ||
.join(','); | ||
|
||
window.open(url, '_blank', settings); | ||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,106 @@ | ||
import { selection } from 'd3-selection'; | ||
|
||
|
||
/** | ||
* UiSourceSwitch | ||
* This component adds the source switcher control to the footer. | ||
*/ | ||
export class UiSourceSwitch { | ||
|
||
/** | ||
* @constructor | ||
* @param `context` Global shared application context | ||
*/ | ||
constructor(context) { | ||
this.context = context; | ||
|
||
this._isLive = true; // default to live | ||
|
||
// D3 selections | ||
this.$parent = null; | ||
|
||
// Ensure methods used as callbacks always have `this` bound correctly. | ||
// (This is also necessary when using `d3-selection.call`) | ||
this.render = this.render.bind(this); | ||
this.rerender = (() => this.render()); // call render without argument | ||
this.toggle = this.toggle.bind(this); | ||
} | ||
|
||
|
||
/** | ||
* render | ||
* Accepts a parent selection, and renders the content under it. | ||
* (The parent selection is required the first time, but can be inferred on subsequent renders) | ||
* @param {d3-selection} $parent - A d3-selection to a HTMLElement that this component should render itself into | ||
*/ | ||
render($parent = this.$parent) { | ||
if ($parent instanceof selection) { | ||
this.$parent = $parent; | ||
} else { | ||
return; // no parent - called too early? | ||
} | ||
|
||
const context = this.context; | ||
const keys = context.apiConnections; | ||
const l10n = context.systems.l10n; | ||
const showSourceSwitcher = (Array.isArray(keys) && keys.length === 2); | ||
|
||
// Create/remove wrapper div if necessary | ||
let $wrap = $parent.selectAll('.source-switch') | ||
.data(showSourceSwitcher ? [0] : []); | ||
|
||
$wrap.exit() | ||
.remove(); | ||
|
||
const $$wrap = $wrap.enter() | ||
.append('div') | ||
.attr('class', 'source-switch'); | ||
|
||
$$wrap | ||
.append('a') | ||
.attr('href', '#') | ||
.attr('class', 'source-switch-link') | ||
.on('click', this.toggle); | ||
|
||
// update | ||
$wrap = $wrap.merge($$wrap); | ||
|
||
$wrap.selectAll('.source-switch-link') | ||
.classed('live', this._isLive) | ||
.classed('chip', this._isLive) | ||
.text(this._isLive ? l10n.t('source_switch.live') : l10n.t('source_switch.dev')); | ||
} | ||
|
||
|
||
/** | ||
* toggle | ||
* Toggles between live and dev database | ||
* @param {Event} e - event that triggered the toggle (if any) | ||
*/ | ||
toggle(e) { | ||
if (e) e.preventDefault(); | ||
|
||
const context = this.context; | ||
const editor = context.systems.editor; | ||
const keys = context.apiConnections; | ||
const l10n = context.systems.l10n; | ||
const osm = context.services.osm; | ||
|
||
if (!osm) return; | ||
if (context.inIntro) return; | ||
if (context.mode?.id === 'save') return; | ||
if (!Array.isArray(keys) || keys.length !== 2) return; | ||
|
||
if (editor.hasChanges() && !window.confirm(l10n.t('source_switch.lose_changes'))) return; | ||
|
||
this._isLive = !this._isLive; | ||
|
||
context.enter('browse'); | ||
editor.clearBackup(); // remove saved history | ||
|
||
context.resetAsync() // remove downloaded data | ||
.then(() => osm.switchAsync(this._isLive ? keys[0] : keys[1])) | ||
.then(this.rerender); | ||
} | ||
|
||
} |
Oops, something went wrong.