Cut/copy/paste (WIP)
This commit is contained in:
parent
823b445e94
commit
fa2a23a83d
|
@ -1,5 +1,6 @@
|
||||||
<script>
|
<script>
|
||||||
import { tick, beforeUpdate, afterUpdate } from 'svelte'
|
import { tick, beforeUpdate, afterUpdate } from 'svelte'
|
||||||
|
import { insertAfter, insertBefore, removeAll, replace } from './actions.js'
|
||||||
import {
|
import {
|
||||||
DEFAULT_LIMIT,
|
DEFAULT_LIMIT,
|
||||||
STATE_EXPANDED,
|
STATE_EXPANDED,
|
||||||
|
@ -14,6 +15,7 @@
|
||||||
import { expandSelection } from './selection.js'
|
import { expandSelection } from './selection.js'
|
||||||
import { singleton } from './singleton.js'
|
import { singleton } from './singleton.js'
|
||||||
import {
|
import {
|
||||||
|
deleteIn,
|
||||||
existsIn,
|
existsIn,
|
||||||
getIn,
|
getIn,
|
||||||
setIn,
|
setIn,
|
||||||
|
@ -26,6 +28,7 @@
|
||||||
import { isEqual, isNumber, initial, last, cloneDeep } 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'
|
||||||
|
import { isObject } from './utils/typeUtils.js'
|
||||||
|
|
||||||
let divContents
|
let divContents
|
||||||
|
|
||||||
|
@ -41,21 +44,12 @@
|
||||||
let selection = null
|
let selection = null
|
||||||
let selectionMap = {}
|
let selectionMap = {}
|
||||||
|
|
||||||
// $: {
|
|
||||||
// selectionMap = {}
|
|
||||||
// if (selection != null) {
|
|
||||||
// selection.forEach(path => {
|
|
||||||
// selectionMap[compileJSONPointer(path)] = true
|
|
||||||
// })
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
export let onChangeJson = () => {}
|
export let onChangeJson = () => {}
|
||||||
|
|
||||||
let clipboard = null
|
let clipboard = null
|
||||||
$: canCut = selection != null
|
$: canCut = selection != null && selection.paths != null
|
||||||
$: canCopy = selection != null
|
$: canCopy = selection != null && selection.paths != null
|
||||||
$: canPaste = clipboard != null
|
$: canPaste = clipboard != null && selection != null
|
||||||
|
|
||||||
$: state = syncState(doc, state, [], (path) => path.length < 1)
|
$: 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() {
|
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() {
|
function handleCopy() {
|
||||||
if (selection) {
|
if (selection && selection.paths) {
|
||||||
clipboard = selection.map(path => cloneDeep(getIn(doc, path)))
|
clipboard = selectionToClipboard(selection)
|
||||||
console.log('copied to clipboard', clipboard)
|
console.log('copy', { clipboard })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handlePaste() {
|
function handlePaste() {
|
||||||
if (clipboard) {
|
if (selection && clipboard) {
|
||||||
console.log('focus', singleton.focus)
|
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 = {
|
selection = {
|
||||||
paths: pathsMap
|
paths,
|
||||||
|
pathsMap
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.error('Unknown type of selection', newSelection)
|
console.error('Unknown type of selection', newSelection)
|
||||||
|
@ -383,6 +418,19 @@
|
||||||
handleRedo()
|
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>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<script>
|
<script>
|
||||||
import { debounce, initial, isEqual } from 'lodash-es'
|
import { debounce, initial, isEqual, last } from 'lodash-es'
|
||||||
import {
|
import {
|
||||||
DEBOUNCE_DELAY,
|
DEBOUNCE_DELAY,
|
||||||
DEFAULT_LIMIT,
|
DEFAULT_LIMIT,
|
||||||
|
@ -194,10 +194,8 @@
|
||||||
|
|
||||||
function handleMouseDown (event) {
|
function handleMouseDown (event) {
|
||||||
// unselect existing selection on mouse down if any
|
// unselect existing selection on mouse down if any
|
||||||
if (singleton.selectionAnchor != null && singleton.selectionFocus != null) {
|
if (selection) {
|
||||||
singleton.selectionAnchor = null
|
onSelect(null)
|
||||||
singleton.selectionFocus = null
|
|
||||||
onSelect(null, null)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// check if the mouse down is not happening in the key or value input fields
|
// check if the mouse down is not happening in the key or value input fields
|
||||||
|
@ -266,13 +264,15 @@
|
||||||
event.stopPropagation()
|
event.stopPropagation()
|
||||||
|
|
||||||
onSelect({
|
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
|
// FIXME: this is not efficient. Create a nested object with the selection and pass that
|
||||||
$: selected = (selection && selection.paths)
|
$: selected = (selection && selection.pathsMap)
|
||||||
? selection.paths[compileJSONPointer(path)] === true
|
? selection.pathsMap[compileJSONPointer(path)] === true
|
||||||
: false
|
: false
|
||||||
|
|
||||||
$: selectedBefore = (selection && selection.beforePath)
|
$: selectedBefore = (selection && selection.beforePath)
|
||||||
|
@ -280,7 +280,7 @@
|
||||||
: false
|
: false
|
||||||
|
|
||||||
$: selectedAfter = (selection && selection.afterPath)
|
$: selectedAfter = (selection && selection.afterPath)
|
||||||
? isEqual(selection.afterPath, path)
|
? isEqual(initial(selection.afterPath), path)
|
||||||
: false
|
: false
|
||||||
|
|
||||||
$: indentationStyle = getIndentationStyle(path.length)
|
$: indentationStyle = getIndentationStyle(path.length)
|
||||||
|
|
|
@ -47,6 +47,7 @@ $search-size: 24px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
width: 100px;
|
width: 100px;
|
||||||
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-count {
|
.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