Cut/copy/paste (WIP)
This commit is contained in:
parent
823b445e94
commit
fa2a23a83d
|
@ -1,5 +1,6 @@
|
|||
<script>
|
||||
import { tick, beforeUpdate, afterUpdate } from 'svelte'
|
||||
import { insertAfter, insertBefore, removeAll, replace } from './actions.js'
|
||||
import {
|
||||
DEFAULT_LIMIT,
|
||||
STATE_EXPANDED,
|
||||
|
@ -14,6 +15,7 @@
|
|||
import { expandSelection } from './selection.js'
|
||||
import { singleton } from './singleton.js'
|
||||
import {
|
||||
deleteIn,
|
||||
existsIn,
|
||||
getIn,
|
||||
setIn,
|
||||
|
@ -26,6 +28,7 @@
|
|||
import { isEqual, isNumber, initial, last, cloneDeep } from 'lodash-es'
|
||||
import jump from './assets/jump.js/src/jump.js'
|
||||
import { syncState } from './utils/syncState.js'
|
||||
import { isObject } from './utils/typeUtils.js'
|
||||
|
||||
let divContents
|
||||
|
||||
|
@ -41,21 +44,12 @@
|
|||
let selection = null
|
||||
let selectionMap = {}
|
||||
|
||||
// $: {
|
||||
// selectionMap = {}
|
||||
// if (selection != null) {
|
||||
// selection.forEach(path => {
|
||||
// selectionMap[compileJSONPointer(path)] = true
|
||||
// })
|
||||
// }
|
||||
// }
|
||||
|
||||
export let onChangeJson = () => {}
|
||||
|
||||
let clipboard = null
|
||||
$: canCut = selection != null
|
||||
$: canCopy = selection != null
|
||||
$: canPaste = clipboard != null
|
||||
$: canCut = selection != null && selection.paths != null
|
||||
$: canCopy = selection != null && selection.paths != null
|
||||
$: canPaste = clipboard != null && selection != null
|
||||
|
||||
$: state = syncState(doc, state, [], (path) => path.length < 1)
|
||||
|
||||
|
@ -134,20 +128,60 @@
|
|||
}
|
||||
}
|
||||
|
||||
function selectionToClipboard (selection) {
|
||||
if (!selection || !selection.paths) {
|
||||
return null
|
||||
}
|
||||
|
||||
return selection.paths.map(path => {
|
||||
return {
|
||||
key: String(last(path)),
|
||||
value: cloneDeep(getIn(doc, path))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function handleCut() {
|
||||
// FIXME: implement
|
||||
if (selection && selection.paths) {
|
||||
clipboard = selectionToClipboard(selection)
|
||||
|
||||
const operations = removeAll(selection.paths)
|
||||
handlePatch(operations)
|
||||
|
||||
console.log('cut', { selection, clipboard })
|
||||
}
|
||||
}
|
||||
|
||||
function handleCopy() {
|
||||
if (selection) {
|
||||
clipboard = selection.map(path => cloneDeep(getIn(doc, path)))
|
||||
console.log('copied to clipboard', clipboard)
|
||||
if (selection && selection.paths) {
|
||||
clipboard = selectionToClipboard(selection)
|
||||
console.log('copy', { clipboard })
|
||||
}
|
||||
}
|
||||
|
||||
function handlePaste() {
|
||||
if (clipboard) {
|
||||
console.log('focus', singleton.focus)
|
||||
if (selection && clipboard) {
|
||||
console.log('paste', { clipboard, selection })
|
||||
|
||||
if (selection.beforePath) {
|
||||
const operations = insertBefore(doc, selection.beforePath, clipboard)
|
||||
console.log('patch', operations)
|
||||
handlePatch(operations)
|
||||
|
||||
// FIXME: must adjust STATE_PROPS of the object where we inserted the clipboard
|
||||
} else if (selection.afterPath) {
|
||||
const operations = insertAfter(doc, selection.afterPath, clipboard)
|
||||
console.log('patch', operations)
|
||||
handlePatch(operations)
|
||||
|
||||
// FIXME: must adjust STATE_PROPS of the object where we inserted the clipboard
|
||||
} else if (selection.paths) {
|
||||
const operations = replace(doc, selection.paths, clipboard)
|
||||
console.log('patch', operations)
|
||||
handlePatch(operations)
|
||||
|
||||
// FIXME: must adjust STATE_PROPS of the object where we inserted the clipboard
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -313,7 +347,8 @@
|
|||
})
|
||||
|
||||
selection = {
|
||||
paths: pathsMap
|
||||
paths,
|
||||
pathsMap
|
||||
}
|
||||
} else {
|
||||
console.error('Unknown type of selection', newSelection)
|
||||
|
@ -383,6 +418,19 @@
|
|||
handleRedo()
|
||||
}
|
||||
}
|
||||
|
||||
if (combo === 'Ctrl+X' || combo === 'Command+X') {
|
||||
event.preventDefault()
|
||||
handleCut()
|
||||
}
|
||||
if (combo === 'Ctrl+C' || combo === 'Command+C') {
|
||||
event.preventDefault()
|
||||
handleCopy()
|
||||
}
|
||||
if (combo === 'Ctrl+V' || combo === 'Command+V') {
|
||||
event.preventDefault()
|
||||
handlePaste()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script>
|
||||
import { debounce, initial, isEqual } from 'lodash-es'
|
||||
import { debounce, initial, isEqual, last } from 'lodash-es'
|
||||
import {
|
||||
DEBOUNCE_DELAY,
|
||||
DEFAULT_LIMIT,
|
||||
|
@ -194,10 +194,8 @@
|
|||
|
||||
function handleMouseDown (event) {
|
||||
// unselect existing selection on mouse down if any
|
||||
if (singleton.selectionAnchor != null && singleton.selectionFocus != null) {
|
||||
singleton.selectionAnchor = null
|
||||
singleton.selectionFocus = null
|
||||
onSelect(null, null)
|
||||
if (selection) {
|
||||
onSelect(null)
|
||||
}
|
||||
|
||||
// check if the mouse down is not happening in the key or value input fields
|
||||
|
@ -266,13 +264,15 @@
|
|||
event.stopPropagation()
|
||||
|
||||
onSelect({
|
||||
afterPath: path
|
||||
afterPath: type === 'array'
|
||||
? path.concat(items.length)
|
||||
: path.concat(last(props).key)
|
||||
})
|
||||
}
|
||||
|
||||
// FIXME: this is not efficient. Create a nested object with the selection and pass that
|
||||
$: selected = (selection && selection.paths)
|
||||
? selection.paths[compileJSONPointer(path)] === true
|
||||
$: selected = (selection && selection.pathsMap)
|
||||
? selection.pathsMap[compileJSONPointer(path)] === true
|
||||
: false
|
||||
|
||||
$: selectedBefore = (selection && selection.beforePath)
|
||||
|
@ -280,7 +280,7 @@
|
|||
: false
|
||||
|
||||
$: selectedAfter = (selection && selection.afterPath)
|
||||
? isEqual(selection.afterPath, path)
|
||||
? isEqual(initial(selection.afterPath), path)
|
||||
: false
|
||||
|
||||
$: indentationStyle = getIndentationStyle(path.length)
|
||||
|
|
|
@ -47,6 +47,7 @@ $search-size: 24px;
|
|||
margin: 0;
|
||||
flex: 1;
|
||||
width: 100px;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.search-count {
|
||||
|
|
|
@ -0,0 +1,188 @@
|
|||
import first from 'lodash/first'
|
||||
import initial from 'lodash/initial'
|
||||
import last from 'lodash/last'
|
||||
import { getIn } from './utils/immutabilityHelpers'
|
||||
import { compileJSONPointer } from './utils/jsonPointer'
|
||||
import { findUniqueName } from './utils/stringUtils'
|
||||
|
||||
/**
|
||||
* Create a JSONPatch for an insert action.
|
||||
*
|
||||
* This function needs the current data in order to be able to determine
|
||||
* a unique property name for the inserted node in case of duplicating
|
||||
* and object property
|
||||
*
|
||||
* @param {JSON} json
|
||||
* @param {Path} path
|
||||
* @param {Array.<{key?: string, value: JSON}>} values
|
||||
* @return {Array}
|
||||
*/
|
||||
export function insertBefore (json, path, values) { // TODO: find a better name and define datastructure for values
|
||||
const parentPath = initial(path)
|
||||
const parent = getIn(json, parentPath)
|
||||
|
||||
if (Array.isArray(parent)) {
|
||||
const startIndex = parseInt(last(path), 10)
|
||||
return values.map((entry, offset) => ({
|
||||
op: 'add',
|
||||
path: compileJSONPointer(parentPath.concat(startIndex + offset)),
|
||||
value: entry.value
|
||||
}))
|
||||
}
|
||||
else { // 'object'
|
||||
const addActions = values.map(entry => {
|
||||
const newProp = findUniqueName(entry.key, parent)
|
||||
return {
|
||||
op: 'add',
|
||||
path: compileJSONPointer(parentPath.concat(newProp)),
|
||||
value: entry.value
|
||||
}
|
||||
})
|
||||
|
||||
// we move all lower down properties to the same parent again,
|
||||
// to force them to move under the inserted properties instead of the
|
||||
// new properties appearing at the bottom of the object
|
||||
const keys = Object.keys(parent)
|
||||
const beforeKey = last(path)
|
||||
const beforeIndex = keys.indexOf(beforeKey)
|
||||
const keysToMove = (beforeIndex !== -1)
|
||||
? keys.slice(beforeIndex)
|
||||
: []
|
||||
const moveActions = keysToMove.map(key => {
|
||||
const movePath = compileJSONPointer(parentPath.concat(key))
|
||||
return {
|
||||
op: 'move',
|
||||
from: movePath,
|
||||
path: movePath
|
||||
}
|
||||
})
|
||||
|
||||
return addActions.concat(moveActions)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a JSONPatch for an insert action.
|
||||
*
|
||||
* This function needs the current data in order to be able to determine
|
||||
* a unique property name for the inserted node in case of duplicating
|
||||
* and object property
|
||||
*
|
||||
* @param {JSON} json
|
||||
* @param {Path} path
|
||||
* @param {Array.<{key?: string, value: JSON}>} values
|
||||
* @return {Array}
|
||||
*/
|
||||
export function insertAfter (json, path, values) { // TODO: find a better name and define datastructure for values
|
||||
// TODO: refactor. path should be parent path
|
||||
const parentPath = initial(path)
|
||||
const parent = getIn(json, parentPath)
|
||||
|
||||
if (Array.isArray(parent)) {
|
||||
const startIndex = parseInt(last(path), 10)
|
||||
return values.map((entry, offset) => ({
|
||||
op: 'add',
|
||||
path: compileJSONPointer(parentPath.concat(startIndex + 1 + offset)), // +1 to insert after
|
||||
value: entry.value
|
||||
}))
|
||||
}
|
||||
else { // 'object'
|
||||
return values.map(entry => {
|
||||
const newProp = findUniqueName(entry.key, parent)
|
||||
return {
|
||||
op: 'add',
|
||||
path: compileJSONPointer(parentPath.concat(newProp)),
|
||||
value: entry.value
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a JSONPatch for an insert action.
|
||||
*
|
||||
* This function needs the current data in order to be able to determine
|
||||
* a unique property name for the inserted node in case of duplicating
|
||||
* and object property
|
||||
*
|
||||
* @param {JSON} json
|
||||
* @param {Path[]} paths
|
||||
* @param {Array.<{key?: string, value: JSON}>} values
|
||||
* @return {Array}
|
||||
*/
|
||||
export function replace (json, paths, values) { // TODO: find a better name and define datastructure for values
|
||||
const firstPath = first(paths)
|
||||
const parentPath = initial(firstPath)
|
||||
const parent = getIn(json, parentPath)
|
||||
|
||||
if (Array.isArray(parent)) {
|
||||
const firstPath = first(paths)
|
||||
const offset = firstPath ? parseInt(last(firstPath), 10) : 0
|
||||
|
||||
const removeActions = removeAll(paths)
|
||||
const insertActions = values.map((entry, index) => ({
|
||||
op: 'add',
|
||||
path: compileJSONPointer(parentPath.concat(index + offset)),
|
||||
value: entry.value
|
||||
}))
|
||||
|
||||
return removeActions.concat(insertActions)
|
||||
}
|
||||
else { // parent is Object
|
||||
const removeActions = removeAll(paths)
|
||||
const insertActions = values.map(entry => {
|
||||
const newProp = findUniqueName(entry.key, parent)
|
||||
return {
|
||||
op: 'add',
|
||||
path: compileJSONPointer(parentPath.concat(newProp)),
|
||||
value: entry.value
|
||||
}
|
||||
})
|
||||
|
||||
// we move all lower down properties to the same parent again,
|
||||
// to force them to move under the inserted properties instead of the
|
||||
// new properties appearing at the bottom of the object
|
||||
const keys = Object.keys(parent)
|
||||
const beforeKey = last(firstPath)
|
||||
const beforeIndex = keys.indexOf(beforeKey)
|
||||
const keysToMove = (beforeIndex !== -1)
|
||||
? keys.slice(beforeIndex)
|
||||
: []
|
||||
const moveActions = keysToMove.map(key => {
|
||||
const movePath = compileJSONPointer(parentPath.concat(key))
|
||||
return {
|
||||
op: 'move',
|
||||
from: movePath,
|
||||
path: movePath
|
||||
}
|
||||
})
|
||||
|
||||
return removeActions.concat(insertActions, moveActions)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a JSONPatch for a remove action
|
||||
* @param {Path} path
|
||||
* @return {JSONPatchDocument}
|
||||
*/
|
||||
export function remove (path) {
|
||||
return [{
|
||||
op: 'remove',
|
||||
path: compileJSONPointer(path)
|
||||
}]
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a JSONPatch for a multiple remove action
|
||||
* @param {Path[]} paths
|
||||
* @return {JSONPatchDocument}
|
||||
*/
|
||||
export function removeAll (paths) {
|
||||
return paths
|
||||
.map(path => ({
|
||||
op: 'remove',
|
||||
path: compileJSONPointer(path)
|
||||
}))
|
||||
.reverse() // reverse is needed for arrays: delete the last index first
|
||||
}
|
|
@ -1,9 +0,0 @@
|
|||
|
||||
function createClipboard () {
|
||||
const contents = undefined
|
||||
|
||||
return {
|
||||
isEmpty: () => {},
|
||||
cut: function () {}
|
||||
}
|
||||
}
|
|
@ -50,5 +50,5 @@
|
|||
*/
|
||||
|
||||
/**
|
||||
* @typedef {{paths: Path[]} | {beforePath: Path}} Selection
|
||||
* @typedef {{paths: Path[], pathsMap: Object<string, boolean>}} | {beforePath: Path} | {afterPath: Path}} Selection
|
||||
*/
|
||||
|
|
|
@ -0,0 +1,62 @@
|
|||
/**
|
||||
* Comparator to sort an array in ascending order
|
||||
*
|
||||
* Usage:
|
||||
* [4,2,5].sort(compareAsc) // [2,4,5]
|
||||
*
|
||||
* @param a
|
||||
* @param b
|
||||
* @return {number}
|
||||
*/
|
||||
export function compareAsc (a, b) {
|
||||
return a > b ? 1 : a < b ? -1 : 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Comparator to sort an array in ascending order
|
||||
*
|
||||
* Usage:
|
||||
* [4,2,5].sort(compareDesc) // [5,4,2]
|
||||
*
|
||||
* @param a
|
||||
* @param b
|
||||
* @return {number}
|
||||
*/
|
||||
export function compareDesc (a, b) {
|
||||
return a > b ? -1 : a < b ? 1 : 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Test whether all items of an array are strictly equal
|
||||
* @param {Array} a
|
||||
* @param {Array} b
|
||||
*/
|
||||
export function strictShallowEqual (a, b) {
|
||||
if (a.length !== b.length) {
|
||||
return false
|
||||
}
|
||||
|
||||
for (let i = 0; i < a.length; i++) {
|
||||
if (a[i] !== b[i]) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
export function compareArrays(a, b) {
|
||||
const minLength = Math.min(a.length, b.length)
|
||||
|
||||
for (let i = 0; i < minLength; i++) {
|
||||
if (a[i] < b[i]) {
|
||||
return -1
|
||||
}
|
||||
|
||||
if (a[i] > b[i]) {
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
return a.length - b.length
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
import { compareArrays } from './arrayUtils'
|
||||
|
||||
test('compareArrays', () => {
|
||||
expect(compareArrays([], [])).toEqual(0)
|
||||
expect(compareArrays(['a'], ['a'])).toEqual(0)
|
||||
expect(compareArrays(['a'], ['b'])).toEqual(-1)
|
||||
expect(compareArrays(['b'], ['a'])).toEqual(1)
|
||||
expect(compareArrays(['a'], ['a', 'b'])).toEqual(-1)
|
||||
expect(compareArrays(['a', 'b'], ['a'])).toEqual(1)
|
||||
expect(compareArrays(['a', 'b'], ['a', 'b'])).toEqual(0)
|
||||
|
||||
const arrays = [
|
||||
['b', 'a'],
|
||||
['a'],
|
||||
[],
|
||||
['b', 'c'],
|
||||
['b'],
|
||||
]
|
||||
|
||||
expect(arrays.sort(compareArrays)).toEqual([
|
||||
[],
|
||||
['a'],
|
||||
['b'],
|
||||
['b', 'a'],
|
||||
['b', 'c']
|
||||
])
|
||||
})
|
Loading…
Reference in New Issue