From c348d35c7ecb6c74f71cf90b982412f267c5d807 Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Tue, 10 Feb 2026 02:19:05 +0000
Subject: [PATCH] =minor fixes to schema system

---
 assets/js/concise/Popup.js |  212 ++++++++++++++++++++++++++++++++++++----------------
 1 files changed, 146 insertions(+), 66 deletions(-)

diff --git a/assets/js/concise/Popup.js b/assets/js/concise/Popup.js
index 7ef07c9..b22d317 100644
--- a/assets/js/concise/Popup.js
+++ b/assets/js/concise/Popup.js
@@ -1,79 +1,159 @@
 class Popup {
-	constructor(config = {}) {
-		this.config = {
-			toggle: document.querySelector('[data-toggles]'),
-			popup: document.querySelector('.jvb-pop'),
-			name: 'Popup',
-			onOpen: () =>{},
-			onClose: () =>{},
-			onEscape: () =>{},
-			... config
-		};
+	constructor() {
+		if (Popup.instance) {
+			return Popup.instance;
+		}
+		Popup.instance = this;
 
 		this.a11y = window.jvbA11y;
-		this.toggleID = this.config.toggle.id === '' ? '.'+this.config.toggle.className.split(' ').join('.') : this.config.toggle.id;
+		this.popups = new Map();
+		this.activePopup = null;
 
-		this.initListeners();
-	}
-
-	initListeners()
-	{
 		this.clickHandler = this.handleClick.bind(this);
 		this.keyHandler = this.handleEscape.bind(this);
-
-		document.addEventListener('click', this.handleToggle.bind(this));
-	}
-
-	handleToggle(e) {
-		if (e.target === this.config.toggle || e.target.closest(this.toggleID)){
-			this.togglePopup();
-		}
-	}
-	handleClick(e) {
-		if (!this.config.popup.contains(e.target)
-			&& e.target !== this.config.toggle) {
-			this.closePopup();
-		} else if (window.targetCheck(e, '.close')) {
-			this.closePopup();
-		}
-	}
-
-	togglePopup() {
-		if (!this.config.popup.classList.contains('expanded')) {
-			this.openPopup();
-		} else {
-			this.closePopup();
-		}
-	}
-	openPopup(message = `Opened ${this.config.name}`) {
-		this.config.popup.classList.add('expanded');
-		this.config.toggle.classList.add('expanded');
-		this.config.toggle.title = `Hide ${this.config.name}`;
-		this.config.toggle.ariaExpanded = true;
-		this.config.toggle.querySelector('span').textContent = `Close ${this.config.name}`;
-		this.a11y.announce(message);
-		this.config.onOpen();
-		document.addEventListener('keydown', this.keyHandler);
 		document.addEventListener('click', this.clickHandler);
 	}
-	closePopup(message = `Closed ${this.config.name}`) {
-		this.config.popup.classList.remove('expanded');
-		this.config.toggle.classList.remove('expanded');
-		this.config.toggle.title = `Show ${this.config.name}`;
-		this.config.toggle.ariaExpanded = false;
 
-		this.config.toggle.querySelector('span').textContent = '';
-		this.a11y.announce(message);
-		this.config.onClose();
-		document.removeEventListener('keydown', this.keyHandler);
-		document.removeEventListener('click', this.clickHandler);
-	}
-	handleEscape(e) {
-		if (e.key === 'Escape') {
-			this.closePopup(`Closed ${this.config.name} with escape key`);
-			this.config.onEscape();
+	/**
+	 * Register a new popup
+	 * @param {Object} config - { toggle, popup, name, onOpen, onClose, onEscape }
+	 * @returns {Object} API for this popup
+	 */
+	registerPopup(config = {}) {
+		const popup = config.popup;
+		const toggle = config.toggle;
+
+		if (!popup || !toggle) {
+			console.warn('Popup registration requires both popup and toggle elements');
+			return null;
 		}
+
+		const id = toggle.id || '.' + toggle.className.split(' ').join('.');
+
+		const entry = {
+			id,
+			toggle,
+			popup,
+			name: config.name || 'Popup',
+			onOpen: config.onOpen || (() => {}),
+			onClose: config.onClose || (() => {}),
+			onEscape: config.onEscape || (() => {}),
+			isOpen: false
+		};
+
+		this.popups.set(id, entry);
+
+		return this.getPopupAPI(id);
+	}
+
+	getPopupAPI(id) {
+		return {
+			openPopup: (message) => this.openPopup(id, message),
+			closePopup: (message) => this.closePopup(id, message),
+			togglePopup: () => this.togglePopup(id),
+			isOpen: () => this.popups.get(id)?.isOpen || false,
+			destroy: () => this.popups.delete(id)
+		};
+	}
+
+	/**
+	 * Single document-level click handler for all popups
+	 */
+	handleClick(e) {
+		// Check if click is on any registered toggle
+		for (const [id, entry] of this.popups) {
+			if (e.target === entry.toggle || e.target.closest(id)) {
+				this.togglePopup(id);
+				return;
+			}
+		}
+
+		// If a popup is active, close it on outside click
+		if (this.activePopup) {
+			const entry = this.popups.get(this.activePopup);
+			if (entry && !entry.popup.contains(e.target)) {
+				this.closePopup(this.activePopup);
+			} else if (entry && window.targetCheck(e, '.close')) {
+				this.closePopup(this.activePopup);
+			}
+		}
+	}
+
+	handleEscape(e) {
+		if (e.key === 'Escape' && this.activePopup) {
+			const entry = this.popups.get(this.activePopup);
+			this.closePopup(this.activePopup, `Closed ${entry.name} with escape key`);
+			entry.onEscape();
+		}
+	}
+
+	togglePopup(id) {
+		const entry = this.popups.get(id);
+		if (!entry) return;
+
+		if (entry.isOpen) {
+			this.closePopup(id);
+		} else {
+			this.openPopup(id);
+		}
+	}
+
+	openPopup(id, message) {
+		const entry = this.popups.get(id);
+		if (!entry) return;
+
+		// Close any currently active popup first
+		if (this.activePopup && this.activePopup !== id) {
+			this.closePopup(this.activePopup);
+		}
+
+		entry.isOpen = true;
+		this.activePopup = id;
+
+		entry.popup.classList.add('expanded');
+		entry.toggle.classList.add('expanded');
+		entry.toggle.title = `Hide ${entry.name}`;
+		entry.toggle.ariaExpanded = true;
+		entry.toggle.querySelector('span').textContent = `Close ${entry.name}`;
+
+		this.a11y.announce(message || `Opened ${entry.name}`);
+		entry.onOpen();
+
+		document.addEventListener('keydown', this.keyHandler);
+	}
+
+	closePopup(id, message) {
+		const entry = this.popups.get(id);
+		if (!entry) return;
+
+		entry.isOpen = false;
+		if (this.activePopup === id) {
+			this.activePopup = null;
+		}
+
+		entry.popup.classList.remove('expanded');
+		entry.toggle.classList.remove('expanded');
+		entry.toggle.title = `Show ${entry.name}`;
+		entry.toggle.ariaExpanded = false;
+		entry.toggle.querySelector('span').textContent = '';
+
+		this.a11y.announce(message || `Closed ${entry.name}`);
+		entry.onClose();
+
+		// Only remove keydown if no popups are active
+		if (!this.activePopup) {
+			document.removeEventListener('keydown', this.keyHandler);
+		}
+	}
+
+	destroy() {
+		document.removeEventListener('click', this.clickHandler);
+		document.removeEventListener('keydown', this.keyHandler);
+		this.popups.clear();
+		this.activePopup = null;
 	}
 }
 
-window.jvbPopup = Popup;
+document.addEventListener('DOMContentLoaded', function () {
+	window.jvbPopup = new Popup();
+});

--
Gitblit v1.10.0