UNPKG

grapper-core

Version:

Core libs and helpers for Grapper projects

342 lines (308 loc) 9.2 kB
/** * * Base class for Grapper web component * * @module base * @version 0.0.3 * @author Pablo Almunia * */ import { Simple, define as defineSimple, CHANGE, CONTEXT, FIRE_EVENT } from './simple.js'; import { isUndefined, isFunction } from './helpers/types.js'; import { debounceMethodAsync, posExecution, preCondition } from './helpers/functions.js'; // Constants const DELAY = 1; // Public symbols /** * Represents a unique symbol used as an identifier or key for connections. * Mainly used to mark or handle events related to a connection being * established. This symbol ensures there is no conflict with other * property keys. * @type {symbol} */ const ONCONNECT = Symbol(); /** * A unique symbol used to represent the event of a disconnect action. * Can be utilized as a key or identifier for managing or listening to disconnect * events in an application context. * @type {symbol} */ const ONDISCONNECT = Symbol(); /** * Symbol used for defines the refresh method into the class inherited from Base. * This method is called when the component is rendered and when some property * is changed and REFRESH is defined as pos update action. * @type {symbol} */ const REFRESH = Symbol(); /** * Symbol used for defines the render method into the class inherited from Base * This method is called when the component is created and when some property * is changed and RENDER is define as pos update action. * @type {symbol} */ const RENDER = Symbol(); /** * Symbol used for defines the resize method into the class inherited from Base. * This method is called when the component is resized. * @type {symbol} */ const RESIZE = Symbol(); /** * Symbol used for defines the map with CSS properties info. * @type {symbol} */ const CSS_PROPS = Symbol(); /** * Executes an array of callback functions associated with a given key on an element. * * @param {Object} el - The element object containing the callback array. * @param {string} id - The key identifying the array of callbacks in the element object. */ const runCallbacks = (el, id) => el[id]?.forEach(fn => isFunction(fn) && fn.apply(el)); /** * Base class for Grapper Web Component * * @fires 'ready' - This event fires when the component is ready and its methods and properties are available * @fires 'render' - This event fires when the component is rendered and its visible content is displayed * @fires 'refresh' - This event fires when the component is refreshed, and its visible content is updated * @fires 'resize' - This event fires when the component size is changed * @property {boolean} [ready] - It's true if the component is ready, or false is starting or busy. * @property {boolean} [rendered] - It's true if the component is rendered, and its visible content is displayed. */ class Base extends Simple { /** * @constructor * @param {boolean} [ready] */ constructor (ready) { super(); this.attachShadow({mode : 'open'}); this[CONTEXT].ready = ready || false; this[CONTEXT].rendered = false; if (isUndefined(ready)) { this.ready = true; } } /** * ready state * @type {boolean} */ get ready () { return this[CONTEXT].ready; } set ready (value) { const ctx = this[CONTEXT]; const pre = ctx.ready; ctx.ready = !!value; if (pre === false && ctx.ready === true) { this[FIRE_EVENT]('ready', {ready : true}); if (isFunction(this[RENDER])) { this[RENDER](); } } } /** * rendered state * @returns {boolean} */ get rendered () { return this[CONTEXT].rendered; } set rendered (value) { const ctx = this[CONTEXT]; const pre = ctx.rendered; ctx.rendered = !!value; if (pre === false && ctx.rendered === true) { this[FIRE_EVENT]('render', {rendered : true}); } } /** * Connected with the parent DOM * - Resize observer * @private */ connectedCallback () { let reference = this.getBoundingClientRect(); let flexDirection = getComputedStyle(this).flexDirection; // Specific for Icon class const resize = () => { let {width : currentWidth, height : currentHeight} = this.getBoundingClientRect(); let currentFlexDirection = getComputedStyle(this).flexDirection; if (currentWidth !== reference.width || currentHeight !== reference.height || currentFlexDirection !== flexDirection) { if (isFunction(this[RESIZE])) { this[RESIZE]( currentWidth, currentHeight, currentWidth - reference.width, currentHeight - reference.height ); } reference = {width : currentWidth, height : currentHeight}; flexDirection = currentFlexDirection; this[FIRE_EVENT]('resize', reference); } this [CONTEXT]._resizeObserver = window.requestAnimationFrame(resize); }; resize(); runCallbacks(this, ONCONNECT); } /** * Disconnected of parent DOM * - Remove resize observer * @private */ disconnectedCallback () { window.cancelAnimationFrame(this [CONTEXT]._resizeObserver); runCallbacks(this, ONDISCONNECT); } } /** * Handles the ONCONNECT event for the Base prototype. * This array of functions is triggered when a connection occurs. * @memberof Base * @property [ONCONNECT] */ Base.prototype[ONCONNECT] = []; /** * Handles the ONDISCONNECT event for the Base prototype. * This array of functions is triggered when a disconnection occurs. * @memberof Base * @property [ONCONNECT] */ Base.prototype[ONDISCONNECT] = []; /** * * Property descriptor used into defineProperty * * @typedef {Object} cssPropertyDescriptor * @property {string} name - custom property name * @property {string} [syntax=''] - syntax of the custom property * @property {string} [initialValue=''] - initial value * @property {string} [value=''] - initial value (alias) * @property {boolean} [inherits=false] - inherit */ /** * * Define a CSS property into the class * * @param {Function} Class - class to extend * @param {cssPropertyDescriptor} def - options into a {@link cssPropertyDescriptor} */ function defineStyleProperty (Class, def) { const definition = { name : def.name.startsWith('--') ? def.name : `--${ def.name }`, initialValue : def.initialValue ?? def.value ?? '', syntax : def.syntax ?? '*', inherits : def.inherits ?? true }; if (!Class.prototype[CSS_PROPS]) { Class.prototype[CSS_PROPS] = {}; } Class.prototype[CSS_PROPS][definition.name] = definition; } /** * * Define a Component * * @param {Function} Class - Class for this custom component */ function defineComponent (Class) { // Async call to render method if (isFunction(Class.prototype[RENDER])) { const preRender = Class.prototype[RENDER]; Class.prototype[RENDER] = preCondition( function () { this.rendered = false; return this.ready; }, debounceMethodAsync( posExecution( async function () { return preRender.apply(this); }, function (result) { this.rendered = result !== false; if (this.rendered && isFunction(this[REFRESH])) { this[REFRESH](); } } ), DELAY ) ); } // Async call to refresh method if (isFunction(Class.prototype[REFRESH])) { const prevRefresh = Class.prototype[REFRESH]; Class.prototype[REFRESH] = preCondition( function (force) { if (force) { this[CONTEXT].rendered = true; } return this.ready && this[CONTEXT].rendered; }, debounceMethodAsync( posExecution( async function (...args) { return prevRefresh.apply(this, args); }, function () { this[FIRE_EVENT]('refresh'); } ), DELAY ) ); } } /** * Define a Base or * @param {Function} Class * @returns {object} */ function define (Class) { defineComponent(Class); const def = defineSimple(Class, { style : (...styles) => { styles.forEach(style => defineStyleProperty(Class, {...style})); return def; } }); return def; } Base.RENDER = RENDER; Base.REFRESH = REFRESH; Base.ONCONNECT = ONCONNECT; Base.ONDISCONNECT = ONDISCONNECT; /** * Export */ export { Base as default, Base, Simple, define, RENDER, REFRESH, CHANGE, CONTEXT, RESIZE, CSS_PROPS, FIRE_EVENT, ONCONNECT, ONDISCONNECT };