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

View File

@ -1,11 +1,10 @@
<script>
import { tick } from 'svelte'
import {
append,
duplicate,
insertBefore,
removeAll,
replace
insert,
createNewValue,
removeAll
} from '../logic/operations.js'
import {
STATE_EXPANDED,
@ -18,7 +17,8 @@
import {
createPathsMap,
createSelectionFromOperations,
expandSelection
expandSelection,
getParentPath
} from '../logic/selection.js'
import { isContentEditableDiv } from '../utils/domUtils.js'
import {
@ -34,6 +34,7 @@
import jump from '../assets/jump.js/src/jump.js'
import { expandPath, syncState, getNextKeys, patchProps } from '../logic/documentState.js'
import Menu from './Menu.svelte'
import { isObjectOrArray } from '../utils/typeUtils.js';
let divContents
let domHiddenInput
@ -150,42 +151,10 @@
if (selection && clipboard) {
console.log('paste', { clipboard, selection })
function createNewSelection (operations) {
const paths = operations
.filter(operation => operation.op === 'add')
.map(operation => parseJSONPointer(operation.path))
const operations = insert(doc, state, selection, clipboard)
const newSelection = createSelectionFromOperations(operations)
return {
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)
}
handlePatch(operations, newSelection)
}
}
@ -193,6 +162,8 @@
if (selection && selection.paths) {
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 parentPath = initial(lastPath)
const beforeKey = last(lastPath)
@ -202,17 +173,35 @@
const operations = duplicate(doc, selection.paths, nextKeys)
const newSelection = createSelectionFromOperations(operations)
console.log('newSelection', newSelection)
handlePatch(operations, newSelection)
}
}
function handleInsert() {
/**
* @param {'value' | 'object' | 'array' | 'structure'} type
*/
function handleInsert(type) {
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') {
event.preventDefault()
handleInsert()
handleInsert('structure')
}
if (combo === 'Escape') {
event.preventDefault()
@ -444,6 +433,8 @@
searchText={searchText}
searchResult={searchResult}
bind:showSearch
doc={doc}
selection={selection}
clipboard={clipboard}
@ -451,6 +442,7 @@
onCopy={handleCopy}
onPaste={handlePaste}
onDuplicate={handleDuplicate}
onInsert={handleInsert}
onUndo={handleUndo}
onRedo={handleRedo}

View File

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

View File

@ -1,7 +1,11 @@
import { first, initial, last, pickBy } from 'lodash-es'
import { getIn } from '../utils/immutabilityHelpers'
import { compileJSONPointer } from '../utils/jsonPointer'
import { findUniqueName } from '../utils/stringUtils'
import { first, initial, isEmpty, last, pickBy, cloneDeepWith } from 'lodash-es'
import { STATE_PROPS } from '../constants.js'
import { getNextKeys } from '../logic/documentState.js'
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.
@ -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
* @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 { getIn } from '../utils/immutabilityHelpers.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')
}
/**
*
* @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
* @returns {MultiSelection}

View File

@ -1,5 +1,5 @@
import assert from 'assert'
import { expandSelection } from './selection.js'
import { expandSelection, getParentPath } from './selection.js'
import { syncState } from './documentState.js'
describe ('selection', () => {
@ -65,4 +65,14 @@ describe ('selection', () => {
['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
* @property {string} text
* @property {function} onClick
* @property {string} [title=undefined]
* @property {boolean} [default=false]
**/
*/