UNPKG

@glance-networks/agent-plugin

Version:

Glance Networks Agent Plugin

607 lines (606 loc) 22.8 kB
var __defProp = Object.defineProperty; var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; var __publicField = (obj, key, value) => { __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); return value; }; import { ComboTextField } from "./index.js"; import "svelte/internal"; import "svelte"; function insert(target, node, anchor) { target.insertBefore(node, anchor || null); } function detach(node) { if (node.parentNode) { node.parentNode.removeChild(node); } } function createSvelteSlots(slots) { const svelteSlots = {}; for (const slotName in slots) { svelteSlots[slotName] = [createSlotFn(slots[slotName])]; } function createSlotFn(element) { return function() { return { c: function() { }, // noop m: function mount(target, anchor) { insert(target, element.cloneNode(true), anchor); }, d: function destroy(detaching) { if (detaching) { detach(element); } }, l: function() { } // noop }; }; } return svelteSlots; } function findSlotParent(slot) { let parentEl = slot.parentElement; while (parentEl) { if (parentEl.tagName.indexOf("-") !== -1) return parentEl; parentEl = parentEl.parentElement; } return null; } function unwrap(from) { let node = document.createDocumentFragment(); while (from.firstChild) { node.appendChild(from.firstChild); } return node; } const propMapCache = /* @__PURE__ */ new Map(); const attributeObserver = new MutationObserver((mutations) => { mutations.forEach((mutation) => { const element = mutation.target; const attributeName = mutation.attributeName; const newValue = element.getAttribute(attributeName); const propTypes = mutation.target.propTypes; const parseAttributesTypes = mutation.target.attributeParseTypes; const attributeType = propTypes[attributeName]; if (propTypes && attributeType) { element.forwardAttributeToProp(attributeName, parseAttributesTypes[attributeType](newValue)); } else { element.forwardAttributeToProp(attributeName, newValue); } }); }); function renderElements(timestamp) { let renderQueue = document.querySelectorAll("[data-component-wrapper-render]"); if (renderQueue.length === 0) { return; } for (let element of renderQueue) { if (!element.isConnected) { continue; } if (element.parentElement.closest('[data-component-wrapper-render="light"]') === null) { element.removeAttribute("data-component-wrapper-render"); element._renderSvelteComponent(); } } } function ComponentWrapper(opts) { if (!window.customElements.get("component-wrapper")) { window.customElements.define( "component-wrapper", class extends HTMLElement { // noop } ); window.customElements.define( "component-wrapper-default", class extends HTMLElement { // noop } ); } window.customElements.define( opts.tagname, class extends HTMLElement { constructor() { super(); /* * --- ATTENTION! This is a manual override to the original code. --- * This is a collection of methods to parse the attribute value to the correct type (e.g. string, number, boolean, etc.) * instead of always returning a string. * */ __publicField(this, "attributeParseTypes", { "string": (value) => value, "number": (value) => parseFloat(value), "boolean": (value) => value === "true", "object": (value) => JSON.parse(value), "array": (value) => JSON.parse(value), "function": (value) => new Function(`return ${value}`)() }); __publicField(this, "propTypes", opts.propTypes); this._debug("constructor()"); this.attributesObserved = false; if (opts.shadow) { this.attachShadow({ mode: "open" }); this._root = document.createElement("component-wrapper"); this.shadowRoot.appendChild(this._root); if (opts.href) { let link = document.createElement("link"); link.setAttribute("href", opts.href); link.setAttribute("rel", "stylesheet"); this.shadowRoot.appendChild(link); } } } /** * Attributes we're watching for changes after render (doesn't affect attributes already present prior to render). * * @returns string[] */ static get observedAttributes() { if (Array.isArray(opts.attributes)) { return opts.attributes; } else { return []; } } /** * Attached to DOM. */ connectedCallback() { this._debug("connectedCallback()"); this.slotEls = {}; const isLoading = document.readyState === "loading"; if (!opts.shadow) { if (this.hasAttribute("data-component-wrapper-hydratable")) { if (isLoading) { this._onSlotsReady(() => { this._initLightRoot(); this._hydrateLightSlots(); this._queueForRender(); }); return; } else { this._initLightRoot(); this._hydrateLightSlots(); } } else { this._initLightRoot(); } } if (opts.shadow) { this._observeSlots(true); } else { if (isLoading) { this._observeSlots(true); this._onSlotsReady(() => { this._observeSlots(false); }); } } this._queueForRender(); if (opts.hydratable) { this.setAttribute("data-component-wrapper-hydratable", ""); } } /** * Removed from DOM (could be called inside another custom element that starts rendering after this one). In that * situation, the connectedCallback() will be executed again (most likely with constructor() as well, unfortunately). */ disconnectedCallback() { this._debug("disconnectedCallback()"); this.removeAttribute("data-component-wrapper-render"); this.removeAttribute("data-component-wrapper-hydratable"); this._observeSlots(false); if (this.componentInstance) { try { this.componentInstance.$destroy(); delete this.componentInstance; } catch (err) { console.error( `Error destroying Svelte component in '${this.tagName}'s disconnectedCallback(): ${err}` ); } } if (!opts.shadow) { this._restoreLightSlots(); this.removeChild(this._root); } } /** * Forward modifications to element attributes to the corresponding Svelte prop. * * @param {string} name * @param {string} oldValue * @param {string} newValue */ attributeChangedCallback(name, oldValue, newValue) { this._debug("attributes changed", { name, oldValue, newValue }); this.forwardAttributeToProp(name, newValue); } /** * Forward modifications to element attributes to the corresponding Svelte prop (if applicable). * * @param {string} name * @param {string} value */ forwardAttributeToProp(name, value) { this._debug("forwardAttributeToProp", { name, value }); if (this.componentInstance) { let translatedName = this._translateAttribute(name); this.componentInstance.$set({ [translatedName]: value }); } } /** * Setup a wrapper in the light DOM which can keep the rendered Svelte component separate from the default Slot * content, which is potentially being actively appended (at least while the browser parses during loading). */ _initLightRoot() { let existingRoot = this.querySelector("component-wrapper"); if (existingRoot !== null && existingRoot.parentElement === this) { this._debug("_initLightRoot(): Restore from existing light DOM root"); this._root = existingRoot; } else { this._root = document.createElement("component-wrapper"); this.prepend(this._root); } } /** * Queues the provided callback to execute when we think all slots are fully loaded and available to fetch and * manipulate. * * @param {callback} callback */ _onSlotsReady(callback) { document.addEventListener("readystatechange", () => { if (document.readyState === "interactive") { callback(); } }); } /** * Converts the provided lowercase attribute name to the correct case-sensitive component prop name, if possible. * * @param {string} attributeName * @returns {string} */ _translateAttribute(attributeName) { attributeName = attributeName.toLowerCase(); if (this.propMap && this.propMap.has(attributeName)) { return this.propMap.get(attributeName); } else { this._debug(`_translateAttribute(): ${attributeName} not found`); return attributeName; } } /** * To support context, this traverses the DOM to find potential parent elements (also managed by component-wrapper) which * may contain context necessary to render this component. * * See context functions at: https://svelte.dev/docs/svelte#setcontext * * @returns {Map|null} */ _getAncestorContext() { var _a, _b; let node = this; while (node.parentNode) { node = node.parentNode; let context = (_b = (_a = node == null ? void 0 : node.componentInstance) == null ? void 0 : _a.$$) == null ? void 0 : _b.context; if (context instanceof Map) { return context; } } return null; } /** * Queue this element for render in the next animation frame. This offers the opportunity to render known available * elements all at once and, ideally, from the top down (to preserve context). */ _queueForRender() { if (!this.isConnected) { this._debug("queueForRender(): skipped, already disconnected"); return; } if (this.parentElement.closest('[data-component-wrapper-render="light"]') !== null) { this._debug("queueForRender(): skipped, light DOM parent is queued for render"); return; } this.setAttribute("data-component-wrapper-render", opts.shadow ? "shadow" : "light"); requestAnimationFrame(renderElements); } /** * Renders (or rerenders) the Svelte component into this custom element based on the latest properties and slots * (with slots initialized elsewhere). * * IMPORTANT: * * Despite the intuitive name, this method is private since its functionality requires a deeper understanding * of how it depends on current internal state and how it alters internal state. Be sure to study how it's called * before calling it yourself externally. ("Yarrr! Here be dragons! 🔥🐉") * * That said... this is currently the workflow: * * 1. Wait for connection to DOM * 2. Ensure slots are properly prepared (e.g. in case of hydration) or observed (in case actively parsing DOM, e.g. * IIFE/UMD or shadow DOM) in case there are any changes *after* this render * 3. _queueForRender(): Kick off to requestAnimationFrame() to queue render of the component (instead of rendering * immediately) to ensure that all currently connected and available components are taken into account (this is * necessary for properly supporting context to prevent from initializing components out of order). * 4. renderElements(): Renders through the DOM tree in document order and from the top down (parent to child), * reaching this element instantiating this component, ensuring context is preserved. * */ _renderSvelteComponent() { this._debug("renderSvelteComponent()"); if (opts.shadow) { this.slotEls = this._getShadowSlots(); } else { this.slotEls = this._getLightSlots(); } this._root.innerHTML = ""; const context = this._getAncestorContext() || /* @__PURE__ */ new Map(); let props = { $$scope: {}, // Convert our list of slots into Svelte-specific slot objects $$slots: createSvelteSlots(this.slotEls) // All other *initial* props are pulled dynamically from element attributes (see proxy below)... }; if (!propMapCache.has(this.tagName)) { this.propMap = /* @__PURE__ */ new Map(); propMapCache.set(this.tagName, this.propMap); props = new Proxy(props, { get: (target, prop) => { let propName = prop.toString(); if (prop.indexOf("$$") === -1) { this.propMap.set(propName.toLowerCase(), propName); } let attribValue = this.attributes.getNamedItem(propName); if (attribValue !== null) { if (opts.propTypes && opts.propTypes[propName]) { const type = opts.propTypes[propName]; return target[propName] = this.attributeParseTypes[type](attribValue.value); } return target[propName] = attribValue.value; } else { return target[prop]; } } }); } else { this.propMap = propMapCache.get(this.tagName); for (let attr of [...this.attributes]) { if (attr.name.indexOf("data-component-wrapper") !== -1) continue; props[this._translateAttribute(attr.name)] = attr.value; } } this.componentInstance = new opts.component({ target: this._root, props, context }); if (opts.attributes === true && !this.attributesObserved) { this.attributesObserved = true; if (this.propMap.size > 0) { attributeObserver.observe(this, { attributes: true, // implicit, but... 🤷‍♂️ attributeFilter: [...this.propMap.keys()] }); } else { this._debug("renderSvelteComponent(): skipped attribute observer, no props"); } } this._debug("renderSvelteComponent(): completed"); } /** * Fetches slots from pre-rendered Svelte component HTML using special markers (either data attributes or custom * wrappers). Note that this will only work during initialization and only if the Svelte component instance is * hydratable. */ _hydrateLightSlots() { let existingNamedSlots = this._root.querySelectorAll("[data-component-wrapper-slot]"); for (let slot of existingNamedSlots) { let slotParent = findSlotParent(slot); if (slotParent !== this._root) continue; let slotName = slot.getAttribute("slot"); this.slotEls[slotName] = slot; } let existingDefaultSlot = this.querySelector("component-wrapper-default"); if (existingDefaultSlot !== null) { this.slotEls["default"] = existingDefaultSlot; } this._restoreLightSlots(); return true; } /** * Indicates if the provided <slot> element instance belongs to this custom element or not. * * @param {Element} slot * @returns {boolean} */ _isOwnSlot(slot) { let slotParent = findSlotParent(slot); if (slotParent === null) return false; return slotParent === this; } /** * Returns a map of slot names and the corresponding HTMLElement (named slots) or DocumentFragment (default slots). * * IMPORTANT: Since this custom element is the "root", these slots must be removed (which is done in THIS method). * * TODO: Problematic name. We are "getting" but we're also mangling/mutating state (which *is* necessary). "Get" may be confusing here; is there a better name? * * @returns {SlotList} */ _getLightSlots() { this._debug("_getLightSlots()"); let slots = {}; const queryNamedSlots = this.querySelectorAll("[slot]"); for (let candidate of queryNamedSlots) { if (!this._isOwnSlot(candidate)) continue; slots[candidate.slot] = candidate; if (opts.hydratable) { candidate.setAttribute("data-component-wrapper-slot", ""); } this.removeChild(candidate); } let fragment = document.createDocumentFragment(); if (opts.hydratable) { fragment = document.createElement("component-wrapper-default"); } let childNodes = [...this.childNodes]; let childHTML = ""; for (let childNode of childNodes) { if (childNode instanceof HTMLElement && childNode.tagName === "SVELTE-RETAG") { this._debug("_getLightSlots(): skipping <component-wrapper> container"); continue; } if (childNode instanceof Text) { childHTML += childNode.textContent; } else if (childNode.outerHTML) { childHTML += childNode.outerHTML; } fragment.appendChild(childNode); } if (childHTML.trim() !== "") { if (slots.default) { console.error( `svelteWrapper: '${this.tagName}': Found elements without slot attribute when using slot="default"` ); } else { slots.default = fragment; } } return slots; } /** * Go through originally removed slots and restore back to the custom element. */ _restoreLightSlots() { this._debug("_restoreLightSlots:", this.slotEls); for (let slotName in this.slotEls) { let slotEl = this.slotEls[slotName]; if (slotEl.tagName === "COMPONENT-WRAPPER-DEFAULT") { this.prepend(unwrap(slotEl)); } else { this.prepend(slotEl); if (slotEl instanceof HTMLElement && slotEl.hasAttribute("data-component-wrapper-slot")) { slotEl.removeAttribute("data-component-wrapper-slot"); } } } this.slotEls = {}; } /** * Fetches and returns references to the existing shadow DOM slots. Left unmodified. * * @returns {SlotList} */ _getShadowSlots() { this._debug("_getShadowSlots()"); const namedSlots = this.querySelectorAll("[slot]"); let slots = {}; let htmlLength = this.innerHTML.length; namedSlots.forEach((n) => { slots[n.slot] = document.createElement("slot"); slots[n.slot].setAttribute("name", n.slot); htmlLength -= n.outerHTML.length; }); if (htmlLength > 0) { slots.default = document.createElement("slot"); } return slots; } /** * Toggle on/off the MutationObserver used to watch for changes in child slots. */ _observeSlots(begin) { if (begin === this.slotObserverActive) return; if (!this.slotObserver) { this.slotObserver = new MutationObserver((mutations) => { this._processSlotMutations(mutations); }); } if (begin) { this.slotObserver.observe(this, { childList: true, subtree: false, attributes: false }); this._debug("_observeSlots: OBSERVE"); } else { this.slotObserver.disconnect(); this._debug("_observeSlots: DISCONNECT"); } this.slotObserverActive = begin; } /** * Watches for slot changes, specifically: * * 1. Shadow DOM: All slot changes will queue a rerender of the Svelte component * * 2. Light DOM: Only additions will be accounted for. This is particularly because currently we only support * watching for changes during document parsing (i.e. document.readyState === 'loading', prior to the * 'DOMContentLoaded' event. * * @param {MutationRecord[]} mutations */ _processSlotMutations(mutations) { this._debug("_processSlotMutations()", mutations); let rerender = false; for (let mutation of mutations) { if (mutation.type === "childList") { if (opts.shadow) { rerender = true; break; } else { if (mutation.addedNodes.length > 0) { rerender = true; break; } } } } if (rerender) { if (!opts.shadow) { this._observeSlots(false); this._restoreLightSlots(); this._observeSlots(true); } this._debug("_processMutations(): Queue rerender"); this._queueForRender(); } } /** * Pass through to console.log() but includes a reference to the custom element in the log for easier targeting for * debugging purposes. * * @param {...*} */ _debug() { if (opts.debugMode) { if (opts.debugMode === "cli") { console.log.apply(null, [performance.now(), this.tagName, ...arguments]); } else { console.log.apply(null, [performance.now(), this, ...arguments]); } } } } ); } ComponentWrapper({ component: ComboTextField, tagname: "gds-combo-text-input", // Changes to these attributes will be reactively forwarded to your component attributes: true, // propTypes propTypes: { "disabled": "boolean", "skeleton": "boolean", "size": "string", "label": "string", "placeholder": "string", "value": "string", "id": "string", "shape": "string", "warn": "boolean", "error": "boolean", "errorMessage": "string", "warnMessage": "string", "helpMessage": "string", "severityIcon": "boolean" }, // Use the light DOM shadow: true, // Only necessary if shadow is true href: "https://fonts.googleapis.com/css2?family=Fira+Code:wght@300..700&family=Inter:wght@100..900&display=swap" });