@glance-networks/agent-plugin
Version:
Glance Networks Agent Plugin
607 lines (606 loc) • 22.8 kB
JavaScript
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"
});