Implement selecting before or after a node (WIP)
This commit is contained in:
parent
9a23799d5f
commit
823b445e94
|
@ -8,10 +8,11 @@
|
||||||
} from './constants.js'
|
} from './constants.js'
|
||||||
import SearchBox from './SearchBox.svelte'
|
import SearchBox from './SearchBox.svelte'
|
||||||
import Icon from 'svelte-awesome'
|
import Icon from 'svelte-awesome'
|
||||||
import { faSearch, faUndo, faRedo } from '@fortawesome/free-solid-svg-icons'
|
import { faCut, faCopy, faPaste, faSearch, faUndo, faRedo } from '@fortawesome/free-solid-svg-icons'
|
||||||
import { createHistory } from './history.js'
|
import { createHistory } from './history.js'
|
||||||
import Node from './JSONNode.svelte'
|
import Node from './JSONNode.svelte'
|
||||||
import { expandSelection } from './selection.js'
|
import { expandSelection } from './selection.js'
|
||||||
|
import { singleton } from './singleton.js'
|
||||||
import {
|
import {
|
||||||
existsIn,
|
existsIn,
|
||||||
getIn,
|
getIn,
|
||||||
|
@ -22,42 +23,41 @@
|
||||||
import { keyComboFromEvent } from './utils/keyBindings.js'
|
import { keyComboFromEvent } from './utils/keyBindings.js'
|
||||||
import { flattenSearch, search } from './utils/search.js'
|
import { flattenSearch, search } from './utils/search.js'
|
||||||
import { immutableJSONPatch } from './utils/immutableJSONPatch'
|
import { immutableJSONPatch } from './utils/immutableJSONPatch'
|
||||||
import { isEqual, isNumber, initial, last } from 'lodash-es'
|
import { isEqual, isNumber, initial, last, cloneDeep } from 'lodash-es'
|
||||||
import jump from './assets/jump.js/src/jump.js'
|
import jump from './assets/jump.js/src/jump.js'
|
||||||
import { syncState } from './utils/syncState.js'
|
import { syncState } from './utils/syncState.js'
|
||||||
|
|
||||||
let divContents
|
let divContents
|
||||||
|
|
||||||
beforeUpdate(() => {
|
// beforeUpdate(() => {
|
||||||
console.time('render')
|
// console.time('render')
|
||||||
})
|
// })
|
||||||
afterUpdate(() => {
|
// afterUpdate(() => {
|
||||||
console.timeEnd('render')
|
// console.timeEnd('render')
|
||||||
})
|
// })
|
||||||
|
|
||||||
export let doc = {}
|
export let doc = {}
|
||||||
let state = undefined
|
let state = undefined
|
||||||
let selection = null
|
let selection = null
|
||||||
let selectionMap = {}
|
let selectionMap = {}
|
||||||
|
|
||||||
$: {
|
// $: {
|
||||||
selectionMap = {}
|
// selectionMap = {}
|
||||||
if (selection != null) {
|
// if (selection != null) {
|
||||||
selection.forEach(path => {
|
// selection.forEach(path => {
|
||||||
selectionMap[compileJSONPointer(path)] = true
|
// selectionMap[compileJSONPointer(path)] = true
|
||||||
})
|
// })
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
$: console.log('selectionMap', selectionMap)
|
|
||||||
|
|
||||||
export let onChangeJson = () => {}
|
export let onChangeJson = () => {}
|
||||||
|
|
||||||
$: {
|
let clipboard = null
|
||||||
console.time('syncState')
|
$: canCut = selection != null
|
||||||
state = syncState(doc, state, [], (path) => path.length < 1)
|
$: canCopy = selection != null
|
||||||
console.timeEnd('syncState')
|
$: canPaste = clipboard != null
|
||||||
}
|
|
||||||
|
$: state = syncState(doc, state, [], (path) => path.length < 1)
|
||||||
|
|
||||||
let showSearch = false
|
let showSearch = false
|
||||||
let searchText = ''
|
let searchText = ''
|
||||||
|
@ -134,6 +134,23 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleCut() {
|
||||||
|
// FIXME: implement
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleCopy() {
|
||||||
|
if (selection) {
|
||||||
|
clipboard = selection.map(path => cloneDeep(getIn(doc, path)))
|
||||||
|
console.log('copied to clipboard', clipboard)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handlePaste() {
|
||||||
|
if (clipboard) {
|
||||||
|
console.log('focus', singleton.focus)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function handleUndo() {
|
function handleUndo() {
|
||||||
if (history.getState().canUndo) {
|
if (history.getState().canUndo) {
|
||||||
const item = history.undo()
|
const item = history.undo()
|
||||||
|
@ -271,9 +288,36 @@
|
||||||
state = setIn(state, path.concat(STATE_LIMIT), limit)
|
state = setIn(state, path.concat(STATE_LIMIT), limit)
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleSelect (anchorPath, focusPath) {
|
/**
|
||||||
if (anchorPath && focusPath) {
|
* @param {Selection} newSelection
|
||||||
selection = expandSelection(doc, state, anchorPath, focusPath)
|
*/
|
||||||
|
function handleSelect (newSelection) {
|
||||||
|
if (newSelection) {
|
||||||
|
const { anchorPath, focusPath, beforePath, afterPath } = newSelection
|
||||||
|
|
||||||
|
if (beforePath) {
|
||||||
|
selection = {
|
||||||
|
beforePath
|
||||||
|
}
|
||||||
|
} else if (afterPath) {
|
||||||
|
selection = {
|
||||||
|
afterPath
|
||||||
|
}
|
||||||
|
} else if (anchorPath && focusPath) {
|
||||||
|
// TODO: move expandSelection to JSONNode? (must change expandSelection to support relative path)
|
||||||
|
const paths = expandSelection(doc, state, anchorPath, focusPath)
|
||||||
|
|
||||||
|
const pathsMap = {}
|
||||||
|
paths.forEach(path => {
|
||||||
|
pathsMap[compileJSONPointer(path)] = true
|
||||||
|
})
|
||||||
|
|
||||||
|
selection = {
|
||||||
|
paths: pathsMap
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.error('Unknown type of selection', newSelection)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
selection = null
|
selection = null
|
||||||
}
|
}
|
||||||
|
@ -344,6 +388,33 @@
|
||||||
|
|
||||||
<div class="jsoneditor" on:keydown={handleKeyDown}>
|
<div class="jsoneditor" on:keydown={handleKeyDown}>
|
||||||
<div class="menu">
|
<div class="menu">
|
||||||
|
<button
|
||||||
|
class="button cut"
|
||||||
|
on:click={handleCut}
|
||||||
|
disabled={!canCut}
|
||||||
|
title="Cut (Ctrl+X)"
|
||||||
|
>
|
||||||
|
<Icon data={faCut} />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="button copy"
|
||||||
|
on:click={handleCopy}
|
||||||
|
disabled={!canCopy}
|
||||||
|
title="Copy (Ctrl+C)"
|
||||||
|
>
|
||||||
|
<Icon data={faCopy} />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="button paste"
|
||||||
|
on:click={handlePaste}
|
||||||
|
disabled={!canPaste}
|
||||||
|
title="Paste (Ctrl+V)"
|
||||||
|
>
|
||||||
|
<Icon data={faPaste} />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="separator"></div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="button search"
|
class="button search"
|
||||||
on:click={handleToggleSearch}
|
on:click={handleToggleSearch}
|
||||||
|
@ -351,7 +422,9 @@
|
||||||
>
|
>
|
||||||
<Icon data={faSearch} />
|
<Icon data={faSearch} />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div class="separator"></div>
|
<div class="separator"></div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="button undo"
|
class="button undo"
|
||||||
disabled={!historyState.canUndo}
|
disabled={!historyState.canUndo}
|
||||||
|
@ -368,7 +441,9 @@
|
||||||
>
|
>
|
||||||
<Icon data={faRedo} />
|
<Icon data={faRedo} />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div class="space"></div>
|
<div class="space"></div>
|
||||||
|
|
||||||
{#if showSearch}
|
{#if showSearch}
|
||||||
<div class="search-box-container">
|
<div class="search-box-container">
|
||||||
<SearchBox
|
<SearchBox
|
||||||
|
@ -396,7 +471,7 @@
|
||||||
onExpand={handleExpand}
|
onExpand={handleExpand}
|
||||||
onLimit={handleLimit}
|
onLimit={handleLimit}
|
||||||
onSelect={handleSelect}
|
onSelect={handleSelect}
|
||||||
selectionMap={selectionMap}
|
selection={selection}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
@import './styles.scss';
|
@import './styles.scss';
|
||||||
|
|
||||||
.json-node {
|
.json-node {
|
||||||
|
position: relative;
|
||||||
font-family: $font-family;
|
font-family: $font-family;
|
||||||
font-size: $font-size;
|
font-size: $font-size;
|
||||||
color: $black;
|
color: $black;
|
||||||
|
@ -17,6 +18,46 @@
|
||||||
background-color: $selection-background;
|
background-color: $selection-background;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$selector-height: 8px; // must be about half a line height
|
||||||
|
|
||||||
|
.props,
|
||||||
|
.items {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.before-node-selector,
|
||||||
|
.after-node-selector {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
right: $input-padding;
|
||||||
|
height: $selector-height;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
.selector {
|
||||||
|
margin-top: $selector-height / 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
.selector {
|
||||||
|
border: 1px dashed $selection-background;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.selected {
|
||||||
|
.selector {
|
||||||
|
border: 1px dashed $theme-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.before-node-selector {
|
||||||
|
top: -$selector-height/2 - 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.after-node-selector {
|
||||||
|
bottom: -$selector-height/2 - 1px;
|
||||||
|
}
|
||||||
|
|
||||||
.header {
|
.header {
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,7 +27,7 @@
|
||||||
export let onExpand
|
export let onExpand
|
||||||
export let onLimit
|
export let onLimit
|
||||||
export let onSelect
|
export let onSelect
|
||||||
export let selectionMap
|
export let selection
|
||||||
|
|
||||||
$: expanded = state && state[STATE_EXPANDED]
|
$: expanded = state && state[STATE_EXPANDED]
|
||||||
$: limit = state && state[STATE_LIMIT]
|
$: limit = state && state[STATE_LIMIT]
|
||||||
|
@ -158,7 +158,7 @@
|
||||||
debouncedUpdateValue()
|
debouncedUpdateValue()
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleValueBlur (event) {
|
function handleValueBlur () {
|
||||||
// handle any pending changes still waiting in the debounce function
|
// handle any pending changes still waiting in the debounce function
|
||||||
debouncedUpdateValue.flush()
|
debouncedUpdateValue.flush()
|
||||||
|
|
||||||
|
@ -206,6 +206,9 @@
|
||||||
singleton.mousedown = true
|
singleton.mousedown = true
|
||||||
singleton.selectionAnchor = path
|
singleton.selectionAnchor = path
|
||||||
singleton.selectionFocus = null
|
singleton.selectionFocus = null
|
||||||
|
|
||||||
|
// TODO: select the clicked node directly
|
||||||
|
|
||||||
event.stopPropagation()
|
event.stopPropagation()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -229,7 +232,11 @@
|
||||||
|
|
||||||
if (!isEqual(path, singleton.selectionFocus)) {
|
if (!isEqual(path, singleton.selectionFocus)) {
|
||||||
singleton.selectionFocus = path
|
singleton.selectionFocus = path
|
||||||
onSelect(singleton.selectionAnchor, singleton.selectionFocus)
|
|
||||||
|
onSelect({
|
||||||
|
anchorPath: singleton.selectionAnchor,
|
||||||
|
focusPath: singleton.selectionFocus
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -245,8 +252,38 @@
|
||||||
document.removeEventListener('mouseup', handleMouseUp)
|
document.removeEventListener('mouseup', handleMouseUp)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleSelectBefore (event) {
|
||||||
|
event.preventDefault()
|
||||||
|
event.stopPropagation()
|
||||||
|
|
||||||
|
onSelect({
|
||||||
|
beforePath: path
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSelectAfter (event) {
|
||||||
|
event.preventDefault()
|
||||||
|
event.stopPropagation()
|
||||||
|
|
||||||
|
onSelect({
|
||||||
|
afterPath: path
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// FIXME: this is not efficient. Create a nested object with the selection and pass that
|
// FIXME: this is not efficient. Create a nested object with the selection and pass that
|
||||||
$: selected = selectionMap[compileJSONPointer(path)] === true
|
$: selected = (selection && selection.paths)
|
||||||
|
? selection.paths[compileJSONPointer(path)] === true
|
||||||
|
: false
|
||||||
|
|
||||||
|
$: selectedBefore = (selection && selection.beforePath)
|
||||||
|
? isEqual(selection.beforePath, path)
|
||||||
|
: false
|
||||||
|
|
||||||
|
$: selectedAfter = (selection && selection.afterPath)
|
||||||
|
? isEqual(selection.afterPath, path)
|
||||||
|
: false
|
||||||
|
|
||||||
|
$: indentationStyle = getIndentationStyle(path.length)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
@ -256,8 +293,16 @@
|
||||||
on:mousedown={handleMouseDown}
|
on:mousedown={handleMouseDown}
|
||||||
on:mousemove={handleMouseMove}
|
on:mousemove={handleMouseMove}
|
||||||
>
|
>
|
||||||
|
<div
|
||||||
|
class="before-node-selector"
|
||||||
|
class:selected={selectedBefore}
|
||||||
|
style={indentationStyle}
|
||||||
|
on:click={handleSelectBefore}
|
||||||
|
>
|
||||||
|
<div class="selector"></div>
|
||||||
|
</div>
|
||||||
{#if type === 'array'}
|
{#if type === 'array'}
|
||||||
<div class='header' style={getIndentationStyle(path.length)}>
|
<div class='header' style={indentationStyle}>
|
||||||
<button
|
<button
|
||||||
class='expand'
|
class='expand'
|
||||||
on:click={toggleExpand}
|
on:click={toggleExpand}
|
||||||
|
@ -302,21 +347,29 @@
|
||||||
onExpand={onExpand}
|
onExpand={onExpand}
|
||||||
onLimit={onLimit}
|
onLimit={onLimit}
|
||||||
onSelect={onSelect}
|
onSelect={onSelect}
|
||||||
selectionMap={selectionMap}
|
selection={selection}
|
||||||
/>
|
/>
|
||||||
{/each}
|
{/each}
|
||||||
|
<div
|
||||||
|
class="after-node-selector"
|
||||||
|
class:selected={selectedAfter}
|
||||||
|
style={indentationStyle}
|
||||||
|
on:click={handleSelectAfter}
|
||||||
|
>
|
||||||
|
<div class="selector"></div>
|
||||||
|
</div>
|
||||||
{#if limited}
|
{#if limited}
|
||||||
<div class="limit" style={getIndentationStyle(path.length + 2)}>
|
<div class="limit" style={getIndentationStyle(path.length + 2)}>
|
||||||
(showing {limit} of {value.length} items <button on:click={handleShowMore}>show more</button> <button on:click={handleShowAll}>show all</button>)
|
(showing {limit} of {value.length} items <button on:click={handleShowMore}>show more</button> <button on:click={handleShowAll}>show all</button>)
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<div class="footer" style={getIndentationStyle(path.length)}>
|
<div class="footer" style={indentationStyle}>
|
||||||
<span class="delimiter">]</span>
|
<span class="delimiter">]</span>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{:else if type === 'object'}
|
{:else if type === 'object'}
|
||||||
<div class='header' style={getIndentationStyle(path.length)}>
|
<div class='header' style={indentationStyle}>
|
||||||
<button
|
<button
|
||||||
class='expand'
|
class='expand'
|
||||||
on:click={toggleExpand}
|
on:click={toggleExpand}
|
||||||
|
@ -361,16 +414,24 @@
|
||||||
onExpand={onExpand}
|
onExpand={onExpand}
|
||||||
onLimit={onLimit}
|
onLimit={onLimit}
|
||||||
onSelect={onSelect}
|
onSelect={onSelect}
|
||||||
selectionMap={selectionMap}
|
selection={selection}
|
||||||
/>
|
/>
|
||||||
{/each}
|
{/each}
|
||||||
|
<div
|
||||||
|
class="after-node-selector"
|
||||||
|
class:selected={selectedAfter}
|
||||||
|
style={indentationStyle}
|
||||||
|
on:click={handleSelectAfter}
|
||||||
|
>
|
||||||
|
<div class="selector"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="footer" style={getIndentationStyle(path.length)}>
|
</div>
|
||||||
|
<div class="footer" style={indentationStyle}>
|
||||||
<span class="delimiter">}</span>
|
<span class="delimiter">}</span>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{:else}
|
{:else}
|
||||||
<div class="contents" style={getIndentationStyle(path.length)}>
|
<div class="contents" style={indentationStyle}>
|
||||||
{#if typeof key === 'string'}
|
{#if typeof key === 'string'}
|
||||||
<div
|
<div
|
||||||
class={keyClass}
|
class={keyClass}
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
|
||||||
|
function createClipboard () {
|
||||||
|
const contents = undefined
|
||||||
|
|
||||||
|
return {
|
||||||
|
isEmpty: () => {},
|
||||||
|
cut: function () {}
|
||||||
|
}
|
||||||
|
}
|
|
@ -48,3 +48,7 @@
|
||||||
* error: Error | null
|
* error: Error | null
|
||||||
* }} JSONPatchResult
|
* }} JSONPatchResult
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {{paths: Path[]} | {beforePath: Path}} Selection
|
||||||
|
*/
|
||||||
|
|
Loading…
Reference in New Issue