@jinntec/fore
Version:
Fore - declarative user interfaces in plain HTML
221 lines (190 loc) • 6.72 kB
JavaScript
import ForeElementMixin from '../ForeElementMixin.js';
import { Fore } from '../fore.js';
import { resolveId } from '../xpath-evaluation.js';
export class UIElement extends ForeElementMixin {
constructor() {
super();
this._removeEventListeners = [];
}
connectedCallback() {
super.connectedCallback();
this.ondemand = this.hasAttribute('on-demand');
this.wasOnDemandInitially = this.ondemand;
if (this.ondemand) {
this.addEventListener('show-control', () => {
this.removeAttribute('on-demand');
});
this.addTrashIcon();
}
const ref = this.getAttribute('ref');
// TODO: make this smarter, handling multiple index functions etc
if (ref && ref.includes('index(')) {
const repeatId = ref.match(/index\(['"](?<repeatId>[^'"]*)['"]\)/)?.groups?.repeatId;
if (repeatId) {
/**
* @type {import('./fx-repeat.js').FxRepeat}
*/
const repeat = resolveId(repeatId, this, 'fx-repeat');
const onRepeatItemChanged = () => {
this.getOwnerForm().addToBatchedNotifications(this);
};
repeat.addEventListener('item-changed', onRepeatItemChanged);
this._removeEventListeners.push(() =>
repeat.removeEventListener('item-changed', onRepeatItemChanged),
);
}
}
}
disconnectedCallback() {
if (this.modelItem && typeof this.modelItem.removeObserver === 'function') {
console.log(`[UIElement] Removing observer for ref="${this.ref}"`);
this.modelItem.removeObserver(this);
}
for (const removeEventListener of this._removeEventListeners) {
removeEventListener();
}
}
/*
evalInContext() {
this.dependencies.resetDependencies();
const model = this.getModel();
if (!model) return;
const touchedNodes = new Set();
const domFacade = new DependencyNotifyingDomFacade(node => touchedNodes.add(node));
const context = this.getInScopeContext();
const result = evaluateXPath(this.ref, context, this, domFacade);
this.nodeset = Array.isArray(result) ? result : [result];
touchedNodes.forEach(node => {
const mi = model.getModelItem(node);
if (mi) {
mi.addObserver(this);
console.log(`[UIElement] Dynamically observing ${mi.path} due to XPath dependency`);
}
});
// Manually evaluate predicate parts to ensure detection
const predicateRegex = /\[(.*?)\]/g;
let match;
while ((match = predicateRegex.exec(this.ref)) !== null) {
const predicate = match[1];
try {
const predicateContext = model.getDefaultInstance().getDefaultContext();
const predDomFacade = new DependencyNotifyingDomFacade(n => touchedNodes.add(n));
evaluateXPathToBoolean(predicate, predicateContext, this, predDomFacade);
touchedNodes.forEach(node => {
const mi = model.getModelItem(node);
if (mi) {
mi.addObserver(this);
console.log(`[UIElement] Observing ${mi.path} (from predicate: ${predicate})`);
}
});
} catch (e) {
console.warn('Predicate evaluation failed for dependency tracking:', predicate, e);
}
}
}
*/
attachObserver() {
const modelItem = this.getModelItem();
if (!modelItem || typeof modelItem.addObserver !== 'function') return;
if (!modelItem.observers) {
modelItem.observers = new Set();
}
if (modelItem.observers.has(this)) {
// console.log(`[UIElement] Observer already registered for ref="${this.ref}"`);
return;
}
modelItem.addObserver(this);
// console.log(`[UIElement] attaching observer for ref="${this.ref}"`, this);
// if (typeof this.update === 'function') {
// this.update(modelItem);
// }
}
/**
* Called by ModelItem when it changes
* @param {import('../modelitem.js').ModelItem} modelItem - The ModelItem that changed
*/
/*
update(modelItem) {
if (this.isBound()) {
// console.log('[UIElement] update()', modelItem);
// this.getOwnerForm().addToBatchedNotifications(modelItem);
this.refresh();
}
}
*/
update(_modelItem) {
if (!this.isBound()) return;
const fore = this.getOwnerForm();
if (!fore) return;
// Preserve legacy eager updates unless we're already in a refresh phase.
if (fore.isRefreshPhase) {
fore.addToBatchedNotifications(this);
} else {
this.refresh();
}
}
// init() {
// throw new Error('You have to implement the method init!');
// }
async refresh(force) {
console.log(`🔄 [UIElement] refresh() called for ref="${this.ref}"`);
}
async refreshChildren(force) {
await Fore.refreshChildren(this, force);
}
activate() {
console.log('UIElement.activate() called');
this.removeAttribute('on-demand');
this.style.display = '';
if (this.isBound()) {
this.refresh(true);
}
Fore.dispatch(this, 'show-group', {});
}
attributeChangedCallback(name, _oldValue, newValue) {
if (name === 'on-demand') {
this.ondemand = newValue !== null;
if (!newValue && !this.wasOnDemandInitially) {
this.removeTrashIcon();
} else {
this.wasOnDemandInitially = true;
this.addTrashIcon();
}
}
}
static get observedAttributes() {
return ['on-demand'];
}
addTrashIcon() {
if (!this.closest('[show-icon]')) return;
const trash = this.querySelector('.trash');
if (trash) return;
const icon = document.createElement('span');
icon.innerHTML = `
<svg width="24" height="24" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round"
stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg">
<path d="M17.94 17.94C16.13 19.12 14.13 20 12 20C7 20 2.73 15.88 1 12C1.6 10.66 2.43 9.47 3.46 8.48M10.58 10.58C10.21 11.01 10 11.5 10 12C10 13.11 10.89 14 12 14C12.5 14 12.99 13.79 13.42 13.42M6.53 6.53C7.87 5.54 9.39 5 12 5C17 5 21.27 9.12 23 12C22.4 13.34 21.57 14.53 20.54 15.52M1 1L23 23"/>
</svg>
`;
icon.classList.add('trash');
icon.setAttribute('title', 'Hide');
icon.style.cursor = 'pointer';
icon.style.marginLeft = '0.5em';
icon.addEventListener('click', e => {
e.stopPropagation();
this.setAttribute('on-demand', 'true');
this.style.display = 'none';
document.dispatchEvent(new CustomEvent('update-control-menu'));
Fore.dispatch(this, 'hide-control', {});
});
this.appendChild(icon);
}
removeTrashIcon() {
const icon = this.querySelector('.trash');
if (icon) icon.remove();
}
}
if (!customElements.get('ui-element')) {
customElements.define('ui-element', UIElement);
}