UNPKG

@jinntec/fore

Version:

Fore - declarative user interfaces in plain HTML

208 lines (174 loc) 5.65 kB
import XfAbstractControl from './abstract-control.js'; /** * This class finds and lists all elements with an 'on-demand' attribute and offers them * in a popup list for activation. 'on-demand' is not a state like 'relevant' but just * shows/hides controls on demand. The controls still behave as usual otherwise. * * */ export class FxControlMenu extends XfAbstractControl { connectedCallback() { this.attachShadow({ mode: 'open' }); this.selectExpr = this.getAttribute('select'); const style = ` :host { display: inline-block; position: relative; } .menu { display: none; position: absolute; top: 100%; left: 0; z-index: 10; background: white; border: 1px solid #ccc; padding: 0.5em; box-shadow: 0 2px 6px rgba(0, 0, 0, 0.2); min-width: 10em; white-space:nowrap; } .menu.visible { display: block; } .menu a { display: block; padding: 0.25em 0.5em; text-decoration: none; color: black; cursor: pointer; } .menu a:hover { background-color: #eee; } `; this.shadowRoot.innerHTML = ` <style>${style}</style> <slot></slot> <div class="menu" part="menu"></div> `; this.menuEl = this.shadowRoot.querySelector('.menu'); // Slotted button click const slot = this.shadowRoot.querySelector('slot'); slot.addEventListener('slotchange', () => { const nodes = slot.assignedNodes({ flatten: true }); const button = nodes.find( node => node.nodeType === Node.ELEMENT_NODE && node.tagName === 'BUTTON', ); if (button) { button.addEventListener('click', e => { e.preventDefault(); e.stopPropagation(); this.updateMenu(); this.menuEl.classList.toggle('visible'); }); } }); // Update menu on custom event document.addEventListener('update-control-menu', () => { this.updateMenu(); }); // Close on outside click document.addEventListener('click', e => { const inside = this.contains(e.target) || this.shadowRoot.contains(e.target); if (!inside) { this.menuEl.classList.remove('visible'); } }); // Close on Escape document.addEventListener('keydown', e => { if (e.key === 'Escape') { this.menuEl.classList.remove('visible'); } }); if (this.getAttribute('mode') === 'hide-on-empty') { this.getOwnerForm().addEventListener('ready', () => { const container = document.querySelector(this.selectExpr); if (!container) return; const widgets = container.querySelectorAll('.widget'); widgets.forEach(widget => { const value = widget.value?.trim(); const control = widget.closest('fx-control'); if (control && (value === '' || value == null)) { control.setAttribute('on-demand', 'true'); } }); // After marking empty controls, update the menu this.updateMenu(); }); } const container = document.querySelector(this.selectExpr); container?.addEventListener('show-control', event => { this.updateMenu(); }); this.updateMenu(); } _getScopedContainer() { const repeatItem = this.closest('fx-repeatitem'); if (repeatItem) return repeatItem; if (this.selectExpr) { return document.querySelector(this.selectExpr); } return null; } updateMenu() { const container = this._getScopedContainer(); if (!container) return; let targets = []; // ✅ Include container itself if it has on-demand if (container.hasAttribute('on-demand')) { targets.push(container); } // ✅ Also include any descendant [on-demand] controls if not within repeat if (container.nodeName !== 'FX-REPEAT') { const innerTargets = Array.from(container.querySelectorAll('[on-demand]')); targets.push(...innerTargets); } this._currentTargets = targets; this.menuEl.innerHTML = ''; // Find the button to disable if needed const slot = this.shadowRoot.querySelector('slot'); const assignedNodes = slot.assignedNodes({ flatten: true }); const button = assignedNodes.find( node => node.nodeType === Node.ELEMENT_NODE && node.tagName === 'BUTTON', ); if (button) { button.disabled = targets.length === 0; } if (targets.length === 0) { this.menuEl.classList.remove('visible'); return; } targets.forEach((el, index) => { let label = el.getAttribute('aria-label'); if (!label) { label = el.querySelector('label')?.textContent.trim() || `Item ${index + 1}`; } if (!label) { console.warn( 'no label found - cannot create menu entry for ', el, ' - please add aria-label or label element to control', ); } const item = document.createElement('a'); item.href = '#'; item.textContent = label; item.addEventListener('click', e => { e.preventDefault(); if (typeof el.activate === 'function') { el.activate(); } this.menuEl.classList.remove('visible'); // Wait one frame to let DOM updates (like on-demand removal) take effect requestAnimationFrame(() => { this.updateMenu(); }); }); this.menuEl.appendChild(item); }); } } if (!customElements.get('fx-control-menu')) { customElements.define('fx-control-menu', FxControlMenu); }