基于vue3.0和element-plus的组件库
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1075 lines
28 KiB

3 months ago
<template>
<div
class="json-view"
:class="[
`json-view--${props.theme}`,
{
'json-view--suppress-menu-hover': suppressMenuHover,
'json-view--wrap-lines': wrapsLines,
'json-view--show-line': props.showLine,
},
]"
>
<div
ref="viewportRef"
class="json-view__viewport"
:style="viewportStyle"
role="tree"
aria-label="JSON viewer"
@pointermove="handleViewportPointerMove"
@pointerleave="clearMenuHoverSuppression"
@scroll="handleScroll"
>
<div v-if="props.virtual" class="json-view__spacer" :style="{ height: `${virtualTotalHeight}px` }">
<div
v-for="row in renderedRows"
:key="row.node.id"
class="json-view__row"
:style="getRowStyle(row)"
role="treeitem"
:aria-level="row.node.level + 1"
:aria-setsize="row.node.setSize"
:aria-posinset="row.node.posInSet"
:aria-expanded="row.node.hasToggle ? row.node.isExpanded : undefined"
tabindex="-1"
@click="emit('nodeClick', row.node)"
@mouseenter="emit('nodeMouseover', row.node)"
>
<div v-if="props.showLineNumber" class="json-view__line-number" :style="lineNumberStyle">
{{ getLineNumber(row.node, row.index) }}
</div>
<div class="json-view__line" :style="getLineStyle(row.node)">
<button
v-if="props.showIcon && row.node.hasToggle"
class="json-view__toggle"
type="button"
:class="{ 'is-expanded': row.node.isExpanded }"
:aria-label="row.node.isExpanded ? 'Collapse node' : 'Expand node'"
@click.stop="toggleNode(row.node, 'icon')"
></button>
<span v-if="hasKey(row.node)" class="json-view__key">
<KeyRenderer :node="row.node" />
<span class="json-view__colon">{{ props.showKeyValueSpace ? ": " : ":" }}</span>
</span>
<span
class="json-view__value"
:class="getValueClass(row.node)"
@click.stop="handleBracketClick(row.node)"
>
<ValueRenderer :node="row.node" />
</span>
<span v-if="props.showLength && shouldShowLength(row.node)" class="json-view__length">
({{ row.node.length }})
</span>
<ActionsRenderer v-if="hasActionsRenderer" :node="row.node" />
<div v-if="showsMenu" class="json-view__menu-shell">
<button
class="json-view__menu-trigger"
type="button"
aria-haspopup="menu"
:aria-expanded="isMenuOpen(row.node)"
aria-label="Open node menu"
@click.stop="openMenu(row.node, $event)"
></button>
</div>
</div>
</div>
</div>
<div v-else class="json-view__list">
<div
v-for="row in renderedRows"
:key="row.node.id"
class="json-view__row"
:style="getRowStyle(row)"
role="treeitem"
:aria-level="row.node.level + 1"
:aria-setsize="row.node.setSize"
:aria-posinset="row.node.posInSet"
:aria-expanded="row.node.hasToggle ? row.node.isExpanded : undefined"
tabindex="-1"
@click="emit('nodeClick', row.node)"
@mouseenter="emit('nodeMouseover', row.node)"
>
<div v-if="props.showLineNumber" class="json-view__line-number" :style="lineNumberStyle">
{{ getLineNumber(row.node, row.index) }}
</div>
<div class="json-view__line" :style="getLineStyle(row.node)">
<button
v-if="props.showIcon && row.node.hasToggle"
class="json-view__toggle"
type="button"
:class="{ 'is-expanded': row.node.isExpanded }"
:aria-label="row.node.isExpanded ? 'Collapse node' : 'Expand node'"
@click.stop="toggleNode(row.node, 'icon')"
></button>
<span v-if="hasKey(row.node)" class="json-view__key">
<KeyRenderer :node="row.node" />
<span class="json-view__colon">{{ props.showKeyValueSpace ? ": " : ":" }}</span>
</span>
<span
class="json-view__value"
:class="getValueClass(row.node)"
@click.stop="handleBracketClick(row.node)"
>
<ValueRenderer :node="row.node" />
</span>
<span v-if="props.showLength && shouldShowLength(row.node)" class="json-view__length">
({{ row.node.length }})
</span>
<ActionsRenderer v-if="hasActionsRenderer" :node="row.node" />
<div v-if="showsMenu" class="json-view__menu-shell">
<button
class="json-view__menu-trigger"
type="button"
aria-haspopup="menu"
:aria-expanded="isMenuOpen(row.node)"
aria-label="Open node menu"
@click.stop="openMenu(row.node, $event)"
></button>
</div>
</div>
</div>
</div>
</div>
<el-dropdown
v-if="showsMenu && activeMenuTrigger"
ref="sharedMenuRef"
class="json-view__shared-menu"
trigger="click"
placement="bottom-end"
popper-class="json-view__menu-dropdown"
:popper-options="{ strategy: 'fixed' }"
:virtual-ref="activeMenuTrigger"
virtual-triggering
@command="handleMenuCommand"
@visible-change="handleMenuVisibleChange"
>
<span class="json-view__shared-menu-anchor" aria-hidden="true"></span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item
v-for="item in activeMenuItems"
:key="item.key"
:command="item.key"
:disabled="item.disabled"
>
{{ item.label }}
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</template>
<script lang="tsx" setup>
import { computed, nextTick, onMounted, onUnmounted, ref, useSlots, type CSSProperties, type Slot } from "vue";
import type { DropdownInstance } from "element-plus";
import { measureShrinkWrapWidth, measureTextHeight } from "../list-table-v2/measureText";
import { useVirtualRows } from "../list-table-v2/useVirtualRows";
import { buildVisibleJsonRows } from "./flattenJson";
import type { JsonViewMenuActionContext, JsonViewMenuItem, JsonViewNode, JsonViewProps } from "./types";
const DEFAULT_FONT = "13px ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, Liberation Mono, monospace";
const DEFAULT_LINE_HEIGHT = 20;
const ROW_VERTICAL_PADDING = 0;
const LINE_NUMBER_PADDING_START = 6;
const LINE_NUMBER_PADDING_END = 6;
const MIN_LINE_NUMBER_WIDTH = 24;
const TOGGLE_WIDTH = 20;
const ROW_GAP = 4;
const MENU_TRIGGER_WIDTH = 18;
const EXTRA_GUTTER = 32;
interface RenderedRow {
index: number;
node: JsonViewNode;
height: number;
offsetY?: number;
}
const slots = useSlots() as {
renderNodeKey?: Slot;
renderNodeValue?: Slot;
renderNodeActions?: Slot;
};
const props = withDefaults(defineProps<JsonViewProps>(), {
data: undefined,
rootPath: "root",
indent: 2,
collapsedNodeLength: undefined,
deep: undefined,
showLength: false,
showLine: true,
showLineNumber: false,
showIcon: false,
showDoubleQuotes: true,
showKeyValueSpace: true,
virtual: false,
height: 400,
itemHeight: 20,
dynamicHeight: true,
collapsedOnClickBrackets: true,
theme: "light",
renderNodeKey: undefined,
renderNodeValue: undefined,
renderNodeActions: undefined,
showMenu: true,
menuItems: undefined,
});
const emit = defineEmits<{
(e: "nodeClick", node: JsonViewNode): void;
(e: "nodeMouseover", node: JsonViewNode): void;
(e: "bracketsClick", collapsed: boolean, node: JsonViewNode): void;
(e: "iconClick", collapsed: boolean, node: JsonViewNode): void;
}>();
const viewportRef = ref<HTMLElement | null>(null);
const viewportWidth = ref(0);
let resizeObserver: ResizeObserver | null = null;
const sharedMenuRef = ref<DropdownInstance>();
const expandedState = ref(new Map<string, boolean>());
const activeMenuNode = ref<JsonViewNode | null>(null);
const activeMenuTrigger = ref<HTMLElement | null>(null);
const suppressMenuHover = ref(false);
const indentSize = computed(() => Math.max(props.indent, 1) * 8);
const visibleNodes = computed(() =>
buildVisibleJsonRows(props.data, {
rootPath: props.rootPath,
deep: props.deep,
collapsedNodeLength: props.collapsedNodeLength,
expandedState: expandedState.value,
})
);
const hasActionsRenderer = computed(() => Boolean(props.renderNodeActions || slots.renderNodeActions));
const hasCustomRenderers = computed(() =>
Boolean(
props.renderNodeKey ||
props.renderNodeValue ||
props.renderNodeActions ||
slots.renderNodeKey ||
slots.renderNodeValue ||
slots.renderNodeActions
)
);
const wrapsLines = computed(() => props.dynamicHeight && !hasCustomRenderers.value);
const showsMenu = computed(() => props.showMenu);
const maxVisibleLineNumber = computed(() =>
visibleNodes.value.reduce((maxLineNumber, node) => Math.max(maxLineNumber, node.lineNumber), 1)
);
const lineNumberWidth = computed(() =>
Math.max(
MIN_LINE_NUMBER_WIDTH,
Math.ceil(
measureShrinkWrapWidth(String(maxVisibleLineNumber.value), DEFAULT_FONT) +
LINE_NUMBER_PADDING_START +
LINE_NUMBER_PADDING_END
)
)
);
const lineNumberStyle = computed<CSSProperties>(() => ({
flex: `0 0 ${lineNumberWidth.value}px`,
}));
const activeMenuItems = computed(() => (activeMenuNode.value ? getMenuItems(activeMenuNode.value) : []));
function hasKey(node: JsonViewNode) {
return node.key !== undefined || node.displayKey !== undefined || node.index !== undefined;
}
function shouldShowLength(node: JsonViewNode) {
return node.type === "objectCollapsed" || node.type === "arrayCollapsed";
}
function getDefaultKeyText(node: JsonViewNode) {
if (node.displayKey !== undefined) {
return node.displayKey;
}
if (node.index !== undefined) {
return String(node.index);
}
if (node.key === undefined) {
return "";
}
return props.showDoubleQuotes ? JSON.stringify(node.key) : node.key;
}
function getDefaultValueText(node: JsonViewNode) {
switch (node.type) {
case "objectEnd":
case "objectCollapsed":
case "arrayEnd":
case "arrayCollapsed":
case "content":
return node.showComma ? `${node.content},` : node.content;
case "objectStart":
case "arrayStart":
default:
return node.content;
}
}
function getMeasurementText(node: JsonViewNode) {
const key = getDefaultKeyText(node);
const value = getDefaultValueText(node);
const prefix = key ? `${key}${props.showKeyValueSpace ? ": " : ":"}` : "";
const suffix = props.showLength && shouldShowLength(node) ? ` (${node.length})` : "";
return {
prefix,
value,
suffix,
};
}
function getAvailableWidth(node: JsonViewNode) {
const currentLineNumberWidth = props.showLineNumber ? lineNumberWidth.value : 0;
const toggleWidth = props.showIcon ? TOGGLE_WIDTH + ROW_GAP : 0;
const menuWidth = showsMenu.value ? MENU_TRIGGER_WIDTH + ROW_GAP : 0;
const indentWidth = node.level * indentSize.value;
return Math.max(
80,
viewportWidth.value - currentLineNumberWidth - toggleWidth - menuWidth - indentWidth - EXTRA_GUTTER
);
}
function getLineNumber(node: JsonViewNode, fallbackIndex: number) {
return node.lineNumber || fallbackIndex + 1;
}
function getMeasurementHeight(node: JsonViewNode) {
const availableWidth = getAvailableWidth(node);
const { prefix, value, suffix } = getMeasurementText(node);
const suffixWithGap = suffix ? `${props.showLength ? " " : ""}${suffix.trimStart()}` : "";
const reservedWidth =
(prefix ? measureShrinkWrapWidth(prefix, DEFAULT_FONT) : 0) +
(suffixWithGap ? measureShrinkWrapWidth(suffixWithGap, DEFAULT_FONT) : 0);
const valueWidth = Math.max(80, availableWidth - reservedWidth);
const measurementText = `${value}${suffixWithGap}`;
return (
measureTextHeight(measurementText, DEFAULT_FONT, valueWidth, DEFAULT_LINE_HEIGHT) + ROW_VERTICAL_PADDING * 2
);
}
const rowHeights = computed(() => {
if (!wrapsLines.value || viewportWidth.value <= 0) {
return visibleNodes.value.map(() => props.itemHeight);
}
return visibleNodes.value.map((node) =>
Math.max(props.itemHeight, getMeasurementHeight(node))
);
});
const viewportHeight = computed(() => props.height);
const virtualizer = useVirtualRows(rowHeights, viewportHeight, { overscan: 8 });
const { visibleRows: virtualRows, totalHeight: virtualTotalHeight, onScroll } = virtualizer;
const renderedRows = computed<RenderedRow[]>(() => {
if (props.virtual) {
const rows: RenderedRow[] = [];
for (const row of virtualRows.value) {
const node = visibleNodes.value[row.index];
if (!node) continue;
rows.push({
index: row.index,
node,
height: row.height,
offsetY: row.offsetY,
});
}
return rows;
}
return visibleNodes.value.map((node, index) => ({
index,
node,
height: rowHeights.value[index] ?? props.itemHeight,
}));
});
const viewportStyle = computed<CSSProperties>(() => ({
height: props.virtual ? `${props.height}px` : undefined,
maxHeight: !props.virtual ? `${props.height}px` : undefined,
}));
function getLineStyle(node: JsonViewNode): CSSProperties {
const indentWidth = Math.max(0, node.level * indentSize.value);
const spacerWidth = props.showIcon && !node.hasToggle ? TOGGLE_WIDTH + ROW_GAP : 0;
return {
"--json-indent-size": `${indentSize.value}px`,
"--json-guides-width": `${indentWidth}px`,
"--json-leading-width": `${indentWidth + spacerWidth}px`,
paddingInlineStart: `${indentWidth + spacerWidth}px`,
} as CSSProperties;
}
function getRowStyle(row: RenderedRow): CSSProperties {
const style: CSSProperties = {
height: `${row.height}px`,
};
if (props.virtual) {
style.position = "absolute";
style.left = "0";
style.right = "0";
style.transform = `translateY(${row.offsetY ?? 0}px)`;
}
return style;
}
function updateExpandedState(path: string, nextExpanded: boolean) {
const next = new Map(expandedState.value);
next.set(path, nextExpanded);
expandedState.value = next;
}
function toggleNode(node: JsonViewNode, source: "icon" | "brackets") {
if (!node.hasToggle) return;
const nextExpanded = !node.isExpanded;
updateExpandedState(node.path, nextExpanded);
const collapsed = !nextExpanded;
if (source === "icon") {
emit("iconClick", collapsed, node);
} else {
emit("bracketsClick", collapsed, node);
}
}
function handleBracketClick(node: JsonViewNode) {
if (!props.collapsedOnClickBrackets || !node.hasToggle) {
return;
}
toggleNode(node, "brackets");
}
function handleScroll(event: Event) {
closeMenu();
if (!props.virtual) return;
onScroll((event.target as HTMLElement).scrollTop);
}
function getValueClass(node: JsonViewNode) {
if (node.type !== "content") {
return "json-view__value--container";
}
if (typeof node.value === "string") return "json-view__value--string";
if (typeof node.value === "number") return "json-view__value--number";
if (typeof node.value === "boolean") return "json-view__value--boolean";
if (node.value === null) return "json-view__value--null";
if (node.value === undefined) return "json-view__value--undefined";
return "json-view__value--container";
}
const KeyRenderer = ({ node }: { node: JsonViewNode }) => {
const defaultKey = getDefaultKeyText(node);
if (slots.renderNodeKey) {
return slots.renderNodeKey({ node, defaultKey });
}
if (props.renderNodeKey) {
return props.renderNodeKey({ node, defaultKey });
}
return defaultKey;
};
const ValueRenderer = ({ node }: { node: JsonViewNode }) => {
const defaultValue = getDefaultValueText(node);
if (slots.renderNodeValue) {
return slots.renderNodeValue({ node, defaultValue });
}
if (props.renderNodeValue) {
return props.renderNodeValue({ node, defaultValue });
}
return defaultValue;
};
const ActionsRenderer = ({ node }: { node: JsonViewNode }) => {
const defaultActions = null;
if (slots.renderNodeActions) {
return slots.renderNodeActions({ node, defaultActions });
}
if (props.renderNodeActions) {
return props.renderNodeActions({ node, defaultActions });
}
return null;
};
function toSerializableValue(value: unknown, ancestors = new WeakSet<object>()): unknown {
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean" || value === null) {
return value;
}
if (typeof value === "bigint") {
return `${String(value)}n`;
}
if (value === undefined) {
return "undefined";
}
if (typeof value === "symbol") {
return value.toString();
}
if (typeof value === "function") {
return value.name ? `[Function ${value.name}]` : "[Function]";
}
if (typeof value !== "object") {
return String(value);
}
if (ancestors.has(value)) {
return "[Circular]";
}
ancestors.add(value);
if (Array.isArray(value)) {
const serialized = value.map((entry) => toSerializableValue(entry, ancestors));
ancestors.delete(value);
return serialized;
}
if (value instanceof Set) {
const serialized = { $set: Array.from(value, (entry) => toSerializableValue(entry, ancestors)) };
ancestors.delete(value);
return serialized;
}
if (value instanceof Map) {
const serialized = {
$map: Array.from(value, ([key, entry]) => [
toSerializableValue(key, ancestors),
toSerializableValue(entry, ancestors),
]),
};
ancestors.delete(value);
return serialized;
}
const serialized = Object.fromEntries(
Object.entries(value).map(([key, entry]) => [key, toSerializableValue(entry, ancestors)])
);
ancestors.delete(value);
return serialized;
}
function serializeNodeValue(value: unknown): string {
if (typeof value === "string") {
return value;
}
if (value === undefined) {
return "undefined";
}
try {
const json = JSON.stringify(toSerializableValue(value), null, 2);
if (json !== undefined) {
return json;
}
} catch {
// Fall through to String() for circular or non-JSON-safe values.
}
return String(value);
}
async function copyText(text: string) {
try {
if (navigator.clipboard?.writeText) {
await navigator.clipboard.writeText(text);
return true;
}
} catch {
// Fall back to execCommand below.
}
const textarea = document.createElement("textarea");
textarea.value = text;
textarea.setAttribute("readonly", "true");
textarea.style.position = "fixed";
textarea.style.opacity = "0";
document.body.appendChild(textarea);
textarea.select();
try {
return document.execCommand("copy");
} finally {
document.body.removeChild(textarea);
}
}
function createMenuContext(node: JsonViewNode): JsonViewMenuActionContext {
return {
node,
path: getDisplayPath(node.path),
value: node.value,
copy: copyText,
closeMenu,
};
}
function getDisplayPath(path: string) {
if (path === props.rootPath) {
return "$";
}
if (path.startsWith(`${props.rootPath}.`)) {
return `$${path.slice(props.rootPath.length)}`;
}
if (path.startsWith(`${props.rootPath}[`)) {
return `$${path.slice(props.rootPath.length)}`;
}
return path;
}
function getMenuItems(node: JsonViewNode): JsonViewMenuItem[] {
const context = createMenuContext(node);
const defaultItems: JsonViewMenuItem[] = [
{
key: "copy-value",
label: "Copy Value",
onSelect: async ({ value, copy }) => {
await copy(serializeNodeValue(value));
},
},
{
key: "copy-path",
label: "Copy Path",
onSelect: async ({ path, copy }) => {
await copy(path);
},
},
];
if (!props.menuItems) {
return defaultItems;
}
const customItems = Array.isArray(props.menuItems) ? props.menuItems : props.menuItems(context);
return [...defaultItems, ...customItems];
}
async function handleMenuItemClick(item: JsonViewMenuItem, node: JsonViewNode) {
if (item.disabled) return;
await item.onSelect?.(createMenuContext(node));
closeMenu();
}
function isMenuOpen(node: JsonViewNode) {
return activeMenuNode.value?.id === node.id;
}
function closeMenu() {
sharedMenuRef.value?.handleClose();
suppressMenuHover.value = true;
activeMenuTrigger.value?.blur();
activeMenuNode.value = null;
activeMenuTrigger.value = null;
}
async function openMenu(node: JsonViewNode, event: MouseEvent) {
const trigger = event.currentTarget;
if (!(trigger instanceof HTMLElement)) return;
if (isMenuOpen(node)) {
closeMenu();
return;
}
activeMenuNode.value = node;
activeMenuTrigger.value = trigger;
suppressMenuHover.value = false;
await nextTick();
sharedMenuRef.value?.handleOpen();
}
function handleMenuVisibleChange(visible: boolean) {
if (!visible) {
suppressMenuHover.value = true;
activeMenuTrigger.value?.blur();
activeMenuNode.value = null;
activeMenuTrigger.value = null;
}
}
function handleMenuCommand(command: string | number | object) {
if (!activeMenuNode.value) return;
const item = activeMenuItems.value.find((entry) => entry.key === command);
if (!item) return;
void handleMenuItemClick(item, activeMenuNode.value);
}
function handleViewportPointerMove() {
if (suppressMenuHover.value) {
suppressMenuHover.value = false;
}
}
function clearMenuHoverSuppression() {
suppressMenuHover.value = false;
}
onMounted(() => {
if (viewportRef.value) {
viewportWidth.value = viewportRef.value.getBoundingClientRect().width;
}
resizeObserver = new ResizeObserver((entries) => {
for (const entry of entries) {
viewportWidth.value = entry.contentRect.width;
}
});
if (viewportRef.value) {
resizeObserver.observe(viewportRef.value);
}
});
onUnmounted(() => {
if (resizeObserver) {
resizeObserver.disconnect();
resizeObserver = null;
}
});
</script>
<style lang="scss" scoped>
.json-view {
--json-bg: #fff;
--json-border: #dfe4ea;
--json-color: #1f2937;
--json-muted: #6b7280;
--json-key: #7c3aed;
--json-string: #0f766e;
--json-number: #c2410c;
--json-boolean: #2563eb;
--json-null: #9333ea;
--json-guide: rgba(148, 163, 184, 0.26);
--json-hover: rgba(51, 103, 209, 0.08);
--json-toggle: #64748b;
--json-menu-bg: rgba(255, 255, 255, 0.96);
--json-menu-border: rgba(203, 213, 225, 0.9);
--json-menu-shadow: 0 12px 32px rgba(15, 23, 42, 0.12);
--json-row-gap: 4px;
background: var(--json-bg);
border: 1px solid var(--json-border);
border-radius: 10px;
color: var(--json-color);
font: 13px/20px ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, Liberation Mono, monospace;
}
.json-view--dark {
--json-bg: #0f172a;
--json-border: #243041;
--json-color: #e2e8f0;
--json-muted: #94a3b8;
--json-key: #c084fc;
--json-string: #34d399;
--json-number: #fb923c;
--json-boolean: #60a5fa;
--json-null: #d8b4fe;
--json-guide: rgba(148, 163, 184, 0.18);
--json-hover: rgba(96, 165, 250, 0.12);
--json-toggle: #94a3b8;
--json-menu-bg: rgba(15, 23, 42, 0.96);
--json-menu-border: rgba(71, 85, 105, 0.9);
--json-menu-shadow: 0 12px 32px rgba(2, 6, 23, 0.45);
}
.json-view__viewport {
overflow-y: auto;
overflow-x: auto;
position: relative;
min-height: 80px;
}
.json-view--wrap-lines .json-view__viewport {
overflow-x: hidden;
}
.json-view__spacer {
position: relative;
}
.json-view__list {
position: relative;
}
.json-view__row {
display: flex;
align-items: stretch;
min-width: max-content;
}
.json-view--wrap-lines .json-view__row {
min-width: 100%;
}
.json-view__row:hover {
background: var(--json-hover);
}
.json-view__line-number {
box-sizing: border-box;
color: var(--json-muted);
padding: 0 6px 0 6px;
text-align: right;
user-select: none;
}
.json-view__line {
align-items: flex-start;
box-sizing: border-box;
display: flex;
flex: 1;
gap: var(--json-row-gap);
min-height: 100%;
min-width: 0;
padding: 0 12px 0 8px;
position: relative;
white-space: nowrap;
}
.json-view--wrap-lines .json-view__line {
width: 100%;
}
.json-view--show-line .json-view__line::before {
background-image: repeating-linear-gradient(
to right,
transparent 0 calc(var(--json-indent-size) - 1px),
var(--json-guide) calc(var(--json-indent-size) - 1px) var(--json-indent-size)
);
background-position: left top;
background-repeat: no-repeat;
background-size: var(--json-guides-width) 100%;
content: "";
inset: 0 auto 0 0;
pointer-events: none;
position: absolute;
width: var(--json-leading-width);
}
.json-view__toggle {
align-self: flex-start;
flex: 0 0 20px;
height: 20px;
position: relative;
width: 20px;
}
.json-view__toggle {
background: transparent;
border: none;
border-radius: 6px;
color: var(--json-toggle);
cursor: pointer;
padding: 0;
}
.json-view__toggle:hover {
background: rgba(148, 163, 184, 0.1);
}
.json-view__toggle::before {
border-bottom: 1.5px solid currentColor;
border-right: 1.5px solid currentColor;
content: "";
height: 6px;
inset: 6px auto auto 6px;
position: absolute;
transform: rotate(-45deg);
transition: transform 0.12s ease;
width: 6px;
}
.json-view__toggle.is-expanded::before {
transform: rotate(45deg);
}
.json-view__key {
color: var(--json-key);
flex: 0 1 auto;
}
.json-view__colon {
color: var(--json-muted);
}
.json-view__value {
color: var(--json-color);
flex: 1 1 auto;
min-width: 0;
}
.json-view--wrap-lines .json-view__value {
overflow-wrap: anywhere;
white-space: pre-wrap;
word-break: break-word;
}
.json-view--wrap-lines .json-view__key,
.json-view--wrap-lines .json-view__length {
white-space: nowrap;
}
.json-view:not(.json-view--wrap-lines) .json-view__value {
overflow: hidden;
text-overflow: ellipsis;
}
.json-view__value--string {
color: var(--json-string);
}
.json-view__value--number {
color: var(--json-number);
}
.json-view__value--boolean {
color: var(--json-boolean);
}
.json-view__value--null,
.json-view__value--undefined {
color: var(--json-null);
}
.json-view__value--container {
color: var(--json-color);
}
.json-view__length {
color: var(--json-muted);
}
.json-view__menu-shell {
margin-left: auto;
position: relative;
}
.json-view__menu-trigger {
background: transparent;
border: none;
border-radius: 6px;
color: var(--json-toggle);
cursor: pointer;
height: 18px;
opacity: 0;
padding: 0;
pointer-events: none;
position: relative;
transition:
opacity 0.12s ease,
background-color 0.12s ease;
width: 18px;
}
.json-view__menu-trigger::before {
background: currentColor;
border-radius: 50%;
box-shadow:
0 -4px 0 currentColor,
0 4px 0 currentColor;
content: "";
height: 3px;
inset: 7px auto auto 7px;
position: absolute;
width: 3px;
}
.json-view:not(.json-view--suppress-menu-hover) .json-view__row:hover .json-view__menu-trigger,
.json-view__row:focus-within .json-view__menu-trigger,
.json-view__menu-trigger[aria-expanded="true"] {
opacity: 1;
pointer-events: auto;
}
.json-view__menu-trigger:hover,
.json-view__menu-trigger:focus-visible {
background: rgba(148, 163, 184, 0.1);
outline: none;
}
:deep(.json-view__menu-dropdown.el-popper) {
background: var(--json-menu-bg);
border: 1px solid var(--json-menu-border);
border-radius: 10px;
box-shadow: var(--json-menu-shadow);
padding: 6px;
}
:deep(.json-view__menu-dropdown .el-popper__arrow) {
display: none;
}
:deep(.json-view__menu-dropdown .el-dropdown-menu) {
background: transparent;
border: none;
box-shadow: none;
min-width: 136px;
padding: 0;
}
:deep(.json-view__menu-dropdown .el-dropdown-menu__item) {
border-radius: 8px;
color: inherit;
font: inherit;
line-height: 1.25;
padding: 7px 10px;
}
:deep(.json-view__menu-dropdown .el-dropdown-menu__item:not(.is-disabled):hover),
:deep(.json-view__menu-dropdown .el-dropdown-menu__item:focus-visible) {
background: rgba(148, 163, 184, 0.12);
color: inherit;
}
:deep(.json-view__menu-dropdown .el-dropdown-menu__item.is-disabled) {
opacity: 0.5;
}
.json-view__shared-menu {
height: 0;
left: 0;
overflow: hidden;
pointer-events: none;
position: absolute;
top: 0;
width: 0;
}
.json-view__shared-menu-anchor {
display: block;
height: 0;
width: 0;
}
</style>