feat(组件): JSONPathPicker 组件调整&新增 xpath 选择组件
This commit is contained in:
parent
25d4618192
commit
3185bf181e
|
@ -6,3 +6,4 @@
|
|||
**/*.svg
|
||||
**/*.sh
|
||||
src/auto-import.d.ts
|
||||
src/assets/js
|
|
@ -49,7 +49,6 @@
|
|||
"echarts": "^5.4.3",
|
||||
"hotbox-minder": "1.0.15",
|
||||
"jsencrypt": "^3.3.2",
|
||||
"jsonpath-picker-vanilla": "^1.2.4",
|
||||
"localforage": "^1.10.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
"mitt": "^3.0.1",
|
||||
|
|
|
@ -0,0 +1,481 @@
|
|||
/* eslint-disable */
|
||||
|
||||
|
||||
function _typeof(obj) { if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); }
|
||||
|
||||
/**
|
||||
* Check if arg is either an array with at least 1 element, or a dict with at least 1 key
|
||||
* @return boolean
|
||||
*/
|
||||
function isCollapsable(arg) {
|
||||
return arg instanceof Object && Object.keys(arg).length > 0;
|
||||
}
|
||||
/**
|
||||
* Check if a string represents a valid url
|
||||
* @return boolean
|
||||
*/
|
||||
|
||||
|
||||
function isUrl(string) {
|
||||
const regexp = /^(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#:.?+=&%@!\-/]))?/;
|
||||
return regexp.test(string);
|
||||
}
|
||||
/**
|
||||
* Transform a json object into html representation
|
||||
* @return string
|
||||
*/
|
||||
|
||||
|
||||
function json2html(json, options) {
|
||||
let html = '';
|
||||
|
||||
if (typeof json === 'string') {
|
||||
// Escape tags
|
||||
const tmp = json.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
||||
|
||||
if (json.includes('Number')) {
|
||||
html += "<span class=\"json-literal\">".concat(json.replace(/Number\(([^)]+)\)/g, "$1"), "</span>");
|
||||
} else if (isUrl(tmp)) {
|
||||
html += "<a href=\"".concat(tmp, "\" class=\"json-string\">").concat(tmp, "</a>");
|
||||
} else {
|
||||
html += "<span class=\"json-string\">\"".concat(tmp, "\"</span>");
|
||||
}
|
||||
} else if (typeof json === 'number') {
|
||||
html += "<span class=\"json-literal\">".concat(json, "</span>");
|
||||
} else if (typeof json === 'boolean') {
|
||||
html += "<span class=\"json-literal\">".concat(json, "</span>");
|
||||
} else if (json === null) {
|
||||
html += '<span class="json-literal">null</span>';
|
||||
} else if (json instanceof Array) {
|
||||
if (json.length > 0) {
|
||||
html += '[<ol class="json-array">';
|
||||
|
||||
for (let i = 0; i < json.length; i += 1) {
|
||||
html += "<li data-key-type=\"array\" data-key=\"".concat(i, "\">"); // Add toggle button if item is collapsable
|
||||
|
||||
if (isCollapsable(json[i])) {
|
||||
html += '<a href class="json-toggle"></a>';
|
||||
}
|
||||
|
||||
html += json2html(json[i], options); // Add comma if item is not last
|
||||
|
||||
if (i < json.length - 1) {
|
||||
html += ',';
|
||||
}
|
||||
|
||||
html += '</li>';
|
||||
}
|
||||
|
||||
html += '</ol>]';
|
||||
} else {
|
||||
html += '[]';
|
||||
}
|
||||
} else if (_typeof(json) === 'object') {
|
||||
let keyCount = Object.keys(json).length;
|
||||
|
||||
if (keyCount > 0) {
|
||||
html += '{<ul class="json-dict">';
|
||||
|
||||
for (const key in json) {
|
||||
if (json.hasOwnProperty(key)) {
|
||||
html += "<li data-key-type=\"object\" data-key=\"".concat(key, "\">");
|
||||
const keyRepr = options.outputWithQuotes ? "<span class=\"json-string\">\"".concat(key, "\"</span>") : key; // Add toggle button if item is collapsable
|
||||
|
||||
if (isCollapsable(json[key])) {
|
||||
html += "<a href class=\"json-toggle\">".concat(keyRepr, "</a>");
|
||||
} else {
|
||||
html += keyRepr;
|
||||
}
|
||||
|
||||
html += `<span class="pick-path" title="Pick path">&${ options.pickerIcon };</span>`;
|
||||
html += ": ".concat(json2html(json[key], options)); // Add comma if item is not last
|
||||
|
||||
keyCount -= 1;
|
||||
|
||||
if (keyCount > 0) {
|
||||
html += ',';
|
||||
}
|
||||
|
||||
html += '</li>';
|
||||
}
|
||||
}
|
||||
|
||||
html += '</ul>}';
|
||||
} else {
|
||||
html += '{}';
|
||||
}
|
||||
}
|
||||
|
||||
return html;
|
||||
}
|
||||
/**
|
||||
* Remove an event listener
|
||||
* @param {String} event The event type
|
||||
* @param {Node} elem The element to remove the event to (optional, defaults to window)
|
||||
* @param {Function} callback The callback that ran on the event
|
||||
* @param {Boolean} capture If true, forces bubbling on non-bubbling events
|
||||
*/
|
||||
|
||||
|
||||
function off(event, elem, callback, capture) {
|
||||
let captureIntern = capture;
|
||||
let callbackIntern = callback;
|
||||
let elemIntern = elem;
|
||||
|
||||
if (typeof elem === 'function') {
|
||||
captureIntern = callback;
|
||||
callbackIntern = elem;
|
||||
elemIntern = window;
|
||||
}
|
||||
|
||||
captureIntern = !!captureIntern;
|
||||
elemIntern = typeof elemIntern === 'string' ? document.querySelector(elemIntern) : elemIntern;
|
||||
if (!elemIntern) return;
|
||||
elemIntern.removeEventListener(event, callbackIntern, captureIntern);
|
||||
}
|
||||
/**
|
||||
* Equivalent of JQuery $().siblings(sel) with callback features
|
||||
*
|
||||
* Retrieve all siblings/neighbors of a node
|
||||
* Usage:
|
||||
* - siblings(node, '.collapse', (sib) => { });
|
||||
* - const sibs = siblings(node);
|
||||
* - const sibs = siblings(node, '.collapse');
|
||||
*
|
||||
* @param {HTMLNode} el Element to apply siblings methods
|
||||
* @param {String} sel CSS Selector
|
||||
* @param {Function} callback (sib) => {}
|
||||
*/
|
||||
|
||||
|
||||
function siblings(el, sel, callback) {
|
||||
const sibs = [];
|
||||
|
||||
for (let i = 0; i < el.parentNode.children.length; i += 1) {
|
||||
const child = el.parentNode.children[i];
|
||||
|
||||
if (child !== el && typeof sel === 'string' && child.matches(sel)) {
|
||||
sibs.push(child);
|
||||
}
|
||||
} // If a callback is passed, call it on each sibs
|
||||
|
||||
|
||||
if (callback && typeof callback === 'function') {
|
||||
for (let _i = 0; _i < sibs.length; _i += 1) {
|
||||
callback(sibs[_i]);
|
||||
}
|
||||
}
|
||||
|
||||
return sibs;
|
||||
}
|
||||
/**
|
||||
* Fire a click handler to the specified node.
|
||||
* Event handlers can detect that the event was fired programatically
|
||||
* by testing for a 'synthetic=true' property on the event object
|
||||
* @param {HTMLNode} node The node to fire the event handler on.
|
||||
*/
|
||||
|
||||
|
||||
function fireClick(node) {
|
||||
// Make sure we use the ownerDocument from the provided node to avoid cross-window problems
|
||||
let doc;
|
||||
|
||||
if (node.ownerDocument) {
|
||||
doc = node.ownerDocument;
|
||||
} else if (node.nodeType === 9) {
|
||||
// the node may be the document itself, nodeType 9 = DOCUMENT_NODE
|
||||
doc = node;
|
||||
} else {
|
||||
throw new Error("Invalid node passed to fireEvent: ".concat(node.id));
|
||||
}
|
||||
|
||||
if (node.dispatchEvent) {
|
||||
const eventClass = 'MouseEvents';
|
||||
const event = doc.createEvent(eventClass);
|
||||
event.initEvent('click', true, true); // All events created as bubbling and cancelable.
|
||||
|
||||
event.synthetic = true;
|
||||
node.dispatchEvent(event, true);
|
||||
} else if (node.fireEvent) {
|
||||
// IE-old school style, you can drop this if you don't need to support IE8 and lower
|
||||
const _event = doc.createEventObject();
|
||||
|
||||
_event.synthetic = true; // allow detection of synthetic events
|
||||
|
||||
node.fireEvent('onclick', _event);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Check if an element is visible or not
|
||||
* @param {HTMLNode} elem Element to check
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
|
||||
|
||||
function isHidden(elem) {
|
||||
const width = elem.offsetWidth;
|
||||
const height = elem.offsetHeight;
|
||||
return width === 0 && height === 0 || window.getComputedStyle(elem).display === 'none';
|
||||
}
|
||||
/**
|
||||
* Method use to retrieve parents of given element
|
||||
* @param {HTMLNode} elem Element which we want parents
|
||||
* @param {Strign} sel selector to filter parents (CSS selectors)
|
||||
* @returns {Array<HTMLNode>}
|
||||
*/
|
||||
|
||||
|
||||
function getParents(elem, sel) {
|
||||
const result = [];
|
||||
|
||||
for (let p = elem && elem.parentElement; p; p = p.parentElement) {
|
||||
if (typeof sel === 'string' && p.matches(sel)) {
|
||||
result.push(p);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function HandlerEventToggle(elm, event) {
|
||||
// Change class
|
||||
elm.classList.toggle('collapsed'); // Fetch every json-dict and json-array to toggle them
|
||||
|
||||
const subTarget = siblings(elm, 'ul.json-dict, ol.json-array', function (el) {
|
||||
el.style.display = el.style.display === '' || el.style.display === 'block' ? 'none' : 'block';
|
||||
}); // ForEach subtarget, previous siblings return array so we parse it
|
||||
|
||||
for (let i = 0; i < subTarget.length; i += 1) {
|
||||
if (!isHidden(subTarget[i])) {
|
||||
// Parse every siblings with '.json-placehoder' and remove them (previous add by else)
|
||||
siblings(subTarget[i], '.json-placeholder', function (el) {
|
||||
return el.parentNode.removeChild(el);
|
||||
});
|
||||
} else {
|
||||
// count item in object / array
|
||||
const childs = subTarget[i].children;
|
||||
let count = 0;
|
||||
|
||||
for (let j = 0; j < childs.length; j += 1) {
|
||||
if (childs[j].tagName === 'LI') {
|
||||
count += 1;
|
||||
}
|
||||
}
|
||||
|
||||
const placeholder = count + (count > 1 ? ' items' : ' item'); // Append a placeholder
|
||||
|
||||
subTarget[i].insertAdjacentHTML('afterend', "<a href class=\"json-placeholder\">".concat(placeholder, "</a>"));
|
||||
}
|
||||
} // Prevent propagation
|
||||
|
||||
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
function ToggleEventListener(event) {
|
||||
let t = event.target;
|
||||
|
||||
while (t && t !== this) {
|
||||
if (t.matches('a.json-toggle')) {
|
||||
HandlerEventToggle.call(null, t, event);
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
t = t.parentNode;
|
||||
}
|
||||
}
|
||||
|
||||
; // Simulate click on toggle button when placeholder is clicked
|
||||
|
||||
function SimulateClickHandler(elm, event) {
|
||||
siblings(elm, 'a.json-toggle', function (el) {
|
||||
return fireClick(el);
|
||||
});
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
function SimulateClickEventListener(event) {
|
||||
let t = event.target;
|
||||
|
||||
while (t && t !== this) {
|
||||
if (t.matches('a.json-placeholder')) {
|
||||
SimulateClickHandler.call(null, t, event);
|
||||
}
|
||||
|
||||
t = t.parentNode;
|
||||
}
|
||||
}
|
||||
|
||||
function PickPathHandler(elm) {
|
||||
if (targetList.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const $parentsList = getParents(elm, 'li').reverse();
|
||||
let pathSegments = [];
|
||||
|
||||
for (let i = 0; i < $parentsList.length; i += 1) {
|
||||
let {key} = $parentsList[i].dataset;
|
||||
const {keyType} = $parentsList[i].dataset;
|
||||
|
||||
if (keyType === 'object' && typeof key !== 'number' && options.processKeys && options.keyReplaceRegexPattern !== undefined) {
|
||||
const keyReplaceRegex = new RegExp(options.keyReplaceRegexPattern, options.keyReplaceRegexFlags);
|
||||
const keyReplacementText = options.keyReplacementText === undefined ? '' : options.keyReplacementText;
|
||||
key = key.replace(keyReplaceRegex, keyReplacementText);
|
||||
}
|
||||
|
||||
pathSegments.push({
|
||||
key,
|
||||
keyType
|
||||
});
|
||||
}
|
||||
|
||||
const quotes = {
|
||||
none: '',
|
||||
single: '\'',
|
||||
"double": '"'
|
||||
};
|
||||
const quote = quotes[options.pathQuotesType];
|
||||
pathSegments = pathSegments.map(function (segment, idx) {
|
||||
const isBracketsNotation = options.pathNotation === 'brackets';
|
||||
const isKeyForbiddenInDotNotation = !/^\w+$/.test(segment.key) || typeof segment.key === 'number';
|
||||
|
||||
if (segment.keyType === 'array' || segment.isKeyANumber) {
|
||||
return "[".concat(segment.key, "]");
|
||||
}
|
||||
|
||||
if (isBracketsNotation || isKeyForbiddenInDotNotation) {
|
||||
return "[".concat(quote).concat(segment.key).concat(quote, "]");
|
||||
}
|
||||
|
||||
if (idx > 0) {
|
||||
return ".".concat(segment.key);
|
||||
}
|
||||
|
||||
return segment.key;
|
||||
});
|
||||
const path = pathSegments.join('');
|
||||
|
||||
for (let _i2 = 0; _i2 < targetList.length; _i2 += 1) {
|
||||
if (targetList[_i2].value !== undefined) {
|
||||
targetList[_i2].value = path;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function PickEventListener(event) {
|
||||
let t = event.target;
|
||||
|
||||
while (t && t !== this) {
|
||||
if (t.matches('.pick-path')) {
|
||||
PickPathHandler.call(null, t);
|
||||
}
|
||||
|
||||
t = t.parentNode;
|
||||
}
|
||||
} // Uniq id generator
|
||||
|
||||
|
||||
function uuidv4() {
|
||||
function randomString(length, chars) {
|
||||
let result = '';
|
||||
|
||||
for (let i = length; i > 0; --i) {
|
||||
result += chars[Math.floor(Math.random() * chars.length)];
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
return randomString(32, 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ');
|
||||
}
|
||||
|
||||
var targetList = [];
|
||||
var options = {};
|
||||
/**
|
||||
* Plugin method
|
||||
* @param source: Element
|
||||
* @param json: a javascript object
|
||||
* @param target: NodeListOf<Element> | Element | { value: String }[] | { value: String }
|
||||
* @param opt: an optional options hash
|
||||
*/
|
||||
|
||||
function jsonPathPicker(source, json, target, opt) {
|
||||
options = opt || {};
|
||||
|
||||
if (!(source instanceof Element)) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (target) {
|
||||
if (target.length) {
|
||||
targetList = target;
|
||||
} else if (target.value) {
|
||||
targetList = [target];
|
||||
} else {
|
||||
return 3;
|
||||
}
|
||||
} else {
|
||||
return 3;
|
||||
} // Add to source unique identifier
|
||||
|
||||
|
||||
const uuid = uuidv4();
|
||||
source.id = source.id ? "".concat(source.id, " ").concat(uuid) : uuid;
|
||||
source.setAttribute('data-jsonpath-uniq-id', uuid);
|
||||
options.pathQuotesType = options.pathQuotesType !== undefined ? options.pathQuotesType : 'single'; // Transform to HTML
|
||||
|
||||
options.pickerIcon = options.pickerIcon || '#x1f4cb';
|
||||
let html = json2html(json, options);
|
||||
if (isCollapsable(json)) html = "<a href class=\"json-toggle\"></a>".concat(html); // Insert HTML in target DOM element
|
||||
|
||||
source.innerHTML = html; // Bind click on toggle buttons
|
||||
|
||||
off('click', source);
|
||||
source.addEventListener('click', ToggleEventListener);
|
||||
source.addEventListener('click', SimulateClickEventListener); // Bind picker only if user didn't diseable it
|
||||
|
||||
if (!options.WithoutPicker) {
|
||||
source.addEventListener('click', PickEventListener);
|
||||
} else {
|
||||
// Remove every picker icon
|
||||
const sourceSelector = source.getAttribute('data-jsonpath-uniq-id'); // Prevent affect other jp-picker
|
||||
|
||||
document.querySelectorAll("[id*='".concat(sourceSelector, "'] .pick-path")).forEach(function (el) {
|
||||
return el.parentNode.removeChild(el);
|
||||
});
|
||||
}
|
||||
|
||||
if (options.outputCollapsed === true) {
|
||||
// Trigger click to collapse all nodes
|
||||
const elms = document.querySelectorAll('a.json-toggle');
|
||||
|
||||
for (let i = 0; i < elms.length; i += 1) {
|
||||
fireClick(elms[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Plugin clear method
|
||||
* @param source: Element
|
||||
*/
|
||||
|
||||
|
||||
function clearJsonPathPicker(source) {
|
||||
if (!(source instanceof Element)) {
|
||||
return 1;
|
||||
} // Remove event listener
|
||||
|
||||
|
||||
source.removeEventListener('click', PickEventListener);
|
||||
source.removeEventListener('click', ToggleEventListener);
|
||||
source.removeEventListener('click', SimulateClickEventListener);
|
||||
}
|
||||
|
||||
export default {
|
||||
jsonPathPicker,
|
||||
clearJsonPathPicker
|
||||
};
|
|
@ -0,0 +1,55 @@
|
|||
/* stylelint-disable order/properties-order */
|
||||
ul.json-dict, ol.json-array {
|
||||
list-style-type: none;
|
||||
margin: 0 0 0 1px;
|
||||
border-left: 1px dotted #cccccc;
|
||||
padding-left: 2em;
|
||||
}
|
||||
.json-string {
|
||||
color: #0b7500;
|
||||
}
|
||||
.json-literal {
|
||||
color: #1a01cc;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* Toggle button */
|
||||
a.json-toggle {
|
||||
position: relative;
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
a.json-toggle:focus {
|
||||
outline: none;
|
||||
}
|
||||
a.json-toggle::before {
|
||||
color: #aaaaaa;
|
||||
content: '\25BC'; /* down arrow */
|
||||
position: absolute;
|
||||
display: inline-block;
|
||||
width: 1em;
|
||||
left: -1em;
|
||||
}
|
||||
a.json-toggle.collapsed::before {
|
||||
content: '\25B6'; /* left arrow */
|
||||
}
|
||||
|
||||
/* Collapsable placeholder links */
|
||||
a.json-placeholder {
|
||||
color: #aaaaaa;
|
||||
padding: 0 1em;
|
||||
text-decoration: none;
|
||||
}
|
||||
a.json-placeholder:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* Copy path icon */
|
||||
.pick-path {
|
||||
color: lightgray;
|
||||
cursor: pointer;
|
||||
margin-left: 3px;
|
||||
}
|
||||
.pick-path:hover {
|
||||
color: darkgray;
|
||||
}
|
|
@ -6,8 +6,9 @@
|
|||
<script setup lang="ts">
|
||||
import { onMounted, onUnmounted, Ref, ref } from 'vue';
|
||||
|
||||
import JPPicker from '@/assets/js/jsonpath-picker-vanilla/jsonpath-picker-vanilla';
|
||||
|
||||
import { Recordable } from '#/global';
|
||||
import JPPicker from 'jsonpath-picker-vanilla';
|
||||
|
||||
const jr: Ref<HTMLElement | null> = ref(null);
|
||||
const ip: Ref<HTMLInputElement | null> = ref(null);
|
||||
|
@ -21,10 +22,40 @@
|
|||
}>(),
|
||||
{
|
||||
data: () => ({
|
||||
name1: 'val1',
|
||||
name2: {
|
||||
name3: 'val2',
|
||||
users: [
|
||||
{
|
||||
id: 1,
|
||||
name: 'John',
|
||||
age: 'Number(25.0000000000000000000)',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'Jane',
|
||||
age: 30,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: 'Mike',
|
||||
age: 28,
|
||||
},
|
||||
],
|
||||
products: [
|
||||
{
|
||||
id: 101,
|
||||
name: 'iPhone',
|
||||
price: 999,
|
||||
},
|
||||
{
|
||||
id: 102,
|
||||
name: 'MacBook',
|
||||
price: 1599,
|
||||
},
|
||||
{
|
||||
id: 103,
|
||||
name: 'iPad',
|
||||
price: 799,
|
||||
},
|
||||
],
|
||||
}),
|
||||
opt: () => ({}),
|
||||
}
|
||||
|
@ -55,5 +86,5 @@
|
|||
</script>
|
||||
|
||||
<style lang="css">
|
||||
@import url('jsonpath-picker-vanilla/lib/jsonpath-picker.css');
|
||||
@import url('@/assets/js/jsonpath-picker-vanilla/jsonpath-picker.css');
|
||||
</style>
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
export interface XpathNode {
|
||||
content: string;
|
||||
xpath: string;
|
||||
}
|
|
@ -0,0 +1,117 @@
|
|||
<template>
|
||||
<div v-if="parsedXml">
|
||||
<div v-for="(node, index) in flattenedXml" :key="index">
|
||||
<span style="white-space: pre" @click="copyXPath(node.xpath)" v-html="node.content"></span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { XpathNode } from './types';
|
||||
|
||||
const props = defineProps<{
|
||||
xmlString: string;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits(['pick']);
|
||||
|
||||
const parsedXml = ref<Document | null>(null);
|
||||
const flattenedXml = ref<XpathNode[]>([]);
|
||||
const tempXmls = ref<XpathNode[]>([]);
|
||||
|
||||
/**
|
||||
* 获取同名兄弟节点
|
||||
* @param node 节点
|
||||
*/
|
||||
function getSameNameSiblings(node: HTMLElement | Element) {
|
||||
const siblings = node.parentNode ? Array.from(node.parentNode.children) : [];
|
||||
return siblings.filter((sibling) => sibling.tagName === node.tagName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将xml扁平化
|
||||
* @param node xml节点
|
||||
* @param currentPath 当前路径
|
||||
*/
|
||||
function flattenXml(node: HTMLElement | Element, currentPath: string) {
|
||||
const sameNameSiblings = getSameNameSiblings(node);
|
||||
if (sameNameSiblings.length > 1) {
|
||||
// 同级的同名节点数量大于 1 时,需要给当前节点名的 xpath 添加下标
|
||||
const sameNodesIndex = document.evaluate(
|
||||
`count(ancestor-or-self::*/preceding-sibling::${node.nodeName}) + 1`,
|
||||
node,
|
||||
null,
|
||||
XPathResult.NUMBER_TYPE,
|
||||
null
|
||||
).numberValue; // 这里是执行 XPATH 表达式,获取当前节点在同级节点中的下标
|
||||
|
||||
const xpath = `${currentPath}/${node.tagName}[${sameNodesIndex}]`; // 拼接规则:当前路径/当前节点名[当前节点在同级同名节点中的下标]
|
||||
tempXmls.value.push({ content: node.tagName, xpath });
|
||||
const children = Array.from(node.children);
|
||||
children.forEach((child) => {
|
||||
flattenXml(child, xpath); // 递归处理子节点
|
||||
});
|
||||
} else {
|
||||
// 同级的同名节点数量等于 1 时,不需要给当前节点名的 xpath 添加下标,因为这个标签是唯一的
|
||||
const xpath = `${currentPath}/${node.tagName}`;
|
||||
tempXmls.value.push({ content: node.tagName, xpath });
|
||||
const children = Array.from(node.children);
|
||||
children.forEach((child) => {
|
||||
flattenXml(child, xpath);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function copyXPath(xpath: string) {
|
||||
if (xpath) {
|
||||
emit('pick', xpath);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析xml
|
||||
*/
|
||||
function parseXml() {
|
||||
try {
|
||||
const parser = new DOMParser();
|
||||
const xmlDoc = parser.parseFromString(props.xmlString, 'application/xml');
|
||||
parsedXml.value = xmlDoc;
|
||||
// 先将 XML 字符串解析转换并给每个开始标签加上复制 icon
|
||||
flattenedXml.value = props.xmlString
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/(<\w+\s*[^>]*>)/g, '<span style="color: rgb(var(--primary-5));cursor: pointer">$1📋</span>')
|
||||
.replace(/(<\/\w+\s*[^>]*>)/g, '<span style="color: rgb(var(--primary-5));">$1</span>')
|
||||
.split(/\r?\n/)
|
||||
.map((e) => ({ content: e, xpath: '' }));
|
||||
// 解析真实 XML 并将其扁平化,得到每个节点的 xpath
|
||||
flattenXml(xmlDoc.documentElement, '');
|
||||
// 将扁平化后的 XML 字符串中的每个节点的 xpath 替换为真实的 xpath
|
||||
flattenedXml.value = flattenedXml.value.map((e) => {
|
||||
const targetNodeIndex = tempXmls.value.findIndex((t) => e.content.includes(`<${t.content}`));
|
||||
if (targetNodeIndex >= 0) {
|
||||
const { xpath } = tempXmls.value[targetNodeIndex];
|
||||
tempXmls.value.splice(targetNodeIndex, 1); // 匹配成功后,将匹配到的节点从 tempXmls 中删除,避免重复匹配
|
||||
return {
|
||||
...e,
|
||||
xpath,
|
||||
};
|
||||
}
|
||||
return e;
|
||||
});
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('Error parsing XML:', error);
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.xmlString,
|
||||
() => {
|
||||
parseXml();
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
}
|
||||
);
|
||||
</script>
|
Loading…
Reference in New Issue