@jinntec/fore
Version:
Fore - declarative user interfaces in plain HTML
450 lines (387 loc) • 13.5 kB
JavaScript
import './fx-repeatitem.js';
import { Fore } from '../fore.js';
import ForeElementMixin from '../ForeElementMixin.js';
import { evaluateXPath } from '../xpath-evaluation.js';
import getInScopeContext from '../getInScopeContext.js';
import { XPathUtil } from '../xpath-util.js';
import { withDraggability } from '../withDraggability.js';
// import {DependencyNotifyingDomFacade} from '../DependencyNotifyingDomFacade';
/**
* `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.
* @extends {ForeElementMixin}
*/
export class FxRepeat 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,
},
nodeset: {
type: Array,
},
};
}
constructor() {
super();
this.ref = '';
this.dataTemplate = [];
this.isDraggable = null;
this.dropTarget = null;
this.focusOnCreate = '';
this.initDone = false;
this.repeatIndex = 1;
this.nodeset = [];
this.inited = false;
this.index = 1;
this.repeatSize = 0;
this.attachShadow({ mode: 'open', delegatesFocus: true });
}
get repeatSize() {
return this.querySelectorAll(':scope > fx-repeatitem').length;
}
set repeatSize(size) {
this.size = size;
}
setIndex(index) {
// console.log('new repeat index ', index);
this.index = index;
const rItems = this.querySelectorAll(':scope > fx-repeatitem');
this.applyIndex(rItems[this.index - 1]);
this.getOwnerForm().refresh({ reason: 'index-function' });
}
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);
}
_getRef() {
return this.getAttribute('ref');
}
connectedCallback() {
super.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 => {
const { item } = e.detail;
const idx = Array.from(this.children).indexOf(item);
// Warning: index is one-based
this.setIndex(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 { item } = e.detail;
// const idx = Array.from(this.children).indexOf(item);
const { index } = e.detail;
this.index = parseInt(index, 10);
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 => {
// console.log('mutations', 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);
// console.log('path mutated', path);
// 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 name="header"></slot>
<slot></slot>
`;
this.shadowRoot.innerHTML = `
<style>
${style}
</style>
${html}
`;
// this.init();
}
_createNewRepeatItem() {
const newItem = document.createElement('fx-repeatitem');
if (this.isDraggable) {
newItem.setAttribute('draggable', 'true');
newItem.setAttribute('tabindex', 0);
}
const clone = this._clone();
newItem.appendChild(clone);
return newItem;
}
init() {
// ### there must be a single 'template' child
console.log('##### repeat 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;
}
/**
* 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,
});
}
/*
this.touchedPaths = new Set();
const instance = XPathUtil.resolveInstance(this, this.ref);
const depTrackDomfacade = new DependencyNotifyingDomFacade((node) => {
this.touchedPaths.add(XPathUtil.getPath(node, instance));
});
const rawNodeset = evaluateXPath(this.ref, inscope, this, {}, {}, depTrackDomfacade );
*/
const rawNodeset = evaluateXPath(this.ref, inscope, this);
// console.log('Touched!', this.ref, [...this.touchedPaths].join(', '));
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) {
// console.group('fx-repeat.refresh on', this.id);
if (!this.inited) this.init();
// console.time('repeat-refresh', this);
this._evalNodeset();
// ### register ourselves as boundControl
/*
const modelItem = this.getModelItem();
if (!modelItem.boundControls.includes(this)) {
modelItem.boundControls.push(this);
}
*/
// console.log('repeat refresh nodeset ', this.nodeset);
// console.log('repeatCount', this.repeatCount);
const repeatItems = this.querySelectorAll(':scope > fx-repeatitem');
const 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)
}
}
if (contextSize > repeatItemCount) {
for (let position = repeatItemCount + 1; position <= contextSize; position += 1) {
// add new repeatitem
const newItem = this._createNewRepeatItem();
this.appendChild(newItem);
this._initVariables(newItem);
newItem.nodeset = this.nodeset[position - 1];
newItem.index = position;
if (this.getOwnerForm().createNodes) {
this.getOwnerForm().initData(newItem);
}
// Tell the owner form we might have new template expressions here
this.getOwnerForm().scanForNewTemplateExpressionsNextRefresh();
}
}
// ### update nodeset of repeatitems
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);
// console.timeEnd('repeat-refresh');
// this.replaceWith(clone);
// this.repeatCount = contextSize;
// console.log('repeatCount', this.repeatCount);
}
// 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);
})();
}
_initTemplate() {
this.template = this.querySelector('template');
// console.log('### init template for repeat ', this.id, this.template);
// todo: this.dropTarget not needed?
this.dropTarget = this.template.getAttribute('drop-target');
this.isDraggable = this.template.hasAttribute('draggable')
? this.template.getAttribute('draggable')
: null;
if (this.template === null) {
// 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}` },
}),
);
}
this.shadowRoot.appendChild(this.template);
}
_initRepeatItems() {
this.nodeset.forEach((item, index) => {
const repeatItem = this._createNewRepeatItem();
repeatItem.nodeset = this.nodeset[index];
repeatItem.index = index + 1; // 1-based index
this.appendChild(repeatItem);
if (this.getOwnerForm().createNodes) {
this.getOwnerForm().initData(repeatItem);
}
if (repeatItem.index === 1) {
this.applyIndex(repeatItem);
}
// console.log('*********repeat item created', repeatItem.nodeset)
Fore.dispatch(this, 'item-created', { nodeset: repeatItem.nodeset, pos: index + 1 });
this._initVariables(repeatItem);
});
}
_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() {
// const content = this.template.content.cloneNode(true);
this.template = this.shadowRoot.querySelector('template');
const content = this.template.content.cloneNode(true);
return document.importNode(content, true);
}
_removeIndexMarker() {
Array.from(this.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')) {
window.customElements.define('fx-repeat', FxRepeat);
}