json-edit-react
Advanced tools
Comparing version 0.9.6 to 1.0.0
'use strict'; | ||
Object.defineProperty(exports, '__esModule', { value: true }); | ||
var jsxRuntime = require('react/jsx-runtime'); | ||
@@ -31,3 +29,3 @@ var react = require('react'); | ||
***************************************************************************** */ | ||
/* global Reflect, Promise */ | ||
/* global Reflect, Promise, SuppressedError, Symbol */ | ||
@@ -57,2 +55,7 @@ | ||
typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) { | ||
var e = new Error(message); | ||
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e; | ||
}; | ||
var e=[],t=[];function n(n,r){if(n&&"undefined"!=typeof document){var a,s=!0===r.prepend?"prepend":"append",d=!0===r.singleTag,i="string"==typeof r.container?document.querySelector(r.container):document.getElementsByTagName("head")[0];if(d){var u=e.indexOf(i);-1===u&&(u=e.push(i)-1,t[u]={}),a=t[u]&&t[u][s]?t[u][s]:t[u][s]=c();}else a=c();65279===n.charCodeAt(0)&&(n=n.substring(1)),a.styleSheet?a.styleSheet.cssText+=n:a.appendChild(document.createTextNode(n));}function c(){var e=document.createElement("style");if(e.setAttribute("type","text/css"),r.attributes)for(var t=Object.keys(r.attributes),n=0;n<t.length;n++)e.setAttribute(t[n],r.attributes[t[n]]);var a="prepend"===s?"afterbegin":"beforeend";return i.insertAdjacentElement(a,e),e}} | ||
@@ -319,3 +322,3 @@ | ||
const setTheme = (theme) => setStyles(compileStyles(theme)); | ||
return (jsxRuntime.jsx(ThemeProviderContext.Provider, Object.assign({ value: { styles, setTheme, icons, setIcons } }, { children: children }))); | ||
return (jsxRuntime.jsx(ThemeProviderContext.Provider, { value: { styles, setTheme, icons, setIcons }, children: children })); | ||
}; | ||
@@ -394,6 +397,6 @@ const useTheme = () => react.useContext(ThemeProviderContext); | ||
const dummyValue = value.slice(-1) === '\n' ? value + '.' : value; | ||
return (jsxRuntime.jsxs("div", Object.assign({ style: { display: 'grid' } }, { children: [jsxRuntime.jsx("textarea", { style: Object.assign({ height: 'auto', gridArea: '1 / 1 / 2 / 2', overflowY: 'auto' }, styles.input), rows: 1, className: className, name: name, value: value, onChange: (e) => setValue(e.target.value), autoFocus: true, onFocus: (e) => { | ||
return (jsxRuntime.jsxs("div", { style: { display: 'grid' }, children: [jsxRuntime.jsx("textarea", { style: Object.assign({ height: 'auto', gridArea: '1 / 1 / 2 / 2', overflowY: 'auto' }, styles.input), rows: 1, className: className, name: name, value: value, onChange: (e) => setValue(e.target.value), autoFocus: true, onFocus: (e) => { | ||
if (value.length < 40) | ||
e.target.select(); | ||
}, onKeyDown: handleKeyPress }), jsxRuntime.jsx("span", Object.assign({ className: className, style: Object.assign({ visibility: 'hidden', height: 'auto', gridArea: '1 / 1 / 2 / 2', color: 'red', opacity: 0.9, whiteSpace: 'pre-wrap', overflow: 'clip', border: '1px solid transparent' }, styles.input) }, { children: dummyValue }))] }))); | ||
}, onKeyDown: handleKeyPress }), jsxRuntime.jsx("span", { className: className, style: Object.assign({ visibility: 'hidden', height: 'auto', gridArea: '1 / 1 / 2 / 2', color: 'red', opacity: 0.9, whiteSpace: 'pre-wrap', overflow: 'clip', border: '1px solid transparent' }, styles.input), children: dummyValue })] })); | ||
}; | ||
@@ -412,6 +415,6 @@ | ||
const breakString = (text) => text.split('\n').map((line, index, arr) => (jsxRuntime.jsxs("span", { children: [line, index < arr.length - 1 ? jsxRuntime.jsx("br", {}) : null] }, index))); | ||
return isEditing ? (jsxRuntime.jsx(AutogrowTextArea, { className: "jer-input-text", name: path.join('.'), value: value, setValue: setValue, isEditing: isEditing, handleKeyPress: handleKeyPress })) : (jsxRuntime.jsxs("div", Object.assign({ onDoubleClick: () => setIsEditing(true), onClick: (e) => { | ||
return isEditing ? (jsxRuntime.jsx(AutogrowTextArea, { className: "jer-input-text", name: path.join('.'), value: value, setValue: setValue, isEditing: isEditing, handleKeyPress: handleKeyPress })) : (jsxRuntime.jsxs("div", { onDoubleClick: () => setIsEditing(true), onClick: (e) => { | ||
if (e.getModifierState('Control') || e.getModifierState('Meta')) | ||
setIsEditing(true); | ||
}, className: "jer-value-string", style: styles.string }, { children: ["\"", breakString(truncate(value, stringTruncate)), "\""] }))); | ||
}, className: "jer-value-string", style: styles.string, children: ["\"", breakString(truncate(value, stringTruncate)), "\""] })); | ||
}; | ||
@@ -429,14 +432,66 @@ const NumberValue = ({ value, setValue, isEditing, path, setIsEditing, handleEdit, handleCancel, }) => { | ||
}; | ||
return isEditing ? (jsxRuntime.jsx("input", { className: "jer-input-number", type: "text", name: path.join('.'), value: value, onChange: (e) => setValue(validateNumber(e.target.value)), autoFocus: true, onFocus: (e) => e.target.select(), onKeyDown: handleKeyPress, style: { width: `${String(value).length / 1.5 + 2}em` } })) : (jsxRuntime.jsx("span", Object.assign({ onDoubleClick: () => setIsEditing(true), className: "jer-value-number", style: styles.number }, { children: value }))); | ||
return isEditing ? (jsxRuntime.jsx("input", { className: "jer-input-number", type: "text", name: path.join('.'), value: value, onChange: (e) => setValue(validateNumber(e.target.value)), autoFocus: true, onFocus: (e) => e.target.select(), onKeyDown: handleKeyPress, style: { width: `${String(value).length / 1.5 + 2}em` } })) : (jsxRuntime.jsx("span", { onDoubleClick: () => setIsEditing(true), className: "jer-value-number", style: styles.number, children: value })); | ||
}; | ||
const BooleanValue = ({ value, setValue, isEditing, path, setIsEditing, }) => { | ||
const BooleanValue = ({ value, setValue, isEditing, path, setIsEditing, handleEdit, handleCancel, }) => { | ||
const { styles } = useTheme(); | ||
return isEditing ? (jsxRuntime.jsx("input", { className: "jer-input-boolean", type: "checkbox", name: path.join('.'), checked: value, onChange: () => setValue(!value) })) : (jsxRuntime.jsx("span", Object.assign({ onDoubleClick: () => setIsEditing(true), className: "jer-value-boolean", style: styles.boolean }, { children: String(value) }))); | ||
react.useEffect(() => { | ||
if (isEditing) | ||
document.addEventListener('keydown', listenForSubmit); | ||
return () => document.removeEventListener('keydown', listenForSubmit); | ||
}, [isEditing]); | ||
const listenForSubmit = (event) => { | ||
if (event.key === 'Enter') { | ||
handleEdit(); | ||
} | ||
else if (event.key === 'Escape') | ||
handleCancel(); | ||
}; | ||
return isEditing ? (jsxRuntime.jsx("input", { className: "jer-input-boolean", type: "checkbox", name: path.join('.'), checked: value, onChange: () => setValue(!value) })) : (jsxRuntime.jsx("span", { onDoubleClick: () => setIsEditing(true), className: "jer-value-boolean", style: styles.boolean, children: String(value) })); | ||
}; | ||
const NullValue = ({ value, isEditing, setIsEditing }) => { | ||
const NullValue = ({ value, isEditing, setIsEditing, handleEdit, handleCancel, }) => { | ||
const { styles } = useTheme(); | ||
return isEditing ? (jsxRuntime.jsx("div", Object.assign({ className: "jer-input-null" }, { children: "null" }))) : (jsxRuntime.jsx("div", Object.assign({ onDoubleClick: () => setIsEditing(true), className: "jer-value-null", style: styles.null }, { children: String(value) }))); | ||
react.useEffect(() => { | ||
if (isEditing) | ||
document.addEventListener('keydown', listenForSubmit); | ||
return () => document.removeEventListener('keydown', listenForSubmit); | ||
}, [isEditing]); | ||
const listenForSubmit = (event) => { | ||
if (event.key === 'Enter') { | ||
handleEdit(); | ||
} | ||
else if (event.key === 'Escape') | ||
handleCancel(); | ||
}; | ||
return isEditing ? (jsxRuntime.jsx("div", { className: "jer-input-null", children: "null" })) : (jsxRuntime.jsx("div", { onDoubleClick: () => setIsEditing(true), className: "jer-value-null", style: styles.null, children: String(value) })); | ||
}; | ||
const ObjectValue = () => (jsxRuntime.jsx("span", Object.assign({ className: "jer-value-object" }, { children: '{ }' }))); | ||
const ArrayValue = ({ value }) => (jsxRuntime.jsx("span", Object.assign({ className: "jer-value-array" }, { children: `[${value === null ? '' : value}]` }))); | ||
const ObjectValue = ({ value, translate, isEditing, handleEdit, handleCancel, }) => { | ||
react.useEffect(() => { | ||
if (isEditing) | ||
document.addEventListener('keydown', listenForSubmit); | ||
return () => document.removeEventListener('keydown', listenForSubmit); | ||
}, []); | ||
const listenForSubmit = (event) => { | ||
if (event.key === 'Enter') { | ||
handleEdit(); | ||
} | ||
else if (event.key === 'Escape') | ||
handleCancel(); | ||
}; | ||
return (jsxRuntime.jsx("span", { className: "jer-value-object", children: `{${translate('DEFAULT_NEW_KEY')}: "${value}" }` })); | ||
}; | ||
const ArrayValue = ({ value, isEditing, handleEdit, handleCancel, }) => { | ||
react.useEffect(() => { | ||
if (isEditing) | ||
document.addEventListener('keydown', listenForSubmit); | ||
return () => document.removeEventListener('keydown', listenForSubmit); | ||
}, []); | ||
const listenForSubmit = (event) => { | ||
if (event.key === 'Enter') { | ||
handleEdit(); | ||
} | ||
else if (event.key === 'Escape') | ||
handleCancel(); | ||
}; | ||
return jsxRuntime.jsx("span", { className: "jer-value-array", children: `[${value === null ? '' : value}]` }); | ||
}; | ||
const InvalidValue = ({ value }) => { | ||
@@ -456,3 +511,3 @@ let message = 'Error!'; | ||
} | ||
return jsxRuntime.jsx("span", Object.assign({ className: "jer-value-invalid" }, { children: message })); | ||
return jsxRuntime.jsx("span", { className: "jer-value-invalid", children: message }); | ||
}; | ||
@@ -521,3 +576,3 @@ | ||
}; | ||
return (jsxRuntime.jsxs("div", Object.assign({ className: "jer-edit-buttons", style: isAdding ? { opacity: 1 } : undefined }, { children: [enableClipboard && (jsxRuntime.jsx("div", Object.assign({ onClick: handleCopy, className: "jer-copy-pulse" }, { children: jsxRuntime.jsx(Icon, { name: "copy" }) }))), startEdit && (jsxRuntime.jsx("div", Object.assign({ onClick: startEdit }, { children: jsxRuntime.jsx(Icon, { name: "edit" }) }))), handleDelete && (jsxRuntime.jsx("div", Object.assign({ onClick: handleDelete }, { children: jsxRuntime.jsx(Icon, { name: "delete" }) }))), handleAdd && (jsxRuntime.jsx("div", Object.assign({ onClick: () => { | ||
return (jsxRuntime.jsxs("div", { className: "jer-edit-buttons", style: isAdding ? { opacity: 1 } : undefined, children: [enableClipboard && (jsxRuntime.jsx("div", { onClick: handleCopy, className: "jer-copy-pulse", children: jsxRuntime.jsx(Icon, { name: "copy" }) })), startEdit && (jsxRuntime.jsx("div", { onClick: startEdit, children: jsxRuntime.jsx(Icon, { name: "edit" }) })), handleDelete && (jsxRuntime.jsx("div", { onClick: handleDelete, children: jsxRuntime.jsx(Icon, { name: "delete" }) })), handleAdd && (jsxRuntime.jsx("div", { onClick: () => { | ||
if (type === 'object') | ||
@@ -527,3 +582,3 @@ setIsAdding(true); | ||
handleAdd(''); | ||
} }, { children: jsxRuntime.jsx(Icon, { name: "add" }) }))), isAdding && handleAdd && type === 'object' && (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx("input", { className: "jer-input-new-key", type: "text", name: "new-object-key", value: newKey, onChange: (e) => setNewKey(e.target.value), autoFocus: true, onFocus: (e) => e.target.select(), onKeyDown: handleKeyPress, style: Object.assign({}, styles.input) }), jsxRuntime.jsx(InputButtons, { onOk: () => { | ||
}, children: jsxRuntime.jsx(Icon, { name: "add" }) })), isAdding && handleAdd && type === 'object' && (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx("input", { className: "jer-input-new-key", type: "text", name: "new-object-key", value: newKey, onChange: (e) => setNewKey(e.target.value), autoFocus: true, onFocus: (e) => e.target.select(), onKeyDown: handleKeyPress, style: Object.assign({}, styles.input) }), jsxRuntime.jsx(InputButtons, { onOk: () => { | ||
if (!!newKey) { | ||
@@ -535,6 +590,6 @@ setIsAdding(false); | ||
setIsAdding(false); | ||
} })] }))] }))); | ||
} })] }))] })); | ||
}; | ||
const InputButtons = ({ onOk, onCancel }) => { | ||
return (jsxRuntime.jsxs("div", Object.assign({ className: "jer-confirm-buttons" }, { children: [jsxRuntime.jsx("div", Object.assign({ onClick: onOk }, { children: jsxRuntime.jsx(Icon, { name: "ok" }) })), jsxRuntime.jsx("div", Object.assign({ onClick: onCancel }, { children: jsxRuntime.jsx(Icon, { name: "cancel" }) }))] }))); | ||
return (jsxRuntime.jsxs("div", { className: "jer-confirm-buttons", children: [jsxRuntime.jsx("div", { onClick: onOk, children: jsxRuntime.jsx(Icon, { name: "ok" }) }), jsxRuntime.jsx("div", { onClick: onCancel, children: jsxRuntime.jsx(Icon, { name: "cancel" }) })] })); | ||
}; | ||
@@ -553,5 +608,23 @@ const stringifyPath = (path) => path.reduce((str, part) => { | ||
const ValueNodeWrapper = ({ data, name, path, onEdit, onDelete, enableClipboard, restrictEditFilter, restrictDeleteFilter, showArrayIndices, stringTruncate, indent, translate, }) => { | ||
const getCustomNode = (customNodeDefinitions = [], filterProps) => { | ||
const matchingDefinitions = customNodeDefinitions.filter(({ condition }) => condition(filterProps)); | ||
if (matchingDefinitions.length === 0) | ||
return {}; | ||
const { element, props, hideKey = false } = matchingDefinitions[0]; | ||
return { CustomNode: element, customNodeProps: props, hideKey }; | ||
}; | ||
const CustomNodeWrapper = ({ name, hideKey, children, indent, }) => { | ||
const { styles } = useTheme(); | ||
const indentStyle = indent ? { marginLeft: `${indent / 2}em` } : {}; | ||
return (jsxRuntime.jsx("div", { className: "jer-component jer-value-component", style: indentStyle, children: jsxRuntime.jsxs("div", { className: "jer-value-main-row", style: { | ||
flexWrap: name.length > 10 ? 'wrap' : 'nowrap', | ||
}, children: [!hideKey && (jsxRuntime.jsxs("label", { htmlFor: name, className: "jer-object-key", style: Object.assign(Object.assign({}, styles.property), { minWidth: `${Math.min(String(name).length + 1, 5)}ch`, flexShrink: name.length > 10 ? 1 : 0 }), children: [name, ":", ' '] })), jsxRuntime.jsx("div", { className: "jer-value-and-buttons", style: { paddingLeft: hideKey ? 0 : undefined }, children: children })] }) })); | ||
}; | ||
const ValueNodeWrapper = (props) => { | ||
const { data, parentData, name, path, onEdit, onDelete, enableClipboard, restrictEditFilter, restrictDeleteFilter, restrictTypeSelection, showLabel, stringTruncate, indent, translate, customNodeDefinitions, } = props; | ||
const { styles } = useTheme(); | ||
const [isEditing, setIsEditing] = react.useState(false); | ||
const [isEditingKey, setIsEditingKey] = react.useState(false); | ||
const [value, setValue] = react.useState(typeof data === 'function' ? INVALID_FUNCTION_STRING : data); | ||
@@ -565,4 +638,10 @@ const [error, setError] = react.useState(null); | ||
const handleChangeDataType = (type) => { | ||
setValue(convertValue(value, type)); | ||
setDataType(type); | ||
const customNode = customNodeDefinitions.find((customNode) => customNode.name === type); | ||
if (customNode) { | ||
onEdit(customNode.defaultValue, path); | ||
} | ||
else { | ||
setValue(convertValue(value, type)); | ||
setDataType(type); | ||
} | ||
}; | ||
@@ -579,6 +658,6 @@ const logError = (errorString) => { | ||
case 'object': | ||
newValue = {}; | ||
newValue = { [translate('DEFAULT_NEW_KEY')]: value }; | ||
break; | ||
case 'array': | ||
newValue = value !== null ? [value] : []; | ||
newValue = value !== null ? value : []; | ||
break; | ||
@@ -600,5 +679,23 @@ case 'number': | ||
}; | ||
const handleEditKey = (newKey) => { | ||
setIsEditingKey(false); | ||
if (!parentData) | ||
return; | ||
const parentPath = path.slice(0, -1); | ||
if (!newKey) | ||
return; | ||
const newData = Object.fromEntries(Object.entries(parentData).map(([key, val]) => (key === name ? [newKey, val] : [key, val]))); | ||
onEdit(newData, parentPath); | ||
}; | ||
const handleKeyPress = (e) => { | ||
if (e.key === 'Enter') | ||
handleEditKey(e.target.value); | ||
else if (e.key === 'Escape') | ||
handleCancel(); | ||
}; | ||
const handleCancel = () => { | ||
setIsEditing(false); | ||
setIsEditingKey(false); | ||
setValue(data); | ||
setDataType(getDataType(data)); | ||
}; | ||
@@ -614,4 +711,7 @@ const handleDelete = () => { | ||
const canDelete = react.useMemo(() => !restrictDeleteFilter(filterProps), [filterProps]); | ||
const isArray = typeof path.slice(-1)[0] === 'number'; | ||
const canEditKey = !isArray && canEdit && canDelete; | ||
const inputProps = { | ||
value, | ||
parentData, | ||
setValue, | ||
@@ -624,7 +724,31 @@ isEditing, | ||
stringTruncate, | ||
translate, | ||
}; | ||
return (jsxRuntime.jsx("div", Object.assign({ className: "jer-component jer-value-component", style: { marginLeft: `${indent / 2}em` } }, { children: jsxRuntime.jsxs("div", Object.assign({ className: "jer-value-main-row", style: { | ||
const { CustomNode, customNodeProps, hideKey } = getCustomNode(customNodeDefinitions, { | ||
key: name, | ||
path, | ||
level: path.length, | ||
value: data, | ||
size: 0, | ||
}); | ||
const allDataTypes = [ | ||
...customNodeDefinitions | ||
.filter(({ showInTypesSelector = false }) => showInTypesSelector) | ||
.map(({ name }) => name), | ||
...DataTypes, | ||
]; | ||
const allowedDataTypes = react.useMemo(() => { | ||
if (typeof restrictTypeSelection === 'boolean') | ||
return restrictTypeSelection ? [] : allDataTypes; | ||
if (Array.isArray(restrictTypeSelection)) | ||
return restrictTypeSelection; | ||
const result = restrictTypeSelection(filterProps); | ||
if (typeof result === 'boolean') | ||
return result ? [] : allDataTypes; | ||
return result; | ||
}, [filterProps, restrictTypeSelection]); | ||
return CustomNode ? (jsxRuntime.jsx(CustomNodeWrapper, { name: name, hideKey: hideKey, indent: indent, children: jsxRuntime.jsx(CustomNode, Object.assign({ customProps: customNodeProps }, props)) })) : (jsxRuntime.jsx("div", { className: "jer-component jer-value-component", style: { marginLeft: `${indent / 2}em` }, children: jsxRuntime.jsxs("div", { className: "jer-value-main-row", style: { | ||
flexWrap: name.length > 10 ? 'wrap' : 'nowrap', | ||
} }, { children: [showArrayIndices && (jsxRuntime.jsxs("label", Object.assign({ htmlFor: path.join('.'), className: "jer-object-key", style: Object.assign(Object.assign({}, styles.property), { minWidth: `${Math.min(String(name).length + 1, 5)}ch`, flexShrink: name.length > 10 ? 1 : 0 }) }, { children: [name, ":", ' '] }))), jsxRuntime.jsxs("div", Object.assign({ className: "jer-value-and-buttons" }, { children: [jsxRuntime.jsx("div", Object.assign({ className: "jer-input-component" }, { children: getInputComponent(dataType, inputProps) })), isEditing ? (jsxRuntime.jsx(InputButtons, { onOk: handleEdit, onCancel: handleCancel })) : (dataType !== 'invalid' && | ||
!error && (jsxRuntime.jsx(EditButtons, { startEdit: canEdit ? () => setIsEditing(true) : undefined, handleDelete: canDelete ? handleDelete : undefined, data: data, enableClipboard: enableClipboard, name: name, path: path, translate: translate }))), isEditing && (jsxRuntime.jsxs("div", Object.assign({ className: "jer-select" }, { children: [jsxRuntime.jsx("select", Object.assign({ name: `${name}-type-select`, className: "jer-type-select", onChange: (e) => handleChangeDataType(e.target.value), value: dataType }, { children: DataTypes.map((type) => (jsxRuntime.jsx("option", Object.assign({ value: type }, { children: type }), type))) })), jsxRuntime.jsx("span", { className: "focus" })] }))), !isEditing && error && (jsxRuntime.jsx("span", Object.assign({ className: "jer-error-slug", style: styles.error }, { children: error })))] }))] })) }))); | ||
}, children: [showLabel && !isEditingKey && (jsxRuntime.jsxs("label", { htmlFor: path.join('.'), className: "jer-object-key", style: Object.assign(Object.assign({}, styles.property), { minWidth: `${Math.min(String(name).length + 1, 5)}ch`, flexShrink: name.length > 10 ? 1 : 0 }), onDoubleClick: () => canEditKey && setIsEditingKey(true), children: [name, ":", ' '] })), showLabel && isEditingKey && (jsxRuntime.jsx("input", { className: "jer-object-key", type: "text", name: path.join('.'), defaultValue: name, autoFocus: true, onFocus: (e) => e.target.select(), onKeyDown: handleKeyPress, style: { width: `${String(name).length / 1.5 + 0.5}em` } })), jsxRuntime.jsxs("div", { className: "jer-value-and-buttons", children: [jsxRuntime.jsx("div", { className: "jer-input-component", children: getInputComponent(dataType, inputProps) }), isEditing ? (jsxRuntime.jsx(InputButtons, { onOk: handleEdit, onCancel: handleCancel })) : (dataType !== 'invalid' && | ||
!error && (jsxRuntime.jsx(EditButtons, { startEdit: canEdit ? () => setIsEditing(true) : undefined, handleDelete: canDelete ? handleDelete : undefined, data: data, enableClipboard: enableClipboard, name: name, path: path, translate: translate }))), isEditing && allowedDataTypes.length > 0 && (jsxRuntime.jsxs("div", { className: "jer-select", children: [jsxRuntime.jsx("select", { name: `${name}-type-select`, className: "jer-type-select", onChange: (e) => handleChangeDataType(e.target.value), value: dataType, children: allowedDataTypes.map((type) => (jsxRuntime.jsx("option", { value: type, children: type }, type))) }), jsxRuntime.jsx("span", { className: "focus" })] })), !isEditing && error && (jsxRuntime.jsx("span", { className: "jer-error-slug", style: styles.error, children: error }))] })] }) })); | ||
}; | ||
@@ -654,3 +778,3 @@ const getDataType = (value) => { | ||
case 'object': | ||
return jsxRuntime.jsx(ObjectValue, Object.assign({}, inputProps)); | ||
return jsxRuntime.jsx(ObjectValue, Object.assign({}, inputProps, { value: value })); | ||
case 'array': | ||
@@ -674,3 +798,3 @@ return jsxRuntime.jsx(ArrayValue, Object.assign({}, inputProps)); | ||
case 'object': | ||
return {}; | ||
return value; | ||
case 'array': | ||
@@ -685,6 +809,7 @@ return [value]; | ||
const CollectionNode = (_a) => { | ||
var { data, path, name } = _a, props = __rest(_a, ["data", "path", "name"]); | ||
var { data, path, name, parentData, showCollectionCount } = _a, props = __rest(_a, ["data", "path", "name", "parentData", "showCollectionCount"]); | ||
const { styles } = useTheme(); | ||
const { onEdit, onAdd, onDelete, restrictEditFilter, restrictDeleteFilter, restrictAddFilter, collapseFilter, enableClipboard, indent, keySort, showArrayIndices, defaultValue, translate, } = props; | ||
const { onEdit, onAdd, onDelete, restrictEditFilter, restrictDeleteFilter, restrictAddFilter, collapseFilter, enableClipboard, indent, keySort, showArrayIndices, defaultValue, translate, customNodeDefinitions, } = props; | ||
const [isEditing, setIsEditing] = react.useState(false); | ||
const [isEditingKey, setIsEditingKey] = react.useState(false); | ||
const [stringifiedValue, setStringifiedValue] = react.useState(JSON.stringify(data, null, 2)); | ||
@@ -743,2 +868,18 @@ const [error, setError] = react.useState(null); | ||
}; | ||
const handleEditKey = (newKey) => { | ||
setIsEditingKey(false); | ||
if (!parentData) | ||
return; | ||
const parentPath = path.slice(0, -1); | ||
if (!newKey) | ||
return; | ||
const newData = Object.fromEntries(Object.entries(parentData).map(([key, val]) => (key === name ? [newKey, val] : [key, val]))); | ||
onEdit(newData, parentPath); | ||
}; | ||
const handleKeyPressKeyEdit = (e) => { | ||
if (e.key === 'Enter') | ||
handleEditKey(e.target.value); | ||
else if (e.key === 'Escape') | ||
handleCancel(); | ||
}; | ||
const handleAdd = (key) => { | ||
@@ -778,3 +919,6 @@ setCollapsed(false); | ||
const canAdd = react.useMemo(() => !restrictAddFilter(filterProps), [filterProps]); | ||
const showLabel = showArrayIndices || !(typeof path.slice(-1)[0] === 'number'); | ||
const canEditKey = parentData !== null && canEdit && canAdd && canDelete; | ||
const isArray = typeof path.slice(-1)[0] === 'number'; | ||
const showLabel = showArrayIndices || !isArray; | ||
const showCount = showCollectionCount === 'when-closed' ? collapsed : showCollectionCount; | ||
const keyValueArray = Object.entries(data).map(([key, value]) => [ | ||
@@ -787,16 +931,23 @@ collectionType === 'array' ? Number(key) : key, | ||
const numOfLines = JSON.stringify(data, null, 2).split('\n').length; | ||
return (jsxRuntime.jsxs("div", Object.assign({ className: "jer-component fb-collection-component", style: { marginLeft: `${path.length === 0 ? 0 : indent / 2}em` } }, { children: [jsxRuntime.jsxs("div", Object.assign({ className: "jer-collection-header-row" }, { children: [jsxRuntime.jsx("div", Object.assign({ onClick: handleCollapse }, { children: jsxRuntime.jsx(Icon, { name: "chevron", rotate: collapsed }) })), jsxRuntime.jsxs("div", Object.assign({ className: "jer-collection-name" }, { children: [jsxRuntime.jsx("span", Object.assign({ style: styles.property }, { children: showLabel ? `${name}:` : null })), !isEditing && (jsxRuntime.jsx("span", Object.assign({ className: "jer-brackets", style: styles.bracket }, { children: brackets.open })))] })), !isEditing && (jsxRuntime.jsx("div", Object.assign({ className: "jer-collection-item-count", style: styles.itemCount }, { children: collectionSize === 1 | ||
? translate('ITEM_SINGLE', 1) | ||
: translate('ITEMS_MULTIPLE', collectionSize) }))), jsxRuntime.jsx("div", Object.assign({ className: `jer-brackets${collapsed ? ' jer-visible' : ' jer-hidden'}`, style: styles.bracket }, { children: brackets.close })), !isEditing && (jsxRuntime.jsx(EditButtons, { startEdit: canEdit | ||
? () => { | ||
setIsEditing(true); | ||
setCollapsed(false); | ||
} | ||
: undefined, handleAdd: canAdd ? handleAdd : undefined, handleDelete: canDelete ? handleDelete : undefined, enableClipboard: enableClipboard, type: collectionType, data: data, name: name, path: path, translate: translate }))] })), jsxRuntime.jsxs("div", Object.assign({ className: 'jer-collection-inner', style: { | ||
maxHeight: collapsed ? 0 : !isEditing ? `${numOfLines * 3}em` : undefined, | ||
overflowY: collapsed || isAnimating ? 'hidden' : 'visible', | ||
transition: `max-height ${transitionTime}`, | ||
} }, { children: [isEditing ? (jsxRuntime.jsx("div", Object.assign({ className: "jer-collection-text-edit" }, { children: jsxRuntime.jsxs("div", { children: [jsxRuntime.jsx(AutogrowTextArea, { className: "jer-collection-text-area", name: path.join('.'), value: stringifiedValue, setValue: setStringifiedValue, isEditing: isEditing, handleKeyPress: handleKeyPress }), jsxRuntime.jsx("div", Object.assign({ className: "jer-collection-input-button-row" }, { children: jsxRuntime.jsx(InputButtons, { onOk: handleEdit, onCancel: handleCancel, isCollection: true }) }))] }) }))) : (jsxRuntime.jsx(jsxRuntime.Fragment, { children: !hasBeenOpened.current | ||
? null | ||
: keyValueArray.map(([key, value]) => (jsxRuntime.jsx("div", Object.assign({ className: "jer-collection-element" }, { children: isCollection(value) ? (jsxRuntime.jsx(CollectionNode, Object.assign({ data: value, path: [...path, key], name: key }, props), key)) : (jsxRuntime.jsx(ValueNodeWrapper, Object.assign({ data: value, path: [...path, key], name: key }, props, { showArrayIndices: collectionType === 'object' ? true : showArrayIndices }), key)) }), key))) })), jsxRuntime.jsx("div", Object.assign({ className: isEditing ? 'jer-collection-error-row' : 'jer-collection-error-row-edit' }, { children: error && (jsxRuntime.jsx("span", Object.assign({ className: "jer-error-slug", style: styles.error }, { children: error }))) })), jsxRuntime.jsx("div", Object.assign({ className: "jer-brackets", style: styles.bracket }, { children: brackets.close }))] }))] }))); | ||
const { CustomNode, customNodeProps, hideKey } = getCustomNode(customNodeDefinitions, { | ||
key: name, | ||
path, | ||
level: path.length, | ||
value: data, | ||
size: Object.keys(data).length, | ||
}); | ||
return (jsxRuntime.jsx("div", { className: "jer-component fb-collection-component", style: { marginLeft: `${path.length === 0 ? 0 : indent / 2}em` }, children: CustomNode ? (jsxRuntime.jsx(CustomNodeWrapper, { name: name, hideKey: hideKey, children: jsxRuntime.jsx(CustomNode, Object.assign({ data: data, path: path, name: name, parentData: parentData, customProps: customNodeProps }, props)) })) : (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsxs("div", { className: "jer-collection-header-row", children: [jsxRuntime.jsx("div", { onClick: handleCollapse, children: jsxRuntime.jsx(Icon, { name: "chevron", rotate: collapsed }) }), jsxRuntime.jsxs("div", { className: "jer-collection-name", children: [!isEditingKey && (jsxRuntime.jsx("span", { style: styles.property, onDoubleClick: () => canEditKey && setIsEditingKey(true), children: showLabel && name !== '' && name !== undefined ? `${name}:` : null })), isEditingKey && (jsxRuntime.jsx("input", { className: "jer-collection-name", type: "text", name: path.join('.'), defaultValue: name, autoFocus: true, onFocus: (e) => e.target.select(), onKeyDown: handleKeyPressKeyEdit, style: { width: `${String(name).length / 1.5 + 0.5}em` } })), !isEditing && (jsxRuntime.jsx("span", { className: "jer-brackets", style: styles.bracket, children: brackets.open }))] }), !isEditing && showCollectionCount && (jsxRuntime.jsx("div", { className: `jer-collection-item-count${showCount ? ' jer-visible' : ' jer-hidden'}`, style: styles.itemCount, children: collectionSize === 1 | ||
? translate('ITEM_SINGLE', 1) | ||
: translate('ITEMS_MULTIPLE', collectionSize) })), jsxRuntime.jsx("div", { className: `jer-brackets${collapsed ? ' jer-visible' : ' jer-hidden'}`, style: styles.bracket, children: brackets.close }), !isEditing && (jsxRuntime.jsx(EditButtons, { startEdit: canEdit | ||
? () => { | ||
setIsEditing(true); | ||
setCollapsed(false); | ||
} | ||
: undefined, handleAdd: canAdd ? handleAdd : undefined, handleDelete: canDelete ? handleDelete : undefined, enableClipboard: enableClipboard, type: collectionType, data: data, name: name, path: path, translate: translate }))] }), jsxRuntime.jsxs("div", { className: 'jer-collection-inner', style: { | ||
maxHeight: collapsed ? 0 : !isEditing ? `${numOfLines * 3}em` : undefined, | ||
overflowY: collapsed || isAnimating ? 'hidden' : 'visible', | ||
transition: `max-height ${transitionTime}`, | ||
}, children: [isEditing ? (jsxRuntime.jsx("div", { className: "jer-collection-text-edit", children: jsxRuntime.jsxs("div", { children: [jsxRuntime.jsx(AutogrowTextArea, { className: "jer-collection-text-area", name: path.join('.'), value: stringifiedValue, setValue: setStringifiedValue, isEditing: isEditing, handleKeyPress: handleKeyPress }), jsxRuntime.jsx("div", { className: "jer-collection-input-button-row", children: jsxRuntime.jsx(InputButtons, { onOk: handleEdit, onCancel: handleCancel, isCollection: true }) })] }) })) : (jsxRuntime.jsx(jsxRuntime.Fragment, { children: !hasBeenOpened.current | ||
? null | ||
: keyValueArray.map(([key, value]) => (jsxRuntime.jsx("div", { className: "jer-collection-element", children: isCollection(value) ? (jsxRuntime.jsx(CollectionNode, Object.assign({ data: value, parentData: data, path: [...path, key], name: key, showCollectionCount: showCollectionCount }, props), key)) : (jsxRuntime.jsx(ValueNodeWrapper, Object.assign({ data: value, parentData: data, path: [...path, key], name: key }, props, { showLabel: collectionType === 'object' ? true : showArrayIndices }), key)) }, key))) })), jsxRuntime.jsx("div", { className: isEditing ? 'jer-collection-error-row' : 'jer-collection-error-row-edit', children: error && (jsxRuntime.jsx("span", { className: "jer-error-slug", style: styles.error, children: error })) }), jsxRuntime.jsx("div", { className: "jer-brackets", style: styles.bracket, children: brackets.close })] })] })) })); | ||
}; | ||
@@ -813,2 +964,4 @@ | ||
ERROR_ADD: 'Adding node unsuccessful', | ||
DEFAULT_STRING: 'New data!', | ||
DEFAULT_NEW_KEY: 'key', | ||
}; | ||
@@ -821,3 +974,3 @@ const translate = (translations, key, count) => { | ||
const Editor = ({ data: srcData, rootName = 'root', onUpdate, onEdit: srcEdit = onUpdate, onDelete: srcDelete = onUpdate, onAdd: srcAdd = onUpdate, enableClipboard = true, theme = 'default', icons, indent = 4, collapse = false, restrictEdit = false, restrictDelete = false, restrictAdd = false, keySort = false, showArrayIndices = true, defaultValue = null, minWidth = 250, maxWidth = 'min(600px, 90vw)', stringTruncate = 250, translations = {}, className, }) => { | ||
const Editor = ({ data: srcData, rootName = 'root', onUpdate, onEdit: srcEdit = onUpdate, onDelete: srcDelete = onUpdate, onAdd: srcAdd = onUpdate, enableClipboard = true, theme = 'default', icons, indent = 4, collapse = false, showCollectionCount = true, restrictEdit = false, restrictDelete = false, restrictAdd = false, restrictTypeSelection = false, keySort = false, showArrayIndices = true, defaultValue = null, minWidth = 250, maxWidth = 'min(600px, 90vw)', stringTruncate = 250, translations = {}, className, customNodeDefinitions = [], }) => { | ||
const [data, setData] = react.useState(srcData); | ||
@@ -904,2 +1057,3 @@ const { styles, setTheme, setIcons } = useTheme(); | ||
onAdd, | ||
showCollectionCount, | ||
collapseFilter, | ||
@@ -909,2 +1063,3 @@ restrictEditFilter, | ||
restrictAddFilter, | ||
restrictTypeSelection, | ||
enableClipboard, | ||
@@ -917,6 +1072,8 @@ keySort, | ||
translate, | ||
customNodeDefinitions, | ||
parentData: null, | ||
}; | ||
if (!styles) | ||
return null; | ||
return (jsxRuntime.jsx("div", Object.assign({ className: 'jer-editor-container ' + className, style: Object.assign(Object.assign({}, styles.container), { minWidth, maxWidth }) }, { children: isCollection(data) && jsxRuntime.jsx(CollectionNode, Object.assign({ data: data, path: [] }, otherProps)) }))); | ||
return (jsxRuntime.jsx("div", { className: 'jer-editor-container ' + className, style: Object.assign(Object.assign({}, styles.container), { minWidth, maxWidth }), children: isCollection(data) ? (jsxRuntime.jsx(CollectionNode, Object.assign({ data: data, path: [] }, otherProps))) : (jsxRuntime.jsx(ValueNodeWrapper, Object.assign({ data: data, path: [], showLabel: true }, otherProps))) })); | ||
}; | ||
@@ -951,3 +1108,2 @@ const JsonEditor = (props) => (jsxRuntime.jsx(ThemeProvider, { children: jsxRuntime.jsx(Editor, Object.assign({}, props)) })); | ||
exports.JsonEditor = JsonEditor; | ||
exports.default = JsonEditor; | ||
exports.themes = themes; |
@@ -33,2 +33,4 @@ /// <reference types="react" /> | ||
ERROR_ADD: string; | ||
DEFAULT_STRING: string; | ||
DEFAULT_NEW_KEY: string; | ||
}; | ||
@@ -51,6 +53,7 @@ type LocalisedStrings = typeof localisedStrings; | ||
collapse?: boolean | number | FilterFunction; | ||
showCollectionCount?: boolean | 'when-closed'; | ||
restrictEdit?: boolean | FilterFunction; | ||
restrictDelete?: boolean | FilterFunction; | ||
restrictAdd?: boolean | FilterFunction; | ||
restrictKeyEdit?: boolean | FilterFunction; | ||
restrictTypeSelection?: boolean | DataType[] | TypeFilterFunction; | ||
keySort?: boolean | CompareFunction; | ||
@@ -63,4 +66,8 @@ showArrayIndices?: boolean; | ||
translations?: Partial<LocalisedStrings>; | ||
customNodeDefinitions?: CustomNodeDefinition[]; | ||
} | ||
declare const DataTypes: readonly ["string", "number", "boolean", "null", "object", "array"]; | ||
type DataType = (typeof DataTypes)[number] | 'invalid'; | ||
type CollectionKey = string | number; | ||
type CollectionData = object | unknown[]; | ||
type ErrorString = string; | ||
@@ -84,3 +91,3 @@ interface IconReplacements { | ||
}) => void | ErrorString | false; | ||
type FilterFunction = (input: { | ||
interface FilterProps { | ||
key: CollectionKey; | ||
@@ -91,3 +98,5 @@ path: CollectionKey[]; | ||
size: number | null; | ||
}) => boolean; | ||
} | ||
type FilterFunction = (input: FilterProps) => boolean; | ||
type TypeFilterFunction = (input: FilterProps) => boolean | DataType[]; | ||
type CopyType = 'path' | 'value'; | ||
@@ -102,5 +111,36 @@ type CopyFunction = (input: { | ||
type CompareFunction = (a: string, b: string) => number; | ||
type OnChangeFunction = (value: unknown, path: (string | number)[]) => Promise<string | void>; | ||
interface BaseNodeProps { | ||
data: unknown; | ||
parentData: CollectionData | null; | ||
path: CollectionKey[]; | ||
name: CollectionKey; | ||
onEdit: OnChangeFunction; | ||
onDelete: OnChangeFunction; | ||
enableClipboard: boolean | CopyFunction; | ||
restrictEditFilter: FilterFunction; | ||
restrictDeleteFilter: FilterFunction; | ||
restrictAddFilter: FilterFunction; | ||
restrictTypeSelection: boolean | DataType[] | TypeFilterFunction; | ||
stringTruncate: number; | ||
indent: number; | ||
translate: TranslateFunction; | ||
customNodeDefinitions: CustomNodeDefinition[]; | ||
} | ||
interface CustomNodeProps extends BaseNodeProps { | ||
customProps?: Record<string, unknown>; | ||
parentData: CollectionData | null; | ||
} | ||
interface CustomNodeDefinition { | ||
condition: FilterFunction; | ||
element: React$1.FC<CustomNodeProps>; | ||
name: string; | ||
props?: Record<string, unknown>; | ||
hideKey?: boolean; | ||
defaultValue: unknown; | ||
showInTypesSelector?: boolean; | ||
} | ||
declare const JsonEditor: React$1.FC<JsonEditorProps>; | ||
export { CompareFunction, CopyFunction, FilterFunction, IconReplacements, JsonEditor, JsonEditorProps, LocalisedStrings, Theme, ThemeInput, ThemeName, TranslateFunction, UpdateFunction, JsonEditor as default, themes }; | ||
export { type CompareFunction, type CopyFunction, type FilterFunction, type IconReplacements, JsonEditor, type JsonEditorProps, type LocalisedStrings, type Theme, type ThemeInput, type ThemeName, type TranslateFunction, type UpdateFunction, themes }; |
@@ -27,3 +27,3 @@ import { jsx, jsxs, Fragment } from 'react/jsx-runtime'; | ||
***************************************************************************** */ | ||
/* global Reflect, Promise */ | ||
/* global Reflect, Promise, SuppressedError, Symbol */ | ||
@@ -53,2 +53,7 @@ | ||
typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) { | ||
var e = new Error(message); | ||
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e; | ||
}; | ||
var e=[],t=[];function n(n,r){if(n&&"undefined"!=typeof document){var a,s=!0===r.prepend?"prepend":"append",d=!0===r.singleTag,i="string"==typeof r.container?document.querySelector(r.container):document.getElementsByTagName("head")[0];if(d){var u=e.indexOf(i);-1===u&&(u=e.push(i)-1,t[u]={}),a=t[u]&&t[u][s]?t[u][s]:t[u][s]=c();}else a=c();65279===n.charCodeAt(0)&&(n=n.substring(1)),a.styleSheet?a.styleSheet.cssText+=n:a.appendChild(document.createTextNode(n));}function c(){var e=document.createElement("style");if(e.setAttribute("type","text/css"),r.attributes)for(var t=Object.keys(r.attributes),n=0;n<t.length;n++)e.setAttribute(t[n],r.attributes[t[n]]);var a="prepend"===s?"afterbegin":"beforeend";return i.insertAdjacentElement(a,e),e}} | ||
@@ -315,3 +320,3 @@ | ||
const setTheme = (theme) => setStyles(compileStyles(theme)); | ||
return (jsx(ThemeProviderContext.Provider, Object.assign({ value: { styles, setTheme, icons, setIcons } }, { children: children }))); | ||
return (jsx(ThemeProviderContext.Provider, { value: { styles, setTheme, icons, setIcons }, children: children })); | ||
}; | ||
@@ -390,6 +395,6 @@ const useTheme = () => useContext(ThemeProviderContext); | ||
const dummyValue = value.slice(-1) === '\n' ? value + '.' : value; | ||
return (jsxs("div", Object.assign({ style: { display: 'grid' } }, { children: [jsx("textarea", { style: Object.assign({ height: 'auto', gridArea: '1 / 1 / 2 / 2', overflowY: 'auto' }, styles.input), rows: 1, className: className, name: name, value: value, onChange: (e) => setValue(e.target.value), autoFocus: true, onFocus: (e) => { | ||
return (jsxs("div", { style: { display: 'grid' }, children: [jsx("textarea", { style: Object.assign({ height: 'auto', gridArea: '1 / 1 / 2 / 2', overflowY: 'auto' }, styles.input), rows: 1, className: className, name: name, value: value, onChange: (e) => setValue(e.target.value), autoFocus: true, onFocus: (e) => { | ||
if (value.length < 40) | ||
e.target.select(); | ||
}, onKeyDown: handleKeyPress }), jsx("span", Object.assign({ className: className, style: Object.assign({ visibility: 'hidden', height: 'auto', gridArea: '1 / 1 / 2 / 2', color: 'red', opacity: 0.9, whiteSpace: 'pre-wrap', overflow: 'clip', border: '1px solid transparent' }, styles.input) }, { children: dummyValue }))] }))); | ||
}, onKeyDown: handleKeyPress }), jsx("span", { className: className, style: Object.assign({ visibility: 'hidden', height: 'auto', gridArea: '1 / 1 / 2 / 2', color: 'red', opacity: 0.9, whiteSpace: 'pre-wrap', overflow: 'clip', border: '1px solid transparent' }, styles.input), children: dummyValue })] })); | ||
}; | ||
@@ -408,6 +413,6 @@ | ||
const breakString = (text) => text.split('\n').map((line, index, arr) => (jsxs("span", { children: [line, index < arr.length - 1 ? jsx("br", {}) : null] }, index))); | ||
return isEditing ? (jsx(AutogrowTextArea, { className: "jer-input-text", name: path.join('.'), value: value, setValue: setValue, isEditing: isEditing, handleKeyPress: handleKeyPress })) : (jsxs("div", Object.assign({ onDoubleClick: () => setIsEditing(true), onClick: (e) => { | ||
return isEditing ? (jsx(AutogrowTextArea, { className: "jer-input-text", name: path.join('.'), value: value, setValue: setValue, isEditing: isEditing, handleKeyPress: handleKeyPress })) : (jsxs("div", { onDoubleClick: () => setIsEditing(true), onClick: (e) => { | ||
if (e.getModifierState('Control') || e.getModifierState('Meta')) | ||
setIsEditing(true); | ||
}, className: "jer-value-string", style: styles.string }, { children: ["\"", breakString(truncate(value, stringTruncate)), "\""] }))); | ||
}, className: "jer-value-string", style: styles.string, children: ["\"", breakString(truncate(value, stringTruncate)), "\""] })); | ||
}; | ||
@@ -425,14 +430,66 @@ const NumberValue = ({ value, setValue, isEditing, path, setIsEditing, handleEdit, handleCancel, }) => { | ||
}; | ||
return isEditing ? (jsx("input", { className: "jer-input-number", type: "text", name: path.join('.'), value: value, onChange: (e) => setValue(validateNumber(e.target.value)), autoFocus: true, onFocus: (e) => e.target.select(), onKeyDown: handleKeyPress, style: { width: `${String(value).length / 1.5 + 2}em` } })) : (jsx("span", Object.assign({ onDoubleClick: () => setIsEditing(true), className: "jer-value-number", style: styles.number }, { children: value }))); | ||
return isEditing ? (jsx("input", { className: "jer-input-number", type: "text", name: path.join('.'), value: value, onChange: (e) => setValue(validateNumber(e.target.value)), autoFocus: true, onFocus: (e) => e.target.select(), onKeyDown: handleKeyPress, style: { width: `${String(value).length / 1.5 + 2}em` } })) : (jsx("span", { onDoubleClick: () => setIsEditing(true), className: "jer-value-number", style: styles.number, children: value })); | ||
}; | ||
const BooleanValue = ({ value, setValue, isEditing, path, setIsEditing, }) => { | ||
const BooleanValue = ({ value, setValue, isEditing, path, setIsEditing, handleEdit, handleCancel, }) => { | ||
const { styles } = useTheme(); | ||
return isEditing ? (jsx("input", { className: "jer-input-boolean", type: "checkbox", name: path.join('.'), checked: value, onChange: () => setValue(!value) })) : (jsx("span", Object.assign({ onDoubleClick: () => setIsEditing(true), className: "jer-value-boolean", style: styles.boolean }, { children: String(value) }))); | ||
useEffect(() => { | ||
if (isEditing) | ||
document.addEventListener('keydown', listenForSubmit); | ||
return () => document.removeEventListener('keydown', listenForSubmit); | ||
}, [isEditing]); | ||
const listenForSubmit = (event) => { | ||
if (event.key === 'Enter') { | ||
handleEdit(); | ||
} | ||
else if (event.key === 'Escape') | ||
handleCancel(); | ||
}; | ||
return isEditing ? (jsx("input", { className: "jer-input-boolean", type: "checkbox", name: path.join('.'), checked: value, onChange: () => setValue(!value) })) : (jsx("span", { onDoubleClick: () => setIsEditing(true), className: "jer-value-boolean", style: styles.boolean, children: String(value) })); | ||
}; | ||
const NullValue = ({ value, isEditing, setIsEditing }) => { | ||
const NullValue = ({ value, isEditing, setIsEditing, handleEdit, handleCancel, }) => { | ||
const { styles } = useTheme(); | ||
return isEditing ? (jsx("div", Object.assign({ className: "jer-input-null" }, { children: "null" }))) : (jsx("div", Object.assign({ onDoubleClick: () => setIsEditing(true), className: "jer-value-null", style: styles.null }, { children: String(value) }))); | ||
useEffect(() => { | ||
if (isEditing) | ||
document.addEventListener('keydown', listenForSubmit); | ||
return () => document.removeEventListener('keydown', listenForSubmit); | ||
}, [isEditing]); | ||
const listenForSubmit = (event) => { | ||
if (event.key === 'Enter') { | ||
handleEdit(); | ||
} | ||
else if (event.key === 'Escape') | ||
handleCancel(); | ||
}; | ||
return isEditing ? (jsx("div", { className: "jer-input-null", children: "null" })) : (jsx("div", { onDoubleClick: () => setIsEditing(true), className: "jer-value-null", style: styles.null, children: String(value) })); | ||
}; | ||
const ObjectValue = () => (jsx("span", Object.assign({ className: "jer-value-object" }, { children: '{ }' }))); | ||
const ArrayValue = ({ value }) => (jsx("span", Object.assign({ className: "jer-value-array" }, { children: `[${value === null ? '' : value}]` }))); | ||
const ObjectValue = ({ value, translate, isEditing, handleEdit, handleCancel, }) => { | ||
useEffect(() => { | ||
if (isEditing) | ||
document.addEventListener('keydown', listenForSubmit); | ||
return () => document.removeEventListener('keydown', listenForSubmit); | ||
}, []); | ||
const listenForSubmit = (event) => { | ||
if (event.key === 'Enter') { | ||
handleEdit(); | ||
} | ||
else if (event.key === 'Escape') | ||
handleCancel(); | ||
}; | ||
return (jsx("span", { className: "jer-value-object", children: `{${translate('DEFAULT_NEW_KEY')}: "${value}" }` })); | ||
}; | ||
const ArrayValue = ({ value, isEditing, handleEdit, handleCancel, }) => { | ||
useEffect(() => { | ||
if (isEditing) | ||
document.addEventListener('keydown', listenForSubmit); | ||
return () => document.removeEventListener('keydown', listenForSubmit); | ||
}, []); | ||
const listenForSubmit = (event) => { | ||
if (event.key === 'Enter') { | ||
handleEdit(); | ||
} | ||
else if (event.key === 'Escape') | ||
handleCancel(); | ||
}; | ||
return jsx("span", { className: "jer-value-array", children: `[${value === null ? '' : value}]` }); | ||
}; | ||
const InvalidValue = ({ value }) => { | ||
@@ -452,3 +509,3 @@ let message = 'Error!'; | ||
} | ||
return jsx("span", Object.assign({ className: "jer-value-invalid" }, { children: message })); | ||
return jsx("span", { className: "jer-value-invalid", children: message }); | ||
}; | ||
@@ -517,3 +574,3 @@ | ||
}; | ||
return (jsxs("div", Object.assign({ className: "jer-edit-buttons", style: isAdding ? { opacity: 1 } : undefined }, { children: [enableClipboard && (jsx("div", Object.assign({ onClick: handleCopy, className: "jer-copy-pulse" }, { children: jsx(Icon, { name: "copy" }) }))), startEdit && (jsx("div", Object.assign({ onClick: startEdit }, { children: jsx(Icon, { name: "edit" }) }))), handleDelete && (jsx("div", Object.assign({ onClick: handleDelete }, { children: jsx(Icon, { name: "delete" }) }))), handleAdd && (jsx("div", Object.assign({ onClick: () => { | ||
return (jsxs("div", { className: "jer-edit-buttons", style: isAdding ? { opacity: 1 } : undefined, children: [enableClipboard && (jsx("div", { onClick: handleCopy, className: "jer-copy-pulse", children: jsx(Icon, { name: "copy" }) })), startEdit && (jsx("div", { onClick: startEdit, children: jsx(Icon, { name: "edit" }) })), handleDelete && (jsx("div", { onClick: handleDelete, children: jsx(Icon, { name: "delete" }) })), handleAdd && (jsx("div", { onClick: () => { | ||
if (type === 'object') | ||
@@ -523,3 +580,3 @@ setIsAdding(true); | ||
handleAdd(''); | ||
} }, { children: jsx(Icon, { name: "add" }) }))), isAdding && handleAdd && type === 'object' && (jsxs(Fragment, { children: [jsx("input", { className: "jer-input-new-key", type: "text", name: "new-object-key", value: newKey, onChange: (e) => setNewKey(e.target.value), autoFocus: true, onFocus: (e) => e.target.select(), onKeyDown: handleKeyPress, style: Object.assign({}, styles.input) }), jsx(InputButtons, { onOk: () => { | ||
}, children: jsx(Icon, { name: "add" }) })), isAdding && handleAdd && type === 'object' && (jsxs(Fragment, { children: [jsx("input", { className: "jer-input-new-key", type: "text", name: "new-object-key", value: newKey, onChange: (e) => setNewKey(e.target.value), autoFocus: true, onFocus: (e) => e.target.select(), onKeyDown: handleKeyPress, style: Object.assign({}, styles.input) }), jsx(InputButtons, { onOk: () => { | ||
if (!!newKey) { | ||
@@ -531,6 +588,6 @@ setIsAdding(false); | ||
setIsAdding(false); | ||
} })] }))] }))); | ||
} })] }))] })); | ||
}; | ||
const InputButtons = ({ onOk, onCancel }) => { | ||
return (jsxs("div", Object.assign({ className: "jer-confirm-buttons" }, { children: [jsx("div", Object.assign({ onClick: onOk }, { children: jsx(Icon, { name: "ok" }) })), jsx("div", Object.assign({ onClick: onCancel }, { children: jsx(Icon, { name: "cancel" }) }))] }))); | ||
return (jsxs("div", { className: "jer-confirm-buttons", children: [jsx("div", { onClick: onOk, children: jsx(Icon, { name: "ok" }) }), jsx("div", { onClick: onCancel, children: jsx(Icon, { name: "cancel" }) })] })); | ||
}; | ||
@@ -549,5 +606,23 @@ const stringifyPath = (path) => path.reduce((str, part) => { | ||
const ValueNodeWrapper = ({ data, name, path, onEdit, onDelete, enableClipboard, restrictEditFilter, restrictDeleteFilter, showArrayIndices, stringTruncate, indent, translate, }) => { | ||
const getCustomNode = (customNodeDefinitions = [], filterProps) => { | ||
const matchingDefinitions = customNodeDefinitions.filter(({ condition }) => condition(filterProps)); | ||
if (matchingDefinitions.length === 0) | ||
return {}; | ||
const { element, props, hideKey = false } = matchingDefinitions[0]; | ||
return { CustomNode: element, customNodeProps: props, hideKey }; | ||
}; | ||
const CustomNodeWrapper = ({ name, hideKey, children, indent, }) => { | ||
const { styles } = useTheme(); | ||
const indentStyle = indent ? { marginLeft: `${indent / 2}em` } : {}; | ||
return (jsx("div", { className: "jer-component jer-value-component", style: indentStyle, children: jsxs("div", { className: "jer-value-main-row", style: { | ||
flexWrap: name.length > 10 ? 'wrap' : 'nowrap', | ||
}, children: [!hideKey && (jsxs("label", { htmlFor: name, className: "jer-object-key", style: Object.assign(Object.assign({}, styles.property), { minWidth: `${Math.min(String(name).length + 1, 5)}ch`, flexShrink: name.length > 10 ? 1 : 0 }), children: [name, ":", ' '] })), jsx("div", { className: "jer-value-and-buttons", style: { paddingLeft: hideKey ? 0 : undefined }, children: children })] }) })); | ||
}; | ||
const ValueNodeWrapper = (props) => { | ||
const { data, parentData, name, path, onEdit, onDelete, enableClipboard, restrictEditFilter, restrictDeleteFilter, restrictTypeSelection, showLabel, stringTruncate, indent, translate, customNodeDefinitions, } = props; | ||
const { styles } = useTheme(); | ||
const [isEditing, setIsEditing] = useState(false); | ||
const [isEditingKey, setIsEditingKey] = useState(false); | ||
const [value, setValue] = useState(typeof data === 'function' ? INVALID_FUNCTION_STRING : data); | ||
@@ -561,4 +636,10 @@ const [error, setError] = useState(null); | ||
const handleChangeDataType = (type) => { | ||
setValue(convertValue(value, type)); | ||
setDataType(type); | ||
const customNode = customNodeDefinitions.find((customNode) => customNode.name === type); | ||
if (customNode) { | ||
onEdit(customNode.defaultValue, path); | ||
} | ||
else { | ||
setValue(convertValue(value, type)); | ||
setDataType(type); | ||
} | ||
}; | ||
@@ -575,6 +656,6 @@ const logError = (errorString) => { | ||
case 'object': | ||
newValue = {}; | ||
newValue = { [translate('DEFAULT_NEW_KEY')]: value }; | ||
break; | ||
case 'array': | ||
newValue = value !== null ? [value] : []; | ||
newValue = value !== null ? value : []; | ||
break; | ||
@@ -596,5 +677,23 @@ case 'number': | ||
}; | ||
const handleEditKey = (newKey) => { | ||
setIsEditingKey(false); | ||
if (!parentData) | ||
return; | ||
const parentPath = path.slice(0, -1); | ||
if (!newKey) | ||
return; | ||
const newData = Object.fromEntries(Object.entries(parentData).map(([key, val]) => (key === name ? [newKey, val] : [key, val]))); | ||
onEdit(newData, parentPath); | ||
}; | ||
const handleKeyPress = (e) => { | ||
if (e.key === 'Enter') | ||
handleEditKey(e.target.value); | ||
else if (e.key === 'Escape') | ||
handleCancel(); | ||
}; | ||
const handleCancel = () => { | ||
setIsEditing(false); | ||
setIsEditingKey(false); | ||
setValue(data); | ||
setDataType(getDataType(data)); | ||
}; | ||
@@ -610,4 +709,7 @@ const handleDelete = () => { | ||
const canDelete = useMemo(() => !restrictDeleteFilter(filterProps), [filterProps]); | ||
const isArray = typeof path.slice(-1)[0] === 'number'; | ||
const canEditKey = !isArray && canEdit && canDelete; | ||
const inputProps = { | ||
value, | ||
parentData, | ||
setValue, | ||
@@ -620,7 +722,31 @@ isEditing, | ||
stringTruncate, | ||
translate, | ||
}; | ||
return (jsx("div", Object.assign({ className: "jer-component jer-value-component", style: { marginLeft: `${indent / 2}em` } }, { children: jsxs("div", Object.assign({ className: "jer-value-main-row", style: { | ||
const { CustomNode, customNodeProps, hideKey } = getCustomNode(customNodeDefinitions, { | ||
key: name, | ||
path, | ||
level: path.length, | ||
value: data, | ||
size: 0, | ||
}); | ||
const allDataTypes = [ | ||
...customNodeDefinitions | ||
.filter(({ showInTypesSelector = false }) => showInTypesSelector) | ||
.map(({ name }) => name), | ||
...DataTypes, | ||
]; | ||
const allowedDataTypes = useMemo(() => { | ||
if (typeof restrictTypeSelection === 'boolean') | ||
return restrictTypeSelection ? [] : allDataTypes; | ||
if (Array.isArray(restrictTypeSelection)) | ||
return restrictTypeSelection; | ||
const result = restrictTypeSelection(filterProps); | ||
if (typeof result === 'boolean') | ||
return result ? [] : allDataTypes; | ||
return result; | ||
}, [filterProps, restrictTypeSelection]); | ||
return CustomNode ? (jsx(CustomNodeWrapper, { name: name, hideKey: hideKey, indent: indent, children: jsx(CustomNode, Object.assign({ customProps: customNodeProps }, props)) })) : (jsx("div", { className: "jer-component jer-value-component", style: { marginLeft: `${indent / 2}em` }, children: jsxs("div", { className: "jer-value-main-row", style: { | ||
flexWrap: name.length > 10 ? 'wrap' : 'nowrap', | ||
} }, { children: [showArrayIndices && (jsxs("label", Object.assign({ htmlFor: path.join('.'), className: "jer-object-key", style: Object.assign(Object.assign({}, styles.property), { minWidth: `${Math.min(String(name).length + 1, 5)}ch`, flexShrink: name.length > 10 ? 1 : 0 }) }, { children: [name, ":", ' '] }))), jsxs("div", Object.assign({ className: "jer-value-and-buttons" }, { children: [jsx("div", Object.assign({ className: "jer-input-component" }, { children: getInputComponent(dataType, inputProps) })), isEditing ? (jsx(InputButtons, { onOk: handleEdit, onCancel: handleCancel })) : (dataType !== 'invalid' && | ||
!error && (jsx(EditButtons, { startEdit: canEdit ? () => setIsEditing(true) : undefined, handleDelete: canDelete ? handleDelete : undefined, data: data, enableClipboard: enableClipboard, name: name, path: path, translate: translate }))), isEditing && (jsxs("div", Object.assign({ className: "jer-select" }, { children: [jsx("select", Object.assign({ name: `${name}-type-select`, className: "jer-type-select", onChange: (e) => handleChangeDataType(e.target.value), value: dataType }, { children: DataTypes.map((type) => (jsx("option", Object.assign({ value: type }, { children: type }), type))) })), jsx("span", { className: "focus" })] }))), !isEditing && error && (jsx("span", Object.assign({ className: "jer-error-slug", style: styles.error }, { children: error })))] }))] })) }))); | ||
}, children: [showLabel && !isEditingKey && (jsxs("label", { htmlFor: path.join('.'), className: "jer-object-key", style: Object.assign(Object.assign({}, styles.property), { minWidth: `${Math.min(String(name).length + 1, 5)}ch`, flexShrink: name.length > 10 ? 1 : 0 }), onDoubleClick: () => canEditKey && setIsEditingKey(true), children: [name, ":", ' '] })), showLabel && isEditingKey && (jsx("input", { className: "jer-object-key", type: "text", name: path.join('.'), defaultValue: name, autoFocus: true, onFocus: (e) => e.target.select(), onKeyDown: handleKeyPress, style: { width: `${String(name).length / 1.5 + 0.5}em` } })), jsxs("div", { className: "jer-value-and-buttons", children: [jsx("div", { className: "jer-input-component", children: getInputComponent(dataType, inputProps) }), isEditing ? (jsx(InputButtons, { onOk: handleEdit, onCancel: handleCancel })) : (dataType !== 'invalid' && | ||
!error && (jsx(EditButtons, { startEdit: canEdit ? () => setIsEditing(true) : undefined, handleDelete: canDelete ? handleDelete : undefined, data: data, enableClipboard: enableClipboard, name: name, path: path, translate: translate }))), isEditing && allowedDataTypes.length > 0 && (jsxs("div", { className: "jer-select", children: [jsx("select", { name: `${name}-type-select`, className: "jer-type-select", onChange: (e) => handleChangeDataType(e.target.value), value: dataType, children: allowedDataTypes.map((type) => (jsx("option", { value: type, children: type }, type))) }), jsx("span", { className: "focus" })] })), !isEditing && error && (jsx("span", { className: "jer-error-slug", style: styles.error, children: error }))] })] }) })); | ||
}; | ||
@@ -650,3 +776,3 @@ const getDataType = (value) => { | ||
case 'object': | ||
return jsx(ObjectValue, Object.assign({}, inputProps)); | ||
return jsx(ObjectValue, Object.assign({}, inputProps, { value: value })); | ||
case 'array': | ||
@@ -670,3 +796,3 @@ return jsx(ArrayValue, Object.assign({}, inputProps)); | ||
case 'object': | ||
return {}; | ||
return value; | ||
case 'array': | ||
@@ -681,6 +807,7 @@ return [value]; | ||
const CollectionNode = (_a) => { | ||
var { data, path, name } = _a, props = __rest(_a, ["data", "path", "name"]); | ||
var { data, path, name, parentData, showCollectionCount } = _a, props = __rest(_a, ["data", "path", "name", "parentData", "showCollectionCount"]); | ||
const { styles } = useTheme(); | ||
const { onEdit, onAdd, onDelete, restrictEditFilter, restrictDeleteFilter, restrictAddFilter, collapseFilter, enableClipboard, indent, keySort, showArrayIndices, defaultValue, translate, } = props; | ||
const { onEdit, onAdd, onDelete, restrictEditFilter, restrictDeleteFilter, restrictAddFilter, collapseFilter, enableClipboard, indent, keySort, showArrayIndices, defaultValue, translate, customNodeDefinitions, } = props; | ||
const [isEditing, setIsEditing] = useState(false); | ||
const [isEditingKey, setIsEditingKey] = useState(false); | ||
const [stringifiedValue, setStringifiedValue] = useState(JSON.stringify(data, null, 2)); | ||
@@ -739,2 +866,18 @@ const [error, setError] = useState(null); | ||
}; | ||
const handleEditKey = (newKey) => { | ||
setIsEditingKey(false); | ||
if (!parentData) | ||
return; | ||
const parentPath = path.slice(0, -1); | ||
if (!newKey) | ||
return; | ||
const newData = Object.fromEntries(Object.entries(parentData).map(([key, val]) => (key === name ? [newKey, val] : [key, val]))); | ||
onEdit(newData, parentPath); | ||
}; | ||
const handleKeyPressKeyEdit = (e) => { | ||
if (e.key === 'Enter') | ||
handleEditKey(e.target.value); | ||
else if (e.key === 'Escape') | ||
handleCancel(); | ||
}; | ||
const handleAdd = (key) => { | ||
@@ -774,3 +917,6 @@ setCollapsed(false); | ||
const canAdd = useMemo(() => !restrictAddFilter(filterProps), [filterProps]); | ||
const showLabel = showArrayIndices || !(typeof path.slice(-1)[0] === 'number'); | ||
const canEditKey = parentData !== null && canEdit && canAdd && canDelete; | ||
const isArray = typeof path.slice(-1)[0] === 'number'; | ||
const showLabel = showArrayIndices || !isArray; | ||
const showCount = showCollectionCount === 'when-closed' ? collapsed : showCollectionCount; | ||
const keyValueArray = Object.entries(data).map(([key, value]) => [ | ||
@@ -783,16 +929,23 @@ collectionType === 'array' ? Number(key) : key, | ||
const numOfLines = JSON.stringify(data, null, 2).split('\n').length; | ||
return (jsxs("div", Object.assign({ className: "jer-component fb-collection-component", style: { marginLeft: `${path.length === 0 ? 0 : indent / 2}em` } }, { children: [jsxs("div", Object.assign({ className: "jer-collection-header-row" }, { children: [jsx("div", Object.assign({ onClick: handleCollapse }, { children: jsx(Icon, { name: "chevron", rotate: collapsed }) })), jsxs("div", Object.assign({ className: "jer-collection-name" }, { children: [jsx("span", Object.assign({ style: styles.property }, { children: showLabel ? `${name}:` : null })), !isEditing && (jsx("span", Object.assign({ className: "jer-brackets", style: styles.bracket }, { children: brackets.open })))] })), !isEditing && (jsx("div", Object.assign({ className: "jer-collection-item-count", style: styles.itemCount }, { children: collectionSize === 1 | ||
? translate('ITEM_SINGLE', 1) | ||
: translate('ITEMS_MULTIPLE', collectionSize) }))), jsx("div", Object.assign({ className: `jer-brackets${collapsed ? ' jer-visible' : ' jer-hidden'}`, style: styles.bracket }, { children: brackets.close })), !isEditing && (jsx(EditButtons, { startEdit: canEdit | ||
? () => { | ||
setIsEditing(true); | ||
setCollapsed(false); | ||
} | ||
: undefined, handleAdd: canAdd ? handleAdd : undefined, handleDelete: canDelete ? handleDelete : undefined, enableClipboard: enableClipboard, type: collectionType, data: data, name: name, path: path, translate: translate }))] })), jsxs("div", Object.assign({ className: 'jer-collection-inner', style: { | ||
maxHeight: collapsed ? 0 : !isEditing ? `${numOfLines * 3}em` : undefined, | ||
overflowY: collapsed || isAnimating ? 'hidden' : 'visible', | ||
transition: `max-height ${transitionTime}`, | ||
} }, { children: [isEditing ? (jsx("div", Object.assign({ className: "jer-collection-text-edit" }, { children: jsxs("div", { children: [jsx(AutogrowTextArea, { className: "jer-collection-text-area", name: path.join('.'), value: stringifiedValue, setValue: setStringifiedValue, isEditing: isEditing, handleKeyPress: handleKeyPress }), jsx("div", Object.assign({ className: "jer-collection-input-button-row" }, { children: jsx(InputButtons, { onOk: handleEdit, onCancel: handleCancel, isCollection: true }) }))] }) }))) : (jsx(Fragment, { children: !hasBeenOpened.current | ||
? null | ||
: keyValueArray.map(([key, value]) => (jsx("div", Object.assign({ className: "jer-collection-element" }, { children: isCollection(value) ? (jsx(CollectionNode, Object.assign({ data: value, path: [...path, key], name: key }, props), key)) : (jsx(ValueNodeWrapper, Object.assign({ data: value, path: [...path, key], name: key }, props, { showArrayIndices: collectionType === 'object' ? true : showArrayIndices }), key)) }), key))) })), jsx("div", Object.assign({ className: isEditing ? 'jer-collection-error-row' : 'jer-collection-error-row-edit' }, { children: error && (jsx("span", Object.assign({ className: "jer-error-slug", style: styles.error }, { children: error }))) })), jsx("div", Object.assign({ className: "jer-brackets", style: styles.bracket }, { children: brackets.close }))] }))] }))); | ||
const { CustomNode, customNodeProps, hideKey } = getCustomNode(customNodeDefinitions, { | ||
key: name, | ||
path, | ||
level: path.length, | ||
value: data, | ||
size: Object.keys(data).length, | ||
}); | ||
return (jsx("div", { className: "jer-component fb-collection-component", style: { marginLeft: `${path.length === 0 ? 0 : indent / 2}em` }, children: CustomNode ? (jsx(CustomNodeWrapper, { name: name, hideKey: hideKey, children: jsx(CustomNode, Object.assign({ data: data, path: path, name: name, parentData: parentData, customProps: customNodeProps }, props)) })) : (jsxs(Fragment, { children: [jsxs("div", { className: "jer-collection-header-row", children: [jsx("div", { onClick: handleCollapse, children: jsx(Icon, { name: "chevron", rotate: collapsed }) }), jsxs("div", { className: "jer-collection-name", children: [!isEditingKey && (jsx("span", { style: styles.property, onDoubleClick: () => canEditKey && setIsEditingKey(true), children: showLabel && name !== '' && name !== undefined ? `${name}:` : null })), isEditingKey && (jsx("input", { className: "jer-collection-name", type: "text", name: path.join('.'), defaultValue: name, autoFocus: true, onFocus: (e) => e.target.select(), onKeyDown: handleKeyPressKeyEdit, style: { width: `${String(name).length / 1.5 + 0.5}em` } })), !isEditing && (jsx("span", { className: "jer-brackets", style: styles.bracket, children: brackets.open }))] }), !isEditing && showCollectionCount && (jsx("div", { className: `jer-collection-item-count${showCount ? ' jer-visible' : ' jer-hidden'}`, style: styles.itemCount, children: collectionSize === 1 | ||
? translate('ITEM_SINGLE', 1) | ||
: translate('ITEMS_MULTIPLE', collectionSize) })), jsx("div", { className: `jer-brackets${collapsed ? ' jer-visible' : ' jer-hidden'}`, style: styles.bracket, children: brackets.close }), !isEditing && (jsx(EditButtons, { startEdit: canEdit | ||
? () => { | ||
setIsEditing(true); | ||
setCollapsed(false); | ||
} | ||
: undefined, handleAdd: canAdd ? handleAdd : undefined, handleDelete: canDelete ? handleDelete : undefined, enableClipboard: enableClipboard, type: collectionType, data: data, name: name, path: path, translate: translate }))] }), jsxs("div", { className: 'jer-collection-inner', style: { | ||
maxHeight: collapsed ? 0 : !isEditing ? `${numOfLines * 3}em` : undefined, | ||
overflowY: collapsed || isAnimating ? 'hidden' : 'visible', | ||
transition: `max-height ${transitionTime}`, | ||
}, children: [isEditing ? (jsx("div", { className: "jer-collection-text-edit", children: jsxs("div", { children: [jsx(AutogrowTextArea, { className: "jer-collection-text-area", name: path.join('.'), value: stringifiedValue, setValue: setStringifiedValue, isEditing: isEditing, handleKeyPress: handleKeyPress }), jsx("div", { className: "jer-collection-input-button-row", children: jsx(InputButtons, { onOk: handleEdit, onCancel: handleCancel, isCollection: true }) })] }) })) : (jsx(Fragment, { children: !hasBeenOpened.current | ||
? null | ||
: keyValueArray.map(([key, value]) => (jsx("div", { className: "jer-collection-element", children: isCollection(value) ? (jsx(CollectionNode, Object.assign({ data: value, parentData: data, path: [...path, key], name: key, showCollectionCount: showCollectionCount }, props), key)) : (jsx(ValueNodeWrapper, Object.assign({ data: value, parentData: data, path: [...path, key], name: key }, props, { showLabel: collectionType === 'object' ? true : showArrayIndices }), key)) }, key))) })), jsx("div", { className: isEditing ? 'jer-collection-error-row' : 'jer-collection-error-row-edit', children: error && (jsx("span", { className: "jer-error-slug", style: styles.error, children: error })) }), jsx("div", { className: "jer-brackets", style: styles.bracket, children: brackets.close })] })] })) })); | ||
}; | ||
@@ -809,2 +962,4 @@ | ||
ERROR_ADD: 'Adding node unsuccessful', | ||
DEFAULT_STRING: 'New data!', | ||
DEFAULT_NEW_KEY: 'key', | ||
}; | ||
@@ -817,3 +972,3 @@ const translate = (translations, key, count) => { | ||
const Editor = ({ data: srcData, rootName = 'root', onUpdate, onEdit: srcEdit = onUpdate, onDelete: srcDelete = onUpdate, onAdd: srcAdd = onUpdate, enableClipboard = true, theme = 'default', icons, indent = 4, collapse = false, restrictEdit = false, restrictDelete = false, restrictAdd = false, keySort = false, showArrayIndices = true, defaultValue = null, minWidth = 250, maxWidth = 'min(600px, 90vw)', stringTruncate = 250, translations = {}, className, }) => { | ||
const Editor = ({ data: srcData, rootName = 'root', onUpdate, onEdit: srcEdit = onUpdate, onDelete: srcDelete = onUpdate, onAdd: srcAdd = onUpdate, enableClipboard = true, theme = 'default', icons, indent = 4, collapse = false, showCollectionCount = true, restrictEdit = false, restrictDelete = false, restrictAdd = false, restrictTypeSelection = false, keySort = false, showArrayIndices = true, defaultValue = null, minWidth = 250, maxWidth = 'min(600px, 90vw)', stringTruncate = 250, translations = {}, className, customNodeDefinitions = [], }) => { | ||
const [data, setData] = useState(srcData); | ||
@@ -900,2 +1055,3 @@ const { styles, setTheme, setIcons } = useTheme(); | ||
onAdd, | ||
showCollectionCount, | ||
collapseFilter, | ||
@@ -905,2 +1061,3 @@ restrictEditFilter, | ||
restrictAddFilter, | ||
restrictTypeSelection, | ||
enableClipboard, | ||
@@ -913,6 +1070,8 @@ keySort, | ||
translate, | ||
customNodeDefinitions, | ||
parentData: null, | ||
}; | ||
if (!styles) | ||
return null; | ||
return (jsx("div", Object.assign({ className: 'jer-editor-container ' + className, style: Object.assign(Object.assign({}, styles.container), { minWidth, maxWidth }) }, { children: isCollection(data) && jsx(CollectionNode, Object.assign({ data: data, path: [] }, otherProps)) }))); | ||
return (jsx("div", { className: 'jer-editor-container ' + className, style: Object.assign(Object.assign({}, styles.container), { minWidth, maxWidth }), children: isCollection(data) ? (jsx(CollectionNode, Object.assign({ data: data, path: [] }, otherProps))) : (jsx(ValueNodeWrapper, Object.assign({ data: data, path: [], showLabel: true }, otherProps))) })); | ||
}; | ||
@@ -946,2 +1105,2 @@ const JsonEditor = (props) => (jsx(ThemeProvider, { children: jsx(Editor, Object.assign({}, props)) })); | ||
export { JsonEditor, JsonEditor as default, themes }; | ||
export { JsonEditor, themes }; |
{ | ||
"name": "json-edit-react", | ||
"version": "0.9.6", | ||
"version": "1.0.0", | ||
"description": "React component for editing or viewing JSON/object data", | ||
@@ -30,12 +30,12 @@ "main": "build/index.cjs.js", | ||
"object-property-assigner": "^1.0.1", | ||
"object-property-extractor": "^1.0.6", | ||
"react-icons": "^4.8.0" | ||
"object-property-extractor": "^1.0.7", | ||
"react-icons": "^4.12.0" | ||
}, | ||
"devDependencies": { | ||
"@rollup/plugin-typescript": "^11.1.1", | ||
"@types/node": "^20.1.4", | ||
"@types/react": "^18.2.6", | ||
"rimraf": "^5.0.0", | ||
"rollup": "^3.22.0", | ||
"rollup-plugin-dts": "^5.3.0", | ||
"@rollup/plugin-typescript": "^11.1.5", | ||
"@types/node": "^20.9.0", | ||
"@types/react": "^18.2.37", | ||
"rimraf": "^5.0.5", | ||
"rollup": "^4.4.1", | ||
"rollup-plugin-dts": "^6.1.0", | ||
"rollup-plugin-peer-deps-external": "^2.2.4", | ||
@@ -46,5 +46,5 @@ "rollup-plugin-sizes": "^1.0.5", | ||
"ts-node": "^10.9.1", | ||
"tslib": "^2.5.1", | ||
"typescript": "^5.0.4" | ||
"tslib": "^2.6.2", | ||
"typescript": "^5.2.2" | ||
} | ||
} |
114
README.md
@@ -31,2 +31,3 @@ # json-edit-react | ||
- [Localisation](#localisation) | ||
- [Custom nodes](#custom-nodes) | ||
- [Undo functionality](#undo-functionality) | ||
@@ -63,3 +64,3 @@ - [Issues, bugs, suggestions?](#issues-bugs-suggestions) | ||
- Double-click a value to edit it | ||
- Double-click a value (or a key) to edit it | ||
- When editing a string, use `Cmd/Ctrl/Shift-Enter` to add a new line (`Enter` submits the value) | ||
@@ -74,26 +75,29 @@ - It's the opposite when editing a full object/array node (which you do by clicking "edit" on an object or array value) — `Enter` for new line, and `Cmd/Ctrl/Shift-Enter` for submit | ||
| prop | type | default | description | | ||
| ------------------ | -------------------------------------------- | ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | ||
| `data` | `object\|array` | | The data to be displayed / edited | | ||
| `rootName` | `string` | `"data"` | A name to display in the editor as the root of the data object. | | ||
| `onUpdate` | `UpdateFunction` | | A function to run whenever a value is **updated** (edit, delete *or* add) in the editor. See [Update functions](#update-functions). | | ||
| `onUpdate` | `UpdateFunction` | | A function to run whenever a value is **edited**. | | ||
| `onDelete` | `UpdateFunction` | | A function to run whenever a value is **deleted**. | | ||
| `onAdd` | `UpdateFunction` | | A function to run whenever a new property is **added**. | | ||
| `enableClipboard` | `boolean\|CopyFunction` | `true` | Whether or not to enable the "Copy to clipboard" button in the UI. If a function is provided, `true` is assumed and this function will be run whenever an item is copied. | | ||
| `indent` | `number` | `4` | Specify the amount of indentation for each level of nesting in the displayed data. | | ||
| `collapse` | `boolean\|number\|FilterFunction` | `false` | Defines which nodes of the JSON tree will be displayed "opened" in the UI on load. If `boolean`, it'll be either all or none. A `number` specifies a nesting depth after which nodes will be closed. For more fine control a function can be provided — see [Filter functions](#filter-functions). | | ||
| `restrictEdit` | `boolean\|FilterFunction` | `false` | If `false`, no editing is permitted. A function can be provided for more specificity — see [Filter functions](#filter-functions) | | ||
| `restrictDelete` | `boolean\|FilterFunction` | `false` | As with `restrictEdit` but for deletion | | ||
| `restrictAdd` | `boolean\|FilterFunction` | `false` | As with `restrictEdit` but for adding new properties | | ||
| `keySort` | `boolean\|CompareFunction` | `false` | If `true`, object keys will be ordered (using default JS `.sort()`). A [compare function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort) can also be provided to define sorting behaviour. | | ||
| `showArrayIndices` | `boolean` | `true` | Whether or not to display the index (as a property key) for array elements. | | ||
| `defaultValue` | `any` | `null` | When a new property is added, it is initialised with this value. | | ||
| `stringTruncate` | `number` | `250` | String values longer than this many characters will be displayed truncated (with `...`). The full string will always be visible when editing. | | ||
| `translations` | `LocalisedStrings` object | `{ }` | UI strings (such as error messages) can be translated by passing an object containing localised string values (there are only a few). See [Localisation](#localisation) | | ||
| `theme` | `string\|ThemeObject\|[string, ThemeObject]` | `"default"` | Either the name of one of the built-in themes, or an object specifying some or all theme properties. See [Themes](#themes). | | ||
| `className` | `string` | | Name of a CSS class to apply to the component. In most cases, specifying `theme` properties will be more straightforward. | | ||
| `icons` | `{[iconName]: JSX.Element, ... }` | `{ }` | Replace the built-in icons by specifying them here. See [Themes](#themes). | | | ||
| `minWidth` | `number\|string` (CSS value) | `250` | Minimum width for the editor container. | | ||
| `maxWidth` | `number\|string` (CSS value) | `600` | Maximum width for the editor container. | | ||
| prop | type | default | description | | ||
| ----------------------- | -------------------------------------------- | ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | ||
| `data` | `object\|array` | | The data to be displayed / edited | | ||
| `rootName` | `string` | `"data"` | A name to display in the editor as the root of the data object. | | ||
| `onUpdate` | `UpdateFunction` | | A function to run whenever a value is **updated** (edit, delete *or* add) in the editor. See [Update functions](#update-functions). | | ||
| `onUpdate` | `UpdateFunction` | | A function to run whenever a value is **edited**. | | ||
| `onDelete` | `UpdateFunction` | | A function to run whenever a value is **deleted**. | | ||
| `onAdd` | `UpdateFunction` | | A function to run whenever a new property is **added**. | | ||
| `enableClipboard` | `boolean\|CopyFunction` | `true` | Whether or not to enable the "Copy to clipboard" button in the UI. If a function is provided, `true` is assumed and this function will be run whenever an item is copied. | | ||
| `indent` | `number` | `4` | Specify the amount of indentation for each level of nesting in the displayed data. | | ||
| `collapse` | `boolean\|number\|FilterFunction` | `false` | Defines which nodes of the JSON tree will be displayed "opened" in the UI on load. If `boolean`, it'll be either all or none. A `number` specifies a nesting depth after which nodes will be closed. For more fine control a function can be provided — see [Filter functions](#filter-functions). | | ||
| `restrictEdit` | `boolean\|FilterFunction` | `false` | If `false`, no editing is permitted. A function can be provided for more specificity — see [Filter functions](#filter-functions) | | ||
| `restrictDelete` | `boolean\|FilterFunction` | `false` | As with `restrictEdit` but for deletion | | ||
| `restrictAdd` | `boolean\|FilterFunction` | `false` | As with `restrictEdit` but for adding new properties | | ||
| `restrictTypeSelection` | `boolean\|DataType[]\|TypeFilterFunction` | `false` | For restricting the data types the user can select. This varies slightly from the above restrictions in that the value (or output of the `TypeFilterFunction`) can be a list of data types *or* a `boolean`. | | ||
| `keySort` | `boolean\|CompareFunction` | `false` | If `true`, object keys will be ordered (using default JS `.sort()`). A [compare function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort) can also be provided to define sorting behaviour. | | ||
| `showArrayIndices` | `boolean` | `true` | Whether or not to display the index (as a property key) for array elements. | | ||
| `showCollectionCount` | `boolean\|"when-closed"` | `true` | Whether or not to display the number of items in each collection (object or array). | | ||
| `defaultValue` | `any` | `null` | When a new property is added, it is initialised with this value. | | ||
| `stringTruncate` | `number` | `250` | String values longer than this many characters will be displayed truncated (with `...`). The full string will always be visible when editing. | | ||
| `translations` | `LocalisedStrings` object | `{ }` | UI strings (such as error messages) can be translated by passing an object containing localised string values (there are only a few). See [Localisation](#localisation) | | ||
| `theme` | `string\|ThemeObject\|[string, ThemeObject]` | `"default"` | Either the name of one of the built-in themes, or an object specifying some or all theme properties. See [Themes](#themes). | | ||
| `className` | `string` | | Name of a CSS class to apply to the component. In most cases, specifying `theme` properties will be more straightforward. | | ||
| `icons` | `{[iconName]: JSX.Element, ... }` | `{ }` | Replace the built-in icons by specifying them here. See [Themes](#themes). | | | ||
| `minWidth` | `number\|string` (CSS value) | `250` | Minimum width for the editor container. | | ||
| `maxWidth` | `number\|string` (CSS value) | `600` | Maximum width for the editor container. | | ||
| `customNodeDefinitions` | `CustomNodeDefinition[]` | | You can provide customised components to override specific nodes in the data tree, according to a condition function. See see [Custom nodes](#custom-nodes) for more detail. | | ||
@@ -137,3 +141,3 @@ ## Update functions | ||
You can control which nodes of the data structure can be edited, deleted, or added to by passing Filter functions. These will be called on each property in the data and the attribute will be enforced depending on whether the function returns `true` or `false` (`true` means *cannot* be edited). | ||
You can control which nodes of the data structure can be edited, deleted, or added to, or have their data type changed, by passing Filter functions. These will be called on each property in the data and the attribute will be enforced depending on whether the function returns `true` or `false` (`true` means *cannot* be edited). | ||
@@ -153,2 +157,14 @@ The function receives the following object: | ||
For restricting data types, the (Type) filter function is slightly more sophisticated. The input is the same, but the output can be either a `boolean` (which would restrict the available types for a given node to either *all* or *none*), or an array of data types to be restricted to. The available values are: | ||
- `"string"` | ||
- `"number"` | ||
- `"boolean"` | ||
- `"null"` | ||
- `"object"` | ||
- `"array"` | ||
There is no specific restriction function for editing object key names, but they must return `true` for *both* `restrictEdit` and `restrictDelete` (and `restrictAdd` for collections), since changing a key name is equivalent to deleting a property and adding a new one. | ||
Using all these restriction filters together can allow you to enforce a reasonably sophisticated data schema. | ||
### Examples | ||
@@ -182,2 +198,16 @@ | ||
- Multiple type restrictions: | ||
- `string` values can only be changed to strings or objects (for nesting) | ||
- `null` is not allowed anywhere | ||
- `boolean` values must remain boolean | ||
- data nested below the "user" field can be any simple property (i.e. not objects or arrays), and doesn't have to follow the above rules (except no "null") | ||
```js | ||
restrictTypeSelection = { ({ path, value }) => { | ||
if (path.includes('user')) return ['string', 'number', 'boolean'] | ||
if (typeof value === 'boolean') return false | ||
if (typeof value === 'string') return ['string', 'object'] | ||
return ['string', 'number', 'boolean', 'array', 'object'] // no "null" | ||
} } | ||
``` | ||
## Themes | ||
@@ -322,2 +352,28 @@ | ||
## Custom nodes | ||
You can replace certain nodes in the data tree with your own custom components. An example might be for an image display, or a custom date editor, or just to add some visual bling. See the "Custom Nodes" data set in the [interactive demo](https://carlosnz.github.io/json-edit-react/) to see it in action. | ||
Custom nodes are provided in the `customNodeDefinitions` prop, as an array of objects of following structure: | ||
```js | ||
{ | ||
condition, // a FilterFunction, as above | ||
element, // React Component | ||
props, // object (optional) | ||
hideKey, // boolean (optional) | ||
showInTypesSelector, // boolean (optional) | ||
defaultValue, // JSON value for a new instance of your component | ||
name // string | ||
} | ||
``` | ||
The `condition` is just a [Filter function](#filter-functions), with the same input parameters (`key`, `path`, `level`, `value`, `size`), and `element` is a React component. Every node in the data structure will be run through each condition function, and any that match will be replaced by your custom component. Note that if a node matches more than one custom definition conditions (from multiple components), the *first* one will be used, so place them in the array in priority order. | ||
The component will receive *all* the same props as a standard node component (see codebase), but you can pass additional props to your component if required through the `props` object. | ||
By default, your component will be presented to the right of the property key it belongs to, like any other value. However, you can hide the key itself by setting `hideKey: true`, and the custom component will take the whole row. (See the "Presented by" box in the "Custom Nodes" data set for an example.) | ||
You can allow users to create new instances of your special nodes by selecting them as a "Type" in the types selector when editing/adding values. Set `showInTypesSelector: true` to enable this. However, if this is enabled you need to also provide a `name` (which is what the user will see in the selector) and a `defaultValue` which is the data that is inserted when the user selects this "type". (The `defaultValue` must return `true` if passed through the `condition` function in order for it to be immediately displayed using your custom component.) | ||
## Undo functionality | ||
@@ -347,2 +403,8 @@ | ||
- **1.0.0**: | ||
- [Custom nodes](#custom-nodes) | ||
- Allow editing of keys | ||
- Option to define restrictions on data type selection | ||
- Option to hide array/object item counts | ||
- Improve keyboard interaction | ||
- **0.9.6**: Performance improvement by not processing child elements if not visible | ||
@@ -349,0 +411,0 @@ - **0.9.4**: |
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
185404
2343
1
410
Updatedreact-icons@^4.12.0