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