Implement Duplicate
This commit is contained in:
parent
401a6e19fd
commit
9ac6ca95c4
|
@ -1,7 +1,7 @@
|
|||
<script>
|
||||
import { tick } from 'svelte'
|
||||
import {
|
||||
append,
|
||||
append, duplicate,
|
||||
insertBefore,
|
||||
removeAll,
|
||||
replace
|
||||
|
@ -15,10 +15,14 @@
|
|||
} from './constants.js'
|
||||
import SearchBox from './SearchBox.svelte'
|
||||
import Icon from 'svelte-awesome'
|
||||
import { faCut, faCopy, faPaste, faSearch, faUndo, faRedo } from '@fortawesome/free-solid-svg-icons'
|
||||
import { faCut, faClone, faCopy, faPaste, faSearch, faUndo, faRedo } from '@fortawesome/free-solid-svg-icons'
|
||||
import { createHistory } from './history.js'
|
||||
import JSONNode from './JSONNode.svelte'
|
||||
import { expandSelection } from './selection.js'
|
||||
import {
|
||||
createPathsMap,
|
||||
createSelectionFromOperations,
|
||||
expandSelection
|
||||
} from './selection.js'
|
||||
import { isContentEditableDiv } from './utils/domUtils.js'
|
||||
import {
|
||||
existsIn,
|
||||
|
@ -45,9 +49,8 @@
|
|||
export let onChangeJson = () => {}
|
||||
|
||||
let clipboard = null
|
||||
$: canCut = selection != null && selection.paths != null
|
||||
$: canCopy = selection != null && selection.paths != null
|
||||
$: canPaste = clipboard != null && selection != null
|
||||
$: hasSelectionContents = selection != null && selection.paths != null
|
||||
$: hasClipboardContents = clipboard != null && selection != null
|
||||
|
||||
$: state = syncState(doc, state, [], (path) => path.length < 1)
|
||||
|
||||
|
@ -168,12 +171,12 @@
|
|||
const props = getIn(state, parentPath.concat(STATE_PROPS))
|
||||
const nextKeys = getNextKeys(props, parentPath, beforeKey, true)
|
||||
const operations = insertBefore(doc, selection.beforePath, clipboard, nextKeys)
|
||||
const newSelection = createNewSelection(operations)
|
||||
const newSelection = createSelectionFromOperations(operations)
|
||||
|
||||
handlePatch(operations, newSelection)
|
||||
} else if (selection.appendPath) {
|
||||
const operations = append(doc, selection.appendPath, clipboard)
|
||||
const newSelection = createNewSelection(operations)
|
||||
const newSelection = createSelectionFromOperations(operations)
|
||||
|
||||
handlePatch(operations, newSelection)
|
||||
} else if (selection.paths) {
|
||||
|
@ -183,13 +186,31 @@
|
|||
const props = getIn(state, parentPath.concat(STATE_PROPS))
|
||||
const nextKeys = getNextKeys(props, parentPath, beforeKey, true)
|
||||
const operations = replace(doc, selection.paths, clipboard, nextKeys)
|
||||
const newSelection = createNewSelection(operations)
|
||||
const newSelection = createSelectionFromOperations(operations)
|
||||
|
||||
handlePatch(operations, newSelection)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleDuplicate() {
|
||||
if (selection && selection.paths) {
|
||||
console.log('duplicate', { selection })
|
||||
|
||||
const lastPath = last(selection.paths) // FIXME: here we assume selection.paths is sorted correctly, that's a dangerous assumption
|
||||
const parentPath = initial(lastPath)
|
||||
const beforeKey = last(lastPath)
|
||||
const props = getIn(state, parentPath.concat(STATE_PROPS))
|
||||
const nextKeys = getNextKeys(props, parentPath, beforeKey, false)
|
||||
|
||||
const operations = duplicate(doc, selection.paths, nextKeys)
|
||||
const newSelection = createSelectionFromOperations(operations)
|
||||
|
||||
console.log('newSelection', newSelection)
|
||||
handlePatch(operations, newSelection)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: cleanup
|
||||
$: console.log('doc', doc)
|
||||
$: console.log('state', state)
|
||||
|
@ -370,16 +391,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
function createPathsMap (paths) {
|
||||
const pathsMap = {}
|
||||
|
||||
paths.forEach(path => {
|
||||
pathsMap[compileJSONPointer(path)] = true
|
||||
})
|
||||
|
||||
return pathsMap
|
||||
}
|
||||
|
||||
/**
|
||||
* Expand all nodes on given path
|
||||
* @param {Path} path
|
||||
|
@ -417,6 +428,10 @@
|
|||
event.preventDefault()
|
||||
handlePaste()
|
||||
}
|
||||
if (combo === 'Ctrl+D' || combo === 'Command+D') {
|
||||
event.preventDefault()
|
||||
handleDuplicate()
|
||||
}
|
||||
if (combo === 'Escape') {
|
||||
event.preventDefault()
|
||||
selection = null
|
||||
|
@ -467,7 +482,7 @@
|
|||
<button
|
||||
class="button cut"
|
||||
on:click={handleCut}
|
||||
disabled={!canCut}
|
||||
disabled={!hasSelectionContents}
|
||||
title="Cut (Ctrl+X)"
|
||||
>
|
||||
<Icon data={faCut} />
|
||||
|
@ -475,7 +490,7 @@
|
|||
<button
|
||||
class="button copy"
|
||||
on:click={handleCopy}
|
||||
disabled={!canCopy}
|
||||
disabled={!hasSelectionContents}
|
||||
title="Copy (Ctrl+C)"
|
||||
>
|
||||
<Icon data={faCopy} />
|
||||
|
@ -483,7 +498,7 @@
|
|||
<button
|
||||
class="button paste"
|
||||
on:click={handlePaste}
|
||||
disabled={!canPaste}
|
||||
disabled={!hasClipboardContents}
|
||||
title="Paste (Ctrl+V)"
|
||||
>
|
||||
<Icon data={faPaste} />
|
||||
|
@ -491,6 +506,17 @@
|
|||
|
||||
<div class="separator"></div>
|
||||
|
||||
<button
|
||||
class="button duplicate"
|
||||
on:click={handleDuplicate}
|
||||
disabled={!hasSelectionContents}
|
||||
title="Duplicate (Ctrl+D)"
|
||||
>
|
||||
<Icon data={faClone} />
|
||||
</button>
|
||||
|
||||
<div class="separator"></div>
|
||||
|
||||
<button
|
||||
class="button search"
|
||||
on:click={handleToggleSearch}
|
||||
|
|
|
@ -16,7 +16,7 @@ import { findUniqueName } from './utils/stringUtils'
|
|||
* @param {string[]} nextKeys A list with all keys *after* the renamed key,
|
||||
* these keys will be moved down, so the renamed
|
||||
* key will maintain it's position above these keys
|
||||
* @return {Array}
|
||||
* @return {JSONPatchDocument}
|
||||
*/
|
||||
export function insertBefore (json, path, values, nextKeys) { // TODO: find a better name and define datastructure for values
|
||||
const parentPath = initial(path)
|
||||
|
@ -59,7 +59,7 @@ export function insertBefore (json, path, values, nextKeys) { // TODO: find a b
|
|||
* @param {JSON} json
|
||||
* @param {Path} path
|
||||
* @param {Array.<{key?: string, value: JSON}>} values
|
||||
* @return {Array}
|
||||
* @return {JSONPatchDocument}
|
||||
*/
|
||||
export function append (json, path, values) { // TODO: find a better name and define datastructure for values
|
||||
const parent = getIn(json, path)
|
||||
|
@ -94,7 +94,7 @@ export function append (json, path, values) { // TODO: find a better name and d
|
|||
* @param {string[]} nextKeys A list with all keys *after* the renamed key,
|
||||
* these keys will be moved down, so the renamed
|
||||
* key will maintain it's position above these keys
|
||||
* @returns {Array}
|
||||
* @returns {JSONPatchDocument}
|
||||
*/
|
||||
export function rename(parentPath, oldKey, newKey, nextKeys) {
|
||||
return [
|
||||
|
@ -123,7 +123,7 @@ export function rename(parentPath, oldKey, newKey, nextKeys) {
|
|||
* @param {string[]} nextKeys A list with all keys *after* the renamed key,
|
||||
* these keys will be moved down, so the renamed
|
||||
* key will maintain it's position above these keys
|
||||
* @return {Array}
|
||||
* @return {JSONPatchDocument}
|
||||
*/
|
||||
export function replace (json, paths, values, nextKeys) { // TODO: find a better name and define datastructure for values
|
||||
const firstPath = first(paths)
|
||||
|
@ -173,6 +173,62 @@ export function replace (json, paths, values, nextKeys) { // TODO: find a bette
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a JSONPatch for a duplicate action.
|
||||
*
|
||||
* This function needs the current data in order to be able to determine
|
||||
* a unique property name for the duplicated node in case of duplicating
|
||||
* and object property
|
||||
*
|
||||
* @param {JSON} json
|
||||
* @param {Path[]} paths
|
||||
* @param {string[]} nextKeys A list with all keys *after* the renamed key,
|
||||
* these keys will be moved down, so the renamed
|
||||
* key will maintain it's position above these keys
|
||||
* @return {JSONPatchDocument}
|
||||
*/
|
||||
export function duplicate (json, paths, nextKeys) {
|
||||
const firstPath = first(paths)
|
||||
const parentPath = initial(firstPath)
|
||||
const parent = getIn(json, parentPath)
|
||||
|
||||
if (Array.isArray(parent)) {
|
||||
const lastPath = last(paths)
|
||||
const offset = lastPath ? (parseInt(last(lastPath), 10) + 1) : 0
|
||||
|
||||
return [
|
||||
// copy operations
|
||||
...paths.map((path, index) => ({
|
||||
op: 'copy',
|
||||
from: compileJSONPointer(path),
|
||||
path: compileJSONPointer(parentPath.concat(index + offset))
|
||||
})),
|
||||
|
||||
// move down operations
|
||||
// move all lower down keys so the renamed key will maintain it's position
|
||||
...nextKeys.map(key => moveDown(parentPath, key))
|
||||
]
|
||||
} else { // 'object'
|
||||
return [
|
||||
// copy operations
|
||||
...paths.map(path => {
|
||||
const prop = last(path)
|
||||
const newProp = findUniqueName(prop, parent)
|
||||
|
||||
return {
|
||||
op: 'copy',
|
||||
from: compileJSONPointer(path),
|
||||
path: compileJSONPointer(parentPath.concat(newProp))
|
||||
}
|
||||
}),
|
||||
|
||||
// move down operations
|
||||
// move all lower down keys so the renamed key will maintain it's position
|
||||
...nextKeys.map(key => moveDown(parentPath, key))
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a JSONPatch for a remove operation
|
||||
* @param {Path} path
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { first, initial, isEmpty, isEqual, last } from 'lodash-es'
|
||||
import { STATE_PROPS } from './constants.js'
|
||||
import { getIn } from './utils/immutabilityHelpers.js'
|
||||
import { compileJSONPointer, parseJSONPointer } from './utils/jsonPointer.js'
|
||||
import { isObject } from './utils/typeUtils.js'
|
||||
|
||||
/**
|
||||
|
@ -64,6 +65,35 @@ export function expandSelection (doc, state, anchorPath, focusPath) {
|
|||
throw new Error('Failed to create selection')
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {JSONPatchDocument} operations
|
||||
* @returns {MultiSelection}
|
||||
*/
|
||||
export function createSelectionFromOperations (operations) {
|
||||
const paths = operations
|
||||
.filter(operation => operation.op === 'add' || operation.op === 'copy')
|
||||
.map(operation => parseJSONPointer(operation.path))
|
||||
|
||||
return {
|
||||
paths,
|
||||
pathsMap: createPathsMap(paths)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Path[]} paths
|
||||
* @returns {Object}
|
||||
*/
|
||||
export function createPathsMap (paths) {
|
||||
const pathsMap = {}
|
||||
|
||||
paths.forEach(path => {
|
||||
pathsMap[compileJSONPointer(path)] = true
|
||||
})
|
||||
|
||||
return pathsMap
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the common path of two paths.
|
||||
* For example findCommonRoot(['arr', '1', 'name'], ['arr', '1', 'address', 'contact']) returns ['arr', '1']
|
||||
|
|
|
@ -9,9 +9,12 @@ export function findUniqueName (name, existingProps) {
|
|||
let validName = name
|
||||
let i = 1
|
||||
|
||||
// remove any " (copy)" or " (copy 2)" suffix from the name
|
||||
const nameWithoutCopySuffix = name.replace(/ \(copy( \d+)?\)$/, '')
|
||||
|
||||
while (validName in existingProps) {
|
||||
const copy = 'copy' + (i > 1 ? (' ' + i) : '')
|
||||
validName = `${name} (${copy})`
|
||||
validName = `${nameWithoutCopySuffix} (${copy})`
|
||||
i++
|
||||
}
|
||||
|
||||
|
|
|
@ -11,7 +11,10 @@ describe('stringUtils', () => {
|
|||
assert.deepStrictEqual(findUniqueName('other', {'a': true, 'b': true, 'c': true}), 'other')
|
||||
assert.deepStrictEqual(findUniqueName('b', {'a': true, 'b': true, 'c': true}), 'b (copy)')
|
||||
assert.deepStrictEqual(findUniqueName('b', {'a': true, 'b': true, 'c': true, 'b (copy)': true}), 'b (copy 2)')
|
||||
assert.deepStrictEqual(findUniqueName('b (copy)', {'a': true, 'b': true, 'c': true, 'b (copy)': true}), 'b (copy 2)')
|
||||
assert.deepStrictEqual(findUniqueName('b', {'a': true, 'b': true, 'c': true, 'b (copy)': true, 'b (copy 2)': true}), 'b (copy 3)')
|
||||
assert.deepStrictEqual(findUniqueName('b (copy)', {'a': true, 'b': true, 'c': true, 'b (copy)': true, 'b (copy 2)': true}), 'b (copy 3)')
|
||||
assert.deepStrictEqual(findUniqueName('b (copy 2)', {'a': true, 'b': true, 'c': true, 'b (copy)': true, 'b (copy 2)': true}), 'b (copy 3)')
|
||||
})
|
||||
|
||||
it('toCapital', () => {
|
||||
|
|
|
@ -76,7 +76,7 @@ export function patchProps (state, operations) {
|
|||
}
|
||||
}
|
||||
|
||||
if (operation.op === 'add') {
|
||||
if (operation.op === 'add' || operation.op === 'copy') {
|
||||
const pathTo = parseJSONPointer(operation.path)
|
||||
const parentPath = initial(pathTo)
|
||||
const key = last(pathTo)
|
||||
|
|
Loading…
Reference in New Issue