Changeset 0.25.0 (#160)
This commit is contained in:
154
src/js/user-menu.js
Normal file
154
src/js/user-menu.js
Normal file
@@ -0,0 +1,154 @@
|
||||
// ==========================================
|
||||
// Chat Switchboard — UserMenu Component
|
||||
// ==========================================
|
||||
// v0.25.0: Extracted from chat.html + ui-core.js.
|
||||
// Reusable across all authenticated surfaces.
|
||||
// Pattern: Go template partial (user-menu.html) + JS factory (this file).
|
||||
//
|
||||
// Usage:
|
||||
// const menu = UserMenu.create({ id: 'global' });
|
||||
// menu.setUser({ display_name: 'Jeff', username: 'jeff', avatar: null });
|
||||
// menu.showAdmin(true);
|
||||
// menu.destroy();
|
||||
|
||||
const UserMenu = {
|
||||
primary: null,
|
||||
_instances: new Map(),
|
||||
|
||||
create(opts) {
|
||||
const pfx = opts.id || '';
|
||||
const instance = {
|
||||
id: pfx,
|
||||
btnEl: document.getElementById(pfx + 'userMenuBtn'),
|
||||
avatarEl: document.getElementById(pfx + 'userAvatar'),
|
||||
letterEl: document.getElementById(pfx + 'avatarLetter'),
|
||||
nameEl: document.getElementById(pfx + 'userName'),
|
||||
flyoutEl: document.getElementById(pfx + 'userFlyout'),
|
||||
settingsEl: document.getElementById(pfx + 'menuSettings'),
|
||||
adminEl: document.getElementById(pfx + 'menuAdmin'),
|
||||
teamAdminEl: document.getElementById(pfx + 'menuTeamAdmin'),
|
||||
debugEl: document.getElementById(pfx + 'menuDebug'),
|
||||
signoutEl: document.getElementById(pfx + 'menuSignout'),
|
||||
_open: false,
|
||||
_listeners: [],
|
||||
|
||||
// ── User display ────────────────────
|
||||
|
||||
setUser(user) {
|
||||
if (!user) return;
|
||||
const name = user.display_name || user.username || 'User';
|
||||
const letter = (name[0] || '?').toUpperCase();
|
||||
|
||||
// Avatar image or initial letter
|
||||
if (this.avatarEl) {
|
||||
let img = this.avatarEl.querySelector('.user-avatar-img');
|
||||
if (user.avatar) {
|
||||
if (this.letterEl) this.letterEl.style.display = 'none';
|
||||
if (img) {
|
||||
img.src = user.avatar;
|
||||
} else {
|
||||
img = document.createElement('img');
|
||||
img.src = user.avatar;
|
||||
img.alt = '';
|
||||
img.className = 'user-avatar-img';
|
||||
img.onerror = function() {
|
||||
this.remove();
|
||||
if (instance.letterEl) {
|
||||
instance.letterEl.style.display = '';
|
||||
instance.letterEl.textContent = letter;
|
||||
}
|
||||
};
|
||||
this.avatarEl.insertBefore(img, this.letterEl);
|
||||
}
|
||||
} else {
|
||||
if (img) img.remove();
|
||||
if (this.letterEl) {
|
||||
this.letterEl.style.display = '';
|
||||
this.letterEl.textContent = letter;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (this.nameEl) this.nameEl.textContent = name;
|
||||
},
|
||||
|
||||
// ── Visibility controls ─────────────
|
||||
|
||||
showAdmin(show) {
|
||||
if (this.adminEl) this.adminEl.style.display = show ? '' : 'none';
|
||||
if (this.debugEl) this.debugEl.style.display = show ? '' : 'none';
|
||||
},
|
||||
|
||||
showTeamAdmin(show) {
|
||||
if (this.teamAdminEl) this.teamAdminEl.style.display = show ? '' : 'none';
|
||||
},
|
||||
|
||||
// ── Flyout toggle ───────────────────
|
||||
|
||||
open() {
|
||||
if (this.flyoutEl) this.flyoutEl.classList.add('open');
|
||||
this._open = true;
|
||||
},
|
||||
|
||||
close() {
|
||||
if (this.flyoutEl) this.flyoutEl.classList.remove('open');
|
||||
this._open = false;
|
||||
},
|
||||
|
||||
toggle() {
|
||||
if (this._open) this.close(); else this.open();
|
||||
},
|
||||
|
||||
// ── Event wiring ────────────────────
|
||||
|
||||
/**
|
||||
* Wire event handlers. Accepts callbacks for menu actions.
|
||||
* Call once after create().
|
||||
*/
|
||||
bind(handlers) {
|
||||
handlers = handlers || {};
|
||||
|
||||
// Toggle flyout on avatar click
|
||||
this._on(this.btnEl, 'click', (e) => {
|
||||
e.stopPropagation();
|
||||
this.toggle();
|
||||
});
|
||||
|
||||
// Close on outside click
|
||||
this._on(document, 'click', (e) => {
|
||||
if (this._open && !e.target.closest('#' + pfx + 'userMenuWrap')) {
|
||||
this.close();
|
||||
}
|
||||
});
|
||||
|
||||
// Menu items
|
||||
this._on(this.settingsEl, 'click', () => { this.close(); if (handlers.onSettings) handlers.onSettings(); });
|
||||
this._on(this.adminEl, 'click', () => { this.close(); if (handlers.onAdmin) handlers.onAdmin(); });
|
||||
this._on(this.teamAdminEl, 'click', () => { this.close(); if (handlers.onTeamAdmin) handlers.onTeamAdmin(); });
|
||||
this._on(this.debugEl, 'click', () => { this.close(); if (handlers.onDebug) handlers.onDebug(); });
|
||||
this._on(this.signoutEl, 'click', () => { this.close(); if (handlers.onSignout) handlers.onSignout(); });
|
||||
},
|
||||
|
||||
// ── Lifecycle ───────────────────────
|
||||
|
||||
destroy() {
|
||||
this._listeners.forEach(({ el, event, handler }) => el.removeEventListener(event, handler));
|
||||
this._listeners = [];
|
||||
UserMenu._instances.delete(this.id);
|
||||
if (UserMenu.primary === this) UserMenu.primary = null;
|
||||
},
|
||||
|
||||
// ── Internal ────────────────────────
|
||||
|
||||
_on(el, event, handler) {
|
||||
if (!el) return;
|
||||
el.addEventListener(event, handler);
|
||||
this._listeners.push({ el, event, handler });
|
||||
},
|
||||
};
|
||||
|
||||
UserMenu._instances.set(pfx, instance);
|
||||
return instance;
|
||||
},
|
||||
|
||||
get(id) { return this._instances.get(id) || null; },
|
||||
};
|
||||
Reference in New Issue
Block a user