UNPKG

@jinntec/fore

Version:

Fore - declarative user interfaces in plain HTML

411 lines (359 loc) 12.2 kB
import { Fore } from '../fore.js'; import { evaluateXPath } from '../xpath-evaluation.js'; import getInScopeContext from '../getInScopeContext.js'; import { XPathUtil } from '../xpath-util.js'; import ForeElementMixin from '../ForeElementMixin.js'; import { withDraggability } from '../withDraggability.js'; /** * `fx-repeat` * * Repeats its template for each node in its' bound nodeset. * * Template is a standard HTML `<template>` element. Once instanciated the template * is moved to the shadowDOM of the repeat for safe re-use. * * * * @customElement * @demo demo/todo.html * * todo: it should be seriously be considered to extend FxContainer instead but needs refactoring first. */ export class FxRepeatAttributes extends withDraggability(ForeElementMixin, false) { static get properties() { return { ...super.properties, index: { type: Number, }, template: { type: Object, }, focusOnCreate: { type: String, }, initDone: { type: Boolean, }, repeatIndex: { type: Number, }, repeatSize: { type: Number, }, nodeset: { type: Array, }, }; } constructor() { super(); this.ref = ''; this.dataTemplate = []; this.isDraggable = null; this.focusOnCreate = ''; this.initDone = false; this.repeatIndex = 1; this.nodeset = []; this.inited = false; this.host = {}; this.index = 1; this.repeatSize = 0; this.attachShadow({ mode: 'open', delegatesFocus: true }); } get repeatSize() { return this.querySelectorAll(':scope > .fx-repeatitem').length; } set repeatSize(size) { super.repeatSize = size; } setIndex(index) { // console.log('new repeat index ', index); this.index = index; const refd = this.querySelector('[data-ref]'); const rItems = refd.querySelectorAll(':scope > *'); this.applyIndex(rItems[this.index - 1]); } applyIndex(repeatItem) { this._removeIndexMarker(); if (repeatItem) { repeatItem.setAttribute('repeat-index', ''); } } get index() { return parseInt(this.getAttribute('index'), 10); } set index(idx) { this.setAttribute('index', idx); } _getRepeatedItems() { const refd = this.querySelector('[data-ref]'); return refd.children; } async connectedCallback() { // console.log('connectedCallback',this); // this.display = window.getComputedStyle(this, null).getPropertyValue("display"); this.ref = this.getAttribute('ref'); // this.ref = this._getRef(); // console.log('### fx-repeat connected ', this.id); this.addEventListener('item-changed', e => { // console.log('handle index event ', e); const { item } = e.detail; const repeatedItems = this._getRepeatedItems(); const idx = Array.from(repeatedItems).indexOf(item); this.applyIndex(repeatedItems[idx]); this.index = idx + 1; }); // todo: review - this is just used by append action - event consolidation ? document.addEventListener('index-changed', e => { e.stopPropagation(); if (!e.target === this) return; const { index } = e.detail; this.index = Number(index); this.applyIndex(this.children[index - 1]); }); /* document.addEventListener('insert', e => { const nodes = e.detail.insertedNodes; this.index = e.detail.position; console.log('insert catched', nodes, this.index); }); */ // if (this.getOwnerForm().lazyRefresh) { this.mutationObserver = new MutationObserver(mutations => { if (mutations[0].type === 'childList') { const added = mutations[0].addedNodes[0]; if (added) { const instance = XPathUtil.resolveInstance(this, this.ref); const path = XPathUtil.getPath(added, instance); // this.dispatch('path-mutated',{'path':path,'nodeset':this.nodeset,'index': this.index}); // this.index = index; // const prev = mutations[0].previousSibling.previousElementSibling; // const index = prev.index(); // this.applyIndex(this.index -1); Fore.dispatch(this, 'path-mutated', { path, index: this.index }); } } }); // } this.getOwnerForm().registerLazyElement(this); const style = ` :host{ } .fade-out-bottom { -webkit-animation: fade-out-bottom 0.7s cubic-bezier(0.250, 0.460, 0.450, 0.940) both; animation: fade-out-bottom 0.7s cubic-bezier(0.250, 0.460, 0.450, 0.940) both; } .fade-out-bottom { -webkit-animation: fade-out-bottom 0.7s cubic-bezier(0.250, 0.460, 0.450, 0.940) both; animation: fade-out-bottom 0.7s cubic-bezier(0.250, 0.460, 0.450, 0.940) both; } `; const html = ` <slot></slot> `; this.shadowRoot.innerHTML = ` <style> ${style} </style> ${html} `; // this.init(); } async init() { // ### there must be a single 'template' child const inited = new Promise(resolve => { // console.log('##### repeat-attributes init ', this.id); // if(!this.inited) this.init(); // does not use this.evalInContext as it is expecting a nodeset instead of single node this._evalNodeset(); // console.log('##### ',this.id, this.nodeset); this._initTemplate(); // this._initRepeatItems(); this.setAttribute('index', this.index); this.inited = true; resolve('done'); }); return inited; } _getRef() { return this.getAttribute('ref'); } /** * repeat has no own modelItems * @private */ _evalNodeset() { // const inscope = this.getInScopeContext(); const inscope = getInScopeContext(this.getAttributeNode('ref') || this, this.ref); // console.log('##### inscope ', inscope); // console.log('##### ref ', this.ref); // now we got a nodeset and attach MutationObserver to it if (this.mutationObserver && inscope.nodeName) { this.mutationObserver.observe(inscope, { childList: true, subtree: true, }); } const rawNodeset = evaluateXPath(this.ref, inscope, this); if (rawNodeset.length === 1 && Array.isArray(rawNodeset[0])) { // This XPath likely returned an XPath array. Just collapse to that array this.nodeset = rawNodeset[0]; return; } this.nodeset = rawNodeset; } async refresh(force) { if (!this.inited) this.init(); this._evalNodeset(); let repeatItems = this.querySelectorAll('.fx-repeatitem'); let repeatItemCount = repeatItems.length; let nodeCount = 1; if (Array.isArray(this.nodeset)) { nodeCount = this.nodeset.length; } // const contextSize = this.nodeset.length; const contextSize = nodeCount; // todo: review - cant the context really never be smaller than the repeat count? // todo: this code can be deprecated probably but check first if (contextSize < repeatItemCount) { for (let position = repeatItemCount; position > contextSize; position -= 1) { // remove repeatitem const itemToRemove = repeatItems[position - 1]; itemToRemove.parentNode.removeChild(itemToRemove); this.getOwnerForm().unRegisterLazyElement(itemToRemove); // this._fadeOut(itemToRemove); // Fore.fadeOutElement(itemToRemove) this.getOwnerForm().someInstanceDataStructureChanged = true; } } if (contextSize > repeatItemCount) { for (let position = repeatItemCount + 1; position <= contextSize; position += 1) { // add new repeatitem const clonedTemplate = this._clone(); if (!clonedTemplate) return; // ### cloned templates are always appended to the binding element - the one having the data-ref const bindingElement = this.querySelector('[data-ref]'); bindingElement.appendChild(clonedTemplate); clonedTemplate.classList.add('fx-repeatitem'); clonedTemplate.setAttribute('index', position); clonedTemplate.addEventListener('click', this._dispatchIndexChange); // this.addEventListener('focusin', this._handleFocus); clonedTemplate.addEventListener('focusin', this._dispatchIndexChange); // this._initVariables(clonedTemplate); // newItem.nodeset = this.nodeset[position - 1]; // newItem.index = position; this.getOwnerForm().someInstanceDataStructureChanged = true; } } // ### update nodeset of repeatitems repeatItems = this.querySelectorAll(':scope > .fx-repeatitem'); repeatItemCount = repeatItems.length; for (let position = 0; position < repeatItemCount; position += 1) { const item = repeatItems[position]; this.getOwnerForm().registerLazyElement(item); if (item.nodeset !== this.nodeset[position]) { item.nodeset = this.nodeset[position]; } } // Fore.refreshChildren(clone,true); const fore = this.getOwnerForm(); if (!fore.lazyRefresh || force) { Fore.refreshChildren(this, force); } // this.style.display = 'block'; // this.style.display = this.display; this.setIndex(this.index); } _dispatchIndexChange() { this.dispatchEvent( new CustomEvent('item-changed', { composed: false, bubbles: true, detail: { item: this, index: this.index }, }), ); } // eslint-disable-next-line class-methods-use-this _fadeOut(el) { el.style.opacity = 1; (function fade() { // eslint-disable-next-line no-cond-assign if ((el.style.opacity -= 0.1) < 0) { el.style.display = 'none'; } else { requestAnimationFrame(fade); } })(); } // eslint-disable-next-line class-methods-use-this _fadeIn(el) { if (!el) return; el.style.opacity = 0; el.style.display = this.display; (function fade() { // setTimeout(() => { let val = parseFloat(el.style.opacity); // eslint-disable-next-line no-cond-assign if (!((val += 0.1) > 1)) { el.style.opacity = val; requestAnimationFrame(fade); } // }, 40); })(); } async _initTemplate() { // const defaultSlot = this.shadowRoot.querySelector('slot'); // todo: this is still weak - should handle that better maybe by an explicit slot? // this.template = this.firstElementChild; this.template = this.querySelector('template'); /* if (this.template === null) { // console.error('### no template found for this repeat:', this.id); // todo: catch this on form element this.dispatchEvent( new CustomEvent('no-template-error', { composed: true, bubbles: true, detail: { message: `no template found for repeat:${this.id}` }, }), ); } */ if (!this.template) { return; } this.shadowRoot.appendChild(this.template); } _initVariables(newRepeatItem) { const inScopeVariables = new Map(this.inScopeVariables); newRepeatItem.setInScopeVariables(inScopeVariables); (function registerVariables(node) { for (const child of node.children) { if ('setInScopeVariables' in child) { child.setInScopeVariables(inScopeVariables); } registerVariables(child); } })(newRepeatItem); } _clone() { this.template = this.shadowRoot.querySelector('template'); if (!this.template) return; return this.template.content.firstElementChild.cloneNode(true); } _removeIndexMarker() { const refd = this.querySelector('[data-ref]'); Array.from(refd.children).forEach(item => { item.removeAttribute('repeat-index'); }); } setInScopeVariables(inScopeVariables) { // Repeats are interesting: the variables should be scoped per repeat item, they should not be // able to see the variables in adjacent repeat items! this.inScopeVariables = new Map(inScopeVariables); } } if (!customElements.get('fx-repeat-attributes')) { window.customElements.define('fx-repeat-attributes', FxRepeatAttributes); }