Implement selecting before or after a node (WIP)

This commit is contained in:
Jos de Jong 2020-07-04 20:34:35 +02:00
parent 9a23799d5f
commit 823b445e94
5 changed files with 228 additions and 38 deletions

View File

@ -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>

View File

@ -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;
}

View File

@ -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}

9
src/clipboard.js Normal file
View File

@ -0,0 +1,9 @@
function createClipboard () {
const contents = undefined
return {
isEmpty: () => {},
cut: function () {}
}
}

View File

@ -48,3 +48,7 @@
* error: Error | null
* }} JSONPatchResult
*/
/**
* @typedef {{paths: Path[]} | {beforePath: Path}} Selection
*/