UNPKG

@furo/fbp

Version:

Declarative programming with web-components.

821 lines 34.1 kB
/** * furo-fbp base class * * [read the guide](https://fbp.furo.pro/) * * * ### **_FBPTriggerWire** * <small>**_FBPTriggerWire**(*wire* `` *detailData* `` ) ⟹ `void`</small> * * Triggers a wire * * - <small>wire (String) Name of the wire like --buttonClicked</small> * - <small>detailData (*) data to pass</small> * * * ### **_FBPAddWireHook** * <small>**_FBPAddWireHook**(*wire* `` *cb* `` *before* `` ) ⟹ `number`</small> * * * * - <small>wire (String) Name of the wire</small> * - <small>cb (function) Callback function cb(detailData)</small> * - <small>before (Boolean) append before the components are triggered, default is false</small> * * * ### **_FBPTraceWires** * <small>**_FBPTraceWires**() ⟹ `void`</small> * * Log all triggered wires for this component. This function may help you at debugging. * Select your element in the dev console and call `$0._FBPTraceWires()` * * * * ### **_FBPDebug** * <small>**_FBPDebug**(*wire* `` *openDebugger* `` ) ⟹ `void`</small> * * Get information for the triggered wire. This function may help you at debugging. * Select your element in the dev console and call `$0._FBPDebug('--dataReceived')` * * - <small>wire </small> * - <small>openDebugger opens the debugger console, so you can inspect your component.</small> * * * * @summary Please read the guide for a better understanding * @polymer * @mixinFunction FBP */ import { FbpBreakpoints } from './FbpBreakpoints.js'; export const FBP = superClass => /** * @polymerMixinClass */ class extends superClass { constructor() { super(); /** * used to store the listeners * @type {*[]} * @private */ this.__FBPEventlistener = []; /** * * @type {{}} * @private */ this.__wirebundle = {}; /** * * @type {*[]} * @private */ this.__wireQueue = []; } /** * Auto append fbp for lit elements * @private */ firstUpdated() { // ensure to append only once if (!this.__fbpAppended) { this._appendFBP(this.shadowRoot); this.__fbpAppended = true; } super.firstUpdated(); } /** * Triggers a wire * @param wire (String) Name of the wire like --buttonClicked * @param detailData (*) data to pass * @private */ _FBPTriggerWire(wire, detailData) { if (this.__fbp_ready) { /** * Check breakpoints, this slows down every wire (if you have set breakpoints). * While debuging, this is absolute OK. */ FbpBreakpoints.Breakpoints().forEach(breakpoint => { // start with cheapest if (breakpoint.enabled && breakpoint.wire === wire && breakpoint.path.endsWith(this.tagName.toLowerCase())) { if (this.__domPath === undefined) { this.__domPath = FbpBreakpoints.getDomPath(this); } if (this.__domPath === breakpoint.path) { // eslint-disable-next-line default-case switch (breakpoint.kind) { case 'BREAKPOINT': // eslint-disable-next-line no-unused-vars,no-case-declarations const tagName = this.tagName.toLowerCase(); // eslint-disable-next-line no-unused-expressions wire; // eslint-disable-next-line no-unused-expressions detailData; // eslint-disable-next-line no-case-declarations,no-unused-vars const domnode = FbpBreakpoints.GetElementByPath(breakpoint.path); // eslint-disable-next-line no-case-declarations,no-unused-vars const targets = this.__wirebundle[wire]; // eslint-disable-next-line no-debugger debugger; break; case 'CONDITIONAL': // eslint-disable-next-line no-case-declarations,no-new-func const c = new Function('data', `return (${breakpoint.condition})`); if (c.call(this, detailData)) { // eslint-disable-next-line no-case-declarations,no-unused-vars const { condition } = breakpoint; // eslint-disable-next-line no-case-declarations,no-unused-vars,no-shadow const tagName = this.tagName.toLowerCase(); // eslint-disable-next-line no-case-declarations,no-unused-vars,no-unused-expressions wire; // eslint-disable-next-line no-case-declarations,no-unused-vars,no-unused-expressions detailData; // eslint-disable-next-line no-unused-vars,no-shadow const domnode = FbpBreakpoints.GetElementByPath(breakpoint.path); // eslint-disable-next-line no-unused-vars,no-shadow const targets = this.__wirebundle[wire]; // eslint-disable-next-line no-debugger debugger; } break; case 'TRACE': // eslint-disable-next-line no-case-declarations const ua = navigator.userAgent.toLowerCase(); // eslint-disable-next-line no-case-declarations let agent = true; if (ua.indexOf('safari') !== -1) { if (ua.indexOf('chrome') > -1) { agent = true; // Chrome } else { agent = false; // Safari } } if (agent) { // eslint-disable-next-line no-console console.group('Trace', `${this.nodeName}: ${wire}`); // eslint-disable-next-line no-console console.log(FbpBreakpoints.GetElementByPath(breakpoint.path)); // eslint-disable-next-line no-console console.group('Target Elements'); // eslint-disable-next-line no-console console.table(this.__wirebundle[wire]); // eslint-disable-next-line no-console console.groupEnd(); // eslint-disable-next-line no-console console.groupCollapsed('Data'); // eslint-disable-next-line no-console console.log(detailData); // eslint-disable-next-line no-console console.groupEnd(); // eslint-disable-next-line no-console console.groupCollapsed('Call Stack'); // eslint-disable-next-line no-console console.log(new Error().stack); // eslint-disable-next-line no-console console.groupEnd(); // eslint-disable-next-line no-console console.groupEnd(); } break; } } } }); if (this.__wirebundle[wire]) { this.__wirebundle[wire].forEach(receiver => { // check for hooks if (typeof receiver === 'function') { receiver(detailData); } else if (typeof receiver.element[receiver.method] === 'function') { this._call(detailData, receiver); } else if (receiver.property) { let data = detailData; if (receiver.path) { data = this._pathGet(detailData, receiver.path); } this._pathSet(receiver.element, receiver.property, data); } else if (receiver.element.localName.includes('-')) { // retry call with whenDefined because sometimes the components are just not defined at the time ƒ-method is triggered customElements .whenDefined(receiver.element.localName) .then(() => { if (typeof receiver.element[receiver.method] === 'function') { this._call(detailData, receiver); } else { // eslint-disable-next-line no-console console.warn(`${receiver.method} is not a method of ${receiver.element.nodeName}`, receiver.element); } }); } else { // eslint-disable-next-line no-console console.warn(`${receiver.method} is not a method of ${receiver.element.nodeName}`, receiver.element); } }); } } else { this.__enqueueTrigger(wire, detailData); } } /** * * @param detailData * @param receiver * @private */ _call(detailData, receiver) { let response; // array spreaden if (Array.isArray(detailData) && receiver.element[receiver.method].length > 1) { // eslint-disable-next-line prefer-spread response = receiver.element[receiver.method].apply(receiver.element, detailData); } else { let data = detailData; if (receiver.path) { data = this._pathGet(detailData, receiver.path); } response = receiver.element[receiver.method](data); } // fnret-function auslösen const fnret = new Event(`fnret-${receiver.attrName}`, { composed: false, bubbles: false, }); fnret.detail = response; receiver.element.dispatchEvent(fnret); // @-ƒ-function auslösen const customEvent = new Event(`ƒ-${receiver.attrName}`, { composed: false, bubbles: false, }); customEvent.detail = response; receiver.element.dispatchEvent(customEvent); } /** * * @param wire (String) Name of the wire * @param cb (function) Callback function cb(detailData) * @param [before] (Boolean) append before the components are triggered, default is false * @returns {number} Index of hook * @private */ _FBPAddWireHook(wire, cb, before) { // eslint-disable-next-line no-param-reassign before = before || false; if (this.__wirebundle[wire]) { if (before) { this.__wirebundle[wire].unshift(cb); return 0; } const l = this.__wirebundle[wire].push(cb); return l - 1; } this.__wirebundle[wire] = [cb]; return 1; } /** * Log all triggered wires for this component. This function may help you at debugging. * Select your element in the dev console and call `$0._FBPTraceWires()` * * * @private */ _FBPTraceWires() { const self = this; // eslint-disable-next-line guard-for-in,no-restricted-syntax for (const wire in this.__wirebundle) { this._FBPAddWireHook(wire, e => { const ua = navigator.userAgent.toLowerCase(); let agent = true; if (ua.indexOf('safari') !== -1) { if (ua.indexOf('chrome') > -1) { agent = true; // Chrome } else { agent = false; // Safari } } if (agent) { // eslint-disable-next-line no-console console.groupCollapsed('Trace for', `${this.nodeName}: ${wire}`); // eslint-disable-next-line no-console console.table([{ host: self, wire, data: e }]); // eslint-disable-next-line no-console console.groupCollapsed('Data'); // eslint-disable-next-line no-console console.log(e); // eslint-disable-next-line no-console console.groupEnd(); // eslint-disable-next-line no-console console.groupCollapsed('Target Elements'); // eslint-disable-next-line no-console console.table(self.__wirebundle[wire]); // eslint-disable-next-line no-console console.groupEnd(); // eslint-disable-next-line no-console console.groupCollapsed('Call Stack'); // eslint-disable-next-line no-console console.log(new Error().stack); // eslint-disable-next-line no-console console.groupEnd(); // eslint-disable-next-line no-console console.groupEnd(); } }, true); } } /** * Get information for the triggered wire. This function may help you at debugging. * Select your element in the dev console and call `$0._FBPDebug('--dataReceived')` * * @param wire * @param openDebugger opens the debugger console, so you can inspect your component. * @private */ _FBPDebug(wire, openDebugger) { const self = this; this._FBPAddWireHook(wire, e => { if (openDebugger) { // eslint-disable-next-line no-debugger debugger; } else { const ua = navigator.userAgent.toLowerCase(); let agent = true; if (ua.indexOf('safari') !== -1) { if (ua.indexOf('chrome') > -1) { agent = true; // Chrome } else { agent = false; // Safari } } if (agent) { // eslint-disable-next-line no-console console.group('Debug', `${this.nodeName}: ${wire}`); // eslint-disable-next-line no-console console.group('Target Elements'); // eslint-disable-next-line no-console console.table(self.__wirebundle[wire]); // eslint-disable-next-line no-console console.groupEnd(); // eslint-disable-next-line no-console console.groupCollapsed('Data'); // eslint-disable-next-line no-console console.log(e); // eslint-disable-next-line no-console console.groupEnd(); // eslint-disable-next-line no-console console.groupCollapsed('Call Stack'); // eslint-disable-next-line no-console console.log(new Error().stack); // eslint-disable-next-line no-console console.groupEnd(); // eslint-disable-next-line no-console console.groupEnd(); } } }, true); } /** * * @param str * @return {*} * @private */ // eslint-disable-next-line class-methods-use-this __toCamelCase(str) { return str.replace(/-([a-z])/g, g => g[1].toUpperCase()); } /** * parses the dom for flowbased programming tags * @param dom dom node * @private */ _appendFBP(dom) { const self = this; const wirebundle = this.__wirebundle; // get all elements which live in the host const nl = dom.querySelectorAll('*'); const l = nl.length - 1; // eslint-disable-next-line func-names const _collectReceivers = function (element, i, attr) { // collect receiver element.attributes[i].value.split(',').forEach(w => { const r = this.__resolveWireAndPath(w); // create empty if not exist if (!wirebundle[r.receivingWire]) { wirebundle[r.receivingWire] = []; } wirebundle[r.receivingWire].push({ element, method: this.__toCamelCase(attr), attrName: attr, path: r.path, }); }); }; // eslint-disable-next-line func-names const _collectPropertySetters = function (element, i, property) { // split multiple wires element.attributes[i].value.split(',').forEach(w => { const r = this.__resolveWireAndPath(w); // create empty if not exist if (!wirebundle[r.receivingWire]) { wirebundle[r.receivingWire] = []; } wirebundle[r.receivingWire].push({ element, property: this.__toCamelCase(property), path: r.path, }); }); }; // eslint-disable-next-line func-names const _extractEventWires = function (fwire) { let wire; const trimmedWire = fwire.trim(); let type = 'call'; if (trimmedWire.startsWith('((')) { wire = trimmedWire.substring(2, trimmedWire.length - 2); type = 'setValue'; } else if (trimmedWire.startsWith('-^')) { wire = trimmedWire.substring(2); type = 'fireOnHost'; } else if (trimmedWire.startsWith('^')) { wire = trimmedWire.substring(1); type = 'fire'; if (trimmedWire.startsWith('^^')) { wire = trimmedWire.substring(2); type = 'fireBubble'; } } else if (trimmedWire === ':STOP') { type = 'stop'; wire = 'stop'; } else if (trimmedWire === ':PREVENTDEFAULT') { type = 'preventdefault'; wire = 'preventdefault'; } else { wire = trimmedWire; type = 'call'; } return { type, wire }; }; // eslint-disable-next-line no-plusplus for (let x = l; x >= 0; --x) { const element = nl[x]; // skip template tags if (element.tagName === 'TEMPLATE') { // eslint-disable-next-line no-continue continue; } for (let i = 0; i < element.attributes.length; i += 1) { // collect receiving tags if (element.attributes[i].name.startsWith('fn-')) { const attr = element.attributes[i].name.substring(3); _collectReceivers.call(this, element, i, attr); // eslint-disable-next-line no-continue continue; } // collect sending tags if (element.attributes[i].name.startsWith('at-')) { const eventname = element.attributes[i].name.substring(3); const fwires = element.attributes[i].value; fwires.split(',').forEach(fwire => { const __ret = _extractEventWires(fwire); // eslint-disable-next-line no-use-before-define registerEvent(eventname, __ret.type, __ret.wire, element); }); // eslint-disable-next-line no-continue continue; } // collect data property setter receiver if (element.attributes[i].name.startsWith('set-')) { const property = element.attributes[i].name.substring(4); _collectPropertySetters.call(this, element, i, property); // eslint-disable-next-line no-continue continue; } // collect data property receiver if (element.attributes[i].name.startsWith('ƒ-.')) { const property = element.attributes[i].name.substring(3); _collectPropertySetters.call(this, element, i, property); // eslint-disable-next-line no-continue continue; } // collect receiving tags if (element.attributes[i].name.startsWith('ƒ-')) { const attr = element.attributes[i].name.substring(2); _collectReceivers.call(this, element, i, attr); // eslint-disable-next-line no-continue continue; } // collect sending tags if (element.attributes[i].name.startsWith('@-')) { const eventname = element.attributes[i].name.substring(2); const fwires = element.attributes[i].value; fwires.split(',').forEach(fwire => { const __ret = _extractEventWires(fwire); // eslint-disable-next-line no-use-before-define registerEvent(eventname, __ret.type, __ret.wire, element); }); // eslint-disable-next-line no-continue continue; } } } /** * register event on current element * @param eventname * @param type * @param wire * @private */ function registerEvent(eventname, type, wire, element) { // find properties in wire // eslint-disable-next-line no-useless-escape const match = wire.match(/([a-z0-9\-_*\.]+)/gi); const handler = { // prevent default and stop propagation stop(e) { e.stopPropagation(); }, preventdefault(e) { e.preventDefault(); }, call(e) { /** * Prüfe ob die Funktion mit einem Wert aus dem Host oder mit den Details des Events ausgeführt werden soll. * --wire(hostName) ==> wirft this.hostName in die Funktion sonst wird e.detail verwendet * */ let effectiveWire = wire; let detailData = e.detail; if (match !== null && match.length > 1) { // --wireName(*) sends the raw event // --wireName(*.mouseX) sends property mouseX of the event if (match[1].startsWith('*')) { if (match[1].length === 1) { // send raw event detailData = e; } else { // send event subprop with *.notDetail.xxx detailData = self._pathGet(e, match[1].substring(2, match[1].length)); } } else { // send host property detailData = self._pathGet(self, match[1]); } // eslint-disable-next-line prefer-destructuring effectiveWire = match[0]; } self._FBPTriggerWire(effectiveWire, detailData); }, fire(e) { if (match !== null && match.length > 1) { const prop = match[1]; const theEvent = match[0]; const customEvent = new Event(theEvent, { composed: false, bubbles: true, }); // send details with *.sub or * if (prop.startsWith('*')) { if (prop.length === 1) { // send raw event customEvent.detail = e; } else { customEvent.detail = self._pathGet(e, prop.substring(2)); } } else { customEvent.detail = self._pathGet(self, prop); } e.currentTarget.dispatchEvent(customEvent); } else { const customEvent = new Event(wire, { composed: false, bubbles: true, }); customEvent.detail = e.detail; e.currentTarget.dispatchEvent(customEvent); } }, fireOnHost(e) { if (match !== null && match.length > 1) { const prop = match[1]; const theEvent = match[0]; const customEvent = new Event(theEvent, { composed: false, bubbles: true, }); // send details with *.sub or * if (prop.startsWith('*')) { if (prop.length === 1) { // send raw event customEvent.detail = e; } else { customEvent.detail = self._pathGet(e, prop.substring(2)); } } else { customEvent.detail = self._pathGet(self, prop); } self.dispatchEvent(customEvent); } else { const customEvent = new Event(wire, { composed: false, bubbles: true, }); customEvent.detail = e.detail; self.dispatchEvent(customEvent); } }, fireBubble(e) { if (match !== null && match.length > 1) { const prop = match[1]; const theEvent = match[0]; const customEvent = new Event(theEvent, { composed: true, bubbles: true, }); // send details with *.sub or * if (prop.startsWith('*')) { if (prop.length === 1) { // send raw event customEvent.detail = e; } else { customEvent.detail = self._pathGet(e, prop.substring(2)); } } else { customEvent.detail = self._pathGet(self, prop); } e.currentTarget.dispatchEvent(customEvent); } else { const customEvent = new Event(wire, { composed: true, bubbles: true, }); customEvent.detail = e.detail; e.currentTarget.dispatchEvent(customEvent); } }, setValue(e) { self._pathSet(self, wire, e.detail); // self.set(wire, e.detail, self); }, }; element.addEventListener(eventname, handler[type]); self.__FBPEventlistener.push({ element, event: eventname, handler: handler[type], }); } // queueing for _FBPTriggerWire if (!this.__fbp_ready) { this._FBPReady(); const queuelength = this.__wireQueue.length; for (let i = 0; i < queuelength; i += 1) { const t = this.__wireQueue.shift(); this._FBPTriggerWire(t.w, t.d); } } } /** * Livecycle method * This method is called, when the wires are ready. * And triggers the `|--FBPready` wire. This does *not* respect a lit updateComplete * @private */ _FBPReady() { this.__fbp_ready = true; this._FBPTriggerWire('|--FBPready', this); } /** * * @param wire * @param detailData * @private */ __enqueueTrigger(wire, detailData) { this.__wireQueue.push({ w: wire, d: detailData }); } /** * * @param w * @return {{path, receivingWire}} * @private */ // eslint-disable-next-line class-methods-use-this __resolveWireAndPath(w) { // finde --wire(*.xx.yy) => group1 = --wire group2 = xx.yy // eslint-disable-next-line no-useless-escape const match = w.trim().match(/(^[^\(]*)\(?\*?\.?([^\)]*)/); const receivingWire = match[1]; const path = match[2]; return { receivingWire, path }; } /** * Reads a value from a path. If any sub-property in the path is `undefined`, * this method returns `undefined` (will never throw. * * @param {Object} root Object from which to dereference path from * @param {string | !Array<string|number>} path Path to read * @return {*} Value at path, or `undefined` if the path could not be fully dereferenced. * @private */ _pathGet(root, path) { let prop = root; const parts = this._split(path); // Loop over path parts[0..n-1] and dereference for (let i = 0; i < parts.length; i += 1) { if (!prop) { return false; } const part = parts[i]; prop = prop[part]; } return prop; } /** * Sets a value to a path. If any sub-property in the path is `undefined`, * this method will no-op. * * @param {Object} root Object from which to dereference path from * @param {string | !Array<string|number>} path Path to set * @param {*} value Value to set to path * @return {string | boolean} The normalized version of the input path, return false if no prop * @private */ _pathSet(root, path, value) { let prop = root; const parts = this._split(path); const last = parts[parts.length - 1]; // used for @-event="((prop.sub))" if (parts.length > 1) { // Loop over path parts[0..n-2] and dereference for (let i = 0; i < parts.length - 1; i += 1) { const part = parts[i]; prop = prop[part]; if (!prop) { return false; } } // Set value to object at end of path prop[last] = value; } else { // Simple property set prop[path] = value; } return parts.join('.'); } /** * Splits a path into an array of property names. Accepts either arrays * of path parts or strings. * * Example: * * ``` * split(['foo.bar', 0, 'baz']) // ['foo', 'bar', '0', 'baz'] * split('foo.bar.0.baz') // ['foo', 'bar', '0', 'baz'] * ``` * * @param {string | !Array<string|number>} path Input path * @return {!Array<string>} Array of path parts * @suppress {checkTypes} * @private */ // eslint-disable-next-line class-methods-use-this _split(path) { return path.toString().split('.'); } }; //# sourceMappingURL=fbp.js.map