UNPKG

@furo/fbp

Version:

Declarative programming with web-components.

476 lines (431 loc) 13.2 kB
import { FBP } from './fbp.js'; import './empty-fbp-node.js'; /** * `flow-repeat` * * Custom element to repeat Arrays. The repeated items are injected *before* the `flow-repeat` element. If you need the repeated items inside of an other dom node, use [`setInsertRef`](./flow-repeat/#setinsertref) * * * ```html * <flow-repeat ƒ-inject-items="--dataArray"> * <template> * <repeated-item index="${this.index}" ƒ-inject="--init"> * </template> * </flow-repeat> * ``` * > **Note**: if you want to bind a repeater node, use `furo-data-flow-repeat`. * * * ## Available wires in the template: * * > **Note**: Each repeated item has its own closed scope. You can not use the wires outside of the `template`. * > Use events to interact with components outside of the template. * * - `--init` : contains the repeated item, fired only once on creation of the repeated node * - `--item` : contains the repeated item, fired on every inject * - `--firstItem` : contains the repeated item, fired on the first element. * - `--lastItem` : contains the repeated item, fired on the last element. * - `--index` : contains a number with the index of the element. * - `--host` : contains a reference to the host component. * - `--trigger` : contains what was passed in to the triggering method. * - `--triggerFirst` : contains what was passed in to the triggering method. * - `--triggerLast` : contains what was passed in to the triggering method. * - `--itemSelected` : contains `true`, is triggered with select(index). * - `--itemDeSelected` : contains `false`, is triggered when another item is selected with select(index). * * ## Available attributes * **index** contains the current index of the item. Use this to fire a event with an index like `@-click="^^item-clicked(index)"` * **item** contains the current index of the item. Use this to fire a event with the repeated item like `@-click="^^item-selected(item)"` * * * @summary Custom element to allow using FBPs template features in repeated template * * @fires {index of the element} last-element-selected - Fired when the last element is selected. Use this to trigger a load next. * @fires {Number} items-in-dom - Fired when items are attached to the dom, with Number of items. * * @customElement * @mixes FBP */ export class FlowRepeat extends FBP(HTMLElement) { constructor() { super(); /** * @private */ this.template = undefined; /** * @private */ this._insertedItems = []; } /** * Clear the list */ clear() { this.injectItems([]); } /** * Triggers the wire `--itemSelected` on selected item and `--itemDeSelected` on last selected Item. * * @param index {int} - Index of item to select */ select(index) { if (this._insertedItems[index]) { // deselect the last selected if (this.selectedIndex !== undefined) { this.deselect(this.selectedIndex); } this._insertedItems[index].virtualElement._FBPTriggerWire( '--itemSelected', true ); this.selectedIndex = index; } } /** * Select item by its identity. * * Using this method only works when you have set the `identity-path`. * * @param identifier {*} Identity from `identity-path` */ selectIdentity(identifier) { if (this._insertedItems.length === 0) { this._selIdentityQueue = identifier; } else { this._selIdentityQueue = undefined; // eslint-disable-next-line no-restricted-syntax const arrayLength = this._insertedItems.length; let i = 0; do { if (this._insertedItems[i].identity === identifier) { this.select(i); break; } i += 1; } while (i < arrayLength); } } /** * Selects next index. If none was selected, the first index will be selected. * * If you reached the last index, the first index will be selected. * * If you reach the last element, `last-element-selected` will fire. * * Triggers the wire `--itemSelected` on selected item and `--itemDeSelected` on last selected Item */ selectNextIndex() { let i = this.selectedIndex + 1; // loop around if (!this._insertedItems[i]) { i = 0; } this.select(i); if (this._insertedItems.length - 1 === this.selectedIndex) { const customEvent = new Event('last-element-selected', { composed: true, bubbles: true, }); customEvent.detail = i; this.dispatchEvent(customEvent); } } /** * Selects the previous index. * * If you are on the first item, the last will be selected. * * Triggers the wire `--itemSelected` on selected item and `--itemDeSelected` on last selected Item */ selectPreviousIndex() { let i = this.selectedIndex - 1; // loop around if (i < 0) { i = this._insertedItems.length - 1; } this.select(i); } /** * Triggers the currently selected item. * * Triggers the wire `--trigger` on the every item. * * Triggers the wire `--triggerIndex` on the every item. * * @param data {*} - Data to forward. */ triggerSelected(data) { this.triggerIndex(this.selectedIndex, data); } /** * Triggers all nodes. * * Triggers the wire `--trigger` on the every item. * * Triggers the wire `--triggerIndex` on the every item. * * @param data {*} - data to forward */ triggerAll(data) { for (let i = 0; i < this._insertedItems.length; i += 1) { this.triggerIndex(i, data); } } /** * Triggers the wire `--itemDeSelected` on the last selected item */ deselect() { if ( this.selectedIndex !== undefined && this._insertedItems[this.selectedIndex] ) { this._insertedItems[this.selectedIndex].virtualElement._FBPTriggerWire( '--itemDeSelected', false ); this.selectedIndex = undefined; } } /** * Set a reference to append the repeated elements in to the ref instead of appending them before the repeater itself. * * @param ref {DomNode} - Node to append the repeated items. */ setInsertRef(ref) { this._insertMode = 'appendchild'; this._insertTarget = ref; } /** * * @param attachedElem * @param reference * @private */ _insertToDom(attachedElem, reference) { if (this._insertMode === 'appendchild') { this._insertTarget.appendChild(attachedElem); } else { this.parentNode.insertBefore(attachedElem, reference); } } /** * Triggers the wire `--itemDeSelected` on all items */ deselectAll() { this._insertedItems.forEach(item => { item.virtualElement._FBPTriggerWire('--itemDeSelected', false); this.selectedIndex = undefined; }); } /** * * @param parent * @return {null|*} * @private */ _findFirstHost(parent) { if (parent && parent.host) { return parent.host; } if (parent === null) { return null; } return this._findFirstHost(parent.parentNode); } /** * Inject items to repeat. * * @param items {Array} - Items to repeat */ injectItems(items) { if (!Array.isArray(items)) { // eslint-disable-next-line no-console console.info('Items is not an array ', items, this); // make the list empty // eslint-disable-next-line no-param-reassign items = []; } this._firstHost = this._findFirstHost(this.parentNode); items.forEach((e, i, a) => { let identity = false; if (this.identityPath) { identity = this.identityPath .split('.') .reduce((acc, part) => acc && acc[part], e); } let elem; // wenn das element noch nicht existiert if (!this._insertedItems[i]) { elem = this._buildDomNode(identity, i); elem._FBPTriggerWire('--init', e); } else if ( this._insertedItems[i].identity === false || this._insertedItems[i].identity !== identity ) { elem = this._buildDomNode(identity, i); // Schiebe alle elemente des Knotens vor das erste kind des nächsten möglichen knoten let reference = this; if ( this._insertedItems[i + 1] && this._insertedItems[i + 1].children[0] ) { [reference] = this._insertedItems[i + 1].children; } // move the nodes this._insertedItems[i].children.forEach(attachedElem => { this._insertToDom(attachedElem, reference); }); elem._FBPTriggerWire('--init', e); } else { elem = this._insertedItems[i].virtualElement; } // set item and index value on created element elem.item = e; elem.index = i; // trigger wires elem._FBPTriggerWire(this._internalWire, { item: e, index: i }); if (i === 0) { elem._FBPTriggerWire('--firstItem', e); } if (i === a.length - 1) { elem._FBPTriggerWire('--lastItem', e); } elem._FBPTriggerWire('--item', e); elem._FBPTriggerWire('--host', this._firstHost); elem._FBPTriggerWire('--index', i); }); // überzählige elemente aus dem dom entfernen this._insertedItems .slice(items.length, this._insertedItems.length) .forEach(handle => { handle.children.forEach(attachedElem => { attachedElem.remove(); }); }); // überzählige elemente aus dem array entfernen this._insertedItems.splice(items.length); if (items.length > 0) { setTimeout(() => { const customEvent = new Event('items-in-dom', { composed: true, bubbles: false, }); customEvent.detail = items.length; this.dispatchEvent(customEvent); }, 0); } // selectByIdentity queue if (this._selIdentityQueue) { this.selectIdentity(this._selIdentityQueue); } } /** * * @param identity * @param i * @return {HTMLElement} * @private */ _buildDomNode(identity, i) { // build hidden elem const elem = document.createElement('empty-fbp-node'); elem.attachShadow({ mode: 'open' }); // this is sometimes needed when template is="flow-repeat" is used. if (this.template === undefined) { const t = this.querySelector('template'); if (t && t.content) { this.template = t.content; } } elem.shadowRoot.appendChild(this.template.cloneNode(true)); elem._appendFBP(elem.shadowRoot); const handle = { virtualElement: elem, children: [].slice.call(elem.shadowRoot.children), identity, }; // remove old entries if (this._insertedItems[i]) { this._insertedItems[i].children.forEach(attachedElem => { attachedElem.remove(); }); } this._insertedItems[i] = handle; this._insertToDom(elem.shadowRoot.firstElementChild, this); return elem; } /** * @private */ connectedCallback() { this.style.display = 'none'; // Create a shadow root to the element. const t = this.querySelector('template'); if (t && t.content) { this.template = t.content; } /** * Identity path of a single item. * Use this if you have a field which identifies the item. * @type {*string} */ this.identityPath = this.getAttribute('identity-path') || false; this._internalWire = this.getAttribute('internal-wire') || '--itemInjected'; } /** * Triggers the wire `--trigger` on the first item. * * Triggers the wire --triggerFirst on the first item. * * @param data {*} - data to forward to the item. */ triggerFirst(data) { if (this._insertedItems[0]) { this._insertedItems[0].virtualElement._FBPTriggerWire('--trigger', data); this._insertedItems[0].virtualElement._FBPTriggerWire( '--triggerFirst', data ); } } /** * Triggers the wire `--trigger` on the last item. * * Triggers the wire --triggerLast on the last item. * * @param data {*} - data to forward to the item. */ triggerLast(data) { if (this._insertedItems[this._insertedItems.length - 1]) { this._insertedItems[ this._insertedItems.length - 1 ].virtualElement._FBPTriggerWire('--trigger', data); this._insertedItems[ this._insertedItems.length - 1 ].virtualElement._FBPTriggerWire('--triggerLast', data); } } /** * Triggers the wire `--trigger` on the item. * * Triggers the wire `--triggerIndex` on the item. * * * @param i {int} - index of item that you want to trigger. * @param data {*} - data to forward to the item. */ triggerIndex(i, data) { if (this._insertedItems[i]) { this._insertedItems[i].virtualElement._FBPTriggerWire('--trigger', data); this._insertedItems[i].virtualElement._FBPTriggerWire( '--triggerIndex', data ); } else { // eslint-disable-next-line no-console console.warn('Out of index', this); } } } window.customElements.define('flow-repeat', FlowRepeat);