Implement insert

This commit is contained in:
Jos de Jong 2020-07-27 16:05:51 +02:00
parent a6bb790f5e
commit eea6e09bd8
7 changed files with 169 additions and 72 deletions

View File

@ -10,7 +10,6 @@
export let title = null export let title = null
export let width = '120px' export let width = '120px'
export let visible = false export let visible = false
export let disabled = false
function toggleShow (event) { function toggleShow (event) {
event.stopPropagation() event.stopPropagation()
@ -51,7 +50,11 @@
<ul> <ul>
{#each items as item} {#each items as item}
<li> <li>
<button on:click={item.onClick} disabled={disabled}> <button
on:click={item.onClick}
title={item.title}
disabled={item.disabled}
>
{item.text} {item.text}
</button> </button>
</li> </li>

View File

@ -1,11 +1,10 @@
<script> <script>
import { tick } from 'svelte' import { tick } from 'svelte'
import { import {
append,
duplicate, duplicate,
insertBefore, insert,
removeAll, createNewValue,
replace removeAll
} from '../logic/operations.js' } from '../logic/operations.js'
import { import {
STATE_EXPANDED, STATE_EXPANDED,
@ -18,7 +17,8 @@
import { import {
createPathsMap, createPathsMap,
createSelectionFromOperations, createSelectionFromOperations,
expandSelection expandSelection,
getParentPath
} from '../logic/selection.js' } from '../logic/selection.js'
import { isContentEditableDiv } from '../utils/domUtils.js' import { isContentEditableDiv } from '../utils/domUtils.js'
import { import {
@ -34,6 +34,7 @@
import jump from '../assets/jump.js/src/jump.js' import jump from '../assets/jump.js/src/jump.js'
import { expandPath, syncState, getNextKeys, patchProps } from '../logic/documentState.js' import { expandPath, syncState, getNextKeys, patchProps } from '../logic/documentState.js'
import Menu from './Menu.svelte' import Menu from './Menu.svelte'
import { isObjectOrArray } from '../utils/typeUtils.js';
let divContents let divContents
let domHiddenInput let domHiddenInput
@ -150,42 +151,10 @@
if (selection && clipboard) { if (selection && clipboard) {
console.log('paste', { clipboard, selection }) console.log('paste', { clipboard, selection })
function createNewSelection (operations) { const operations = insert(doc, state, selection, clipboard)
const paths = operations const newSelection = createSelectionFromOperations(operations)
.filter(operation => operation.op === 'add')
.map(operation => parseJSONPointer(operation.path))
return { handlePatch(operations, newSelection)
paths,
pathsMap: createPathsMap(paths)
}
}
if (selection.beforePath) {
const parentPath = initial(selection.beforePath)
const beforeKey = last(selection.beforePath)
const props = getIn(state, parentPath.concat(STATE_PROPS))
const nextKeys = getNextKeys(props, beforeKey, true)
const operations = insertBefore(doc, selection.beforePath, clipboard, nextKeys)
const newSelection = createSelectionFromOperations(operations)
handlePatch(operations, newSelection)
} else if (selection.appendPath) {
const operations = append(doc, selection.appendPath, clipboard)
const newSelection = createSelectionFromOperations(operations)
handlePatch(operations, newSelection)
} else if (selection.paths) {
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, beforeKey, true)
const operations = replace(doc, selection.paths, clipboard, nextKeys)
const newSelection = createSelectionFromOperations(operations)
handlePatch(operations, newSelection)
}
} }
} }
@ -193,6 +162,8 @@
if (selection && selection.paths) { if (selection && selection.paths) {
console.log('duplicate', { selection }) console.log('duplicate', { selection })
// TODO: move this logic inside duplicate()
const lastPath = last(selection.paths) // FIXME: here we assume selection.paths is sorted correctly, that's a dangerous assumption const lastPath = last(selection.paths) // FIXME: here we assume selection.paths is sorted correctly, that's a dangerous assumption
const parentPath = initial(lastPath) const parentPath = initial(lastPath)
const beforeKey = last(lastPath) const beforeKey = last(lastPath)
@ -202,17 +173,35 @@
const operations = duplicate(doc, selection.paths, nextKeys) const operations = duplicate(doc, selection.paths, nextKeys)
const newSelection = createSelectionFromOperations(operations) const newSelection = createSelectionFromOperations(operations)
console.log('newSelection', newSelection)
handlePatch(operations, newSelection) handlePatch(operations, newSelection)
} }
} }
function handleInsert() { /**
* @param {'value' | 'object' | 'array' | 'structure'} type
*/
function handleInsert(type) {
if (selection != null) { if (selection != null) {
console.log('insert', { selection }) console.log('insert', { type, selection })
// TODO: impelemnt insert const value = createNewValue(doc, selection, type)
const values = [
{
key: 'new',
value
}
]
const operations = insert(doc, state, selection, values)
const newSelection = createSelectionFromOperations(operations)
handlePatch(operations, newSelection)
if (isObjectOrArray(value)) {
// expand the new object/array in case of inserting a structure
operations
.filter(operation => operation.op === 'add')
.forEach(operation => handleExpand(parseJSONPointer(operation.path), true, true))
}
} }
} }
@ -391,7 +380,7 @@
} }
if (combo === 'Ctrl+Insert' || combo === 'Command+Insert') { if (combo === 'Ctrl+Insert' || combo === 'Command+Insert') {
event.preventDefault() event.preventDefault()
handleInsert() handleInsert('structure')
} }
if (combo === 'Escape') { if (combo === 'Escape') {
event.preventDefault() event.preventDefault()
@ -444,6 +433,8 @@
searchText={searchText} searchText={searchText}
searchResult={searchResult} searchResult={searchResult}
bind:showSearch bind:showSearch
doc={doc}
selection={selection} selection={selection}
clipboard={clipboard} clipboard={clipboard}
@ -451,6 +442,7 @@
onCopy={handleCopy} onCopy={handleCopy}
onPaste={handlePaste} onPaste={handlePaste}
onDuplicate={handleDuplicate} onDuplicate={handleDuplicate}
onInsert={handleInsert}
onUndo={handleUndo} onUndo={handleUndo}
onRedo={handleRedo} onRedo={handleRedo}

View File

@ -3,10 +3,13 @@
import { faCut, faClone, faCopy, faPaste, faSearch, faUndo, faRedo, faPlus } from '@fortawesome/free-solid-svg-icons' import { faCut, faClone, faCopy, faPaste, faSearch, faUndo, faRedo, faPlus } from '@fortawesome/free-solid-svg-icons'
import SearchBox from './SearchBox.svelte' import SearchBox from './SearchBox.svelte'
import DropdownMenu from './DropdownMenu.svelte' import DropdownMenu from './DropdownMenu.svelte'
import { getParentPath } from '../logic/selection'
import { getIn } from '../utils/immutabilityHelpers'
export let searchText export let searchText
export let searchResult export let searchResult
export let showSearch = false export let showSearch = false
export let doc
export let selection export let selection
export let clipboard export let clipboard
export let historyState export let historyState
@ -15,6 +18,7 @@
export let onCopy export let onCopy
export let onPaste export let onPaste
export let onDuplicate export let onDuplicate
export let onInsert
export let onUndo export let onUndo
export let onRedo export let onRedo
@ -24,6 +28,7 @@
$: hasSelection = selection != null $: hasSelection = selection != null
$: hasSelectionContents = selection != null && selection.paths != null $: hasSelectionContents = selection != null && selection.paths != null
$: hasSelectionWithoutContents = selection != null && selection.paths == null
$: hasClipboardContents = clipboard != null && selection != null $: hasClipboardContents = clipboard != null && selection != null
function handleToggleSearch() { function handleToggleSearch() {
@ -36,33 +41,36 @@
} }
function handleInsertValue () { function handleInsertValue () {
console.log('TODO: insert value') onInsert('value')
} }
/** @type {MenuDropdownItem[]} */ /** @type {MenuDropdownItem[]} */
const insertItems = [ $: insertItems = [
{ {
text: "Insert value", text: 'Insert value',
title: 'Insert a new value',
onClick: handleInsertValue, onClick: handleInsertValue,
disabled: !hasSelectionWithoutContents,
default: true default: true
}, },
{ {
text: "Insert object", text: 'Insert object',
onClick: () => { title: 'Insert a new object',
console.log('TODO: insert object') onClick: () => onInsert('object'),
} disabled: !hasSelectionWithoutContents
}, },
{ {
text: "Insert array", text: 'Insert array',
onClick: () => { title: 'Insert a new array',
console.log('TODO: insert array') onClick: () => onInsert('array'),
} disabled: !hasSelectionWithoutContents
}, },
{ {
text: "Insert structure", text: 'Insert structure',
onClick: () => { title: 'Insert a new item with the same structure as other items. ' +
console.log('TODO: insert structure') 'Only applicable inside an array',
} onClick: () => onInsert('structure'),
disabled: !hasSelectionWithoutContents
} }
] ]
</script> </script>
@ -107,13 +115,12 @@
<DropdownMenu <DropdownMenu
items={insertItems} items={insertItems}
title="Insert new value (Ctrl+Insert)" title="Insert new value (Ctrl+Insert)"
disabled={!hasSelection}
> >
<button <button
class="button insert" class="button insert"
slot="defaultItem" slot="defaultItem"
on:click={handleInsertValue} on:click={handleInsertValue}
disabled={!hasSelection} disabled={!hasSelectionWithoutContents}
> >
<Icon data={faPlus} /> <Icon data={faPlus} />
</button> </button>

View File

@ -1,7 +1,11 @@
import { first, initial, last, pickBy } from 'lodash-es' import { first, initial, isEmpty, last, pickBy, cloneDeepWith } from 'lodash-es'
import { getIn } from '../utils/immutabilityHelpers' import { STATE_PROPS } from '../constants.js'
import { compileJSONPointer } from '../utils/jsonPointer' import { getNextKeys } from '../logic/documentState.js'
import { findUniqueName } from '../utils/stringUtils' import { getParentPath } from '../logic/selection.js'
import { getIn } from '../utils/immutabilityHelpers.js'
import { compileJSONPointer } from '../utils/jsonPointer.js'
import { findUniqueName } from '../utils/stringUtils.js'
import { isObjectOrArray } from '../utils/typeUtils.js'
/** /**
* Create a JSONPatch for an insert operation. * Create a JSONPatch for an insert operation.
@ -229,6 +233,66 @@ export function duplicate (json, paths, nextKeys) {
} }
} }
export function insert (doc, state, selection, values) {
if (selection.beforePath) {
const parentPath = initial(selection.beforePath)
const beforeKey = last(selection.beforePath)
const props = getIn(state, parentPath.concat(STATE_PROPS))
const nextKeys = getNextKeys(props, beforeKey, true)
const operations = insertBefore(doc, selection.beforePath, values, nextKeys)
return operations
} else if (selection.appendPath) {
const operations = append(doc, selection.appendPath, values)
return operations
} else if (selection.paths) {
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, beforeKey, true)
const operations = replace(doc, selection.paths, values, nextKeys)
return operations
}
}
export function createNewValue (doc, selection, type) {
switch (type) {
case 'value':
return ''
case 'object':
return {}
case 'array':
return []
case 'structure':
const parentPath = getParentPath(selection)
const parent = getIn(doc, parentPath)
if (Array.isArray(parent) && !isEmpty(parent)) {
const jsonExample = first(parent)
const structure = cloneDeepWith(jsonExample, (value) => {
return isObjectOrArray(value)
? undefined // leave as is
: ''
})
console.log('structure', jsonExample, structure)
return structure
} else {
// no example structure
return ''
}
default: ''
}
}
/** /**
* Create a JSONPatch for a remove operation * Create a JSONPatch for a remove operation
* @param {Path} path * @param {Path} path

View File

@ -1,4 +1,4 @@
import { isEqual } from 'lodash-es' import { first, initial, isEqual } from 'lodash-es'
import { STATE_PROPS } from '../constants.js' import { STATE_PROPS } from '../constants.js'
import { getIn } from '../utils/immutabilityHelpers.js' import { getIn } from '../utils/immutabilityHelpers.js'
import { compileJSONPointer, parseJSONPointer } from '../utils/jsonPointer.js' import { compileJSONPointer, parseJSONPointer } from '../utils/jsonPointer.js'
@ -65,6 +65,26 @@ export function expandSelection (doc, state, anchorPath, focusPath) {
throw new Error('Failed to create selection') throw new Error('Failed to create selection')
} }
/**
*
* @param {Selection} selection
* @return {Path} Returns parent path
*/
export function getParentPath (selection) {
if (selection.beforePath) {
return initial(selection.beforePath)
}
if (selection.appendPath) {
return selection.appendPath
}
if (selection.paths) {
const firstPath = first(selection.paths)
return initial(firstPath)
}
}
/** /**
* @param {JSONPatchDocument} operations * @param {JSONPatchDocument} operations
* @returns {MultiSelection} * @returns {MultiSelection}

View File

@ -1,5 +1,5 @@
import assert from 'assert' import assert from 'assert'
import { expandSelection } from './selection.js' import { expandSelection, getParentPath } from './selection.js'
import { syncState } from './documentState.js' import { syncState } from './documentState.js'
describe ('selection', () => { describe ('selection', () => {
@ -65,4 +65,14 @@ describe ('selection', () => {
['obj', 'arr'] ['obj', 'arr']
]) ])
}) })
it('should get parent path from a selection', () => {
assert.deepStrictEqual(getParentPath({ beforePath: ['a', 'b']}), ['a'])
assert.deepStrictEqual(getParentPath({ appendPath: ['a', 'b']}), ['a', 'b'])
assert.deepStrictEqual(getParentPath({ paths:[
['a', 'b'],
['a', 'c'],
['a', 'd']
]}), ['a'])
})
}) })

View File

@ -80,5 +80,6 @@
* @typedef {Object} MenuDropdownItem * @typedef {Object} MenuDropdownItem
* @property {string} text * @property {string} text
* @property {function} onClick * @property {function} onClick
* @property {string} [title=undefined]
* @property {boolean} [default=false] * @property {boolean} [default=false]
**/ */