diff --git a/src/lib/components/JSONEditor.scss b/src/lib/components/JSONEditor.scss index 8fe5e3fe..d29adde6 100644 --- a/src/lib/components/JSONEditor.scss +++ b/src/lib/components/JSONEditor.scss @@ -12,6 +12,9 @@ display: flex; flex-direction: row; - &.jse-focus { + // lighter selection color when the editor doesn't have focus + &:not(.jse-focus) { + --jse-selection-background-color: #{$selection-background-inactive-color}; + --jse-context-menu-pointer-background: #{$context-menu-pointer-hover-background}; } } diff --git a/src/lib/components/__snapshots__/JSONEditor.test.ts.snap b/src/lib/components/__snapshots__/JSONEditor.test.ts.snap index c08f86bb..113c9341 100644 --- a/src/lib/components/__snapshots__/JSONEditor.test.ts.snap +++ b/src/lib/components/__snapshots__/JSONEditor.test.ts.snap @@ -4,10 +4,10 @@ exports[`JSONEditor > render table mode 1`] = `
render table mode 1`] = `
@@ -439,7 +463,7 @@ exports[`JSONEditor > render table mode 1`] = ` class="jse-table-invisible-end-section" > {/each} {#if showRefreshButton} diff --git a/src/lib/components/modes/tablemode/tag/InlineValue.scss b/src/lib/components/modes/tablemode/tag/InlineValue.scss index 59bfc007..a01a6011 100644 --- a/src/lib/components/modes/tablemode/tag/InlineValue.scss +++ b/src/lib/components/modes/tablemode/tag/InlineValue.scss @@ -8,15 +8,7 @@ padding: 0 $padding-half; background: transparent; color: inherit; - cursor: pointer; - - &:hover { - background: $hover-background-color; - } - - &.jse-selected { - background: $selection-background-color; - } + cursor: inherit; &.jse-highlight { background-color: $search-match-color; diff --git a/src/lib/components/modes/treemode/CollapsedItems.scss b/src/lib/components/modes/treemode/CollapsedItems.scss index eef165f4..4c946620 100644 --- a/src/lib/components/modes/treemode/CollapsedItems.scss +++ b/src/lib/components/modes/treemode/CollapsedItems.scss @@ -39,6 +39,11 @@ div.jse-collapsed-items { display: flex; + &.jse-selected { + background-color: $selection-background-color; + --jse-collapsed-items-background-color: #{$collapsed-items-selected-background-color}; + } + div.jse-text, button.jse-expand-items { margin: 0 $padding-half; diff --git a/src/lib/components/modes/treemode/JSONKey.scss b/src/lib/components/modes/treemode/JSONKey.scss index 33f7a9c9..f842022e 100644 --- a/src/lib/components/modes/treemode/JSONKey.scss +++ b/src/lib/components/modes/treemode/JSONKey.scss @@ -9,20 +9,11 @@ border-radius: 1px; vertical-align: top; color: $key-color; - cursor: $contents-cursor; word-break: normal; overflow-wrap: normal; // not anywhere as for JSONValue white-space: pre-wrap; // important for rendering multiple consecutive spaces - &:hover { - background: $hover-background-color; - } - - &:hover { - background: $hover-background-color; - } - &.jse-empty { min-width: 3em; outline: 1px dotted $tag-background; diff --git a/src/lib/components/modes/treemode/JSONKey.svelte b/src/lib/components/modes/treemode/JSONKey.svelte index db393d43..cb33f7bf 100644 --- a/src/lib/components/modes/treemode/JSONKey.svelte +++ b/src/lib/components/modes/treemode/JSONKey.svelte @@ -16,7 +16,6 @@ import { UpdateSelectionAfterChange } from '$lib/types.js' import { type JSONPath, type JSONPointer, parseJSONPointer } from 'immutable-json-patch' import ContextMenuPointer from '../../../components/controls/contextmenu/ContextMenuPointer.svelte' - import { classnames } from '$lib/utils/cssUtils.js' export let pointer: JSONPointer export let key: string @@ -41,12 +40,6 @@ } } - function getKeyClass(key: string) { - return classnames('jse-key', { - 'jse-empty': key === '' - }) - } - function handleChangeValue(newKey: string, updateSelection: UpdateSelectionAfterChange) { const updatedKey = onUpdateKey(key, context.normalization.unescapeValue(newKey)) const updatedPath = initial(path).concat(updatedKey) @@ -82,7 +75,8 @@
{#if searchResultItems} diff --git a/src/lib/components/modes/treemode/JSONNode.scss b/src/lib/components/modes/treemode/JSONNode.scss index ce0f36f8..769a5a03 100644 --- a/src/lib/components/modes/treemode/JSONNode.scss +++ b/src/lib/components/modes/treemode/JSONNode.scss @@ -74,12 +74,6 @@ padding-bottom: $contents-padding; box-sizing: border-box; - > .jse-header-outer :global(.jse-context-menu-pointer), - > .jse-contents-outer > .jse-contents :global(.jse-context-menu-pointer) { - top: 0; - right: calc(-2px - $context-menu-pointer-size); - } - > .jse-contents-outer > .jse-contents { padding-left: 0; } @@ -123,6 +117,11 @@ .jse-contents { padding-left: $indent-size; + cursor: $contents-cursor; + + .jse-value-outer { + display: inline-flex; + } } .jse-footer { @@ -137,7 +136,6 @@ } .jse-insert-selection-area { - visibility: hidden; padding: 0 $padding-half; flex: 1; // must fill all left over space at the right side of the editor, so you can click there @@ -171,97 +169,68 @@ margin-right: $padding-half; outline: $height-half solid; // color depends on hovered/selected/inactive+selected - :global(.jse-context-menu-pointer) { - right: -$height-half; - background: $context-menu-pointer-hover-background; - } - &.jse-hovered { outline-color: $context-menu-pointer-hover-background; } } - &:hover > .jse-contents-outer .jse-insert-selection-area:not(.jse-selected), - .jse-header-outer:hover > .jse-insert-selection-area:not(.jse-selected), - .jse-footer-outer:hover .jse-insert-selection-area:not(.jse-selected) { - visibility: visible; + .jse-key-outer { + position: relative; } - &.jse-hovered { - > .jse-header-outer > .jse-header > .jse-meta, - .jse-props .jse-header, - .jse-items .jse-header, - .jse-props .jse-contents, - .jse-items .jse-contents, - .jse-footer { + .jse-key-outer, + .jse-value-outer, + .jse-meta, + .jse-footer { + &:hover { background: $hover-background-color; + cursor: $contents-cursor; } } - // TODO: simplify the styling for selected keys/values/multi (get rid of globals?) - - // entry selected (key and value) + // key and value selected &.jse-selected { - > .jse-header-outer > .jse-header > .jse-meta, - .jse-props .jse-header, - .jse-items .jse-header, - .jse-props .jse-contents, - .jse-items .jse-contents, .jse-header, .jse-contents, - .jse-footer, - :global(.jse-key), - :global(.jse-value) { + .jse-footer { background: $selection-background-color; cursor: $contents-selected-cursor; } - .jse-expand { - background: $selection-background-color; + .jse-key-outer, + .jse-value-outer, + .jse-meta, + .jse-footer { + &:hover { + background: inherit; + cursor: inherit; + } } } // key selected - &.jse-selected-key { - > .jse-contents-outer > .jse-contents > :global(.jse-identifier) > :global(.jse-key), - > .jse-header-outer > .jse-header > :global(.jse-identifier) > :global(.jse-key) { - background: $selection-background-color; - cursor: $contents-selected-cursor; - } - } - - // value selected (part 1) - &.jse-selected-value > .jse-contents-outer > .jse-contents > :global(.jse-value) { + .jse-key-outer.jse-selected-key { background: $selection-background-color; cursor: $contents-selected-cursor; } - :global(.jse-collapsed-items.jse-selected), - &.jse-selected :global(.jse-collapsed-items), - &.jse-selected-value :global(.jse-collapsed-items) { - background-color: $selection-background-color; - --jse-collapsed-items-background-color: #{$collapsed-items-selected-background-color}; - } - - // value selected (part 2) + // value selected &.jse-selected-value { + .jse-value-outer, .jse-meta, - > .jse-header-outer > .jse-header > .jse-meta, - > .jse-footer-outer > .jse-footer, - .jse-props .jse-contents, - .jse-props .jse-header, - .jse-props .jse-footer, - .jse-props .jse-expand, - .jse-items .jse-contents, .jse-items .jse-header, - .jse-items .jse-footer, - .jse-items .jse-expand { + .jse-items .jse-contents, + .jse-props .jse-header, + .jse-props .jse-contents, + .jse-footer { background: $selection-background-color; + cursor: $contents-selected-cursor; + } - :global(.jse-key), - :global(.jse-value) { - background: $selection-background-color; - cursor: $contents-selected-cursor; + .jse-key-outer { + &:hover { + background: inherit; + cursor: inherit; } } } @@ -275,24 +244,4 @@ outline-color: $context-menu-pointer-background; } } - - .jse-insert-area { - &.jse-selected { - :global(.jse-context-menu-pointer) { - background: $context-menu-pointer-background; - - &:hover { - background: $context-menu-pointer-background-highlight; - } - } - } - } -} - -// lighter selection color when the editor doesn't have focus -:global(.jse-main:not(.jse-focus)) { - .jse-json-node { - --jse-selection-background-color: #{$selection-background-inactive-color}; - --jse-context-menu-pointer-background: #{$context-menu-pointer-hover-background}; - } } diff --git a/src/lib/components/modes/treemode/JSONNode.svelte b/src/lib/components/modes/treemode/JSONNode.svelte index c21077af..fbc3126d 100644 --- a/src/lib/components/modes/treemode/JSONNode.svelte +++ b/src/lib/components/modes/treemode/JSONNode.svelte @@ -573,7 +573,6 @@ style:--level={path.length} class:jse-root={root} class:jse-selected={isNodeSelected && isMultiSelection(selection)} - class:jse-selected-key={isNodeSelected && isKeySelection(selection)} class:jse-selected-value={isNodeSelected && isValueSelection(selection)} class:jse-readonly={context.readOnly} class:jse-hovered={hover === HOVER_COLLECTION} @@ -624,7 +623,7 @@
{#if !context.readOnly && isNodeSelected && selection && (isValueSelection(selection) || isMultiSelection(selection)) && !isEditingSelection(selection) && isEqual(getFocusPath(selection), path)}
- +
{/if} @@ -659,6 +658,7 @@ title={INSERT_EXPLANATION} > @@ -755,7 +755,7 @@ {#if !context.readOnly && isNodeSelected && selection && (isValueSelection(selection) || isMultiSelection(selection)) && !isEditingSelection(selection) && isEqual(getFocusPath(selection), path)}
- +
{/if} @@ -790,6 +790,7 @@ title={INSERT_EXPLANATION} > @@ -806,10 +807,12 @@ ? validationErrors.properties[key] : undefined} + {@const nestedPath = path.concat(key)} + {@const nestedSelection = selectionIfOverlapping( context.getJson(), selection, - path.concat(key) + nestedPath )} -
+
:
{/if} - +
+ +
{#if !context.readOnly && isNodeSelected && selection && (isValueSelection(selection) || isMultiSelection(selection)) && !isEditingSelection(selection) && isEqual(getFocusPath(selection), path)}
- +
{/if}
@@ -892,6 +902,7 @@ title={INSERT_EXPLANATION} > diff --git a/src/lib/components/modes/treemode/JSONValue.svelte b/src/lib/components/modes/treemode/JSONValue.svelte index 9a534381..6435a992 100644 --- a/src/lib/components/modes/treemode/JSONValue.svelte +++ b/src/lib/components/modes/treemode/JSONValue.svelte @@ -18,6 +18,7 @@ $: renderers = context.onRenderValue({ path, value, + mode: context.mode, readOnly: context.readOnly, enforceString, isEditing, diff --git a/src/lib/components/modes/treemode/TreeMode.svelte b/src/lib/components/modes/treemode/TreeMode.svelte index 9e3e8475..d336298a 100644 --- a/src/lib/components/modes/treemode/TreeMode.svelte +++ b/src/lib/components/modes/treemode/TreeMode.svelte @@ -1814,6 +1814,7 @@ // it should only change when a config option like readOnly or onClassName is changed let context: TreeModeContext $: context = { + mode: Mode.tree, readOnly, parser, normalization, diff --git a/src/lib/logic/documentState.test.ts b/src/lib/logic/documentState.test.ts index e71243aa..c9af9bcf 100644 --- a/src/lib/logic/documentState.test.ts +++ b/src/lib/logic/documentState.test.ts @@ -8,6 +8,7 @@ import { createDocumentState, createObjectDocumentState, createValueDocumentState, + deleteInDocumentState, documentStateFactory, documentStatePatch, ensureRecursiveState, @@ -1892,6 +1893,29 @@ describe('documentState', () => { }) }) }) + + describe('deleteInDocumentState', () => { + const json = { value: '42' } + const documentState: DocumentState = { + type: 'object', + expanded: true, + properties: { + value: { type: 'value', enforceString: true } + } + } + + test('delete existing state', () => { + expect(deleteInDocumentState(json, documentState, ['value'])).toEqual({ + type: 'object', + expanded: true, + properties: {} + }) + }) + + test('delete non-existing state', () => { + expect(deleteInDocumentState(json, documentState, ['foo'])).toEqual(documentState) + }) + }) }) /** diff --git a/src/lib/logic/documentState.ts b/src/lib/logic/documentState.ts index 81ef91ee..0763bceb 100644 --- a/src/lib/logic/documentState.ts +++ b/src/lib/logic/documentState.ts @@ -1,6 +1,7 @@ import { compileJSONPointer, deleteIn, + existsIn, getIn, immutableJSONPatch, isJSONArray, @@ -555,7 +556,11 @@ export function deleteInDocumentState( documentState: T | undefined, path: JSONPath ): T | undefined { - return deleteIn(documentState, toRecursiveStatePath(json, path)) + const recursivePath = toRecursiveStatePath(json, path) + + return existsIn(documentState, recursivePath) + ? deleteIn(documentState, toRecursiveStatePath(json, path)) + : documentState } export function documentStateAdd( diff --git a/src/lib/plugins/value/components/EditableValue.svelte b/src/lib/plugins/value/components/EditableValue.svelte index 4375b5b0..ef9b990a 100644 --- a/src/lib/plugins/value/components/EditableValue.svelte +++ b/src/lib/plugins/value/components/EditableValue.svelte @@ -7,23 +7,25 @@ import { createValueSelection, getFocusPath, isEditingSelection } from '$lib/logic/selection.js' import { getValueClass } from '$lib/plugins/value/components/utils/getValueClass.js' import EditableDiv from '../../../components/controls/EditableDiv.svelte' - import type { - FindNextInside, - JSONParser, - JSONSelection, - OnFind, - OnJSONSelect, - OnPasteJson, - OnPatch, - ValueNormalization + import { + type FindNextInside, + type JSONParser, + type JSONSelection, + Mode, + type OnFind, + type OnJSONSelect, + type OnPasteJson, + type OnPatch, + UpdateSelectionAfterChange, + type ValueNormalization } from '$lib/types.js' - import { UpdateSelectionAfterChange } from '$lib/types.js' import { isEqual } from 'lodash-es' import { expandSmart } from '$lib/logic/documentState' export let path: JSONPath export let value: unknown export let selection: JSONSelection | undefined + export let mode: Mode export let parser: JSONParser export let normalization: ValueNormalization export let enforceString: boolean @@ -108,7 +110,7 @@ } function handleOnValueClass(value: string): string { - return getValueClass(convert(normalization.unescapeValue(value)), parser) + return getValueClass(convert(normalization.unescapeValue(value)), mode, parser) } diff --git a/src/lib/plugins/value/components/EnumValue.scss b/src/lib/plugins/value/components/EnumValue.scss index edd2da04..11fd7945 100644 --- a/src/lib/plugins/value/components/EnumValue.scss +++ b/src/lib/plugins/value/components/EnumValue.scss @@ -22,9 +22,3 @@ color: $text-color; } } - -:global(.jse-json-node.jse-selected) { - .jse-enum-value { - background: transparent; - } -} diff --git a/src/lib/plugins/value/components/ReadonlyValue.scss b/src/lib/plugins/value/components/ReadonlyValue.scss index 9b6205f7..0b70734d 100644 --- a/src/lib/plugins/value/components/ReadonlyValue.scss +++ b/src/lib/plugins/value/components/ReadonlyValue.scss @@ -3,20 +3,21 @@ // TODO: also share most of the following styling with EditableDiv .jse-value { + display: inline-block; min-width: 2em; padding: 0 $input-padding; box-sizing: border-box; outline: none; border-radius: 1px; vertical-align: top; - cursor: $contents-cursor; word-break: normal; overflow-wrap: anywhere; white-space: pre-wrap; // important for rendering multiple consecutive spaces - &:hover { - background: $hover-background-color; + &.jse-table-cell { + overflow-wrap: normal; + white-space: nowrap; } &.jse-empty { diff --git a/src/lib/plugins/value/components/ReadonlyValue.svelte b/src/lib/plugins/value/components/ReadonlyValue.svelte index 0d53cd38..07fb9d36 100644 --- a/src/lib/plugins/value/components/ReadonlyValue.svelte +++ b/src/lib/plugins/value/components/ReadonlyValue.svelte @@ -6,17 +6,19 @@ import SearchResultHighlighter from '../../../components/modes/treemode/highlight/SearchResultHighlighter.svelte' import { getValueClass } from './utils/getValueClass.js' import { addNewLineSuffix } from '$lib/utils/domUtils.js' - import type { - ExtendedSearchResultItem, - JSONParser, - OnJSONSelect, - ValueNormalization + import { + type ExtendedSearchResultItem, + type JSONParser, + Mode, + type OnJSONSelect, + type ValueNormalization } from '$lib/types.js' import type { JSONPath } from 'immutable-json-patch' import { isCtrlKeyDown } from 'svelte-jsoneditor/utils/keyBindings' export let path: JSONPath export let value: unknown + export let mode: Mode export let readOnly: boolean export let normalization: ValueNormalization export let parser: JSONParser @@ -48,7 +50,7 @@ role="button" tabindex="-1" data-type="selectable-value" - class={getValueClass(value, parser)} + class={getValueClass(value, mode, parser)} on:click={handleValueClick} on:dblclick={handleValueDoubleClick} title={valueIsUrl ? 'Ctrl+Click or Ctrl+Enter to open url in new window' : undefined} diff --git a/src/lib/plugins/value/components/utils/getValueClass.ts b/src/lib/plugins/value/components/utils/getValueClass.ts index 135c6985..cf47e9ae 100644 --- a/src/lib/plugins/value/components/utils/getValueClass.ts +++ b/src/lib/plugins/value/components/utils/getValueClass.ts @@ -1,12 +1,13 @@ import { isUrl, valueType } from '$lib/utils/typeUtils.js' -import type { JSONParser } from '$lib/types.js' +import { type JSONParser, Mode } from '$lib/types.js' import { classnames } from '$lib/utils/cssUtils.js' -export function getValueClass(value: unknown, parser: JSONParser): string { +export function getValueClass(value: unknown, mode: Mode, parser: JSONParser): string { const type = valueType(value, parser) return classnames('jse-value', 'jse-' + type, { 'jse-url': isUrl(value), - 'jse-empty': typeof value === 'string' && value.length === 0 + 'jse-empty': typeof value === 'string' && value.length === 0, + 'jse-table-cell': mode === Mode.table }) } diff --git a/src/lib/plugins/value/renderValue.ts b/src/lib/plugins/value/renderValue.ts index 75441f3c..16ea4159 100644 --- a/src/lib/plugins/value/renderValue.ts +++ b/src/lib/plugins/value/renderValue.ts @@ -9,6 +9,7 @@ import TimestampTag from './components/TimestampTag.svelte' export function renderValue({ path, value, + mode, readOnly, selection, enforceString, @@ -46,6 +47,7 @@ export function renderValue({ path, value, selection, + mode, enforceString, parser, normalization, @@ -62,7 +64,7 @@ export function renderValue({ if (!isEditing) { renderers.push({ component: ReadonlyValue, - props: { path, value, readOnly, parser, normalization, searchResultItems, onSelect } + props: { path, value, mode, readOnly, parser, normalization, searchResultItems, onSelect } }) } diff --git a/src/lib/types.ts b/src/lib/types.ts index 9ddc9be0..d3672c36 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -605,6 +605,7 @@ export interface JSONEditorModalProps { } export interface JSONEditorContext { + mode: Mode readOnly: boolean parser: JSONParser normalization: ValueNormalization @@ -637,6 +638,7 @@ export interface TreeModeContext extends JSONEditorContext { export interface RenderValueProps { path: JSONPath value: unknown + mode: Mode readOnly: boolean enforceString: boolean selection: JSONSelection | undefined
0
- 1 +
+ 1 +
+ + + + + +
- - - - - -
+
+ +
+ + + + +
- - - - - -
1
- 2 +
+ 2 +
+ + + + + +
- - - - - -
- Joe +
+ Joe +
+ + + + + +
- - - - - -
2
- 3 +
+ 3 +
+ + + + + +
- - - - - -
+
+ +
+ + + + +
- - - - - -
@@ -469,7 +493,7 @@ exports[`JSONEditor > render text mode 1`] = `
render tree mode 1`] = `
render tree mode 1`] = ` >
0
:
{
@@ -1532,36 +1556,36 @@ exports[`JSONEditor > render tree mode 1`] = `
@@ -1573,29 +1597,33 @@ exports[`JSONEditor > render tree mode 1`] = `
:
- 1 +
+ 1 +
+ + + +
- - - -
@@ -1605,21 +1633,21 @@ exports[`JSONEditor > render tree mode 1`] = `
- {#if isObjectOrArray(value)} - {@const searchResultsByCell = flattenSearchResults( - getInRecursiveState(item, searchResultByRow, column) - )} - - {@const containsActiveSearchResult = searchResultsByCell - ? searchResultsByCell.some((item) => item.active) - : false} - - {:else} - {@const searchResultItemsByCell = getInRecursiveState( - json, - searchResults, - path - )?.searchResults} - - {/if}{#if !readOnly && isSelected && !isEditingSelection(selection)} -
- -
- {/if}{#if validationError} - - {/if} +
+
+ {#if isObjectOrArray(value)} + {@const searchResultsByCell = flattenSearchResults( + getInRecursiveState(item, searchResultByRow, column) + )} + + {@const containsActiveSearchResult = searchResultsByCell + ? searchResultsByCell.some((item) => item.active) + : false} + + {:else} + {@const searchResultItemsByCell = getInRecursiveState( + json, + searchResults, + path + )?.searchResults} + + {/if}{#if !readOnly && isSelected && !isEditingSelection(selection)} +
+ +
+ {/if}{#if validationError} + + {/if} +