Skip to content

Commit

Permalink
Update uiAccount and uiSourceSwitch to class components
Browse files Browse the repository at this point in the history
  • Loading branch information
bhousel committed Nov 11, 2024
1 parent e395b1e commit a06a64f
Show file tree
Hide file tree
Showing 6 changed files with 313 additions and 200 deletions.
198 changes: 198 additions & 0 deletions modules/ui/UiAccount.js
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);
}

}
26 changes: 7 additions & 19 deletions modules/ui/UiMapFooter.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import { selection } from 'd3-selection';

import { utilDetect } from '../util/detect.js';
import {
uiAccount, UiContributors, uiIcon, uiIssuesInfo,
UiScale, uiSourceSwitch, uiTooltip, UiVersionInfo
UiAccount, UiContributors, uiIcon, uiIssuesInfo,
UiScale, UiSourceSwitch, uiTooltip, UiVersionInfo
} from './index.js';


Expand All @@ -25,21 +25,15 @@ export class UiMapFooter {
// this.FilterInfo = uiFeatureInfo(context);
this.IssueInfo = uiIssuesInfo(context);
this.Scale = new UiScale(context);
this.SourceSwitch = new UiSourceSwitch(context);
this.VersionInfo = new UiVersionInfo(context);

if (!context.embed()) {
this.AccountInfo = uiAccount(context);
this.AccountInfo = new UiAccount(context);
} else {
this.AccountInfo = null;
}

const apiConnections = context.apiConnections;
if (Array.isArray(apiConnections) && apiConnections.length > 1) {
this.SourceSwitch = uiSourceSwitch(context).keys(apiConnections);
} else {
this.SourceSwitch = null;
}

// D3 selections
this.$parent = null;

Expand Down Expand Up @@ -91,14 +85,8 @@ export class UiMapFooter {
.attr('class', 'map-footer-info');

$$footerInfo
.call(this.Contributors.render);

if (this.SourceSwitch) {
$$footerInfo
.append('div')
.attr('class', 'source-switch')
.call(this.SourceSwitch);
}
.call(this.Contributors.render)
.call(this.SourceSwitch.render);

$$footerInfo
.append('div')
Expand Down Expand Up @@ -133,7 +121,7 @@ export class UiMapFooter {

if (this.AccountInfo) {
$$footerInfo
.call(this.AccountInfo);
.call(this.AccountInfo.render);
}
}

Expand Down
106 changes: 106 additions & 0 deletions modules/ui/UiSourceSwitch.js
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);
}

}
Loading

0 comments on commit a06a64f

Please sign in to comment.