From 3185bf181ea821c58372e2aa0fb6d8f0453b95bc Mon Sep 17 00:00:00 2001 From: baiqi Date: Tue, 14 Nov 2023 14:30:19 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E7=BB=84=E4=BB=B6):=20=20JSONPathPicker?= =?UTF-8?q?=20=E7=BB=84=E4=BB=B6=E8=B0=83=E6=95=B4&=E6=96=B0=E5=A2=9E=20xp?= =?UTF-8?q?ath=20=E9=80=89=E6=8B=A9=E7=BB=84=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/.prettierignore | 3 +- frontend/package.json | 1 - .../jsonpath-picker-vanilla.js | 481 ++++++++++++++++++ .../jsonpath-picker.css | 55 ++ .../components/pure/jsonpath-picker/index.vue | 43 +- .../components/pure/jsonpath-picker/types.ts | 4 + .../components/pure/jsonpath-picker/xpath.vue | 117 +++++ 7 files changed, 696 insertions(+), 8 deletions(-) create mode 100644 frontend/src/assets/js/jsonpath-picker-vanilla/jsonpath-picker-vanilla.js create mode 100644 frontend/src/assets/js/jsonpath-picker-vanilla/jsonpath-picker.css create mode 100644 frontend/src/components/pure/jsonpath-picker/types.ts create mode 100644 frontend/src/components/pure/jsonpath-picker/xpath.vue diff --git a/frontend/.prettierignore b/frontend/.prettierignore index 8d73cf19ea..1963131606 100644 --- a/frontend/.prettierignore +++ b/frontend/.prettierignore @@ -5,4 +5,5 @@ **/*.svg **/*.sh -src/auto-import.d.ts \ No newline at end of file +src/auto-import.d.ts +src/assets/js \ No newline at end of file diff --git a/frontend/package.json b/frontend/package.json index 01ac7a2812..0caa4e6d3d 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -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", diff --git a/frontend/src/assets/js/jsonpath-picker-vanilla/jsonpath-picker-vanilla.js b/frontend/src/assets/js/jsonpath-picker-vanilla/jsonpath-picker-vanilla.js new file mode 100644 index 0000000000..70a4f8c117 --- /dev/null +++ b/frontend/src/assets/js/jsonpath-picker-vanilla/jsonpath-picker-vanilla.js @@ -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, '"'); + + if (json.includes('Number')) { + html += "".concat(json.replace(/Number\(([^)]+)\)/g, "$1"), ""); + } else if (isUrl(tmp)) { + html += "").concat(tmp, ""); + } else { + html += "\"".concat(tmp, "\""); + } + } else if (typeof json === 'number') { + html += "".concat(json, ""); + } else if (typeof json === 'boolean') { + html += "".concat(json, ""); + } else if (json === null) { + html += 'null'; + } else if (json instanceof Array) { + if (json.length > 0) { + html += '[
    '; + + for (let i = 0; i < json.length; i += 1) { + html += "
  1. "); // Add toggle button if item is collapsable + + if (isCollapsable(json[i])) { + html += ''; + } + + html += json2html(json[i], options); // Add comma if item is not last + + if (i < json.length - 1) { + html += ','; + } + + html += '
  2. '; + } + + html += '
]'; + } else { + html += '[]'; + } + } else if (_typeof(json) === 'object') { + let keyCount = Object.keys(json).length; + + if (keyCount > 0) { + html += '{}'; + } 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} + */ + + +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', "".concat(placeholder, "")); + } + } // 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 | { 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 = "".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 +}; \ No newline at end of file diff --git a/frontend/src/assets/js/jsonpath-picker-vanilla/jsonpath-picker.css b/frontend/src/assets/js/jsonpath-picker-vanilla/jsonpath-picker.css new file mode 100644 index 0000000000..83205c95ae --- /dev/null +++ b/frontend/src/assets/js/jsonpath-picker-vanilla/jsonpath-picker.css @@ -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; +} diff --git a/frontend/src/components/pure/jsonpath-picker/index.vue b/frontend/src/components/pure/jsonpath-picker/index.vue index 236a71ac7f..9a1fe2e1ce 100644 --- a/frontend/src/components/pure/jsonpath-picker/index.vue +++ b/frontend/src/components/pure/jsonpath-picker/index.vue @@ -6,8 +6,9 @@ diff --git a/frontend/src/components/pure/jsonpath-picker/types.ts b/frontend/src/components/pure/jsonpath-picker/types.ts new file mode 100644 index 0000000000..17c2bd72b0 --- /dev/null +++ b/frontend/src/components/pure/jsonpath-picker/types.ts @@ -0,0 +1,4 @@ +export interface XpathNode { + content: string; + xpath: string; +} diff --git a/frontend/src/components/pure/jsonpath-picker/xpath.vue b/frontend/src/components/pure/jsonpath-picker/xpath.vue new file mode 100644 index 0000000000..fb224e125e --- /dev/null +++ b/frontend/src/components/pure/jsonpath-picker/xpath.vue @@ -0,0 +1,117 @@ + + +