Implement selecting before or after a node (WIP)
This commit is contained in:
parent
9a23799d5f
commit
823b445e94
|
@ -8,10 +8,11 @@
|
|||
} from './constants.js'
|
||||
import SearchBox from './SearchBox.svelte'
|
||||
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 Node from './JSONNode.svelte'
|
||||
import { expandSelection } from './selection.js'
|
||||
import { singleton } from './singleton.js'
|
||||
import {
|
||||
existsIn,
|
||||
getIn,
|
||||
|
@ -22,42 +23,41 @@
|
|||
import { keyComboFromEvent } from './utils/keyBindings.js'
|
||||
import { flattenSearch, search } from './utils/search.js'
|
||||
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 { syncState } from './utils/syncState.js'
|
||||
|
||||
let divContents
|
||||
|
||||
beforeUpdate(() => {
|
||||
console.time('render')
|
||||
})
|
||||
afterUpdate(() => {
|
||||
console.timeEnd('render')
|
||||
})
|
||||
// beforeUpdate(() => {
|
||||
// console.time('render')
|
||||
// })
|
||||
// afterUpdate(() => {
|
||||
// console.timeEnd('render')
|
||||
// })
|
||||
|
||||
export let doc = {}
|
||||
let state = undefined
|
||||
let selection = null
|
||||
let selectionMap = {}
|
||||
|
||||
$: {
|
||||
selectionMap = {}
|
||||
if (selection != null) {
|
||||
selection.forEach(path => {
|
||||
selectionMap[compileJSONPointer(path)] = true
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
$: console.log('selectionMap', selectionMap)
|
||||
// $: {
|
||||
// selectionMap = {}
|
||||
// if (selection != null) {
|
||||
// selection.forEach(path => {
|
||||
// selectionMap[compileJSONPointer(path)] = true
|
||||
// })
|
||||
// }
|
||||
// }
|
||||
|
||||
export let onChangeJson = () => {}
|
||||
|
||||
$: {
|
||||
console.time('syncState')
|
||||
state = syncState(doc, state, [], (path) => path.length < 1)
|
||||
console.timeEnd('syncState')
|
||||
}
|
||||
let clipboard = null
|
||||
$: canCut = selection != null
|
||||
$: canCopy = selection != null
|
||||
$: canPaste = clipboard != null
|
||||
|
||||
$: state = syncState(doc, state, [], (path) => path.length < 1)
|
||||
|
||||
let showSearch = false
|
||||
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() {
|
||||
if (history.getState().canUndo) {
|
||||
const item = history.undo()
|
||||
|
@ -271,9 +288,36 @@
|
|||
state = setIn(state, path.concat(STATE_LIMIT), limit)
|
||||
}
|
||||
|
||||
function handleSelect (anchorPath, focusPath) {
|
||||
if (anchorPath && focusPath) {
|
||||
selection = expandSelection(doc, state, anchorPath, focusPath)
|
||||
/**
|
||||
* @param {Selection} newSelection
|
||||
*/
|
||||
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 {
|
||||
selection = null
|
||||
}
|
||||
|
@ -344,6 +388,33 @@
|
|||
|
||||
<div class="jsoneditor" on:keydown={handleKeyDown}>
|
||||
<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
|
||||
class="button search"
|
||||
on:click={handleToggleSearch}
|
||||
|
@ -351,7 +422,9 @@
|
|||
>
|
||||
<Icon data={faSearch} />
|
||||
</button>
|
||||
|
||||
<div class="separator"></div>
|
||||
|
||||
<button
|
||||
class="button undo"
|
||||
disabled={!historyState.canUndo}
|
||||
|
@ -368,7 +441,9 @@
|
|||
>
|
||||
<Icon data={faRedo} />
|
||||
</button>
|
||||
|
||||
<div class="space"></div>
|
||||
|
||||
{#if showSearch}
|
||||
<div class="search-box-container">
|
||||
<SearchBox
|
||||
|
@ -396,7 +471,7 @@
|
|||
onExpand={handleExpand}
|
||||
onLimit={handleLimit}
|
||||
onSelect={handleSelect}
|
||||
selectionMap={selectionMap}
|
||||
selection={selection}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
@import './styles.scss';
|
||||
|
||||
.json-node {
|
||||
position: relative;
|
||||
font-family: $font-family;
|
||||
font-size: $font-size;
|
||||
color: $black;
|
||||
|
@ -17,6 +18,46 @@
|
|||
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 {
|
||||
position: relative;
|
||||
}
|
||||
|
|
|
@ -27,7 +27,7 @@
|
|||
export let onExpand
|
||||
export let onLimit
|
||||
export let onSelect
|
||||
export let selectionMap
|
||||
export let selection
|
||||
|
||||
$: expanded = state && state[STATE_EXPANDED]
|
||||
$: limit = state && state[STATE_LIMIT]
|
||||
|
@ -158,7 +158,7 @@
|
|||
debouncedUpdateValue()
|
||||
}
|
||||
|
||||
function handleValueBlur (event) {
|
||||
function handleValueBlur () {
|
||||
// handle any pending changes still waiting in the debounce function
|
||||
debouncedUpdateValue.flush()
|
||||
|
||||
|
@ -206,6 +206,9 @@
|
|||
singleton.mousedown = true
|
||||
singleton.selectionAnchor = path
|
||||
singleton.selectionFocus = null
|
||||
|
||||
// TODO: select the clicked node directly
|
||||
|
||||
event.stopPropagation()
|
||||
}
|
||||
|
||||
|
@ -229,7 +232,11 @@
|
|||
|
||||
if (!isEqual(path, singleton.selectionFocus)) {
|
||||
singleton.selectionFocus = path
|
||||
onSelect(singleton.selectionAnchor, singleton.selectionFocus)
|
||||
|
||||
onSelect({
|
||||
anchorPath: singleton.selectionAnchor,
|
||||
focusPath: singleton.selectionFocus
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -245,8 +252,38 @@
|
|||
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
|
||||
$: 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>
|
||||
|
||||
<div
|
||||
|
@ -256,8 +293,16 @@
|
|||
on:mousedown={handleMouseDown}
|
||||
on:mousemove={handleMouseMove}
|
||||
>
|
||||
<div
|
||||
class="before-node-selector"
|
||||
class:selected={selectedBefore}
|
||||
style={indentationStyle}
|
||||
on:click={handleSelectBefore}
|
||||
>
|
||||
<div class="selector"></div>
|
||||
</div>
|
||||
{#if type === 'array'}
|
||||
<div class='header' style={getIndentationStyle(path.length)}>
|
||||
<div class='header' style={indentationStyle}>
|
||||
<button
|
||||
class='expand'
|
||||
on:click={toggleExpand}
|
||||
|
@ -302,21 +347,29 @@
|
|||
onExpand={onExpand}
|
||||
onLimit={onLimit}
|
||||
onSelect={onSelect}
|
||||
selectionMap={selectionMap}
|
||||
selection={selection}
|
||||
/>
|
||||
{/each}
|
||||
<div
|
||||
class="after-node-selector"
|
||||
class:selected={selectedAfter}
|
||||
style={indentationStyle}
|
||||
on:click={handleSelectAfter}
|
||||
>
|
||||
<div class="selector"></div>
|
||||
</div>
|
||||
{#if limited}
|
||||
<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>)
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="footer" style={getIndentationStyle(path.length)}>
|
||||
<div class="footer" style={indentationStyle}>
|
||||
<span class="delimiter">]</span>
|
||||
</div>
|
||||
{/if}
|
||||
{:else if type === 'object'}
|
||||
<div class='header' style={getIndentationStyle(path.length)}>
|
||||
<div class='header' style={indentationStyle}>
|
||||
<button
|
||||
class='expand'
|
||||
on:click={toggleExpand}
|
||||
|
@ -361,16 +414,24 @@
|
|||
onExpand={onExpand}
|
||||
onLimit={onLimit}
|
||||
onSelect={onSelect}
|
||||
selectionMap={selectionMap}
|
||||
selection={selection}
|
||||
/>
|
||||
{/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 class="footer" style={indentationStyle}>
|
||||
<span class="delimiter">}</span>
|
||||
</div>
|
||||
{/if}
|
||||
{:else}
|
||||
<div class="contents" style={getIndentationStyle(path.length)}>
|
||||
<div class="contents" style={indentationStyle}>
|
||||
{#if typeof key === 'string'}
|
||||
<div
|
||||
class={keyClass}
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
|
||||
function createClipboard () {
|
||||
const contents = undefined
|
||||
|
||||
return {
|
||||
isEmpty: () => {},
|
||||
cut: function () {}
|
||||
}
|
||||
}
|
|
@ -48,3 +48,7 @@
|
|||
* error: Error | null
|
||||
* }} JSONPatchResult
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {{paths: Path[]} | {beforePath: Path}} Selection
|
||||
*/
|
||||
|
|
Loading…
Reference in New Issue