UNPKG

@grafana/ui

Version:
331 lines (328 loc) • 10 kB
import { isNumber } from 'lodash'; import { isObject, getObjectName, getType, cssClass, createElement, getValuePreview } from './helpers.mjs'; "use strict"; const DATE_STRING_REGEX = /(^\d{1,4}[\.|\\/|-]\d{1,2}[\.|\\/|-]\d{1,4})(\s*(?:0?[1-9]:[0-5]|1(?=[012])\d:[0-5])\d\s*[ap]m)?$/; const PARTIAL_DATE_REGEX = /\d{2}:\d{2}:\d{2} GMT-\d{4}/; const JSON_DATE_REGEX = /\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z/; const MAX_ANIMATED_TOGGLE_ITEMS = 10; const requestAnimationFrame = typeof window !== "undefined" && window.requestAnimationFrame || ((cb) => { cb(); return 0; }); const _defaultConfig = { animateOpen: true, animateClose: true }; class JsonExplorer { /** * @param {object} json The JSON object you want to render. It has to be an * object or array. Do NOT pass raw JSON string. * * @param {number} [open=1] his number indicates up to how many levels the * rendered tree should expand. Set it to `0` to make the whole tree collapsed * or set it to `Infinity` to expand the tree deeply * * @param {object} [config=defaultConfig] - * defaultConfig = { * hoverPreviewEnabled: false, * hoverPreviewArrayCount: 100, * hoverPreviewFieldCount: 5 * } * * Available configurations: * #####Hover Preview * * `hoverPreviewEnabled`: enable preview on hover * * `hoverPreviewArrayCount`: number of array items to show in preview Any * array larger than this number will be shown as `Array[XXX]` where `XXX` * is length of the array. * * `hoverPreviewFieldCount`: number of object properties to show for object * preview. Any object with more properties that thin number will be * truncated. * * @param {string} [key=undefined] The key that this object in its parent * context */ constructor(json, open = 1, config = _defaultConfig, key) { this.json = json; this.open = open; this.config = config; this.key = key; // Hold the open state after the toggler is used this._isOpen = null; // A reference to the element that we render to this.element = null; this.skipChildren = false; } /* * is formatter open? */ get isOpen() { if (this._isOpen !== null) { return this._isOpen; } else { return this.open > 0; } } /* * set open state (from toggler) */ set isOpen(value) { this._isOpen = value; } /* * is this a date string? */ get isDate() { return this.type === "string" && (DATE_STRING_REGEX.test(this.json) || JSON_DATE_REGEX.test(this.json) || PARTIAL_DATE_REGEX.test(this.json)); } /* * is this a URL string? */ get isUrl() { return this.type === "string" && this.json.indexOf("http") === 0; } /* * is this an array? */ get isArray() { return Array.isArray(this.json); } /* * is this an object? * Note: In this context arrays are object as well */ get isObject() { return isObject(this.json); } /* * is this an empty object with no properties? */ get isEmptyObject() { return !this.keys.length && !this.isArray; } /* * is this an empty object or array? */ get isEmpty() { return this.isEmptyObject || this.keys && !this.keys.length && this.isArray; } /* * did we receive a key argument? * This means that the formatter was called as a sub formatter of a parent formatter */ get hasKey() { return typeof this.key !== "undefined"; } /* * if this is an object, get constructor function name */ get constructorName() { return getObjectName(this.json); } /* * get type of this value * Possible values: all JavaScript primitive types plus "array" and "null" */ get type() { return getType(this.json); } /* * get object keys * If there is an empty key we pad it wit quotes to make it visible */ get keys() { if (this.isObject) { return Object.keys(this.json).map((key) => key ? key : '""'); } else { return []; } } /** * Toggles `isOpen` state * */ toggleOpen() { this.isOpen = !this.isOpen; if (this.element) { if (this.isOpen) { this.appendChildren(this.config.animateOpen); } else { this.removeChildren(this.config.animateClose); } this.element.classList.toggle(cssClass("open")); } } /** * Open all children up to a certain depth. * Allows actions such as expand all/collapse all * */ openAtDepth(depth = 1) { if (depth < 0) { return; } this.open = depth; this.isOpen = depth !== 0; if (this.element) { this.removeChildren(false); if (depth === 0) { this.element.classList.remove(cssClass("open")); } else { this.appendChildren(this.config.animateOpen); this.element.classList.add(cssClass("open")); } } } isNumberArray() { return this.json.length > 0 && this.json.length < 4 && (isNumber(this.json[0]) || isNumber(this.json[1])); } renderArray() { const arrayWrapperSpan = createElement("span"); arrayWrapperSpan.appendChild(createElement("span", "bracket", "[")); if (this.isNumberArray()) { this.json.forEach((val, index) => { if (index > 0) { arrayWrapperSpan.appendChild(createElement("span", "array-comma", ",")); } arrayWrapperSpan.appendChild(createElement("span", "number", val)); }); this.skipChildren = true; } else { arrayWrapperSpan.appendChild(createElement("span", "number", this.json.length)); } arrayWrapperSpan.appendChild(createElement("span", "bracket", "]")); return arrayWrapperSpan; } /** * Renders an HTML element and installs event listeners * * @returns {HTMLDivElement} */ render(skipRoot = false) { this.element = createElement("div", "row"); const togglerLink = createElement("a", "toggler-link"); const togglerIcon = createElement("span", "toggler"); if (this.isObject) { togglerLink.appendChild(togglerIcon); } if (this.hasKey) { togglerLink.appendChild(createElement("span", "key", `${this.key}:`)); } if (this.isObject) { const value = createElement("span", "value"); const objectWrapperSpan = createElement("span"); const constructorName = createElement("span", "constructor-name", this.constructorName); objectWrapperSpan.appendChild(constructorName); if (this.isArray) { const arrayWrapperSpan = this.renderArray(); objectWrapperSpan.appendChild(arrayWrapperSpan); } value.appendChild(objectWrapperSpan); togglerLink.appendChild(value); } else { const value = this.isUrl ? createElement("a") : createElement("span"); value.classList.add(cssClass(this.type)); if (this.isDate) { value.classList.add(cssClass("date")); } if (this.isUrl) { value.classList.add(cssClass("url")); value.setAttribute("href", this.json); } const valuePreview = getValuePreview(this.json, this.json); value.appendChild(document.createTextNode(valuePreview)); togglerLink.appendChild(value); } const children = createElement("div", "children"); if (this.isObject) { children.classList.add(cssClass("object")); } if (this.isArray) { children.classList.add(cssClass("array")); } if (this.isEmpty) { children.classList.add(cssClass("empty")); } if (this.config && this.config.theme) { this.element.classList.add(cssClass(this.config.theme)); } if (this.isOpen) { this.element.classList.add(cssClass("open")); } if (!skipRoot) { this.element.appendChild(togglerLink); } if (!this.skipChildren) { this.element.appendChild(children); } else { togglerLink.removeChild(togglerIcon); } if (this.isObject && this.isOpen) { this.appendChildren(); } if (this.isObject) { togglerLink.addEventListener("click", this.toggleOpen.bind(this)); } return this.element; } /** * Appends all the children to children element * Animated option is used when user triggers this via a click */ appendChildren(animated = false) { const children = this.element && this.element.querySelector(`div.${cssClass("children")}`); if (!children || this.isEmpty) { return; } if (animated) { let index = 0; const addAChild = () => { const key = this.keys[index]; const formatter = new JsonExplorer(this.json[key], this.open - 1, this.config, key); children.appendChild(formatter.render()); index += 1; if (index < this.keys.length) { if (index > MAX_ANIMATED_TOGGLE_ITEMS) { addAChild(); } else { requestAnimationFrame(addAChild); } } }; requestAnimationFrame(addAChild); } else { this.keys.forEach((key) => { const formatter = new JsonExplorer(this.json[key], this.open - 1, this.config, key); children.appendChild(formatter.render()); }); } } /** * Removes all the children from children element * Animated option is used when user triggers this via a click */ removeChildren(animated = false) { const childrenElement = this.element && this.element.querySelector(`div.${cssClass("children")}`); if (animated) { let childrenRemoved = 0; const removeAChild = () => { if (childrenElement && childrenElement.children.length) { childrenElement.removeChild(childrenElement.children[0]); childrenRemoved += 1; if (childrenRemoved > MAX_ANIMATED_TOGGLE_ITEMS) { removeAChild(); } else { requestAnimationFrame(removeAChild); } } }; requestAnimationFrame(removeAChild); } else { if (childrenElement) { childrenElement.innerHTML = ""; } } } } export { JsonExplorer }; //# sourceMappingURL=json_explorer.mjs.map